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