1 define(function(require) {
6 var _ = require('libs/underscore');
9 var INSERTION = function(implementation) {
10 var toret = function(node, options) {
11 var insertion = this.getNodeInsertion(node),
12 nodeWasContained = this.document.containsNode(insertion.ofNode),
15 options = options || {};
16 if(!(this.document.containsNode(this))) {
17 nodeParent = insertion.ofNode.parent();
19 returned = implementation.call(this, insertion.ofNode);
20 if(!options.silent && returned.sameNode(insertion.ofNode)) {
21 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
28 var documentNodeTransformations = {
30 var parent = this.parent();
32 this.triggerChangeEvent('nodeDetached', {parent: parent});
36 replaceWith: function(node) {
39 return this.document.replaceRoot(node);
41 toret = this.after(node);
46 after: INSERTION(function(node) {
47 var next = this.next();
48 if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
49 next.setText(node.getText() + next.getText());
52 this._$.after(node.nativeNode);
56 before: INSERTION(function(node) {
57 var prev = this.prev();
58 if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
59 prev.setText(prev.getText() + node.getText());
62 this._$.before(node.nativeNode);
66 wrapWith: function(node) {
67 var insertion = this.getNodeInsertion(node);
69 this.before(insertion.ofNode);
71 insertion.ofNode.append(this);
72 return insertion.ofNode;
76 * Removes parent of a node if node has no siblings.
82 var parent = this.parent(),
84 if(parent.contents().length === 1) {
85 grandParent = parent.parent();
86 parent.unwrapContent();
92 var elementNodeTransformations = {
96 if(this.parent() && this.isSurroundedByTextElements()) {
98 this.prev().appendText(next.getText());
101 return this.__super__.detach();
104 setTag: function(tagName) {
105 var node = this.document.createDocumentNode({tagName: tagName}),
106 oldTagName = this.getTagName(),
107 myContents = this._$.contents();
109 this.getAttrs().forEach(function(attribute) {
110 node.setAttr(attribute.name, attribute.value, true);
112 node.setData(this.getData());
114 if(this.sameNode(this.document.root)) {
115 this.document._defineDocumentProperties(node._$);
118 /* TODO: This invalidates old references to this node. Caching instances on nodes would fix this. */
119 this._$.replaceWith(node._$);
120 this._setNativeNode(node._$[0]);
121 this._$.append(myContents);
122 this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
126 setAttr: function(name, value, silent) {
127 var oldVal = this.getAttr(name);
128 this._$.attr(name, value);
130 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
134 append: INSERTION(function(node) {
135 var last = _.last(this.contents());
136 if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
137 last.setText(last.getText() + node.getText());
140 this._$.append(node.nativeNode);
145 prepend: INSERTION(function(node) {
146 var first = this.contents()[0];
147 if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
148 first.setText(node.getText() + first.getText());
151 this._$.prepend(node.nativeNode);
156 insertAtIndex: function(nativeNode, index) {
157 var contents = this.contents();
158 if(index < contents.length) {
159 return contents[index].before(nativeNode);
160 } else if(index === contents.length) {
161 return this.append(nativeNode);
165 unwrapContent: function() {
166 var parent = this.parent();
171 var myContents = this.contents(),
172 myIdx = parent.indexOf(this);
175 if(myContents.length === 0) {
176 return this.detach();
180 var childrenLength = this.contents().length,
183 this.contents().forEach(function(child) {
184 var returned = this.before(child);
185 if(first && !(returned.sameNode(child))) {
194 element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
195 element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
199 wrapText: function(params) {
200 return this.document._wrapText(_.extend({inside: this}, params));
204 var textNodeTransformations = {
206 impl: function(t, text) {
207 t.oldText = this.getText();
208 this.nativeNode.data = text;
209 this.triggerTextChangeEvent();
212 this.setText(t.oldText);
216 before: INSERTION(function(node) {
217 if(node.nodeType === Node.TEXT_NODE) {
218 this.prependText(node.getText());
221 return this.__super__.before(node, {silent:true});
225 after: INSERTION(function(node) {
226 if(node.nodeType === Node.TEXT_NODE) {
227 this.appendText(node.getText());
230 return this.__super__.after(node, {silent:true});
234 appendText: function(text) {
235 this.nativeNode.data = this.nativeNode.data + text;
236 this.triggerTextChangeEvent();
239 prependText: function(text) {
240 this.nativeNode.data = text + this.nativeNode.data;
241 this.triggerTextChangeEvent();
244 wrapWith: function(desc) {
245 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
246 return this.document._wrapText({
247 inside: this.parent(),
248 textNodeIdx: this.parent().indexOf(this),
249 offsetStart: Math.min(desc.start, desc.end),
250 offsetEnd: Math.max(desc.start, desc.end),
251 _with: {tagName: desc.tagName, attrs: desc.attrs}
254 return this.__super__.wrapWith.call(this, desc);
258 split: function(params) {
259 var parentElement = this.parent(),
261 succeedingChildren = [],
262 prefix = this.getText().substr(0, params.offset),
263 suffix = this.getText().substr(params.offset);
265 parentElement.contents().forEach(function(child) {
267 succeedingChildren.push(child);
269 if(child.sameNode(this)) {
274 if(prefix.length > 0) {
275 this.setText(prefix);
282 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
283 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
284 parentElement.after(newElement);
286 if(suffix.length > 0) {
287 newElement.append({text: suffix});
289 succeedingChildren.forEach(function(child) {
290 newElement.append(child);
293 return {first: parentElement, second: newElement};
296 divideWithElementNode: function(node, params) {
297 var insertion = this.getNodeInsertion(node),
298 myText = this.getText();
300 if(params.offset === myText.length) {
301 return this.after(node);
303 if(params.offset === 0) {
304 return this.before(node);
307 var lhsText = myText.substr(0, params.offset),
308 rhsText = myText.substr(params.offset),
309 rhsTextNode = this.document.createDocumentNode({text: rhsText});
311 this.setText(lhsText);
312 this.after(insertion.ofNode);
313 insertion.ofNode.after(rhsTextNode);
314 return insertion.ofNode;
318 var documentTransformations = {
319 wrapNodes: function(params) {
320 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
321 throw new Error('Wrapping non-sibling nodes not supported.');
324 var parent = params.node1.parent(),
325 parentContents = parent.contents(),
326 wrapper = this.createDocumentNode({
327 tagName: params._with.tagName,
328 attrs: params._with.attrs}),
329 idx1 = parent.indexOf(params.node1),
330 idx2 = parent.indexOf(params.node2);
338 var insertingMethod, insertingTarget;
340 insertingMethod = 'prepend';
341 insertingTarget = parent;
343 insertingMethod = 'after';
344 insertingTarget = parentContents[idx1-1];
347 for(var i = idx1; i <= idx2; i++) {
348 wrapper.append(parentContents[i].detach());
351 insertingTarget[insertingMethod](wrapper);
355 _wrapText: function(params) {
356 params = _.extend({textNodeIdx: 0}, params);
357 if(typeof params.textNodeIdx === 'number') {
358 params.textNodeIdx = [params.textNodeIdx];
361 var contentsInside = params.inside.contents(),
362 idx1 = Math.min.apply(Math, params.textNodeIdx),
363 idx2 = Math.max.apply(Math, params.textNodeIdx),
364 textNode1 = contentsInside[idx1],
365 textNode2 = contentsInside[idx2],
366 sameNode = textNode1.sameNode(textNode2),
367 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
368 prefixInside = textNode1.getText().substr(params.offsetStart),
369 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
370 suffixOutside = textNode2.getText().substr(params.offsetEnd)
373 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
374 throw new Error('Wrapping text in non-sibling text nodes not supported.');
377 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
378 textNode1.after(wrapperElement);
381 if(prefixOutside.length > 0) {
382 wrapperElement.before({text:prefixOutside});
385 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
386 wrapperElement.append({text: core});
389 if(prefixInside.length > 0) {
390 wrapperElement.append({text: prefixInside});
392 for(var i = idx1 + 1; i < idx2; i++) {
393 wrapperElement.append(contentsInside[i]);
395 if(suffixInside.length > 0) {
396 wrapperElement.append({text: suffixInside});
399 if(suffixOutside.length > 0) {
400 wrapperElement.after({text: suffixOutside});
402 return wrapperElement;
404 replaceRoot: function(node) {
405 var insertion = this.getNodeInsertion(node);
407 this._defineDocumentProperties(insertion.ofNode._$);
408 insertion.ofNode.triggerChangeEvent('nodeAdded');
409 return insertion.ofNode;
415 transformations: documentTransformations
418 transformations: documentNodeTransformations
421 transformations: elementNodeTransformations
424 transformations: textNodeTransformations