smartxml: wrapping TextNode content
[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     before: function(node) {
40         this._$.before(node.nativeNode);
41     },
42
43     wrapWith: function(node) {
44         node = node instanceof ElementNode ? node : this.document.createElementNode(node);
45
46         if(this.parent()) {
47             this.before(node);
48         }
49         node.append(this);
50         return node;
51     },
52
53     triggerChangeEvent: function(type, metaData) {
54         var event = new events.ChangeEvent(type, $.extend({node: this}, metaData || {}));
55         this.document.trigger('change', event);
56     },
57 });
58
59 var ElementNode = function(nativeNode, document) {
60     DocumentNode.call(this, nativeNode, document);
61 };
62
63 $.extend(ElementNode.prototype, DocumentNode.prototype, {
64     nodeType: Node.ELEMENT_NODE,
65
66     setData: function(key, value) {
67         if(value !== undefined) {
68             this._$.data(key, value);
69         } else {
70             this._$.removeData(_.keys(this._$.data()));
71             this._$.data(key);
72         }
73     },
74
75     getData: function(key) {
76         if(key) {
77             return this._$.data(key);
78         }
79         return this._$.data();
80     },
81
82     getTagName: function() {
83         return this.nativeNode.tagName.toLowerCase();
84     },
85
86     contents: function() {
87         var toret = [],
88             document = this.document;
89         this._$.contents().each(function() {
90             if(this.nodeType === Node.ELEMENT_NODE) {
91                 toret.push(document.createElementNode(this));
92             }
93             else if(this.nodeType === Node.TEXT_NODE) {
94                 toret.push(document.createTextNode(this));
95             }
96         });
97         return toret;
98     },
99
100     indexOf: function(node) {
101         return this._$.contents().index(node._$);
102     },
103
104     setTag: function(tagName) {
105         var node = this.document.createElementNode({tagName: tagName}),
106             oldTagName = this.getTagName(),
107             myContents = this._$.contents();
108
109         this.getAttrs().forEach(function(attribute) {
110             node.setAttr(attribute.name, attribute.value, true);
111         });
112         node.setData(this.getData());
113
114         if(this.sameNode(this.document.root)) {
115             defineDocumentProperties(this.document, node._$);
116         }
117         this._$.replaceWith(node._$);
118         this._setNativeNode(node._$[0]);
119         this._$.append(myContents);
120         this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
121     },
122
123     getAttr: function(name) {
124         return this._$.attr(name);
125     },
126
127     setAttr: function(name, value, silent) {
128         var oldVal = this.getAttr(name);
129         this._$.attr(name, value);
130         if(!silent) {
131             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
132         }
133     },
134
135     getAttrs: function() {
136         var toret = [];
137         for(var i = 0; i < this.nativeNode.attributes.length; i++) {
138             toret.push(this.nativeNode.attributes[i]);
139         }
140         return toret;
141     },
142
143     append: function(documentNode) {
144         this._$.append(documentNode.nativeNode);
145     },
146
147     unwrapContent: function() {
148         var parent = this.parent();
149         if(!parent) {
150             return;
151         }
152
153         var parentContents = parent.contents(),
154             myContents = this.contents(),
155             myIdx = parent.indexOf(this);
156
157         if(myContents.length === 0) {
158             return this.detach();
159         }
160
161         var moveLeftRange, moveRightRange, leftMerged;
162
163         if(myIdx > 0 && (parentContents[myIdx-1].nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
164             parentContents[myIdx-1].appendText(myContents[0].getText());
165             myContents[0].detach();
166             moveLeftRange = true;
167             leftMerged = true;
168         } else {
169             leftMerged = false;
170         }
171
172         if(!(leftMerged && myContents.length === 1)) {
173             if(myIdx < parentContents.length - 1 && (parentContents[myIdx+1].nodeType === TEXT_NODE) && (myContents[myContents.length-1].nodeType === TEXT_NODE)) {
174                 parentContents[myIdx+1].prependText(myContents[myContents.length-1].getText());
175                 myContents[myContents.length-1].detach();
176                 moveRightRange = true;
177             }
178         }
179
180         var childrenLength = this.contents().length;
181         this.contents().forEach(function(child) {
182             this.before(child);
183         }.bind(this));
184
185         this.detach();
186
187         return {
188             element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
189             element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
190         };
191     },
192
193     toXML: function() {
194         var wrapper = $('<div>');
195         wrapper.append(this._getXMLDOMToDump());
196         return wrapper.html();
197     },
198     
199     _getXMLDOMToDump: function() {
200         return this._$;
201     }
202 });
203
204 var TextNode = function(nativeNode, document) {
205     DocumentNode.call(this, nativeNode, document);
206 };
207
208 $.extend(TextNode.prototype, DocumentNode.prototype, {
209     nodeType: Node.TEXT_NODE,
210
211     getText: function() {
212         return this.nativeNode.data;
213     },
214
215     setText: function(text) {
216         this.nativeNode.data = text;
217         this.triggerTextChangeEvent();
218     },
219
220     appendText: function(text) {
221         this.nativeNode.data = this.nativeNode.data + text;
222         this.triggerTextChangeEvent();
223     },
224
225     prependText: function(text) {
226         this.nativeNode.data = text + this.nativeNode.data;
227         this.triggerTextChangeEvent();
228     },
229
230     triggerTextChangeEvent: function() {
231         var event = new events.ChangeEvent('nodeTextChange', {node: this});
232         this.document.trigger('change', event);
233     }
234 });
235
236
237 var parseXML = function(xml) {
238     return $(xml)[0];
239 };
240
241 var Document = function(xml) {
242     this.loadXML(xml);
243 };
244
245 $.extend(Document.prototype, Backbone.Events, {
246     ElementNodeFactory: ElementNode,
247     TextNodeFactory: TextNode,
248
249     createElementNode: function(from) {
250         if(!(from instanceof HTMLElement)) {
251             from = $('<' + from.tagName + '>')[0];
252         }
253         return new this.ElementNodeFactory(from, this);
254     },
255
256     createTextNode: function(nativeNode) {
257         return new this.TextNodeFactory(nativeNode, this);
258     },
259
260     loadXML: function(xml, options) {
261         options = options || {};
262         defineDocumentProperties(this, $(parseXML(xml)));
263         if(!options.silent) {
264             this.trigger('contentSet');
265         }
266     },
267
268     toXML: function() {
269         return this.root.toXML();
270     }
271 });
272
273 var defineDocumentProperties = function(doc, $document) {
274     Object.defineProperty(doc, 'root', {get: function() {
275         return doc.createElementNode($document[0]);
276     }, configurable: true});
277     Object.defineProperty(doc, 'dom', {get: function() {
278         return $document[0];
279     }, configurable: true});
280 };
281
282 return {
283     documentFromXML: function(xml) {
284         return new Document(parseXML(xml));
285     },
286
287     elementNodeFromXML: function(xml) {
288         return this.documentFromXML(xml).root;
289     },
290
291     Document: Document,
292     DocumentNode: DocumentNode,
293     ElementNode: ElementNode
294 };
295
296 });