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) {
12 var insertion = this.getNodeInsertion(node),
13 nodeWasContained = this.document.containsNode(insertion.ofNode),
15 if(!(this.document.containsNode(this))) {
16 nodeParent = insertion.ofNode.parent();
18 implementation.call(this, insertion.ofNode.nativeNode);
19 this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
20 return insertion.ofNode;
25 var documentNodeTransformations = {
27 var parent = this.parent();
29 this.triggerChangeEvent('nodeDetached', {parent: parent});
33 replaceWith: function(node) {
36 return this.document.replaceRoot(node);
38 toret = this.after(node);
43 after: INSERTION(function(nativeNode) {
44 return this._$.after(nativeNode);
47 before: INSERTION(function(nativeNode) {
48 return this._$.before(nativeNode);
51 wrapWith: function(node) {
52 var insertion = this.getNodeInsertion(node);
54 this.before(insertion.ofNode);
56 insertion.ofNode.append(this);
57 return insertion.ofNode;
61 * Removes parent of a node if node has no siblings.
67 var parent = this.parent(),
69 if(parent.contents().length === 1) {
70 grandParent = parent.parent();
71 parent.unwrapContent();
77 var elementNodeTransformations = {
81 if(this.parent() && this.isSurroundedByTextElements()) {
83 this.prev().appendText(next.getText());
86 return this.__super__.detach();
89 setTag: function(tagName) {
90 var node = this.document.createDocumentNode({tagName: tagName}),
91 oldTagName = this.getTagName(),
92 myContents = this._$.contents();
94 this.getAttrs().forEach(function(attribute) {
95 node.setAttr(attribute.name, attribute.value, true);
97 node.setData(this.getData());
99 if(this.sameNode(this.document.root)) {
100 this.document._defineDocumentProperties(node._$);
103 /* TODO: This invalidates old references to this node. Caching instances on nodes would fix this. */
104 this._$.replaceWith(node._$);
105 this._setNativeNode(node._$[0]);
106 this._$.append(myContents);
107 this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
111 setAttr: function(name, value, silent) {
112 var oldVal = this.getAttr(name);
113 this._$.attr(name, value);
115 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
119 append: INSERTION(function(nativeNode) {
120 this._$.append(nativeNode);
123 prepend: INSERTION(function(nativeNode) {
124 this._$.prepend(nativeNode);
127 insertAtIndex: function(nativeNode, index) {
128 var contents = this.contents();
129 if(index < contents.length) {
130 return contents[index].before(nativeNode);
131 } else if(index === contents.length) {
132 return this.append(nativeNode);
136 unwrapContent: function() {
137 var parent = this.parent();
142 var myContents = this.contents(),
143 myIdx = parent.indexOf(this);
146 if(myContents.length === 0) {
147 return this.detach();
150 var prev = this.prev(),
152 moveLeftRange, moveRightRange, leftMerged;
154 if(prev && (prev.nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
155 prev.appendText(myContents[0].getText());
156 myContents[0].detach();
157 moveLeftRange = true;
163 if(!(leftMerged && myContents.length === 1)) {
164 var lastContents = _.last(myContents);
165 if(next && (next.nodeType === TEXT_NODE) && (lastContents.nodeType === TEXT_NODE)) {
166 next.prependText(lastContents.getText());
167 lastContents.detach();
168 moveRightRange = true;
172 var childrenLength = this.contents().length;
173 this.contents().forEach(function(child) {
180 element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
181 element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
185 wrapText: function(params) {
186 return this.document._wrapText(_.extend({inside: this}, params));
190 var textNodeTransformations = {
192 impl: function(t, text) {
193 t.oldText = this.getText();
194 this.nativeNode.data = text;
195 this.triggerTextChangeEvent();
198 this.setText(t.oldText);
202 appendText: function(text) {
203 this.nativeNode.data = this.nativeNode.data + text;
204 this.triggerTextChangeEvent();
207 prependText: function(text) {
208 this.nativeNode.data = text + this.nativeNode.data;
209 this.triggerTextChangeEvent();
212 wrapWith: function(desc) {
213 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
214 return this.document._wrapText({
215 inside: this.parent(),
216 textNodeIdx: this.parent().indexOf(this),
217 offsetStart: Math.min(desc.start, desc.end),
218 offsetEnd: Math.max(desc.start, desc.end),
219 _with: {tagName: desc.tagName, attrs: desc.attrs}
222 return this.__super__.wrapWith.call(this, desc);
226 split: function(params) {
227 var parentElement = this.parent(),
229 succeedingChildren = [],
230 prefix = this.getText().substr(0, params.offset),
231 suffix = this.getText().substr(params.offset);
233 parentElement.contents().forEach(function(child) {
235 succeedingChildren.push(child);
237 if(child.sameNode(this)) {
242 if(prefix.length > 0) {
243 this.setText(prefix);
250 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
251 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
252 parentElement.after(newElement);
254 if(suffix.length > 0) {
255 newElement.append({text: suffix});
257 succeedingChildren.forEach(function(child) {
258 newElement.append(child);
261 return {first: parentElement, second: newElement};
264 divideWithElementNode: function(node, params) {
265 var insertion = this.getNodeInsertion(node),
266 myText = this.getText();
268 if(params.offset === myText.length) {
269 return this.after(node);
271 if(params.offset === 0) {
272 return this.before(node);
275 var lhsText = myText.substr(0, params.offset),
276 rhsText = myText.substr(params.offset),
277 rhsTextNode = this.document.createDocumentNode({text: rhsText});
279 this.setText(lhsText);
280 this.after(insertion.ofNode);
281 insertion.ofNode.after(rhsTextNode);
282 return insertion.ofNode;
286 var documentTransformations = {
287 wrapNodes: function(params) {
288 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
289 throw new Error('Wrapping non-sibling nodes not supported.');
292 var parent = params.node1.parent(),
293 parentContents = parent.contents(),
294 wrapper = this.createDocumentNode({
295 tagName: params._with.tagName,
296 attrs: params._with.attrs}),
297 idx1 = parent.indexOf(params.node1),
298 idx2 = parent.indexOf(params.node2);
306 var insertingMethod, insertingTarget;
308 insertingMethod = 'prepend';
309 insertingTarget = parent;
311 insertingMethod = 'after';
312 insertingTarget = parentContents[idx1-1];
315 for(var i = idx1; i <= idx2; i++) {
316 wrapper.append(parentContents[i].detach());
319 insertingTarget[insertingMethod](wrapper);
323 _wrapText: function(params) {
324 params = _.extend({textNodeIdx: 0}, params);
325 if(typeof params.textNodeIdx === 'number') {
326 params.textNodeIdx = [params.textNodeIdx];
329 var contentsInside = params.inside.contents(),
330 idx1 = Math.min.apply(Math, params.textNodeIdx),
331 idx2 = Math.max.apply(Math, params.textNodeIdx),
332 textNode1 = contentsInside[idx1],
333 textNode2 = contentsInside[idx2],
334 sameNode = textNode1.sameNode(textNode2),
335 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
336 prefixInside = textNode1.getText().substr(params.offsetStart),
337 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
338 suffixOutside = textNode2.getText().substr(params.offsetEnd)
341 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
342 throw new Error('Wrapping text in non-sibling text nodes not supported.');
345 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
346 textNode1.after(wrapperElement);
349 if(prefixOutside.length > 0) {
350 wrapperElement.before({text:prefixOutside});
353 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
354 wrapperElement.append({text: core});
357 if(prefixInside.length > 0) {
358 wrapperElement.append({text: prefixInside});
360 for(var i = idx1 + 1; i < idx2; i++) {
361 wrapperElement.append(contentsInside[i]);
363 if(suffixInside.length > 0) {
364 wrapperElement.append({text: suffixInside});
367 if(suffixOutside.length > 0) {
368 wrapperElement.after({text: suffixOutside});
370 return wrapperElement;
372 replaceRoot: function(node) {
373 var insertion = this.getNodeInsertion(node);
375 this._defineDocumentProperties(insertion.ofNode._$);
376 insertion.ofNode.triggerChangeEvent('nodeAdded');
377 return insertion.ofNode;
383 transformations: documentTransformations
386 transformations: documentNodeTransformations
389 transformations: elementNodeTransformations
392 transformations: textNodeTransformations