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)) || !insertion.isNew) {
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(),
31 existed = this.document.containsNode(this);
34 this.triggerChangeEvent('nodeDetached', {parent: parent});
39 replaceWith: function(node) {
42 return this.document.replaceRoot(node);
44 toret = this.after(node);
49 after: INSERTION(function(node) {
50 var next = this.next();
51 if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
52 next.setText(node.getText() + next.getText());
56 this._$.after(node.nativeNode);
60 before: INSERTION(function(node) {
61 var prev = this.prev();
62 if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
63 prev.setText(prev.getText() + node.getText());
67 this._$.before(node.nativeNode);
71 wrapWith: function(node) {
72 var insertion = this.getNodeInsertion(node);
74 this.before(insertion.ofNode);
76 insertion.ofNode.append(this);
77 return insertion.ofNode;
81 * Removes parent of a node if node has no siblings.
87 var parent = this.parent(),
89 if(parent.contents().length === 1) {
90 grandParent = parent.parent();
91 parent.unwrapContent();
97 var elementNodeTransformations = {
101 if(this.parent() && this.isSurroundedByTextElements()) {
103 this.prev().appendText(next.getText());
106 return this.__super__.detach();
109 setTag: function(tagName) {
110 var node = this.document.createDocumentNode({tagName: tagName}),
111 oldTagName = this.getTagName(),
112 myContents = this._$.contents();
114 this.getAttrs().forEach(function(attribute) {
115 node.setAttr(attribute.name, attribute.value, true);
117 node.setData(this.getData());
119 if(this.sameNode(this.document.root)) {
120 this.document._defineDocumentProperties(node._$);
123 /* TODO: This invalidates old references to this node. Caching instances on nodes would fix this. */
124 this._$.replaceWith(node._$);
125 this._setNativeNode(node._$[0]);
126 this._$.append(myContents);
127 this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
131 setAttr: function(name, value, silent) {
132 var oldVal = this.getAttr(name);
133 this._$.attr(name, value);
135 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
139 append: INSERTION(function(node) {
140 var last = _.last(this.contents());
141 if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
142 last.setText(last.getText() + node.getText());
146 this._$.append(node.nativeNode);
151 prepend: INSERTION(function(node) {
152 var first = this.contents()[0];
153 if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
154 first.setText(node.getText() + first.getText());
158 this._$.prepend(node.nativeNode);
163 insertAtIndex: function(nativeNode, index) {
164 var contents = this.contents();
165 if(index < contents.length) {
166 return contents[index].before(nativeNode);
167 } else if(index === contents.length) {
168 return this.append(nativeNode);
172 unwrapContent: function() {
173 var parent = this.parent();
178 var myContents = this.contents(),
179 myIdx = parent.indexOf(this);
182 if(myContents.length === 0) {
183 return this.detach();
187 var childrenLength = this.contents().length,
190 this.contents().forEach(function(child) {
191 var returned = this.before(child);
192 if(first && !(returned.sameNode(child))) {
201 element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
202 element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
206 wrapText: function(params) {
207 return this.document._wrapText(_.extend({inside: this}, params));
211 var textNodeTransformations = {
213 impl: function(t, text) {
214 t.oldText = this.getText();
215 this.nativeNode.data = text;
216 this.triggerTextChangeEvent();
219 this.setText(t.oldText);
223 before: INSERTION(function(node) {
224 if(node.nodeType === Node.TEXT_NODE) {
225 this.prependText(node.getText());
229 return this.__super__.before(node, {silent:true});
233 after: INSERTION(function(node) {
234 if(node.nodeType === Node.TEXT_NODE) {
235 this.appendText(node.getText());
239 return this.__super__.after(node, {silent:true});
243 append: function(node) {
244 if(node.nodeType === Node.TEXT_NODE) {
245 this.appendText(node.getText());
250 prepend: function(node) {
251 if(node.nodeType === Node.TEXT_NODE) {
252 this.prependText(node.getText());
258 appendText: function(text) {
259 this.nativeNode.data = this.nativeNode.data + text;
260 this.triggerTextChangeEvent();
263 prependText: function(text) {
264 this.nativeNode.data = text + this.nativeNode.data;
265 this.triggerTextChangeEvent();
268 wrapWith: function(desc) {
269 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
270 return this.document._wrapText({
271 inside: this.parent(),
272 textNodeIdx: this.parent().indexOf(this),
273 offsetStart: Math.min(desc.start, desc.end),
274 offsetEnd: Math.max(desc.start, desc.end),
275 _with: {tagName: desc.tagName, attrs: desc.attrs}
278 return this.__super__.wrapWith.call(this, desc);
282 split: function(params) {
283 var parentElement = this.parent(),
285 succeedingChildren = [],
286 prefix = this.getText().substr(0, params.offset),
287 suffix = this.getText().substr(params.offset);
289 parentElement.contents().forEach(function(child) {
291 succeedingChildren.push(child);
293 if(child.sameNode(this)) {
298 if(prefix.length > 0) {
299 this.setText(prefix);
306 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
307 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
308 parentElement.after(newElement);
310 if(suffix.length > 0) {
311 newElement.append({text: suffix});
313 succeedingChildren.forEach(function(child) {
314 newElement.append(child);
317 return {first: parentElement, second: newElement};
320 divideWithElementNode: function(node, params) {
321 var insertion = this.getNodeInsertion(node),
322 myText = this.getText();
324 if(params.offset === myText.length) {
325 return this.after(node);
327 if(params.offset === 0) {
328 return this.before(node);
331 var lhsText = myText.substr(0, params.offset),
332 rhsText = myText.substr(params.offset),
333 rhsTextNode = this.document.createDocumentNode({text: rhsText});
335 this.setText(lhsText);
336 this.after(insertion.ofNode);
337 insertion.ofNode.after(rhsTextNode);
338 return insertion.ofNode;
342 var documentTransformations = {
343 wrapNodes: function(params) {
344 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
345 throw new Error('Wrapping non-sibling nodes not supported.');
348 var parent = params.node1.parent(),
349 parentContents = parent.contents(),
350 wrapper = this.createDocumentNode({
351 tagName: params._with.tagName,
352 attrs: params._with.attrs}),
353 idx1 = parent.indexOf(params.node1),
354 idx2 = parent.indexOf(params.node2);
362 var insertingMethod, insertingTarget;
364 insertingMethod = 'prepend';
365 insertingTarget = parent;
367 insertingMethod = 'after';
368 insertingTarget = parentContents[idx1-1];
371 for(var i = idx1; i <= idx2; i++) {
372 wrapper.append(parentContents[i].detach());
375 insertingTarget[insertingMethod](wrapper);
379 _wrapText: function(params) {
380 params = _.extend({textNodeIdx: 0}, params);
381 if(typeof params.textNodeIdx === 'number') {
382 params.textNodeIdx = [params.textNodeIdx];
385 var contentsInside = params.inside.contents(),
386 idx1 = Math.min.apply(Math, params.textNodeIdx),
387 idx2 = Math.max.apply(Math, params.textNodeIdx),
388 textNode1 = contentsInside[idx1],
389 textNode2 = contentsInside[idx2],
390 sameNode = textNode1.sameNode(textNode2),
391 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
392 prefixInside = textNode1.getText().substr(params.offsetStart),
393 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
394 suffixOutside = textNode2.getText().substr(params.offsetEnd)
397 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
398 throw new Error('Wrapping text in non-sibling text nodes not supported.');
401 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
402 textNode1.after(wrapperElement);
405 if(prefixOutside.length > 0) {
406 wrapperElement.before({text:prefixOutside});
409 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
410 wrapperElement.append({text: core});
413 if(prefixInside.length > 0) {
414 wrapperElement.append({text: prefixInside});
416 for(var i = idx1 + 1; i < idx2; i++) {
417 wrapperElement.append(contentsInside[i]);
419 if(suffixInside.length > 0) {
420 wrapperElement.append({text: suffixInside});
423 if(suffixOutside.length > 0) {
424 wrapperElement.after({text: suffixOutside});
426 return wrapperElement;
428 replaceRoot: function(node) {
429 var insertion = this.getNodeInsertion(node);
431 this._defineDocumentProperties(insertion.ofNode._$);
432 insertion.ofNode.triggerChangeEvent('nodeAdded');
433 return insertion.ofNode;
435 deleteText: function(params) {
436 var ptr, next, toDetach, middle, text;
438 if(params.from.node.sameNode(params.to.node)) {
439 ptr = params.from.node;
440 text = ptr.getText();
441 ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
445 // Both edge text nodes need to be edited before anything else happen in case that
446 // they get merged when detaching content between them.
447 params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
448 params.to.node.setText(params.to.node.getText().substr(params.to.offset));
450 ptr = params.from.node;
453 while(next || ptr.parent()) {
455 if(next.sameNode(params.to.node)) {
458 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
472 if(!this.containsNode(params.to.node)) {
473 // The end node was merged during detaching nodes above - there is nothing more left to do.
477 ptr = middle.contents()[0];
478 while(ptr && !ptr.sameNode(params.to.node)) {
479 if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
480 ptr = ptr.contents()[0];
492 transformations: documentTransformations
495 transformations: documentNodeTransformations
498 transformations: elementNodeTransformations
501 transformations: textNodeTransformations