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