X-Git-Url: https://git.mdrn.pl/fnpeditor.git/blobdiff_plain/fcecb8e0ed56cd955b1d2bcbc12e2675f40f0243..5766035cdc88ebe92caab7af494e5e077235d5e0:/src/smartxml/smartxml.js
diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js
index d168e6d..c158bac 100644
--- a/src/smartxml/smartxml.js
+++ b/src/smartxml/smartxml.js
@@ -4,24 +4,36 @@ define([
'libs/backbone',
'smartxml/events',
'smartxml/transformations',
- 'smartxml/core'
-], function($, _, Backbone, events, transformations, coreTransformations) {
+ 'smartxml/core',
+ 'smartxml/fragments'
+], function($, _, Backbone, events, transformations, coreTransformations, fragments) {
'use strict';
/* globals Node */
+var privateKey = '_smartxml';
+
var DocumentNode = function(nativeNode, document) {
if(!document) {
throw new Error('undefined document for a node');
}
this.document = document;
+ this.object = {};
this._setNativeNode(nativeNode);
};
$.extend(DocumentNode.prototype, {
+ getProperty: function(propName) {
+ var toret = this.object[propName];
+ if(toret && _.isFunction(toret)) {
+ toret = toret.call(this);
+ }
+ return toret;
+ },
+
transform: function(Transformation, args) {
var transformation = new Transformation(this.document, this, args);
return this.document.transform(transformation);
@@ -38,7 +50,7 @@ $.extend(DocumentNode.prototype, {
clone.find('*').addBack().each(function() {
var el = this,
clonedData = $(this).data();
-
+ $(el).removeData(privateKey);
_.pairs(clonedData).forEach(function(pair) {
var key = pair[0],
value = pair[1];
@@ -66,7 +78,7 @@ $.extend(DocumentNode.prototype, {
}
});
- if(idx !== 'undefined') {
+ if(idx !== undefined) {
nodePath = nodePath.slice(0, idx);
}
toret = nodePath.map(function(node) {return node.getIndex(); });
@@ -78,6 +90,14 @@ $.extend(DocumentNode.prototype, {
return this.document.root.sameNode(this);
},
+ isInDocument: function() {
+ return this.document.containsNode(this);
+ },
+
+ isSiblingOf: function(node) {
+ return node && this.parent().sameNode(node.parent());
+ },
+
sameNode: function(otherNode) {
return !!(otherNode) && this.nativeNode === otherNode.nativeNode;
},
@@ -113,10 +133,18 @@ $.extend(DocumentNode.prototype, {
return myIdx < parentContents.length - 1 ? parentContents[myIdx+1] : null;
},
- isSurroundedByTextElements: function() {
- var prev = this.prev(),
- next = this.next();
- return prev && (prev.nodeType === Node.TEXT_NODE) && next && (next.nodeType === Node.TEXT_NODE);
+ isSurroundedByTextNodes: function() {
+ return this.isPrecededByTextNode() && this.isFollowedByTextNode();
+ },
+
+ isPrecededByTextNode: function() {
+ var prev = this.prev();
+ return prev && prev.nodeType === Node.TEXT_NODE;
+ },
+
+ isFollowedByTextNode: function() {
+ var next = this.next();
+ return next && next.nodeType === Node.TEXT_NODE;
},
triggerChangeEvent: function(type, metaData, origParent, nodeWasContained) {
@@ -125,7 +153,7 @@ $.extend(DocumentNode.prototype, {
if(type === 'nodeDetached' || this.document.containsNode(event.meta.node)) {
this.document.trigger('change', event);
}
- if((type === 'nodeAdded' || type === 'nodeMoved') && !this.document.containsNode(this) && nodeWasContained) {
+ if(type === 'nodeAdded' && !this.document.containsNode(this) && nodeWasContained) {
event = new events.ChangeEvent('nodeDetached', {node: node, parent: origParent});
this.document.trigger('change', event);
}
@@ -136,28 +164,41 @@ $.extend(DocumentNode.prototype, {
},
getIndex: function() {
+ var parent;
+
if(this.isRoot()) {
return 0;
}
- return this.parent().indexOf(this);
+
+ parent = this.parent();
+ return parent ? parent.indexOf(this) : undefined;
+ },
+
+ getNearestElementNode: function() {
+ return this.nodeType === Node.ELEMENT_NODE ? this : this.parent();
}
});
var ElementNode = function(nativeNode, document) {
DocumentNode.call(this, nativeNode, document);
+ $(nativeNode).data(privateKey, {node: this});
};
ElementNode.prototype = Object.create(DocumentNode.prototype);
$.extend(ElementNode.prototype, {
nodeType: Node.ELEMENT_NODE,
- setData: function(key, value) {
- if(value !== undefined) {
- this._$.data(key, value);
+ setData: function(arg1, arg2) {
+ if(arguments.length === 2) {
+ if(_.isUndefined(arg2)) {
+ this._$.removeData(arg1);
+ } else {
+ this._$.data(arg1, arg2);
+ }
} else {
this._$.removeData(_.keys(this._$.data()));
- this._$.data(key);
+ this._$.data(arg1);
}
},
@@ -165,7 +206,9 @@ $.extend(ElementNode.prototype, {
if(key) {
return this._$.data(key);
}
- return this._$.data();
+ var toret = _.clone(this._$.data());
+ delete toret[privateKey];
+ return toret;
},
getTagName: function() {
@@ -203,6 +246,36 @@ $.extend(ElementNode.prototype, {
return toret;
},
+ containsNode: function(node) {
+ return node && (node.nativeNode === this.nativeNode || node._$.parents().index(this._$) !== -1);
+ },
+
+ getFirstTextNode: function() {
+ return this._getTextNode('first');
+ },
+
+ getLastTextNode: function() {
+ return this._getTextNode('last');
+ },
+
+ _getTextNode: function(which) {
+ var contents = this.contents(),
+ toret;
+ if(which === 'last') {
+ contents = contents.reverse();
+ }
+ contents.some(function(node) {
+ if(node.nodeType === Node.TEXT_NODE) {
+ toret = node;
+ return true;
+ }
+ toret = node.getLastTextNode();
+ return !!toret;
+ });
+
+ return toret;
+ },
+
toXML: function() {
var wrapper = $('
');
wrapper.append(this._getXMLDOMToDump());
@@ -217,16 +290,42 @@ $.extend(ElementNode.prototype, {
var TextNode = function(nativeNode, document) {
DocumentNode.call(this, nativeNode, document);
+ this._data = Object.create({});
+ nativeNode.__smartxmlTextNodeInstance = this;
};
TextNode.prototype = Object.create(DocumentNode.prototype);
$.extend(TextNode.prototype, {
nodeType: Node.TEXT_NODE,
+ setData: function(arg1, arg2) {
+ if(arguments.length === 2) {
+ if(_.isUndefined(arg2)) {
+ delete this._data[arg1];
+ } else {
+ this._data[arg1] = arg2;
+ }
+ } else {
+ this._data = _.extend({}, arg1);
+ }
+ },
+
+ getData: function(key) {
+ if(key) {
+ return this._data[key];
+ }
+ return this._data;
+ },
+
getText: function() {
return this.nativeNode.data;
},
+
+ containsNode: function() {
+ return false;
+ },
+
triggerTextChangeEvent: function() {
var event = new events.ChangeEvent('nodeTextChange', {node: this});
this.document.trigger('change', event);
@@ -236,7 +335,7 @@ $.extend(TextNode.prototype, {
var parseXML = function(xml) {
var toret = $($.trim(xml));
- if(!toret.length) {
+ if(toret.length !== 1) {
throw new Error('Unable to parse XML: ' + xml);
}
return toret[0];
@@ -266,7 +365,7 @@ var registerMethod = function(methodName, method, target) {
var Document = function(xml, extensions) {
this.undoStack = [];
this.redoStack = [];
- this._transactionStack = [];
+ this._currentTransaction = null;
this._transformationLevel = 0;
this._nodeMethods = {};
@@ -284,12 +383,20 @@ var Document = function(xml, extensions) {
this.loadXML(xml);
};
-$.extend(Document.prototype, Backbone.Events, {
+$.extend(Document.prototype, Backbone.Events, fragments, {
ElementNodeFactory: ElementNode,
TextNodeFactory: TextNode,
createDocumentNode: function(from) {
- if(!(from instanceof Node)) {
+ var cached;
+
+ if(from instanceof Node) {
+ /* globals Text */
+ cached = from instanceof Text ? from.__smartxmlTextNodeInstance : ($(from).data(privateKey) || {}).node;
+ if(cached instanceof DocumentNode) {
+ return cached;
+ }
+ } else {
if(typeof from === 'string') {
from = parseXML(from);
this.normalizeXML(from);
@@ -354,7 +461,7 @@ $.extend(Document.prototype, Backbone.Events, {
},
containsNode: function(node) {
- return this.root && (node.nativeNode === this.root.nativeNode || node._$.parents().index(this.root._$) !== -1);
+ return this.root && this.root.containsNode(node);
},
getSiblingParents: function(params) {
@@ -366,13 +473,17 @@ $.extend(Document.prototype, Backbone.Events, {
return noSiblingParents;
}
- var i;
- for(i = 0; i < Math.min(parents1.length, parents2.length); i++) {
+ var stop = Math.min(parents1.length, parents2.length),
+ i;
+ for(i = 0; i < stop; i++) {
if(parents1[i].sameNode(parents2[i])) {
continue;
}
break;
}
+ if(i === stop) {
+ i--;
+ }
return {node1: parents1[i], node2: parents2[i]};
},
@@ -441,9 +552,36 @@ $.extend(Document.prototype, Backbone.Events, {
});
},
+ ifChanged: function(context, action, documentChangedHandler, documentUnchangedHandler) {
+ var hasChanged = false,
+ changeMonitor = function() {
+ hasChanged = true;
+ };
+
+ this.on('change', changeMonitor);
+ action.call(context);
+ this.off('change', changeMonitor);
+
+ if(hasChanged) {
+ if(documentChangedHandler) {
+ documentChangedHandler.call(context);
+ }
+ } else {
+ if(documentUnchangedHandler) {
+ documentUnchangedHandler.call(context);
+ }
+ }
+ },
+
transform: function(Transformation, args) {
var toret, transformation;
+ if(!this._currentTransaction) {
+ return this.transaction(function() {
+ return this.transform(Transformation, args);
+ }, {context: this});
+ }
+
if(typeof Transformation === 'function') {
transformation = new Transformation(this, this, args);
} else {
@@ -451,17 +589,20 @@ $.extend(Document.prototype, Backbone.Events, {
}
if(transformation) {
this._transformationLevel++;
- toret = transformation.run({beUndoable:this._transformationLevel === 1});
- if(this._transformationLevel === 1 && !this._undoInProgress) {
- if(this._transactionInProgress) {
- this._transactionStack.push(transformation);
- } else {
- this.undoStack.push(transformation);
+
+ this.ifChanged(
+ this,
+ function() {
+ toret = transformation.run({beUndoable:this._transformationLevel === 1});
+ },
+ function() {
+ if(this._transformationLevel === 1 && !this._undoInProgress) {
+ this._currentTransaction.pushTransformation(transformation);
+ this.redoStack = [];
+ }
}
- }
- if(!this._undoInProgress && this._transformationLevel === 1) {
- this.redoStack = [];
- }
+ );
+
this._transformationLevel--;
return toret;
} else {
@@ -469,24 +610,18 @@ $.extend(Document.prototype, Backbone.Events, {
}
},
undo: function() {
- var transformationObject = this.undoStack.pop(),
+ var transaction = this.undoStack.pop(),
doc = this,
transformations, stopAt;
- if(transformationObject) {
+ if(transaction) {
this._undoInProgress = true;
- if(_.isArray(transformationObject)) {
- // We will modify this array in a minute so make sure we work on a copy.
- transformations = transformationObject.slice(0);
- } else {
- // Lets normalize single transformation to a transaction containing one transformation.
- transformations = [transformationObject];
- }
+ // We will modify this array in a minute so make sure we work on a copy.
+ transformations = transaction.transformations.slice(0);
if(transformations.length > 1) {
// In case of real transactions we don't want to run undo on all of transformations if we don't have to.
- stopAt = undefined;
transformations.some(function(t, idx) {
if(!t.undo && t.getChangeRoot().sameNode(doc.root)) {
stopAt = idx;
@@ -505,45 +640,80 @@ $.extend(Document.prototype, Backbone.Events, {
});
this._undoInProgress = false;
- this.redoStack.push(transformationObject);
+ this.redoStack.push(transaction);
+ this.trigger('operationEnd');
}
},
redo: function() {
- var transformationObject = this.redoStack.pop(),
- transformations;
- if(transformationObject) {
+ var transaction = this.redoStack.pop();
+ if(transaction) {
this._transformationLevel++;
- transformations = _.isArray(transformationObject) ? transformationObject : [transformationObject];
- transformations.forEach(function(t) {
+ transaction.transformations.forEach(function(t) {
t.run({beUndoable: true});
});
this._transformationLevel--;
- this.undoStack.push(transformationObject);
+ this.undoStack.push(transaction);
+ this.trigger('operationEnd');
+
}
},
- startTransaction: function() {
- if(this._transactionInProgress) {
+ startTransaction: function(metadata) {
+ if(this._currentTransaction) {
throw new Error('Nested transactions not supported!');
}
- this._transactionInProgress = true;
+ this._rollbackBackup = this.root.clone();
+ this._currentTransaction = new Transaction([], metadata);
},
endTransaction: function() {
- if(!this._transactionInProgress) {
+ if(!this._currentTransaction) {
throw new Error('End of transaction requested, but there is no transaction in progress!');
}
- this._transactionInProgress = false;
- if(this._transactionStack.length) {
- this.undoStack.push(this._transactionStack);
- this._transactionStack = [];
+ if(this._currentTransaction.hasTransformations()) {
+ this.undoStack.push(this._currentTransaction);
+ this.trigger('operationEnd');
+ }
+ this._currentTransaction = null;
+ },
+
+ rollbackTransaction: function() {
+ if(!this._currentTransaction) {
+ throw new Error('Transaction rollback requested, but there is no transaction in progress!');
+ }
+ this.replaceRoot(this._rollbackBackup);
+ this._rollbackBackup = null;
+ this._currentTransaction = null;
+ this._transformationLevel = 0;
+ },
+
+ transaction: function(callback, params) {
+ var toret;
+ params = params || {};
+ this.startTransaction(params.metadata);
+ try {
+ toret = callback.call(params.context || this);
+ } catch(e) {
+ if(params.error) {
+ params.error(e);
+ }
+ this.rollbackTransaction();
+ return;
+ }
+ this.endTransaction();
+ if(params.success) {
+ params.success(toret);
}
+ return toret;
},
getNodeByPath: function(path) {
var toret = this.root;
- path.forEach(function(idx) {
+ path.some(function(idx) {
toret = toret.contents()[idx];
+ if(!toret) {
+ return true;
+ }
});
return toret;
},
@@ -551,11 +721,37 @@ $.extend(Document.prototype, Backbone.Events, {
_defineDocumentProperties: function($document) {
var doc = this;
Object.defineProperty(doc, 'root', {get: function() {
+ if(!$document) {
+ return null;
+ }
return doc.createDocumentNode($document[0]);
}, configurable: true});
Object.defineProperty(doc, 'dom', {get: function() {
+ if(!$document) {
+ return null;
+ }
return $document[0];
}, configurable: true});
+ },
+
+ createFragment: function(Type, params) {
+ if(!Type.prototype instanceof fragments.Fragment) {
+ throw new Error('Can\'t create a fragment: `Type` is not a valid Fragment');
+ }
+ return new Type(this, params);
+ }
+});
+
+var Transaction = function(transformations, metadata) {
+ this.transformations = transformations || [];
+ this.metadata = metadata;
+};
+$.extend(Transaction.prototype, {
+ pushTransformation: function(transformation) {
+ this.transformations.push(transformation);
+ },
+ hasTransformations: function() {
+ return this.transformations.length > 0;
}
});