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             normalizeStrategy: 'merge'
 
 129         if(this.parent() && this.isSurroundedByTextNodes()) {
 
 130             if(params.normalizeStrategy === 'detach-left') {
 
 131                 this.prev().detach();
 
 132             } else if(params.normalizeStrategy === 'detach-right') {
 
 133                 this.next().detach();
 
 134             } else if(params.normalizeStrategy === 'merge') {
 
 136                 this.prev().appendText(next.getText());
 
 139                 throw new Error('unknown normalize strategy for detach');
 
 142         return this.__super__.detach();
 
 145     setTag: function(tagName) {
 
 146         var node = this.document.createDocumentNode({tagName: tagName});
 
 148         this.getAttrs().forEach(function(attribute) {
 
 149             node.setAttr(attribute.name, attribute.value);
 
 152         this.contents().forEach(function(child) {
 
 156         node.setData(this.getData());
 
 158         this.replaceWith(node);
 
 162     setAttr: function(name, value, silent) {
 
 163         var oldVal = this.getAttr(name);
 
 164         this._$.attr(name, value);
 
 166             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
 
 170     append: INSERTION(function(node) {
 
 171         var last = _.last(this.contents());
 
 172         if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
 
 173             last.setText(last.getText() + node.getText());
 
 177             this._$.append(node.nativeNode);
 
 182     prepend: INSERTION(function(node) {
 
 183         var first = this.contents()[0];
 
 184         if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
 
 185             first.setText(node.getText() + first.getText());
 
 189             this._$.prepend(node.nativeNode);
 
 194     insertAtIndex: function(nativeNode, index) {
 
 195         var contents = this.contents();
 
 196         if(index < contents.length) {
 
 197             return contents[index].before(nativeNode);
 
 198         } else if(index === contents.length) {
 
 199             return this.append(nativeNode);
 
 203     unwrapContent: function() {
 
 204         var parent = this.parent();
 
 210             .filter(function(child) {
 
 211                 return child.getProperty('describesParent');
 
 213             .forEach(function(child) {
 
 217         var myContents = this.contents(),
 
 218             myIdx = parent.indexOf(this);
 
 220         if(myContents.length === 0) {
 
 221             return this.detach();
 
 224         var childrenLength = this.contents().length,
 
 227         this.contents().forEach(function(child) {
 
 228             var returned = this.before(child);
 
 229             if(first && !(returned.sameNode(child))) {
 
 238             element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
 
 239             element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
 
 243     wrapText: function(params) {
 
 244         return this.document._wrapText(_.extend({inside: this}, params));
 
 248 var textNodeTransformations = {
 
 250         impl: function(t, text) {
 
 251             t.oldText = this.getText();
 
 252             this.nativeNode.data = text;
 
 253             this.triggerTextChangeEvent();
 
 256             this.setText(t.oldText);
 
 260     before: INSERTION(function(node) {
 
 261         if(node.nodeType === Node.TEXT_NODE) {
 
 262             this.prependText(node.getText());
 
 266             return this.__super__.before(node, {silent:true});
 
 270     after: INSERTION(function(node) {
 
 271         if(node.nodeType === Node.TEXT_NODE) {
 
 272             this.appendText(node.getText());
 
 276             return this.__super__.after(node, {silent:true});
 
 280     append: function(node) {
 
 281         if(node.nodeType === Node.TEXT_NODE) {
 
 282             this.appendText(node.getText());
 
 287     prepend: function(node) {
 
 288         if(node.nodeType === Node.TEXT_NODE) {
 
 289             this.prependText(node.getText());
 
 295     appendText: function(text) {
 
 296         this.nativeNode.data = this.nativeNode.data + text;
 
 297         this.triggerTextChangeEvent();
 
 300     prependText: function(text) {
 
 301         this.nativeNode.data = text + this.nativeNode.data;
 
 302         this.triggerTextChangeEvent();
 
 305     wrapWith: function(desc) {
 
 306         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
 
 307             return this.document._wrapText({
 
 308                 inside: this.parent(),
 
 309                 textNodeIdx: this.parent().indexOf(this),
 
 310                 offsetStart: Math.min(desc.start, desc.end),
 
 311                 offsetEnd: Math.max(desc.start, desc.end),
 
 312                 _with: {tagName: desc.tagName, attrs: desc.attrs}
 
 315             return this.__super__.wrapWith.call(this, desc);
 
 319     split: function(params) {
 
 320         var parentElement = this.parent(),
 
 322             succeedingChildren = [],
 
 323             prefix = this.getText().substr(0, params.offset),
 
 324             suffix = this.getText().substr(params.offset);
 
 326         parentElement.contents().forEach(function(child) {
 
 328                 succeedingChildren.push(child);
 
 330             if(child.sameNode(this)) {
 
 335         if(prefix.length > 0) {
 
 336             this.setText(prefix);
 
 343         parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
 
 344         var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
 
 345         parentElement.after(newElement);
 
 347         succeedingChildren.reverse().forEach(function(child) {
 
 348             newElement.prepend(child);
 
 350         if(suffix.length > 0) {
 
 351             newElement.prepend({text: suffix});
 
 354         return {first: parentElement, second: newElement};
 
 357     divideWithElementNode: function(node, params) {
 
 358         var insertion = this.getNodeInsertion(node),
 
 359             myText = this.getText();
 
 361         if(params.offset === myText.length) {
 
 362             return this.after(node);
 
 364         if(params.offset === 0) {
 
 365             return this.before(node);
 
 368         var lhsText = myText.substr(0, params.offset),
 
 369             rhsText = myText.substr(params.offset),
 
 370             rhsTextNode = this.document.createDocumentNode({text: rhsText});
 
 372         this.setText(lhsText);
 
 373         this.after(insertion.ofNode);
 
 374         insertion.ofNode.after(rhsTextNode);
 
 375         return insertion.ofNode;
 
 379 var documentTransformations = {
 
 380     wrapNodes: function(params) {
 
 381         if(!(params.node1.parent().sameNode(params.node2.parent()))) {
 
 382             throw new Error('Wrapping non-sibling nodes not supported.');
 
 385         var parent = params.node1.parent(),
 
 386             parentContents = parent.contents(),
 
 387             wrapper = this.createDocumentNode({
 
 388                 tagName: params._with.tagName,
 
 389                 attrs: params._with.attrs}),
 
 390             idx1 = parent.indexOf(params.node1),
 
 391             idx2 = parent.indexOf(params.node2);
 
 399         var insertingMethod, insertingTarget;
 
 401             insertingMethod = 'prepend';
 
 402             insertingTarget = parent;
 
 404             insertingMethod = 'after';
 
 405             insertingTarget = parentContents[idx1-1];
 
 408         for(var i = idx1; i <= idx2; i++) {
 
 409             if(!parentContents[i].getProperty('describesParent')) {
 
 410                 wrapper.append(parentContents[i].detach());
 
 414         insertingTarget[insertingMethod](wrapper);
 
 418     _wrapText: function(params) {
 
 419         params = _.extend({textNodeIdx: 0}, params);
 
 420         if(typeof params.textNodeIdx === 'number') {
 
 421             params.textNodeIdx = [params.textNodeIdx];
 
 424         var contentsInside = params.inside.contents(),
 
 425             idx1 = Math.min.apply(Math, params.textNodeIdx),
 
 426             idx2 = Math.max.apply(Math, params.textNodeIdx),
 
 427             textNode1 = contentsInside[idx1],
 
 428             textNode2 = contentsInside[idx2],
 
 429             sameNode = textNode1.sameNode(textNode2),
 
 430             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
 
 431             prefixInside = textNode1.getText().substr(params.offsetStart),
 
 432             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
 
 433             suffixOutside = textNode2.getText().substr(params.offsetEnd)
 
 436         if(!(textNode1.parent().sameNode(textNode2.parent()))) {
 
 437             throw new Error('Wrapping text in non-sibling text nodes not supported.');
 
 440         var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
 
 441         textNode1.after(wrapperElement);
 
 444         if(prefixOutside.length > 0) {
 
 445             wrapperElement.before({text:prefixOutside});
 
 448             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
 
 449             wrapperElement.append({text: core});
 
 452             if(prefixInside.length > 0) {
 
 453                 wrapperElement.append({text: prefixInside});
 
 455             for(var i = idx1 + 1; i < idx2; i++) {
 
 456                 if(!contentsInside[i].getProperty('describesParent')) {
 
 457                     wrapperElement.append(contentsInside[i]);
 
 460             if(suffixInside.length > 0) {
 
 461                 wrapperElement.append({text: suffixInside});
 
 464         if(suffixOutside.length > 0) {
 
 465             wrapperElement.after({text: suffixOutside});
 
 467         return wrapperElement;
 
 469     replaceRoot: function(node) {
 
 470         var insertion = this.getNodeInsertion(node);
 
 472         this._defineDocumentProperties(insertion.ofNode._$);
 
 473         insertion.ofNode.triggerChangeEvent('nodeAdded');
 
 474         return insertion.ofNode;
 
 476     deleteText: function(params) {
 
 477         var ptr, next, nextNext, toDetach, middle, text;
 
 479         if(params.from.node.sameNode(params.to.node)) {
 
 480             ptr = params.from.node;
 
 481             text = ptr.getText();
 
 482             ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
 
 486         // Both edge text nodes need to be edited before anything else happen in case that
 
 487         // they get merged when detaching content between them.
 
 488         params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
 
 489         params.to.node.setText(params.to.node.getText().substr(params.to.offset));
 
 491         ptr = params.from.node;
 
 494         while(next || ptr.parent()) {
 
 496                 if(next.sameNode(params.to.node)) {
 
 499                 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
 
 505                     nextNext = next ? next.next() : null;
 
 506                     toDetach.detach({normalizeStrategy: (next && next.sameNode(params.to.node)) ? 'merge' : 'detach-right'});
 
 507                     if(next && !next.isInDocument()) {
 
 517         if(!this.containsNode(params.to.node)) {
 
 518             // The end node was merged during detaching nodes above - there is nothing more left to do.
 
 522         ptr = middle.contents()[0];
 
 523         while(ptr && !ptr.sameNode(params.to.node)) {
 
 524             if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
 
 525                 ptr = ptr.contents()[0];
 
 537         transformations: documentTransformations
 
 540         transformations: documentNodeTransformations
 
 543         transformations: elementNodeTransformations
 
 546         transformations: textNodeTransformations