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