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.insertsNew) {
17 nodeParent = insertion.ofNode.parent();
19 if(!insertion.insertsNew && insertion.ofNode.isSurroundedByTextNodes()) {
20 var prev = insertion.ofNode.prev(),
21 next = insertion.ofNode.next();
22 prev.setText(prev.getText()+next.getText());
25 returned = implementation.call(this, insertion.ofNode);
26 if(!options.silent && returned && returned.sameNode(insertion.ofNode)) {
27 if(!insertion.insertsNew) {
28 this.triggerChangeEvent('nodeDetached', {node: insertion.ofNode, parent: nodeParent, move: true});
30 this.triggerChangeEvent('nodeAdded', {node: insertion.ofNode, move: !insertion.insertsNew}, nodeParent, nodeWasContained);
37 var documentNodeTransformations = {
39 var parent = this.parent(),
40 existed = this.document.containsNode(this);
43 this.triggerChangeEvent('nodeDetached', {parent: parent});
45 // This was the root of the document
46 this.document._defineDocumentProperties(null);
52 replaceWith: function(node) {
55 return this.document.replaceRoot(node);
58 toret = this.after(node);
62 throw new Error('Cannot replace node without a parent.');
65 after: INSERTION(function(node) {
69 var next = this.next();
71 if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
72 next.setText(node.getText() + next.getText());
76 this._$.after(node.nativeNode);
80 before: INSERTION(function(node) {
84 var prev = this.prev();
85 if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
86 prev.setText(prev.getText() + node.getText());
90 this._$.before(node.nativeNode);
94 wrapWith: function(node) {
95 var insertion = this.getNodeInsertion(node);
97 if(this.parent() || this.isRoot()) {
98 this.replaceWith(insertion.ofNode);
100 insertion.ofNode.append(this);
101 return insertion.ofNode;
105 * Removes parent of a node if node has no siblings.
111 var parent = this.parent(),
113 if(parent.contents().length === 1) {
114 grandParent = parent.parent();
115 parent.unwrapContent();
121 var elementNodeTransformations = {
123 detach: function(params) {
126 params = params || {};
128 if(!params.normalizeStrategy) {
129 params.normalizeStrategy = 'merge';
132 if(this.parent() && this.isSurroundedByTextNodes()) {
133 if(params.normalizeStrategy === 'detach-left') {
134 this.prev().detach();
135 } else if(params.normalizeStrategy === 'detach-right') {
136 this.next().detach();
137 } else if(params.normalizeStrategy === 'merge') {
142 previousLen: prev.getText().length
144 prev.appendText(next.getText());
147 throw new Error('unknown normalize strategy for detach');
150 return this.__super__.detach();
153 setTag: function(tagName) {
154 var node = this.document.createDocumentNode({tagName: tagName});
156 this.getAttrs().forEach(function(attribute) {
157 node.setAttr(attribute.name, attribute.value);
160 this.contents().forEach(function(child) {
164 node.setData(this.getData());
166 this.replaceWith(node);
170 setAttr: function(name, value, silent) {
171 var oldVal = this.getAttr(name);
172 if(_.isUndefined(value)) {
173 this._$.removeAttr(name);
175 this._$.attr(name, value);
178 this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
182 append: INSERTION(function(node) {
183 var last = _.last(this.contents());
184 if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
185 last.setText(last.getText() + node.getText());
189 this._$.append(node.nativeNode);
194 prepend: INSERTION(function(node) {
195 var first = this.contents()[0];
196 if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
197 first.setText(node.getText() + first.getText());
201 this._$.prepend(node.nativeNode);
206 insertAtIndex: function(nativeNode, index) {
207 var contents = this.contents();
208 if(index < contents.length) {
209 return contents[index].before(nativeNode);
210 } else if(index === contents.length) {
211 return this.append(nativeNode);
215 unwrapContent: function() {
216 var parent = this.parent();
222 .filter(function(child) {
223 return child.getProperty('describesParent');
225 .forEach(function(child) {
229 var myContents = this.contents(),
230 myIdx = parent.indexOf(this);
232 if(myContents.length === 0) {
233 return this.detach();
236 var childrenLength = this.contents().length,
239 this.contents().forEach(function(child) {
240 var returned = this.before(child);
241 if(first && !(returned.sameNode(child))) {
250 element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
251 element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
255 wrapText: function(params) {
256 return this.document._wrapText(_.extend({inside: this}, params));
260 var textNodeTransformations = {
262 impl: function(t, text) {
263 t.oldText = this.getText();
264 this.nativeNode.data = text;
265 this.triggerTextChangeEvent();
268 this.setText(t.oldText);
272 before: INSERTION(function(node) {
273 if(node.nodeType === Node.TEXT_NODE) {
274 this.prependText(node.getText());
278 return this.__super__.before(node, {silent:true});
282 after: INSERTION(function(node) {
283 if(node.nodeType === Node.TEXT_NODE) {
284 this.appendText(node.getText());
288 return this.__super__.after(node, {silent:true});
292 append: function(node) {
293 if(node.nodeType === Node.TEXT_NODE) {
294 this.appendText(node.getText());
299 prepend: function(node) {
300 if(node.nodeType === Node.TEXT_NODE) {
301 this.prependText(node.getText());
307 appendText: function(text) {
308 this.nativeNode.data = this.nativeNode.data + text;
309 this.triggerTextChangeEvent();
312 prependText: function(text) {
313 this.nativeNode.data = text + this.nativeNode.data;
314 this.triggerTextChangeEvent();
317 wrapWith: function(desc) {
318 if(typeof desc.start === 'number' && typeof desc.end === 'number') {
319 return this.document._wrapText({
320 inside: this.parent(),
321 textNodeIdx: this.parent().indexOf(this),
322 offsetStart: Math.min(desc.start, desc.end),
323 offsetEnd: Math.max(desc.start, desc.end),
324 _with: {tagName: desc.tagName, attrs: desc.attrs}
327 return this.__super__.wrapWith.call(this, desc);
331 split: function(params) {
332 var parentElement = this.parent(),
334 succeedingChildren = [],
335 prefix = this.getText().substr(0, params.offset),
336 suffix = this.getText().substr(params.offset);
338 parentElement.contents().forEach(function(child) {
340 succeedingChildren.push(child);
342 if(child.sameNode(this)) {
347 if(prefix.length > 0) {
348 this.setText(prefix);
355 parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
356 var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
357 parentElement.after(newElement);
359 succeedingChildren.reverse().forEach(function(child) {
360 newElement.prepend(child);
362 if(suffix.length > 0) {
363 newElement.prepend({text: suffix});
366 return {first: parentElement, second: newElement, created: newElement};
369 divideWithElementNode: function(node, params) {
370 var insertion = this.getNodeInsertion(node),
371 myText = this.getText();
373 if(params.offset === myText.length) {
374 return this.after(node);
376 if(params.offset === 0) {
377 return this.before(node);
380 var lhsText = myText.substr(0, params.offset),
381 rhsText = myText.substr(params.offset),
382 rhsTextNode = this.document.createDocumentNode({text: rhsText});
384 this.setText(lhsText);
385 this.after(insertion.ofNode);
386 insertion.ofNode.after(rhsTextNode);
387 return insertion.ofNode;
391 var documentTransformations = {
392 wrapNodes: function(params) {
393 if(!(params.node1.parent().sameNode(params.node2.parent()))) {
394 throw new Error('Wrapping non-sibling nodes not supported.');
397 var parent = params.node1.parent(),
398 parentContents = parent.contents(),
399 wrapper = this.createDocumentNode({
400 tagName: params._with.tagName,
401 attrs: params._with.attrs}),
402 idx1 = parent.indexOf(params.node1),
403 idx2 = parent.indexOf(params.node2);
411 var insertingMethod, insertingTarget;
413 insertingMethod = 'prepend';
414 insertingTarget = parent;
416 insertingMethod = 'after';
417 insertingTarget = parentContents[idx1-1];
420 for(var i = idx1; i <= idx2; i++) {
421 if(!parentContents[i].getProperty('describesParent')) {
422 wrapper.append(parentContents[i].detach());
426 insertingTarget[insertingMethod](wrapper);
430 _wrapText: function(params) {
431 params = _.extend({textNodeIdx: 0}, params);
432 if(typeof params.textNodeIdx === 'number') {
433 params.textNodeIdx = [params.textNodeIdx];
436 var contentsInside = params.inside.contents(),
437 idx1 = Math.min.apply(Math, params.textNodeIdx),
438 idx2 = Math.max.apply(Math, params.textNodeIdx),
439 textNode1 = contentsInside[idx1],
440 textNode2 = contentsInside[idx2],
441 sameNode = textNode1.sameNode(textNode2),
442 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
443 prefixInside = textNode1.getText().substr(params.offsetStart),
444 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
445 suffixOutside = textNode2.getText().substr(params.offsetEnd)
448 if(!(textNode1.parent().sameNode(textNode2.parent()))) {
449 throw new Error('Wrapping text in non-sibling text nodes not supported.');
452 var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
453 textNode1.after(wrapperElement);
456 if(prefixOutside.length > 0) {
457 wrapperElement.before({text:prefixOutside});
460 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
461 wrapperElement.append({text: core});
464 if(prefixInside.length > 0) {
465 wrapperElement.append({text: prefixInside});
467 for(var i = idx1 + 1; i < idx2; i++) {
468 if(!contentsInside[i].getProperty('describesParent')) {
469 wrapperElement.append(contentsInside[i]);
472 if(suffixInside.length > 0) {
473 wrapperElement.append({text: suffixInside});
476 if(suffixOutside.length > 0) {
477 wrapperElement.after({text: suffixOutside});
479 return wrapperElement;
481 replaceRoot: function(node) {
482 var insertion = this.getNodeInsertion(node);
484 this._defineDocumentProperties(insertion.ofNode._$);
485 insertion.ofNode.triggerChangeEvent('nodeAdded');
486 return insertion.ofNode;
488 deleteText: function(params) {
489 var ptr, next, nextNext, toDetach, middle, text;
491 if(params.from.node.sameNode(params.to.node)) {
492 ptr = params.from.node;
493 text = ptr.getText();
494 ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
498 // Both edge text nodes need to be edited before anything else happen in case that
499 // they get merged when detaching content between them.
500 params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
501 params.to.node.setText(params.to.node.getText().substr(params.to.offset));
503 ptr = params.from.node;
506 while(next || ptr.parent()) {
508 if(next.sameNode(params.to.node)) {
511 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
517 nextNext = next ? next.next() : null;
518 toDetach.detach({normalizeStrategy: (next && next.sameNode(params.to.node)) ? 'merge' : 'detach-right'});
519 if(next && !next.isInDocument()) {
529 if(!this.containsNode(params.to.node)) {
530 // The end node was merged during detaching nodes above - there is nothing more left to do.
534 ptr = middle.contents()[0];
535 while(ptr && !ptr.sameNode(params.to.node)) {
536 if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
537 ptr = ptr.contents()[0];
549 transformations: documentTransformations
552 transformations: documentNodeTransformations
555 transformations: elementNodeTransformations
558 transformations: textNodeTransformations