smartxml: Improving get/set data api
[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.nativeNode = nativeNode;
20     this._$ = $(nativeNode);
21 };
22
23 $.extend(DocumentNode.prototype, {
24     detach: function() { this._$.detach(); },
25
26     sameNode: function(otherNode) {
27         return this.nativeNode === otherNode.nativeNode;
28     },
29
30     parent: function() {
31         return this.nativeNode.parentNode ? this.document.createElementNode(this.nativeNode.parentNode) : null;
32     },
33
34     before: function(node) {
35         this._$.before(node.nativeNode);
36     },
37
38     wrapWith: function(node) {
39         if(this.parent()) {
40             this.before(node);
41         }
42         node.append(this);
43     },
44
45     triggerChangeEvent: function(type, metaData) {
46         var event = new events.ChangeEvent(type, $.extend({node: this}, metaData || {}));
47         this.document.trigger('change', event);
48     },
49 });
50
51 var ElementNode = function(nativeNode, document) {
52     DocumentNode.call(this, nativeNode, document);
53 };
54
55 $.extend(ElementNode.prototype, DocumentNode.prototype, {
56     nodeType: Node.ELEMENT_NODE,
57
58     setData: function(key, value) {
59         if(value !== undefined) {
60             this._$.data(key, value);
61         } else {
62             this._$.removeData(_.keys(this._$.data()));
63             this._$.data(key);
64         }
65     },
66
67     getData: function(key) {
68         if(key) {
69             return this._$.data(key);
70         }
71         return this._$.data();
72     },
73
74     getTagName: function() {
75         return this.nativeNode.tagName.toLowerCase();
76     },
77
78     contents: function() {
79         var toret = [],
80             document = this.document;
81         this._$.contents().each(function() {
82             if(this.nodeType === Node.ELEMENT_NODE) {
83                 toret.push(document.createElementNode(this));
84             }
85             else if(this.nodeType === Node.TEXT_NODE) {
86                 toret.push(document.createTextNode(this));
87             }
88         });
89         return toret;
90     },
91
92     indexOf: function(node) {
93         return this._$.contents().index(node._$);
94     },
95
96     getAttr: function(name) {
97         return this._$.attr(name);
98     },
99
100     setAttr: function(name, value) {
101         var oldVal = this.getAttr(name);
102         this._$.attr(name, value);
103         this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
104     },
105
106     getAttrs: function() {
107         var toret = [];
108         for(var i = 0; i < this.nativeNode.attributes.length; i++) {
109             toret.push(this.nativeNode.attributes[i]);
110         }
111         return toret;
112     },
113
114     append: function(documentNode) {
115         this._$.append(documentNode.nativeNode);
116     },
117
118     unwrapContent: function() {
119         var parent = this.parent();
120         if(!parent) {
121             return;
122         }
123
124         var parentContents = parent.contents(),
125             myContents = this.contents(),
126             myIdx = parent.indexOf(this);
127
128         if(myContents.length === 0) {
129             return this.detach();
130         }
131
132         var moveLeftRange, moveRightRange, leftMerged;
133
134         if(myIdx > 0 && (parentContents[myIdx-1].nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
135             parentContents[myIdx-1].appendText(myContents[0].getText());
136             myContents[0].detach();
137             moveLeftRange = true;
138             leftMerged = true;
139         } else {
140             leftMerged = false;
141         }
142
143         if(!(leftMerged && myContents.length === 1)) {
144             if(myIdx < parentContents.length - 1 && (parentContents[myIdx+1].nodeType === TEXT_NODE) && (myContents[myContents.length-1].nodeType === TEXT_NODE)) {
145                 parentContents[myIdx+1].prependText(myContents[myContents.length-1].getText());
146                 myContents[myContents.length-1].detach();
147                 moveRightRange = true;
148             }
149         }
150
151         var childrenLength = this.contents().length;
152         this.contents().forEach(function(child) {
153             this.before(child);
154         }.bind(this));
155
156         this.detach();
157
158         return {
159             element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
160             element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
161         };
162     },
163
164     toXML: function() {
165         var wrapper = $('<div>');
166         wrapper.append(this._$);
167         return wrapper.html();
168     }
169 });
170
171 var TextNode = function(nativeNode, document) {
172     DocumentNode.call(this, nativeNode, document);
173 };
174
175 $.extend(TextNode.prototype, DocumentNode.prototype, {
176     nodeType: Node.TEXT_NODE,
177
178     getText: function() {
179         return this.nativeNode.data;
180     },
181
182     appendText: function(text) {
183         this.nativeNode.data = this.nativeNode.data + text;
184     },
185
186     prependText: function(text) {
187         this.nativeNode.data = text + this.nativeNode.data;
188     }
189 });
190
191
192 var parseXML = function(xml) {
193     return $(xml)[0];
194 };
195
196 var Document = function(xml) {
197     var $document = $(parseXML(xml));
198
199     var doc = this;
200     Object.defineProperty(this, 'root', {get: function() {
201         return doc.createElementNode($document[0]);
202     }});
203     Object.defineProperty(this, 'dom', {get: function() {
204         return $document[0];
205     }});
206 };
207 $.extend(Document.prototype, Backbone.Events, {
208     ElementNodeFactory: ElementNode,
209     TextNodeFactory: TextNode,
210
211     createElementNode: function(nativeNode) {
212         return new this.ElementNodeFactory(nativeNode, this);
213     },
214
215     createTextNode: function(nativeNode) {
216         return new this.TextNodeFactory(nativeNode, this);
217     },
218
219     toXML: function() {
220         return this.root.toXML();
221     }
222 });
223
224
225 return {
226     documentFromXML: function(xml) {
227         return new Document(parseXML(xml));
228     },
229
230     elementNodeFromXML: function(xml) {
231         return this.documentFromXML(xml).root;
232     },
233
234     Document: Document,
235     DocumentNode: DocumentNode,
236     ElementNode: ElementNode
237 };
238
239 });