X-Git-Url: https://git.mdrn.pl/fnpeditor.git/blobdiff_plain/097ad6254d6b36c226072b0c7bdcaa5f945dc483..6b06b64b40cd570ab02227cc913a5204d9562f37:/src/smartxml/core.js?ds=sidebyside diff --git a/src/smartxml/core.js b/src/smartxml/core.js index 867353d..7326507 100644 --- a/src/smartxml/core.js +++ b/src/smartxml/core.js @@ -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,52 +120,87 @@ var documentNodeTransformations = { var elementNodeTransformations = { - detach: function() { - var next; - if(this.parent() && this.isSurroundedByTextElements()) { - next = this.next(); - this.prev().appendText(next.getText()); - next.detach(); + detach: function(params) { + var next, prev; + + params = params || {}; + + if(!params.normalizeStrategy) { + params.normalizeStrategy = 'merge'; + } + + 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(); + prev = this.prev(); + params.ret = { + mergedTo: prev, + previousLen: prev.getText().length + }; + 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); - this._$.attr(name, value); + if(_.isUndefined(value)) { + this._$.removeAttr(name); + } else { + this._$.attr(name, value); + } if(!silent) { this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value}); } }, - 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 +218,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 +258,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,14 +356,14 @@ 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}; + return {first: parentElement, second: newElement, created: newElement}; }, divideWithElementNode: function(node, params) { @@ -307,7 +418,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 +465,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 +484,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(); + } + } } };