b5f8c44b849ad728da802e9c98adb91f3a2efbe4
[fnpeditor.git] / src / smartxml / core.js
1 define([
2
3 ], function() {
4     
5 'use strict';
6
7
8 var INSERTION = function(implementation) {
9     var toret = function(node) {
10         var insertion = this.getNodeInsertion(node),
11             nodeWasContained = this.document.containsNode(insertion.ofNode),
12             nodeParent;
13         if(!(this.document.containsNode(this))) {
14             nodeParent = insertion.ofNode.parent();
15         }
16         implementation.call(this, insertion.ofNode.nativeNode);
17         this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
18         return insertion.ofNode;
19     };
20     return toret;
21 };
22
23 var documentNodeTransformations = {
24     detach: function() {
25         var parent = this.parent();
26         this._$.detach();
27         this.triggerChangeEvent('nodeDetached', {parent: parent});
28         return this;
29     },
30
31     replaceWith: function(node) {
32         var toret;
33         if(this.isRoot()) {
34             return this.document.replaceRoot(node);
35         }
36         toret = this.after(node);
37         this.detach();
38         return toret;
39     },
40
41     after: INSERTION(function(nativeNode) {
42         return this._$.after(nativeNode);
43     }),
44
45     before: INSERTION(function(nativeNode) {
46         return this._$.before(nativeNode);
47     }),
48
49     wrapWith: function(node) {
50         var insertion = this.getNodeInsertion(node);
51         if(this.parent()) {
52             this.before(insertion.ofNode);
53         }
54         insertion.ofNode.append(this);
55         return insertion.ofNode;
56     },
57
58     /**
59     * Removes parent of a node if node has no siblings.
60     */
61     unwrap: function() {
62         if(this.isRoot()) {
63             return;
64         }
65         var parent = this.parent(),
66             grandParent;
67         if(parent.contents().length === 1) {
68             grandParent = parent.parent();
69             parent.unwrapContent();
70             return grandParent;
71         }
72     }
73 };
74
75 var elementNodeTransformations = {
76
77     detach: function() {
78         var next;
79         if(this.parent() && this.isSurroundedByTextElements()) {
80             next = this.next();
81             this.prev().appendText(next.getText());
82             next.detach();
83         }
84         return DocumentNode.prototype.detach.call(this);
85     },
86
87     setTag: function(tagName) {
88         var node = this.document.createDocumentNode({tagName: tagName}),
89             oldTagName = this.getTagName(),
90             myContents = this._$.contents();
91
92         this.getAttrs().forEach(function(attribute) {
93             node.setAttr(attribute.name, attribute.value, true);
94         });
95         node.setData(this.getData());
96
97         if(this.sameNode(this.document.root)) {
98             defineDocumentProperties(this.document, node._$);
99         }
100         this._$.replaceWith(node._$);
101         this._setNativeNode(node._$[0]);
102         this._$.append(myContents);
103         this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
104     },
105
106
107     setAttr: function(name, value, silent) {
108         var oldVal = this.getAttr(name);
109         this._$.attr(name, value);
110         if(!silent) {
111             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
112         }
113     },
114
115     append: INSERTION(function(nativeNode) {
116         this._$.append(nativeNode);
117     }),
118
119     prepend: INSERTION(function(nativeNode) {
120         this._$.prepend(nativeNode);
121     }),
122
123     insertAtIndex: function(nativeNode, index) {
124         var contents = this.contents();
125         if(index < contents.length) {
126             return contents[index].before(nativeNode);
127         } else if(index === contents.length) {
128             return this.append(nativeNode);
129         }
130     },
131
132     unwrapContent: function() {
133         var parent = this.parent();
134         if(!parent) {
135             return;
136         }
137
138         var myContents = this.contents(),
139             myIdx = parent.indexOf(this);
140
141
142         if(myContents.length === 0) {
143             return this.detach();
144         }
145
146         var prev = this.prev(),
147             next = this.next(),
148             moveLeftRange, moveRightRange, leftMerged;
149
150         if(prev && (prev.nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
151             prev.appendText(myContents[0].getText());
152             myContents[0].detach();
153             moveLeftRange = true;
154             leftMerged = true;
155         } else {
156             leftMerged = false;
157         }
158
159         if(!(leftMerged && myContents.length === 1)) {
160             var lastContents = _.last(myContents);
161             if(next && (next.nodeType === TEXT_NODE) && (lastContents.nodeType === TEXT_NODE)) {
162                 next.prependText(lastContents.getText());
163                 lastContents.detach();
164                 moveRightRange = true;
165             }
166         }
167
168         var childrenLength = this.contents().length;
169         this.contents().forEach(function(child) {
170             this.before(child);
171         }.bind(this));
172
173         this.detach();
174
175         return {
176             element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
177             element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
178         };
179     },
180
181     wrapText: function(params) {
182         return this.document._wrapText(_.extend({inside: this}, params));
183     }
184 };
185
186 var textNodeTransformations = {
187     setText: function(text) {
188         //console.log('smartxml: ' + text);
189         this.nativeNode.data = text;
190         this.triggerTextChangeEvent();
191     },
192
193     appendText: function(text) {
194         this.nativeNode.data = this.nativeNode.data + text;
195         this.triggerTextChangeEvent();
196     },
197
198     prependText: function(text) {
199         this.nativeNode.data = text + this.nativeNode.data;
200         this.triggerTextChangeEvent();
201     },
202
203     wrapWith: function(desc) {
204         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
205             return this.document._wrapText({
206                 inside: this.parent(),
207                 textNodeIdx: this.parent().indexOf(this),
208                 offsetStart: Math.min(desc.start, desc.end),
209                 offsetEnd: Math.max(desc.start, desc.end),
210                 _with: {tagName: desc.tagName, attrs: desc.attrs}
211             });
212         } else {
213             return DocumentNode.prototype.wrapWith.call(this, desc);
214         }
215     },
216
217     split: function(params) {
218         var parentElement = this.parent(),
219             passed = false,
220             succeedingChildren = [],
221             prefix = this.getText().substr(0, params.offset),
222             suffix = this.getText().substr(params.offset);
223
224         parentElement.contents().forEach(function(child) {
225             if(passed) {
226                 succeedingChildren.push(child);
227             }
228             if(child.sameNode(this)) {
229                 passed = true;
230             }
231         }.bind(this));
232
233         if(prefix.length > 0) {
234             this.setText(prefix);
235         }
236         else {
237             this.detach();
238         }
239
240         var attrs = {};
241         parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
242         var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
243         parentElement.after(newElement);
244
245         if(suffix.length > 0) {
246             newElement.append({text: suffix});
247         }
248         succeedingChildren.forEach(function(child) {
249             newElement.append(child);
250         });
251
252         return {first: parentElement, second: newElement};
253     }
254 };
255
256 var documentTransformations = {
257     wrapNodes: function(params) {
258         if(!(params.node1.parent().sameNode(params.node2.parent()))) {
259             throw new Error('Wrapping non-sibling nodes not supported.');
260         }
261
262         var parent = params.node1.parent(),
263             parentContents = parent.contents(),
264             wrapper = this.createDocumentNode({
265                 tagName: params._with.tagName,
266                 attrs: params._with.attrs}),
267             idx1 = parent.indexOf(params.node1),
268             idx2 = parent.indexOf(params.node2);
269
270         if(idx1 > idx2) {
271             var tmp = idx1;
272             idx1 = idx2;
273             idx2 = tmp;
274         }
275
276         var insertingMethod, insertingTarget;
277         if(idx1 === 0) {
278             insertingMethod = 'prepend';
279             insertingTarget = parent;
280         } else {
281             insertingMethod = 'after';
282             insertingTarget = parentContents[idx1-1];
283         }
284
285         for(var i = idx1; i <= idx2; i++) {
286             wrapper.append(parentContents[i].detach());
287         }
288
289         insertingTarget[insertingMethod](wrapper);
290         return wrapper;
291     },
292
293     _wrapText: function(params) {
294         params = _.extend({textNodeIdx: 0}, params);
295         if(typeof params.textNodeIdx === 'number') {
296             params.textNodeIdx = [params.textNodeIdx];
297         }
298         
299         var contentsInside = params.inside.contents(),
300             idx1 = Math.min.apply(Math, params.textNodeIdx),
301             idx2 = Math.max.apply(Math, params.textNodeIdx),
302             textNode1 = contentsInside[idx1],
303             textNode2 = contentsInside[idx2],
304             sameNode = textNode1.sameNode(textNode2),
305             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
306             prefixInside = textNode1.getText().substr(params.offsetStart),
307             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
308             suffixOutside = textNode2.getText().substr(params.offsetEnd)
309         ;
310
311         if(!(textNode1.parent().sameNode(textNode2.parent()))) {
312             throw new Error('Wrapping text in non-sibling text nodes not supported.');
313         }
314         
315         var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
316         textNode1.after(wrapperElement);
317         textNode1.detach();
318         
319         if(prefixOutside.length > 0) {
320             wrapperElement.before({text:prefixOutside});
321         }
322         if(sameNode) {
323             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
324             wrapperElement.append({text: core});
325         } else {
326             textNode2.detach();
327             if(prefixInside.length > 0) {
328                 wrapperElement.append({text: prefixInside});
329             }
330             for(var i = idx1 + 1; i < idx2; i++) {
331                 wrapperElement.append(contentsInside[i]);
332             }
333             if(suffixInside.length > 0) {
334                 wrapperElement.append({text: suffixInside});
335             }
336         }
337         if(suffixOutside.length > 0) {
338             wrapperElement.after({text: suffixOutside});
339         }
340         return wrapperElement;
341     },
342     replaceRoot: function(node) {
343         var insertion = this.getNodeInsertion(node);
344         this.root.detach();
345         defineDocumentProperties(this, insertion.ofNode._$);
346         insertion.ofNode.triggerChangeEvent('nodeAdded');
347         return insertion.ofNode;
348     }
349 }
350
351 return {
352     document: {
353         transformations: documentTransformations
354     },
355     documentNode: {
356         transformations: documentNodeTransformations
357     },
358     elementNode: {
359         transformations: elementNodeTransformations
360     },
361     textNode: {
362         transformations: textNodeTransformations
363     }
364 };
365
366 });