d8b6bf03a1c19a75504902642815548248adf440
[fnpeditor.git] / src / smartxml / smartxml.js
1 define([
2     'libs/jquery',
3     'libs/underscore',
4     'libs/backbone',
5     'smartxml/events'
6 ], function($, _, Backbone, events) {
7     
8 'use strict';
9
10
11 var TEXT_NODE = Node.TEXT_NODE;
12
13
14 var DocumentNode = function(nativeNode, document) {
15     if(!document) {
16         throw new Error('undefined document for a node');
17     }
18     this.document = document;
19     this._setNativeNode(nativeNode);
20
21 };
22
23 $.extend(DocumentNode.prototype, {
24     _setNativeNode: function(nativeNode) {
25         this.nativeNode = nativeNode;
26         this._$ = $(nativeNode);
27     },
28
29     detach: function() { this._$.detach(); },
30
31     sameNode: function(otherNode) {
32         return otherNode && this.nativeNode === otherNode.nativeNode;
33     },
34
35     parent: function() {
36         return this.nativeNode.parentNode ? this.document.createElementNode(this.nativeNode.parentNode) : null;
37     },
38
39     after: function(node) {
40         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
41         this._$.after(node.nativeNode);
42         return node;
43     },
44
45     before: function(node) {
46         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
47         this._$.before(node.nativeNode);
48         return node;
49     },
50
51     wrapWith: function(node) {
52         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
53
54         if(this.parent()) {
55             this.before(node);
56         }
57         node.append(this);
58         return node;
59     },
60
61     triggerChangeEvent: function(type, metaData) {
62         var event = new events.ChangeEvent(type, $.extend({node: this}, metaData || {}));
63         this.document.trigger('change', event);
64     },
65 });
66
67 var ElementNode = function(nativeNode, document) {
68     DocumentNode.call(this, nativeNode, document);
69 };
70 ElementNode.prototype = Object.create(DocumentNode.prototype);
71
72 $.extend(ElementNode.prototype, {
73     nodeType: Node.ELEMENT_NODE,
74
75     setData: function(key, value) {
76         if(value !== undefined) {
77             this._$.data(key, value);
78         } else {
79             this._$.removeData(_.keys(this._$.data()));
80             this._$.data(key);
81         }
82     },
83
84     getData: function(key) {
85         if(key) {
86             return this._$.data(key);
87         }
88         return this._$.data();
89     },
90
91     getTagName: function() {
92         return this.nativeNode.tagName.toLowerCase();
93     },
94
95     contents: function() {
96         var toret = [],
97             document = this.document;
98         this._$.contents().each(function() {
99             if(this.nodeType === Node.ELEMENT_NODE) {
100                 toret.push(document.createElementNode(this));
101             }
102             else if(this.nodeType === Node.TEXT_NODE) {
103                 toret.push(document.createTextNode(this));
104             }
105         });
106         return toret;
107     },
108
109     indexOf: function(node) {
110         return this._$.contents().index(node._$);
111     },
112
113     setTag: function(tagName) {
114         var node = this.document.createElementNode({tagName: tagName}),
115             oldTagName = this.getTagName(),
116             myContents = this._$.contents();
117
118         this.getAttrs().forEach(function(attribute) {
119             node.setAttr(attribute.name, attribute.value, true);
120         });
121         node.setData(this.getData());
122
123         if(this.sameNode(this.document.root)) {
124             defineDocumentProperties(this.document, node._$);
125         }
126         this._$.replaceWith(node._$);
127         this._setNativeNode(node._$[0]);
128         this._$.append(myContents);
129         this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
130     },
131
132     getAttr: function(name) {
133         return this._$.attr(name);
134     },
135
136     setAttr: function(name, value, silent) {
137         var oldVal = this.getAttr(name);
138         this._$.attr(name, value);
139         if(!silent) {
140             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
141         }
142     },
143
144     getAttrs: function() {
145         var toret = [];
146         for(var i = 0; i < this.nativeNode.attributes.length; i++) {
147             toret.push(this.nativeNode.attributes[i]);
148         }
149         return toret;
150     },
151
152     append: function(node) {
153         node = node instanceof DocumentNode ? node : this.document.createElementNode(node);
154         this._$.append(node.nativeNode);
155     },
156
157     unwrapContent: function() {
158         var parent = this.parent();
159         if(!parent) {
160             return;
161         }
162
163         var parentContents = parent.contents(),
164             myContents = this.contents(),
165             myIdx = parent.indexOf(this);
166
167         if(myContents.length === 0) {
168             return this.detach();
169         }
170
171         var moveLeftRange, moveRightRange, leftMerged;
172
173         if(myIdx > 0 && (parentContents[myIdx-1].nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
174             parentContents[myIdx-1].appendText(myContents[0].getText());
175             myContents[0].detach();
176             moveLeftRange = true;
177             leftMerged = true;
178         } else {
179             leftMerged = false;
180         }
181
182         if(!(leftMerged && myContents.length === 1)) {
183             if(myIdx < parentContents.length - 1 && (parentContents[myIdx+1].nodeType === TEXT_NODE) && (myContents[myContents.length-1].nodeType === TEXT_NODE)) {
184                 parentContents[myIdx+1].prependText(myContents[myContents.length-1].getText());
185                 myContents[myContents.length-1].detach();
186                 moveRightRange = true;
187             }
188         }
189
190         var childrenLength = this.contents().length;
191         this.contents().forEach(function(child) {
192             this.before(child);
193         }.bind(this));
194
195         this.detach();
196
197         return {
198             element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
199             element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
200         };
201     },
202
203     toXML: function() {
204         var wrapper = $('<div>');
205         wrapper.append(this._getXMLDOMToDump());
206         return wrapper.html();
207     },
208     
209     _getXMLDOMToDump: function() {
210         return this._$;
211     }
212 });
213
214 var TextNode = function(nativeNode, document) {
215     DocumentNode.call(this, nativeNode, document);
216 };
217 TextNode.prototype = Object.create(DocumentNode.prototype);
218
219 $.extend(TextNode.prototype, {
220     nodeType: Node.TEXT_NODE,
221
222     getText: function() {
223         return this.nativeNode.data;
224     },
225
226     setText: function(text) {
227         this.nativeNode.data = text;
228         this.triggerTextChangeEvent();
229     },
230
231     appendText: function(text) {
232         this.nativeNode.data = this.nativeNode.data + text;
233         this.triggerTextChangeEvent();
234     },
235
236     prependText: function(text) {
237         this.nativeNode.data = text + this.nativeNode.data;
238         this.triggerTextChangeEvent();
239     },
240
241     wrapWith: function(desc) {
242         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
243             return this.document._wrapText({
244                 inside: this.parent(),
245                 textNodeIdx: this.parent().indexOf(this),
246                 offsetStart: Math.min(desc.start, desc.end),
247                 offsetEnd: Math.max(desc.start, desc.end),
248                 _with: {tag: desc.tagName, attrs: desc.attrs}
249             });
250         } else {
251             return DocumentNode.prototype.wrapWith.call(this, desc);
252         }
253     },
254
255     triggerTextChangeEvent: function() {
256         var event = new events.ChangeEvent('nodeTextChange', {node: this});
257         this.document.trigger('change', event);
258     }
259 });
260
261
262 var parseXML = function(xml) {
263     return $(xml)[0];
264 };
265
266 var Document = function(xml) {
267     this.loadXML(xml);
268 };
269
270 $.extend(Document.prototype, Backbone.Events, {
271     ElementNodeFactory: ElementNode,
272     TextNodeFactory: TextNode,
273
274     createElementNode: function(from) {
275         if(!(from instanceof HTMLElement)) {
276             if(from.text) {
277                 from = document.createTextNode(from.text);
278             } else {
279                 var node = $('<' + from.tagName + '>');
280
281                 _.keys(from.attrs || {}).forEach(function(key) {
282                     node.attr(key, from.attrs[key]);
283                 });
284
285                 from = node[0];
286             }
287         }
288         return new this.ElementNodeFactory(from, this);
289     },
290
291     createTextNode: function(nativeNode) {
292         return new this.TextNodeFactory(nativeNode, this);
293     },
294
295     loadXML: function(xml, options) {
296         options = options || {};
297         defineDocumentProperties(this, $(parseXML(xml)));
298         if(!options.silent) {
299             this.trigger('contentSet');
300         }
301     },
302
303     toXML: function() {
304         return this.root.toXML();
305     },
306
307     _wrapText: function(params) {
308         params = _.extend({textNodeIdx: 0}, params);
309         if(typeof params.textNodeIdx === 'number') {
310             params.textNodeIdx = [params.textNodeIdx];
311         }
312         
313         var contentsInside = params.inside.contents(),
314             idx1 = Math.min.apply(Math, params.textNodeIdx),
315             idx2 = Math.max.apply(Math, params.textNodeIdx),
316             textNode1 = contentsInside[idx1],
317             textNode2 = contentsInside[idx2],
318             sameNode = textNode1.sameNode(textNode2),
319             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
320             prefixInside = textNode1.getText().substr(params.offsetStart),
321             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
322             suffixOutside = textNode2.getText().substr(params.offsetEnd)
323         ;
324         
325         var wrapperElement = this.createElementNode({tagName: params._with.tag, attrs: params._with.attrs});
326         textNode1.after(wrapperElement);
327         textNode1.detach();
328         
329         if(prefixOutside.length > 0) {
330             wrapperElement.before({text:prefixOutside});
331         }
332         if(sameNode) {
333             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
334             wrapperElement.append({text: core});
335         } else {
336             textNode2.detach();
337             if(prefixInside.length > 0) {
338                 wrapperElement.append({text: prefixInside});
339             }
340             for(var i = idx1 + 1; i < idx2; i++) {
341                 wrapperElement.append(contentsInside[i]);
342             }
343             if(suffixInside.length > 0) {
344                 wrapperElement.append({text: suffixInside});
345             }
346         }
347         if(suffixOutside.length > 0) {
348             wrapperElement.after({text: suffixOutside});
349         }
350         return wrapperElement;
351     }
352 });
353
354 var defineDocumentProperties = function(doc, $document) {
355     Object.defineProperty(doc, 'root', {get: function() {
356         return doc.createElementNode($document[0]);
357     }, configurable: true});
358     Object.defineProperty(doc, 'dom', {get: function() {
359         return $document[0];
360     }, configurable: true});
361 };
362
363 return {
364     documentFromXML: function(xml) {
365         return new Document(parseXML(xml));
366     },
367
368     elementNodeFromXML: function(xml) {
369         return this.documentFromXML(xml).root;
370     },
371
372     Document: Document,
373     DocumentNode: DocumentNode,
374     ElementNode: ElementNode
375 };
376
377 });