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