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