6 ], function($, _, Backbone, events) {
11 var TEXT_NODE = Node.TEXT_NODE;
14 var DocumentNode = function(nativeNode, document) {
16 throw new Error('undefined document for a node');
18 this.document = document;
19 this._setNativeNode(nativeNode);
23 $.extend(DocumentNode.prototype, {
24 _setNativeNode: function(nativeNode) {
25 this.nativeNode = nativeNode;
26 this._$ = $(nativeNode);
34 sameNode: function(otherNode) {
35 return otherNode && this.nativeNode === otherNode.nativeNode;
39 return this.nativeNode.parentNode ? this.document.createElementNode(this.nativeNode.parentNode) : null;
42 after: function(node) {
43 var insertion = this.getNodeInsertion(node);
44 this._$.after(insertion.ofNode.nativeNode);
45 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode});
46 return insertion.ofNode;
49 before: function(node) {
50 var insertion = this.getNodeInsertion(node);
51 this._$.before(insertion.ofNode.nativeNode);
52 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode});
53 return insertion.ofNode;
56 wrapWith: function(node) {
57 node = node instanceof ElementNode ? node : this.document.createElementNode(node);
66 triggerChangeEvent: function(type, metaData) {
67 var event = new events.ChangeEvent(type, $.extend({node: this}, metaData || {}));
68 this.document.trigger('change', event);
71 getNodeInsertion: function(node) {
73 if(node instanceof DocumentNode) {
74 insertion.ofNode = node;
75 insertion.insertsNew = !this.document.containsNode(node);
77 insertion.ofNode = this.document.createElementNode(node);
78 insertion.insertsNew = true;
84 var ElementNode = function(nativeNode, document) {
85 DocumentNode.call(this, nativeNode, document);
87 ElementNode.prototype = Object.create(DocumentNode.prototype);
89 $.extend(ElementNode.prototype, {
90 nodeType: Node.ELEMENT_NODE,
92 setData: function(key, value) {
93 if(value !== undefined) {
94 this._$.data(key, value);
96 this._$.removeData(_.keys(this._$.data()));
101 getData: function(key) {
103 return this._$.data(key);
105 return this._$.data();
108 getTagName: function() {
109 return this.nativeNode.tagName.toLowerCase();
112 contents: function() {
114 document = this.document;
115 this._$.contents().each(function() {
116 if(this.nodeType === Node.ELEMENT_NODE) {
117 toret.push(document.createElementNode(this));
119 else if(this.nodeType === Node.TEXT_NODE) {
120 toret.push(document.createTextNode(this));
126 indexOf: function(node) {
127 return this._$.contents().index(node._$);
130 setTag: function(tagName) {
131 var node = this.document.createElementNode({tagName: tagName}),
132 oldTagName = this.getTagName(),
133 myContents = this._$.contents();
135 this.getAttrs().forEach(function(attribute) {
136 node.setAttr(attribute.name, attribute.value, true);
138 node.setData(this.getData());
140 if(this.sameNode(this.document.root)) {
141 defineDocumentProperties(this.document, node._$);
143 this._$.replaceWith(node._$);
144 this._setNativeNode(node._$[0]);
145 this._$.append(myContents);
146 this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
149 getAttr: function(name) {
150 return this._$.attr(name);
153 setAttr: function(name, value, silent) {
154 var oldVal = this.getAttr(name);
155 this._$.attr(name, value);
157 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
161 getAttrs: function() {
163 for(var i = 0; i < this.nativeNode.attributes.length; i++) {
164 toret.push(this.nativeNode.attributes[i]);
169 append: function(node) {
170 var insertion = this.getNodeInsertion(node);
171 this._$.append(insertion.ofNode.nativeNode);
172 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode});
173 return insertion.ofNode;
176 prepend: function(node) {
177 var insertion = this.getNodeInsertion(node);
178 this._$.prepend(insertion.ofNode.nativeNode);
179 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode});
180 return insertion.ofNode;
183 unwrapContent: function() {
184 var parent = this.parent();
189 var parentContents = parent.contents(),
190 myContents = this.contents(),
191 myIdx = parent.indexOf(this);
193 if(myContents.length === 0) {
194 return this.detach();
197 var moveLeftRange, moveRightRange, leftMerged;
199 if(myIdx > 0 && (parentContents[myIdx-1].nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
200 parentContents[myIdx-1].appendText(myContents[0].getText());
201 myContents[0].detach();
202 moveLeftRange = true;
208 if(!(leftMerged && myContents.length === 1)) {
209 if(myIdx < parentContents.length - 1 && (parentContents[myIdx+1].nodeType === TEXT_NODE) && (myContents[myContents.length-1].nodeType === TEXT_NODE)) {
210 parentContents[myIdx+1].prependText(myContents[myContents.length-1].getText());
211 myContents[myContents.length-1].detach();
212 moveRightRange = true;
216 var childrenLength = this.contents().length;
217 this.contents().forEach(function(child) {
224 element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
225 element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
229 wrapText: function(params) {
230 return this.document._wrapText(_.extend({inside: this}, params));
234 var wrapper = $('<div>');
235 wrapper.append(this._getXMLDOMToDump());
236 return wrapper.html();
239 _getXMLDOMToDump: function() {
244 var TextNode = function(nativeNode, document) {
245 DocumentNode.call(this, nativeNode, document);
247 TextNode.prototype = Object.create(DocumentNode.prototype);
249 $.extend(TextNode.prototype, {
250 nodeType: Node.TEXT_NODE,
252 getText: function() {
253 return this.nativeNode.data;
256 setText: function(text) {
257 this.nativeNode.data = text;
258 this.triggerTextChangeEvent();
261 appendText: function(text) {
262 this.nativeNode.data = this.nativeNode.data + text;
263 this.triggerTextChangeEvent();
266 prependText: function(text) {
267 this.nativeNode.data = text + this.nativeNode.data;
268 this.triggerTextChangeEvent();
271 wrapWith: function(desc) {
272 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
273 return this.document._wrapText({
274 inside: this.parent(),
275 textNodeIdx: this.parent().indexOf(this),
276 offsetStart: Math.min(desc.start, desc.end),
277 offsetEnd: Math.max(desc.start, desc.end),
278 _with: {tag: desc.tagName, attrs: desc.attrs}
281 return DocumentNode.prototype.wrapWith.call(this, desc);
285 triggerTextChangeEvent: function() {
286 var event = new events.ChangeEvent('nodeTextChange', {node: this});
287 this.document.trigger('change', event);
292 var parseXML = function(xml) {
296 var Document = function(xml) {
300 $.extend(Document.prototype, Backbone.Events, {
301 ElementNodeFactory: ElementNode,
302 TextNodeFactory: TextNode,
304 createElementNode: function(from) {
305 if(!(from instanceof HTMLElement)) {
307 from = document.createTextNode(from.text);
309 var node = $('<' + from.tagName + '>');
311 _.keys(from.attrs || {}).forEach(function(key) {
312 node.attr(key, from.attrs[key]);
318 return new this.ElementNodeFactory(from, this);
321 createTextNode: function(nativeNode) {
322 return new this.TextNodeFactory(nativeNode, this);
325 loadXML: function(xml, options) {
326 options = options || {};
327 defineDocumentProperties(this, $(parseXML(xml)));
328 if(!options.silent) {
329 this.trigger('contentSet');
334 return this.root.toXML();
337 containsNode: function(node) {
338 return node._$.parents().index(this.root._$) !== -1;
341 wrapNodes: function(params) {
342 if(!(params.element1.parent().sameNode(params.element2.parent()))) {
343 throw new Error('Wrapping non-sibling nodes not supported.');
346 var parent = params.element1.parent(),
347 parentContents = parent.contents(),
348 wrapper = this.createElementNode({
349 tagName: params._with.tagName,
350 attrs: params._with.attrs}),
351 idx1 = parent.indexOf(params.element1),
352 idx2 = parent.indexOf(params.element2);
360 var insertingMethod, insertingTarget;
362 insertingMethod = 'prepend';
363 insertingTarget = parent;
365 insertingMethod = 'after';
366 insertingTarget = parentContents[idx1-1];
369 for(var i = idx1; i <= idx2; i++) {
370 wrapper.append(parentContents[i].detach());
373 insertingTarget[insertingMethod](wrapper);
377 _wrapText: function(params) {
378 params = _.extend({textNodeIdx: 0}, params);
379 if(typeof params.textNodeIdx === 'number') {
380 params.textNodeIdx = [params.textNodeIdx];
383 var contentsInside = params.inside.contents(),
384 idx1 = Math.min.apply(Math, params.textNodeIdx),
385 idx2 = Math.max.apply(Math, params.textNodeIdx),
386 textNode1 = contentsInside[idx1],
387 textNode2 = contentsInside[idx2],
388 sameNode = textNode1.sameNode(textNode2),
389 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
390 prefixInside = textNode1.getText().substr(params.offsetStart),
391 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
392 suffixOutside = textNode2.getText().substr(params.offsetEnd)
395 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
396 throw new Error('Wrapping text in non-sibling text nodes not supported.');
399 var wrapperElement = this.createElementNode({tagName: params._with.tag, attrs: params._with.attrs});
400 textNode1.after(wrapperElement);
403 if(prefixOutside.length > 0) {
404 wrapperElement.before({text:prefixOutside});
407 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
408 wrapperElement.append({text: core});
411 if(prefixInside.length > 0) {
412 wrapperElement.append({text: prefixInside});
414 for(var i = idx1 + 1; i < idx2; i++) {
415 wrapperElement.append(contentsInside[i]);
417 if(suffixInside.length > 0) {
418 wrapperElement.append({text: suffixInside});
421 if(suffixOutside.length > 0) {
422 wrapperElement.after({text: suffixOutside});
424 return wrapperElement;
428 var defineDocumentProperties = function(doc, $document) {
429 Object.defineProperty(doc, 'root', {get: function() {
430 return doc.createElementNode($document[0]);
431 }, configurable: true});
432 Object.defineProperty(doc, 'dom', {get: function() {
434 }, configurable: true});
438 documentFromXML: function(xml) {
439 return new Document(parseXML(xml));
442 elementNodeFromXML: function(xml) {
443 return this.documentFromXML(xml).root;
447 DocumentNode: DocumentNode,
448 ElementNode: ElementNode