bac53af389e51d98dd0b2a4b31d7cb4d49b4a8a9
[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     getTagName: function() {
58         return this.nativeNode.tagName.toLowerCase();
59     },
60
61     contents: function() {
62         var toret = [],
63             document = this.document;
64         this._$.contents().each(function() {
65             if(this.nodeType === Node.ELEMENT_NODE) {
66                 toret.push(document.createElementNode(this));
67             }
68             else if(this.nodeType === Node.TEXT_NODE) {
69                 toret.push(document.createTextNode(this));
70             }
71         });
72         return toret;
73     },
74
75     indexOf: function(node) {
76         return this._$.contents().index(node._$);
77     },
78
79     getAttr: function(name) {
80         return this._$.attr(name);
81     },
82
83     setAttr: function(name, value) {
84         var oldVal = this.getAttr(name);
85         this._$.attr(name, value);
86         this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
87     },
88
89     getAttrs: function() {
90         var toret = [];
91         for(var i = 0; i < this.nativeNode.attributes.length; i++) {
92             toret.push(this.nativeNode.attributes[i]);
93         }
94         return toret;
95     },
96
97     append: function(documentNode) {
98         this._$.append(documentNode.nativeNode);
99     },
100
101     unwrapContent: function() {
102         var parent = this.parent();
103         if(!parent) {
104             return;
105         }
106
107         var parentContents = parent.contents(),
108             myContents = this.contents(),
109             myIdx = parent.indexOf(this);
110
111         if(myContents.length === 0) {
112             return this.detach();
113         }
114
115         var moveLeftRange, moveRightRange, leftMerged;
116
117         if(myIdx > 0 && (parentContents[myIdx-1].nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
118             parentContents[myIdx-1].appendText(myContents[0].getText());
119             myContents[0].detach();
120             moveLeftRange = true;
121             leftMerged = true;
122         } else {
123             leftMerged = false;
124         }
125
126         if(!(leftMerged && myContents.length === 1)) {
127             if(myIdx < parentContents.length - 1 && (parentContents[myIdx+1].nodeType === TEXT_NODE) && (myContents[myContents.length-1].nodeType === TEXT_NODE)) {
128                 parentContents[myIdx+1].prependText(myContents[myContents.length-1].getText());
129                 myContents[myContents.length-1].detach();
130                 moveRightRange = true;
131             }
132         }
133
134         var childrenLength = this.contents().length;
135         this.contents().forEach(function(child) {
136             this.before(child);
137         }.bind(this));
138
139         this.detach();
140
141         return {
142             element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
143             element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
144         };
145     },
146
147     toXML: function() {
148         var wrapper = $('<div>');
149         wrapper.append(this._$);
150         return wrapper.html();
151     }
152 });
153
154 var TextNode = function(nativeNode, document) {
155     DocumentNode.call(this, nativeNode, document);
156 };
157
158 $.extend(TextNode.prototype, DocumentNode.prototype, {
159     nodeType: Node.TEXT_NODE,
160
161     getText: function() {
162         return this.nativeNode.data;
163     },
164
165     appendText: function(text) {
166         this.nativeNode.data = this.nativeNode.data + text;
167     },
168
169     prependText: function(text) {
170         this.nativeNode.data = text + this.nativeNode.data;
171     }
172 });
173
174
175 var parseXML = function(xml) {
176     return $(xml)[0];
177 };
178
179 var Document = function(xml) {
180     var $document = $(parseXML(xml));
181
182     var doc = this;
183     Object.defineProperty(this, 'root', {get: function() {
184         return doc.createElementNode($document[0]);
185     }});
186     Object.defineProperty(this, 'dom', {get: function() {
187         return $document[0];
188     }});
189 };
190 $.extend(Document.prototype, Backbone.Events, {
191     ElementNodeFactory: ElementNode,
192     TextNodeFactory: TextNode,
193
194     createElementNode: function(nativeNode) {
195         return new this.ElementNodeFactory(nativeNode, this);
196     },
197
198     createTextNode: function(nativeNode) {
199         return new this.TextNodeFactory(nativeNode, this);
200     },
201
202     toXML: function() {
203         return this.root.toXML();
204     }
205 });
206
207
208 return {
209     documentFromXML: function(xml) {
210         return new Document(parseXML(xml));
211     },
212
213     elementNodeFromXML: function(xml) {
214         return this.documentFromXML(xml).root;
215     },
216
217     Document: Document,
218     DocumentNode: DocumentNode,
219     ElementNode: ElementNode
220 };
221
222 });