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