1 define(function(require) {
6 var _ = require('libs/underscore'),
7 TEXT_NODE = Node.TEXT_NODE;
10 var INSERTION = function(implementation) {
11 var toret = function(node, options) {
12 var insertion = this.getNodeInsertion(node),
13 nodeWasContained = this.document.containsNode(insertion.ofNode),
16 options = options || {};
17 if(!(this.document.containsNode(this))) {
18 nodeParent = insertion.ofNode.parent();
20 returned = implementation.call(this, insertion.ofNode);
21 if(!options.silent && returned.sameNode(insertion.ofNode)) {
22 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
29 var documentNodeTransformations = {
31 var parent = this.parent();
33 this.triggerChangeEvent('nodeDetached', {parent: parent});
37 replaceWith: function(node) {
40 return this.document.replaceRoot(node);
42 toret = this.after(node);
47 after: INSERTION(function(node) {
48 var next = this.next();
49 if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
50 next.setText(node.getText() + next.getText());
53 this._$.after(node.nativeNode);
57 before: INSERTION(function(node) {
58 var prev = this.prev();
59 if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
60 prev.setText(prev.getText() + node.getText());
63 this._$.before(node.nativeNode);
67 wrapWith: function(node) {
68 var insertion = this.getNodeInsertion(node);
70 this.before(insertion.ofNode);
72 insertion.ofNode.append(this);
73 return insertion.ofNode;
77 * Removes parent of a node if node has no siblings.
83 var parent = this.parent(),
85 if(parent.contents().length === 1) {
86 grandParent = parent.parent();
87 parent.unwrapContent();
93 var elementNodeTransformations = {
97 if(this.parent() && this.isSurroundedByTextElements()) {
99 this.prev().appendText(next.getText());
102 return this.__super__.detach();
105 setTag: function(tagName) {
106 var node = this.document.createDocumentNode({tagName: tagName}),
107 oldTagName = this.getTagName(),
108 myContents = this._$.contents();
110 this.getAttrs().forEach(function(attribute) {
111 node.setAttr(attribute.name, attribute.value, true);
113 node.setData(this.getData());
115 if(this.sameNode(this.document.root)) {
116 this.document._defineDocumentProperties(node._$);
119 /* TODO: This invalidates old references to this node. Caching instances on nodes would fix this. */
120 this._$.replaceWith(node._$);
121 this._setNativeNode(node._$[0]);
122 this._$.append(myContents);
123 this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
127 setAttr: function(name, value, silent) {
128 var oldVal = this.getAttr(name);
129 this._$.attr(name, value);
131 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
135 append: INSERTION(function(node) {
136 var last = _.last(this.contents());
137 if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
138 last.setText(last.getText() + node.getText());
141 this._$.append(node.nativeNode);
146 prepend: INSERTION(function(node) {
147 var first = this.contents()[0];
148 if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
149 first.setText(node.getText() + first.getText());
152 this._$.prepend(node.nativeNode);
157 insertAtIndex: function(nativeNode, index) {
158 var contents = this.contents();
159 if(index < contents.length) {
160 return contents[index].before(nativeNode);
161 } else if(index === contents.length) {
162 return this.append(nativeNode);
166 unwrapContent: function() {
167 var parent = this.parent();
172 var myContents = this.contents(),
173 myIdx = parent.indexOf(this);
176 if(myContents.length === 0) {
177 return this.detach();
180 var prev = this.prev(),
182 moveLeftRange, moveRightRange, leftMerged;
184 if(prev && (prev.nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
185 prev.appendText(myContents[0].getText());
186 myContents[0].detach();
187 moveLeftRange = true;
193 if(!(leftMerged && myContents.length === 1)) {
194 var lastContents = _.last(myContents);
195 if(next && (next.nodeType === TEXT_NODE) && (lastContents.nodeType === TEXT_NODE)) {
196 next.prependText(lastContents.getText());
197 lastContents.detach();
198 moveRightRange = true;
202 var childrenLength = this.contents().length;
203 this.contents().forEach(function(child) {
210 element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
211 element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
215 wrapText: function(params) {
216 return this.document._wrapText(_.extend({inside: this}, params));
220 var textNodeTransformations = {
222 impl: function(t, text) {
223 t.oldText = this.getText();
224 this.nativeNode.data = text;
225 this.triggerTextChangeEvent();
228 this.setText(t.oldText);
232 before: INSERTION(function(node) {
233 if(node.nodeType === Node.TEXT_NODE) {
234 this.prependText(node.getText());
237 return this.__super__.before(node, {silent:true});
241 after: INSERTION(function(node) {
242 if(node.nodeType === Node.TEXT_NODE) {
243 this.appendText(node.getText());
246 return this.__super__.after(node, {silent:true});
250 appendText: function(text) {
251 this.nativeNode.data = this.nativeNode.data + text;
252 this.triggerTextChangeEvent();
255 prependText: function(text) {
256 this.nativeNode.data = text + this.nativeNode.data;
257 this.triggerTextChangeEvent();
260 wrapWith: function(desc) {
261 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
262 return this.document._wrapText({
263 inside: this.parent(),
264 textNodeIdx: this.parent().indexOf(this),
265 offsetStart: Math.min(desc.start, desc.end),
266 offsetEnd: Math.max(desc.start, desc.end),
267 _with: {tagName: desc.tagName, attrs: desc.attrs}
270 return this.__super__.wrapWith.call(this, desc);
274 split: function(params) {
275 var parentElement = this.parent(),
277 succeedingChildren = [],
278 prefix = this.getText().substr(0, params.offset),
279 suffix = this.getText().substr(params.offset);
281 parentElement.contents().forEach(function(child) {
283 succeedingChildren.push(child);
285 if(child.sameNode(this)) {
290 if(prefix.length > 0) {
291 this.setText(prefix);
298 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
299 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
300 parentElement.after(newElement);
302 if(suffix.length > 0) {
303 newElement.append({text: suffix});
305 succeedingChildren.forEach(function(child) {
306 newElement.append(child);
309 return {first: parentElement, second: newElement};
312 divideWithElementNode: function(node, params) {
313 var insertion = this.getNodeInsertion(node),
314 myText = this.getText();
316 if(params.offset === myText.length) {
317 return this.after(node);
319 if(params.offset === 0) {
320 return this.before(node);
323 var lhsText = myText.substr(0, params.offset),
324 rhsText = myText.substr(params.offset),
325 rhsTextNode = this.document.createDocumentNode({text: rhsText});
327 this.setText(lhsText);
328 this.after(insertion.ofNode);
329 insertion.ofNode.after(rhsTextNode);
330 return insertion.ofNode;
334 var documentTransformations = {
335 wrapNodes: function(params) {
336 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
337 throw new Error('Wrapping non-sibling nodes not supported.');
340 var parent = params.node1.parent(),
341 parentContents = parent.contents(),
342 wrapper = this.createDocumentNode({
343 tagName: params._with.tagName,
344 attrs: params._with.attrs}),
345 idx1 = parent.indexOf(params.node1),
346 idx2 = parent.indexOf(params.node2);
354 var insertingMethod, insertingTarget;
356 insertingMethod = 'prepend';
357 insertingTarget = parent;
359 insertingMethod = 'after';
360 insertingTarget = parentContents[idx1-1];
363 for(var i = idx1; i <= idx2; i++) {
364 wrapper.append(parentContents[i].detach());
367 insertingTarget[insertingMethod](wrapper);
371 _wrapText: function(params) {
372 params = _.extend({textNodeIdx: 0}, params);
373 if(typeof params.textNodeIdx === 'number') {
374 params.textNodeIdx = [params.textNodeIdx];
377 var contentsInside = params.inside.contents(),
378 idx1 = Math.min.apply(Math, params.textNodeIdx),
379 idx2 = Math.max.apply(Math, params.textNodeIdx),
380 textNode1 = contentsInside[idx1],
381 textNode2 = contentsInside[idx2],
382 sameNode = textNode1.sameNode(textNode2),
383 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
384 prefixInside = textNode1.getText().substr(params.offsetStart),
385 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
386 suffixOutside = textNode2.getText().substr(params.offsetEnd)
389 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
390 throw new Error('Wrapping text in non-sibling text nodes not supported.');
393 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
394 textNode1.after(wrapperElement);
397 if(prefixOutside.length > 0) {
398 wrapperElement.before({text:prefixOutside});
401 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
402 wrapperElement.append({text: core});
405 if(prefixInside.length > 0) {
406 wrapperElement.append({text: prefixInside});
408 for(var i = idx1 + 1; i < idx2; i++) {
409 wrapperElement.append(contentsInside[i]);
411 if(suffixInside.length > 0) {
412 wrapperElement.append({text: suffixInside});
415 if(suffixOutside.length > 0) {
416 wrapperElement.after({text: suffixOutside});
418 return wrapperElement;
420 replaceRoot: function(node) {
421 var insertion = this.getNodeInsertion(node);
423 this._defineDocumentProperties(insertion.ofNode._$);
424 insertion.ofNode.triggerChangeEvent('nodeAdded');
425 return insertion.ofNode;
431 transformations: documentTransformations
434 transformations: documentNodeTransformations
437 transformations: elementNodeTransformations
440 transformations: textNodeTransformations