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