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