smartxml: fix - adding nodes before/after root node is not allowed
[fnpeditor.git] / src / smartxml / core.js
1 define(function(require) {
2     
3 'use strict';
4 /* globals Node */
5
6 var _ = require('libs/underscore');
7
8
9 var INSERTION = function(implementation) {
10     var toret = function(node, options) {
11         var insertion = this.getNodeInsertion(node),
12             nodeWasContained = this.document.containsNode(insertion.ofNode),
13             nodeParent,
14             returned;
15         options = options || {};
16         if(!(this.document.containsNode(this)) || !insertion.insertsNew) {
17             nodeParent = insertion.ofNode.parent();
18         }
19         if(!insertion.insertsNew && insertion.ofNode.isSurroundedByTextNodes()) {
20             var prev = insertion.ofNode.prev(),
21                 next = insertion.ofNode.next();
22             prev.setText(prev.getText()+next.getText());
23             next.detach();
24         }
25         returned = implementation.call(this, insertion.ofNode);
26         if(!options.silent && returned && returned.sameNode(insertion.ofNode)) {
27             if(!insertion.insertsNew) {
28                 this.triggerChangeEvent('nodeDetached', {node: insertion.ofNode, parent: nodeParent, move: true});
29             }
30             this.triggerChangeEvent('nodeAdded', {node: insertion.ofNode, move: !insertion.insertsNew}, nodeParent, nodeWasContained);
31         }
32         return returned;
33     };
34     return toret;
35 };
36
37 var documentNodeTransformations = {
38     detach: function() {
39         var parent = this.parent(),
40             existed = this.document.containsNode(this);
41         this._$.detach();
42         if(existed) {
43             this.triggerChangeEvent('nodeDetached', {parent: parent});
44             if(!parent) {
45                 // This was the root of the document
46                 this.document._defineDocumentProperties(null);
47             }
48         }
49         return this;
50     },
51
52     replaceWith: function(node) {
53         var toret;
54         if(this.isRoot()) {
55             return this.document.replaceRoot(node);
56         }
57         if(this.parent()) {
58             toret = this.after(node);
59             this.detach();
60             return toret;
61         }
62         throw new Error('Cannot replace node without a parent.');
63     },
64
65     after: INSERTION(function(node) {
66         if(this.isRoot()) {
67             return;
68         }
69         var next = this.next();
70
71         if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
72             next.setText(node.getText() + next.getText());
73             node.detach();
74             return next;
75         }
76         this._$.after(node.nativeNode);
77         return node;
78     }),
79
80     before: INSERTION(function(node) {
81         if(this.isRoot()) {
82             return;
83         }
84         var prev = this.prev();
85         if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
86             prev.setText(prev.getText() + node.getText());
87             node.detach();
88             return prev;
89         }
90         this._$.before(node.nativeNode);
91         return node;
92     }),
93
94     wrapWith: function(node) {
95         var insertion = this.getNodeInsertion(node);
96
97         if(this.parent() || this.isRoot()) {
98             this.replaceWith(insertion.ofNode);
99         }
100         insertion.ofNode.append(this);
101         return insertion.ofNode;
102     },
103
104     /**
105     * Removes parent of a node if node has no siblings.
106     */
107     unwrap: function() {
108         if(this.isRoot()) {
109             return;
110         }
111         var parent = this.parent(),
112             grandParent;
113         if(parent.contents().length === 1) {
114             grandParent = parent.parent();
115             parent.unwrapContent();
116             return grandParent;
117         }
118     }
119 };
120
121 var elementNodeTransformations = {
122
123     detach: function() {
124         var next;
125         if(this.parent() && this.isSurroundedByTextNodes()) {
126             next = this.next();
127             this.prev().appendText(next.getText());
128             next.detach();
129         }
130         return this.__super__.detach();
131     },
132
133     setTag: function(tagName) {
134         var node = this.document.createDocumentNode({tagName: tagName});
135
136         this.getAttrs().forEach(function(attribute) {
137             node.setAttr(attribute.name, attribute.value);
138         });
139
140         this.contents().forEach(function(child) {
141             node.append(child);
142         });
143
144         node.setData(this.getData());
145
146         this.replaceWith(node);
147         return node;
148     },
149
150     setAttr: function(name, value, silent) {
151         var oldVal = this.getAttr(name);
152         this._$.attr(name, value);
153         if(!silent) {
154             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
155         }
156     },
157
158     append: INSERTION(function(node) {
159         var last = _.last(this.contents());
160         if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
161             last.setText(last.getText() + node.getText());
162             node.detach();
163             return last;
164         } else {
165             this._$.append(node.nativeNode);
166             return node;
167         }
168     }),
169
170     prepend: INSERTION(function(node) {
171         var first = this.contents()[0];
172         if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
173             first.setText(node.getText() + first.getText());
174             node.detach();
175             return first;
176         } else {
177             this._$.prepend(node.nativeNode);
178             return node;
179         }
180     }),
181
182     insertAtIndex: function(nativeNode, index) {
183         var contents = this.contents();
184         if(index < contents.length) {
185             return contents[index].before(nativeNode);
186         } else if(index === contents.length) {
187             return this.append(nativeNode);
188         }
189     },
190
191     unwrapContent: function() {
192         var parent = this.parent();
193         if(!parent) {
194             return;
195         }
196
197         this.contents()
198             .filter(function(child) {
199                 return child.getProperty('describesParent');
200             }.bind(this))
201             .forEach(function(child) {
202                 child.detach();
203             });
204
205         var myContents = this.contents(),
206             myIdx = parent.indexOf(this);
207
208         if(myContents.length === 0) {
209             return this.detach();
210         }
211
212         var childrenLength = this.contents().length,
213             first = true,
214             shiftRange = false;
215         this.contents().forEach(function(child) {
216             var returned = this.before(child);
217             if(first && !(returned.sameNode(child))) {
218                 shiftRange = true;
219                 first = false;
220             }
221         }.bind(this));
222
223         this.detach();
224
225         return {
226             element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
227             element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
228         };
229     },
230
231     wrapText: function(params) {
232         return this.document._wrapText(_.extend({inside: this}, params));
233     }
234 };
235
236 var textNodeTransformations = {
237     setText: {
238         impl: function(t, text) {
239             t.oldText = this.getText();
240             this.nativeNode.data = text;
241             this.triggerTextChangeEvent();
242         },
243         undo: function(t) {
244             this.setText(t.oldText);
245         }
246     },
247
248     before: INSERTION(function(node) {
249         if(node.nodeType === Node.TEXT_NODE) {
250             this.prependText(node.getText());
251             node.detach();
252             return this;
253         } else {
254             return this.__super__.before(node, {silent:true});
255         }
256     }),
257
258     after: INSERTION(function(node) {
259         if(node.nodeType === Node.TEXT_NODE) {
260             this.appendText(node.getText());
261             node.detach();
262             return this;
263         } else {
264             return this.__super__.after(node, {silent:true});
265         }
266     }),
267
268     append: function(node) {
269         if(node.nodeType === Node.TEXT_NODE) {
270             this.appendText(node.getText());
271             node.detach();
272             return this;
273         }
274     },
275     prepend: function(node) {
276         if(node.nodeType === Node.TEXT_NODE) {
277             this.prependText(node.getText());
278             node.detach();
279             return this;
280         }
281     },
282
283     appendText: function(text) {
284         this.nativeNode.data = this.nativeNode.data + text;
285         this.triggerTextChangeEvent();
286     },
287
288     prependText: function(text) {
289         this.nativeNode.data = text + this.nativeNode.data;
290         this.triggerTextChangeEvent();
291     },
292
293     wrapWith: function(desc) {
294         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
295             return this.document._wrapText({
296                 inside: this.parent(),
297                 textNodeIdx: this.parent().indexOf(this),
298                 offsetStart: Math.min(desc.start, desc.end),
299                 offsetEnd: Math.max(desc.start, desc.end),
300                 _with: {tagName: desc.tagName, attrs: desc.attrs}
301             });
302         } else {
303             return this.__super__.wrapWith.call(this, desc);
304         }
305     },
306
307     split: function(params) {
308         var parentElement = this.parent(),
309             passed = false,
310             succeedingChildren = [],
311             prefix = this.getText().substr(0, params.offset),
312             suffix = this.getText().substr(params.offset);
313
314         parentElement.contents().forEach(function(child) {
315             if(passed) {
316                 succeedingChildren.push(child);
317             }
318             if(child.sameNode(this)) {
319                 passed = true;
320             }
321         }.bind(this));
322
323         if(prefix.length > 0) {
324             this.setText(prefix);
325         }
326         else {
327             this.detach();
328         }
329
330         var attrs = {};
331         parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
332         var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
333         parentElement.after(newElement);
334
335         succeedingChildren.reverse().forEach(function(child) {
336             newElement.prepend(child);
337         });
338         if(suffix.length > 0) {
339             newElement.prepend({text: suffix});
340         }
341
342         return {first: parentElement, second: newElement};
343     },
344
345     divideWithElementNode: function(node, params) {
346         var insertion = this.getNodeInsertion(node),
347             myText = this.getText();
348
349         if(params.offset === myText.length) {
350             return this.after(node);
351         }
352         if(params.offset === 0) {
353             return this.before(node);
354         }
355
356         var lhsText = myText.substr(0, params.offset),
357             rhsText = myText.substr(params.offset),
358             rhsTextNode = this.document.createDocumentNode({text: rhsText});
359
360         this.setText(lhsText);
361         this.after(insertion.ofNode);
362         insertion.ofNode.after(rhsTextNode);
363         return insertion.ofNode;
364     }
365 };
366
367 var documentTransformations = {
368     wrapNodes: function(params) {
369         if(!(params.node1.parent().sameNode(params.node2.parent()))) {
370             throw new Error('Wrapping non-sibling nodes not supported.');
371         }
372
373         var parent = params.node1.parent(),
374             parentContents = parent.contents(),
375             wrapper = this.createDocumentNode({
376                 tagName: params._with.tagName,
377                 attrs: params._with.attrs}),
378             idx1 = parent.indexOf(params.node1),
379             idx2 = parent.indexOf(params.node2);
380
381         if(idx1 > idx2) {
382             var tmp = idx1;
383             idx1 = idx2;
384             idx2 = tmp;
385         }
386
387         var insertingMethod, insertingTarget;
388         if(idx1 === 0) {
389             insertingMethod = 'prepend';
390             insertingTarget = parent;
391         } else {
392             insertingMethod = 'after';
393             insertingTarget = parentContents[idx1-1];
394         }
395
396         for(var i = idx1; i <= idx2; i++) {
397             if(!parentContents[i].getProperty('describesParent')) {
398                 wrapper.append(parentContents[i].detach());
399             }
400         }
401
402         insertingTarget[insertingMethod](wrapper);
403         return wrapper;
404     },
405
406     _wrapText: function(params) {
407         params = _.extend({textNodeIdx: 0}, params);
408         if(typeof params.textNodeIdx === 'number') {
409             params.textNodeIdx = [params.textNodeIdx];
410         }
411         
412         var contentsInside = params.inside.contents(),
413             idx1 = Math.min.apply(Math, params.textNodeIdx),
414             idx2 = Math.max.apply(Math, params.textNodeIdx),
415             textNode1 = contentsInside[idx1],
416             textNode2 = contentsInside[idx2],
417             sameNode = textNode1.sameNode(textNode2),
418             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
419             prefixInside = textNode1.getText().substr(params.offsetStart),
420             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
421             suffixOutside = textNode2.getText().substr(params.offsetEnd)
422         ;
423
424         if(!(textNode1.parent().sameNode(textNode2.parent()))) {
425             throw new Error('Wrapping text in non-sibling text nodes not supported.');
426         }
427         
428         var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
429         textNode1.after(wrapperElement);
430         textNode1.detach();
431         
432         if(prefixOutside.length > 0) {
433             wrapperElement.before({text:prefixOutside});
434         }
435         if(sameNode) {
436             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
437             wrapperElement.append({text: core});
438         } else {
439             textNode2.detach();
440             if(prefixInside.length > 0) {
441                 wrapperElement.append({text: prefixInside});
442             }
443             for(var i = idx1 + 1; i < idx2; i++) {
444                 if(!contentsInside[i].getProperty('describesParent')) {
445                     wrapperElement.append(contentsInside[i]);
446                 }
447             }
448             if(suffixInside.length > 0) {
449                 wrapperElement.append({text: suffixInside});
450             }
451         }
452         if(suffixOutside.length > 0) {
453             wrapperElement.after({text: suffixOutside});
454         }
455         return wrapperElement;
456     },
457     replaceRoot: function(node) {
458         var insertion = this.getNodeInsertion(node);
459         this.root.detach();
460         this._defineDocumentProperties(insertion.ofNode._$);
461         insertion.ofNode.triggerChangeEvent('nodeAdded');
462         return insertion.ofNode;
463     },
464     deleteText: function(params) {
465         var ptr, next, toDetach, middle, text;
466
467         if(params.from.node.sameNode(params.to.node)) {
468             ptr = params.from.node;
469             text = ptr.getText();
470             ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
471             return;
472         }
473
474         // Both edge text nodes need to be edited before anything else happen in case that
475         // they get merged when detaching content between them.
476         params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
477         params.to.node.setText(params.to.node.getText().substr(params.to.offset));
478
479         ptr = params.from.node;
480         next = ptr.next();
481
482         while(next || ptr.parent()) {
483             if(next) {
484                 if(next.sameNode(params.to.node)) {
485                     return;
486                 }
487                 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
488                     middle = next;
489                     break;
490                 } else {
491                     toDetach = next;
492                     next = next.next();
493                     toDetach.detach();
494                 }
495             } else {
496                 ptr = ptr.parent();
497                 next = ptr.next();
498             }
499         }
500
501         if(!this.containsNode(params.to.node)) {
502             // The end node was merged during detaching nodes above - there is nothing more left to do.
503             return;
504         }
505
506         ptr = middle.contents()[0];
507         while(ptr && !ptr.sameNode(params.to.node)) {
508             if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
509                 ptr = ptr.contents()[0];
510                 continue;
511             } else {
512                 ptr = ptr.next();
513                 ptr.prev().detach();
514             }
515         }
516     }
517 };
518
519 return {
520     document: {
521         transformations: documentTransformations
522     },
523     documentNode: {
524         transformations: documentNodeTransformations
525     },
526     elementNode: {
527         transformations: elementNodeTransformations
528     },
529     textNode: {
530         transformations: textNodeTransformations
531     }
532 };
533
534 });