editor: wip full backspace/insert support
[fnpeditor.git] / src / smartxml / core.js
index 867353d..f907a20 100644 (file)
@@ -3,30 +3,49 @@ define(function(require) {
 'use strict';
 /* globals Node */
 
-var _ = require('libs/underscore'),
-    TEXT_NODE = Node.TEXT_NODE;
+var _ = require('libs/underscore');
 
 
 var INSERTION = function(implementation) {
-    var toret = function(node) {
+    var toret = function(node, options) {
         var insertion = this.getNodeInsertion(node),
             nodeWasContained = this.document.containsNode(insertion.ofNode),
-            nodeParent;
-        if(!(this.document.containsNode(this))) {
+            nodeParent,
+            returned;
+        options = options || {};
+        if(!(this.document.containsNode(this)) || !insertion.insertsNew) {
             nodeParent = insertion.ofNode.parent();
         }
-        implementation.call(this, insertion.ofNode.nativeNode);
-        this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
-        return insertion.ofNode;
+        if(!insertion.insertsNew && insertion.ofNode.isSurroundedByTextNodes()) {
+            var prev = insertion.ofNode.prev(),
+                next = insertion.ofNode.next();
+            prev.setText(prev.getText()+next.getText());
+            next.detach();
+        }
+        returned = implementation.call(this, insertion.ofNode);
+        if(!options.silent && returned && returned.sameNode(insertion.ofNode)) {
+            if(!insertion.insertsNew) {
+                this.triggerChangeEvent('nodeDetached', {node: insertion.ofNode, parent: nodeParent, move: true});
+            }
+            this.triggerChangeEvent('nodeAdded', {node: insertion.ofNode, move: !insertion.insertsNew}, nodeParent, nodeWasContained);
+        }
+        return returned;
     };
     return toret;
 };
 
 var documentNodeTransformations = {
     detach: function() {
-        var parent = this.parent();
+        var parent = this.parent(),
+            existed = this.document.containsNode(this);
         this._$.detach();
-        this.triggerChangeEvent('nodeDetached', {parent: parent});
+        if(existed) {
+            this.triggerChangeEvent('nodeDetached', {parent: parent});
+            if(!parent) {
+                // This was the root of the document
+                this.document._defineDocumentProperties(null);
+            }
+        }
         return this;
     },
 
@@ -35,23 +54,48 @@ var documentNodeTransformations = {
         if(this.isRoot()) {
             return this.document.replaceRoot(node);
         }
-        toret = this.after(node);
-        this.detach();
-        return toret;
+        if(this.parent()) {
+            toret = this.after(node);
+            this.detach();
+            return toret;
+        }
+        throw new Error('Cannot replace node without a parent.');
     },
 
-    after: INSERTION(function(nativeNode) {
-        return this._$.after(nativeNode);
+    after: INSERTION(function(node) {
+        if(this.isRoot()) {
+            return;
+        }
+        var next = this.next();
+
+        if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            next.setText(node.getText() + next.getText());
+            node.detach();
+            return next;
+        }
+        this._$.after(node.nativeNode);
+        return node;
     }),
 
-    before: INSERTION(function(nativeNode) {
-        return this._$.before(nativeNode);
+    before: INSERTION(function(node) {
+        if(this.isRoot()) {
+            return;
+        }
+        var prev = this.prev();
+        if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            prev.setText(prev.getText() + node.getText());
+            node.detach();
+            return prev;
+        }
+        this._$.before(node.nativeNode);
+        return node;
     }),
 
     wrapWith: function(node) {
         var insertion = this.getNodeInsertion(node);
-        if(this.parent()) {
-            this.before(insertion.ofNode);
+
+        if(this.parent() || this.isRoot()) {
+            this.replaceWith(insertion.ofNode);
         }
         insertion.ofNode.append(this);
         return insertion.ofNode;
@@ -76,37 +120,44 @@ var documentNodeTransformations = {
 
 var elementNodeTransformations = {
 
-    detach: function() {
+    detach: function(params) {
         var next;
-        if(this.parent() && this.isSurroundedByTextElements()) {
-            next = this.next();
-            this.prev().appendText(next.getText());
-            next.detach();
+        params = _.extend({
+            normalizeStrategy: 'merge'
+        }, params);
+
+        if(this.parent() && this.isSurroundedByTextNodes()) {
+            if(params.normalizeStrategy === 'detach-left') {
+                this.prev().detach();
+            } else if(params.normalizeStrategy === 'detach-right') {
+                this.next().detach();
+            } else if(params.normalizeStrategy === 'merge') {
+                next = this.next();
+                this.prev().appendText(next.getText());
+                next.detach();
+            } else {
+                throw new Error('unknown normalize strategy for detach');
+            }
         }
         return this.__super__.detach();
     },
 
     setTag: function(tagName) {
-        var node = this.document.createDocumentNode({tagName: tagName}),
-            oldTagName = this.getTagName(),
-            myContents = this._$.contents();
+        var node = this.document.createDocumentNode({tagName: tagName});
 
         this.getAttrs().forEach(function(attribute) {
-            node.setAttr(attribute.name, attribute.value, true);
+            node.setAttr(attribute.name, attribute.value);
         });
-        node.setData(this.getData());
 
-        if(this.sameNode(this.document.root)) {
-            this.document._defineDocumentProperties(node._$);
-        }
+        this.contents().forEach(function(child) {
+            node.append(child);
+        });
 
-        /* TODO: This invalidates old references to this node. Caching instances on nodes would fix this. */
-        this._$.replaceWith(node._$);
-        this._setNativeNode(node._$[0]);
-        this._$.append(myContents);
-        this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
-    },
+        node.setData(this.getData());
 
+        this.replaceWith(node);
+        return node;
+    },
 
     setAttr: function(name, value, silent) {
         var oldVal = this.getAttr(name);
@@ -116,12 +167,28 @@ var elementNodeTransformations = {
         }
     },
 
-    append: INSERTION(function(nativeNode) {
-        this._$.append(nativeNode);
+    append: INSERTION(function(node) {
+        var last = _.last(this.contents());
+        if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            last.setText(last.getText() + node.getText());
+            node.detach();
+            return last;
+        } else {
+            this._$.append(node.nativeNode);
+            return node;
+        }
     }),
 
-    prepend: INSERTION(function(nativeNode) {
-        this._$.prepend(nativeNode);
+    prepend: INSERTION(function(node) {
+        var first = this.contents()[0];
+        if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+            first.setText(node.getText() + first.getText());
+            node.detach();
+            return first;
+        } else {
+            this._$.prepend(node.nativeNode);
+            return node;
+        }
     }),
 
     insertAtIndex: function(nativeNode, index) {
@@ -139,46 +206,37 @@ var elementNodeTransformations = {
             return;
         }
 
+        this.contents()
+            .filter(function(child) {
+                return child.getProperty('describesParent');
+            }.bind(this))
+            .forEach(function(child) {
+                child.detach();
+            });
+
         var myContents = this.contents(),
             myIdx = parent.indexOf(this);
 
-
         if(myContents.length === 0) {
             return this.detach();
         }
 
-        var prev = this.prev(),
-            next = this.next(),
-            moveLeftRange, moveRightRange, leftMerged;
-
-        if(prev && (prev.nodeType === TEXT_NODE) && (myContents[0].nodeType === TEXT_NODE)) {
-            prev.appendText(myContents[0].getText());
-            myContents[0].detach();
-            moveLeftRange = true;
-            leftMerged = true;
-        } else {
-            leftMerged = false;
-        }
-
-        if(!(leftMerged && myContents.length === 1)) {
-            var lastContents = _.last(myContents);
-            if(next && (next.nodeType === TEXT_NODE) && (lastContents.nodeType === TEXT_NODE)) {
-                next.prependText(lastContents.getText());
-                lastContents.detach();
-                moveRightRange = true;
-            }
-        }
-
-        var childrenLength = this.contents().length;
+        var childrenLength = this.contents().length,
+            first = true,
+            shiftRange = false;
         this.contents().forEach(function(child) {
-            this.before(child);
+            var returned = this.before(child);
+            if(first && !(returned.sameNode(child))) {
+                shiftRange = true;
+                first = false;
+            }
         }.bind(this));
 
         this.detach();
 
         return {
-            element1: parent.contents()[myIdx + (moveLeftRange ? -1 : 0)],
-            element2: parent.contents()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
+            element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
+            element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
         };
     },
 
@@ -188,9 +246,50 @@ var elementNodeTransformations = {
 };
 
 var textNodeTransformations = {
-    setText: function(text) {
-        this.nativeNode.data = text;
-        this.triggerTextChangeEvent();
+    setText: {
+        impl: function(t, text) {
+            t.oldText = this.getText();
+            this.nativeNode.data = text;
+            this.triggerTextChangeEvent();
+        },
+        undo: function(t) {
+            this.setText(t.oldText);
+        }
+    },
+
+    before: INSERTION(function(node) {
+        if(node.nodeType === Node.TEXT_NODE) {
+            this.prependText(node.getText());
+            node.detach();
+            return this;
+        } else {
+            return this.__super__.before(node, {silent:true});
+        }
+    }),
+
+    after: INSERTION(function(node) {
+        if(node.nodeType === Node.TEXT_NODE) {
+            this.appendText(node.getText());
+            node.detach();
+            return this;
+        } else {
+            return this.__super__.after(node, {silent:true});
+        }
+    }),
+
+    append: function(node) {
+        if(node.nodeType === Node.TEXT_NODE) {
+            this.appendText(node.getText());
+            node.detach();
+            return this;
+        }
+    },
+    prepend: function(node) {
+        if(node.nodeType === Node.TEXT_NODE) {
+            this.prependText(node.getText());
+            node.detach();
+            return this;
+        }
     },
 
     appendText: function(text) {
@@ -245,12 +344,12 @@ var textNodeTransformations = {
         var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
         parentElement.after(newElement);
 
+        succeedingChildren.reverse().forEach(function(child) {
+            newElement.prepend(child);
+        });
         if(suffix.length > 0) {
-            newElement.append({text: suffix});
+            newElement.prepend({text: suffix});
         }
-        succeedingChildren.forEach(function(child) {
-            newElement.append(child);
-        });
 
         return {first: parentElement, second: newElement};
     },
@@ -307,7 +406,9 @@ var documentTransformations = {
         }
 
         for(var i = idx1; i <= idx2; i++) {
-            wrapper.append(parentContents[i].detach());
+            if(!parentContents[i].getProperty('describesParent')) {
+                wrapper.append(parentContents[i].detach());
+            }
         }
 
         insertingTarget[insertingMethod](wrapper);
@@ -352,7 +453,9 @@ var documentTransformations = {
                 wrapperElement.append({text: prefixInside});
             }
             for(var i = idx1 + 1; i < idx2; i++) {
-                wrapperElement.append(contentsInside[i]);
+                if(!contentsInside[i].getProperty('describesParent')) {
+                    wrapperElement.append(contentsInside[i]);
+                }
             }
             if(suffixInside.length > 0) {
                 wrapperElement.append({text: suffixInside});
@@ -369,6 +472,63 @@ var documentTransformations = {
         this._defineDocumentProperties(insertion.ofNode._$);
         insertion.ofNode.triggerChangeEvent('nodeAdded');
         return insertion.ofNode;
+    },
+    deleteText: function(params) {
+        var ptr, next, nextNext, toDetach, middle, text;
+
+        if(params.from.node.sameNode(params.to.node)) {
+            ptr = params.from.node;
+            text = ptr.getText();
+            ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
+            return;
+        }
+
+        // Both edge text nodes need to be edited before anything else happen in case that
+        // they get merged when detaching content between them.
+        params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
+        params.to.node.setText(params.to.node.getText().substr(params.to.offset));
+
+        ptr = params.from.node;
+        next = ptr.next();
+
+        while(next || ptr.parent()) {
+            if(next) {
+                if(next.sameNode(params.to.node)) {
+                    return;
+                }
+                else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
+                    middle = next;
+                    break;
+                } else {
+                    toDetach = next;
+                    next = next.next();
+                    nextNext = next ? next.next() : null;
+                    toDetach.detach({normalizeStrategy: (next && next.sameNode(params.to.node)) ? 'merge' : 'detach-right'});
+                    if(next && !next.isInDocument()) {
+                        next = nextNext;
+                    }
+                }
+            } else {
+                ptr = ptr.parent();
+                next = ptr.next();
+            }
+        }
+
+        if(!this.containsNode(params.to.node)) {
+            // The end node was merged during detaching nodes above - there is nothing more left to do.
+            return;
+        }
+
+        ptr = middle.contents()[0];
+        while(ptr && !ptr.sameNode(params.to.node)) {
+            if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
+                ptr = ptr.contents()[0];
+                continue;
+            } else {
+                ptr = ptr.next();
+                ptr.prev().detach();
+            }
+        }
     }
 };