X-Git-Url: https://git.mdrn.pl/fnpeditor.git/blobdiff_plain/fd6329db57fc3cc1cf2305da8d0127de4f2198e2..22e9258db6c11e6e0b042e142836cdb21399cc7b:/src/smartxml/smartxml.js diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index d74ec13..35c5299 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -10,9 +10,6 @@ define([ 'use strict'; /* globals Node */ -var TEXT_NODE = Node.TEXT_NODE; - - var DocumentNode = function(nativeNode, document) { if(!document) { @@ -36,18 +33,28 @@ $.extend(DocumentNode.prototype, { }, clone: function() { - var clone = this._$.clone(true, true); - // clone.find('*').addBack().each(function() { - // var n = $(this); - // if(n.data('canvasElement')) { - // n.data('canvasElement', $.extend(true, {}, n.data('canvasElement'))); - // n.data('canvasElement').$element = n.data('canvasElement').$element.clone(true, true); - // } - // }); + var clone = this._$.clone(true, true), + node = this; + clone.find('*').addBack().each(function() { + var el = this, + clonedData = $(this).data(); + + _.pairs(clonedData).forEach(function(pair) { + var key = pair[0], + value = pair[1]; + if(_.isFunction(value.clone)) { + clonedData[key] = value.clone(node.document.createDocumentNode(el)); + } + }); + }); return this.document.createDocumentNode(clone[0]); }, getPath: function(ancestor) { + if(!(this.document.containsNode(this))) { + return null; + } + var nodePath = [this].concat(this.parents()), toret, idx; ancestor = ancestor || this.document.root; @@ -59,7 +66,7 @@ $.extend(DocumentNode.prototype, { } }); - if(idx !== 'undefined') { + if(idx !== undefined) { nodePath = nodePath.slice(0, idx); } toret = nodePath.map(function(node) {return node.getIndex(); }); @@ -116,6 +123,9 @@ $.extend(DocumentNode.prototype, { var node = (metaData && metaData.node) ? metaData.node : this, event = new events.ChangeEvent(type, $.extend({node: node}, metaData || {})); if(type === 'nodeDetached' || this.document.containsNode(event.meta.node)) { + if(type === 'nodeMoved') { + event.meta.parent = origParent; + } this.document.trigger('change', event); } if((type === 'nodeAdded' || type === 'nodeMoved') && !this.document.containsNode(this) && nodeWasContained) { @@ -196,6 +206,10 @@ $.extend(ElementNode.prototype, { return toret; }, + containsNode: function(node) { + return node && (node.nativeNode === this.nativeNode || node._$.parents().index(this._$) !== -1); + }, + toXML: function() { var wrapper = $('
'); wrapper.append(this._getXMLDOMToDump()); @@ -228,17 +242,21 @@ $.extend(TextNode.prototype, { var parseXML = function(xml) { - return $($.trim(xml))[0]; + var toret = $($.trim(xml)); + if(!toret.length) { + throw new Error('Unable to parse XML: ' + xml); + } + return toret[0]; + }; var registerTransformation = function(desc, name, target) { var Transformation = transformations.createContextTransformation(desc, name); - //+ to sie powinna nazywac registerTransformationFromDesc or sth - //+ ew. spr czy nie override (tylko jesli powyzej sa prototypy to trudno do nich dojsc) - target[name] = function(args) { - var instance = this; + target[name] = function() { + var instance = this, + args = Array.prototype.slice.call(arguments, 0); return instance.transform(Transformation, args); - } + }; }; var registerMethod = function(methodName, method, target) { @@ -252,10 +270,10 @@ var registerMethod = function(methodName, method, target) { }; -var Document = function(xml) { - this.loadXML(xml); +var Document = function(xml, extensions) { this.undoStack = []; this.redoStack = []; + this._currentTransaction = null; this._transformationLevel = 0; this._nodeMethods = {}; @@ -264,6 +282,13 @@ var Document = function(xml) { this._nodeTransformations = {}; this._textNodeTransformations = {}; this._elementNodeTransformations = {}; + + this.registerExtension(coreTransformations); + + (extensions || []).forEach(function(extension) { + this.registerExtension(extension); + }.bind(this)); + this.loadXML(xml); }; $.extend(Document.prototype, Backbone.Events, { @@ -272,17 +297,25 @@ $.extend(Document.prototype, Backbone.Events, { createDocumentNode: function(from) { if(!(from instanceof Node)) { - if(from.text !== undefined) { - /* globals document */ - from = document.createTextNode(from.text); + if(typeof from === 'string') { + from = parseXML(from); + this.normalizeXML(from); } else { - var node = $('<' + from.tagName + '>'); - - _.keys(from.attrs || {}).forEach(function(key) { - node.attr(key, from.attrs[key]); - }); + if(from.text !== undefined) { + /* globals document */ + from = document.createTextNode(from.text); + } else { + if(!from.tagName) { + throw new Error('tagName missing'); + } + var node = $('<' + from.tagName + '>'); + + _.keys(from.attrs || {}).forEach(function(key) { + node.attr(key, from.attrs[key]); + }); - from = node[0]; + from = node[0]; + } } } var Factory, typeMethods, typeTransformations; @@ -298,25 +331,37 @@ $.extend(Document.prototype, Backbone.Events, { var toret = new Factory(from, this); _.extend(toret, this._nodeMethods); _.extend(toret, typeMethods); + _.extend(toret, this._nodeTransformations); _.extend(toret, typeTransformations); + + toret.__super__ = _.extend({}, this._nodeMethods, this._nodeTransformations); + _.keys(toret.__super__).forEach(function(key) { + toret.__super__[key] = _.bind(toret.__super__[key], toret); + }); + return toret; }, loadXML: function(xml, options) { options = options || {}; - defineDocumentProperties(this, $(parseXML(xml))); + this._defineDocumentProperties($(parseXML(xml))); + this.normalizeXML(this.dom); if(!options.silent) { this.trigger('contentSet'); } }, + normalizeXML: function(nativeNode) { + void(nativeNode); // noop + }, + toXML: function() { return this.root.toXML(); }, 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) { @@ -328,18 +373,21 @@ $.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]}; }, trigger: function() { - //console.log('trigger: ' + arguments[0] + (arguments[1] ? ', ' + arguments[1].type : '')); Backbone.Events.trigger.apply(this, arguments); }, @@ -378,9 +426,7 @@ $.extend(Document.prototype, Backbone.Events, { }, registerExtension: function(extension) { - //debugger; - var doc = this, - existingPropertyNames = _.values(this); + var doc = this; ['document', 'documentNode', 'elementNode', 'textNode'].forEach(function(dstName) { var dstExtension = extension[dstName]; @@ -406,11 +452,35 @@ $.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) { - //console.log('transform'); var toret, transformation; - // ref: odrebnie przygotowanie transformacji, odrebnie jej wykonanie (to pierwsze to analog transform z node) + if(!this._currentTransaction) { + return this.transaction(function() { + return this.transform(Transformation, args); + }, this); + } if(typeof Transformation === 'function') { transformation = new Transformation(this, this, args); @@ -419,31 +489,115 @@ $.extend(Document.prototype, Backbone.Events, { } if(transformation) { this._transformationLevel++; - toret = transformation.run(); - if(this._transformationLevel === 1) { - 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 = []; + } + } + ); + this._transformationLevel--; - //console.log('clearing redo stack'); - this.redoStack = []; return toret; } else { throw new Error('Transformation ' + transformation + ' doesn\'t exist!'); } }, undo: function() { - var transformation = this.undoStack.pop(); - if(transformation) { - transformation.undo(); - this.redoStack.push(transformation); + var transaction = this.undoStack.pop(), + doc = this, + transformations, stopAt; + + if(transaction) { + this._undoInProgress = true; + + // 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; + return true; //break + } + }); + if(stopAt !== undefined) { + // We will get away with undoing only this transformations as the one at stopAt reverses the whole document. + transformations = transformations.slice(0, stopAt+1); + } + } + + transformations.reverse(); + transformations.forEach(function(t) { + t.undo(); + }); + + this._undoInProgress = false; + this.redoStack.push(transaction); } }, redo: function() { - var transformation = this.redoStack.pop(); - if(transformation) { - transformation.run(); - this.undoStack.push(transformation); + var transaction = this.redoStack.pop(); + if(transaction) { + this._transformationLevel++; + transaction.transformations.forEach(function(t) { + t.run({beUndoable: true}); + }); + this._transformationLevel--; + this.undoStack.push(transaction); + } + }, + + startTransaction: function(metadata) { + if(this._currentTransaction) { + throw new Error('Nested transactions not supported!'); } + this._rollbackBackup = this.root.clone(); + this._currentTransaction = new Transaction([], metadata); + }, + + endTransaction: function() { + if(!this._currentTransaction) { + throw new Error('End of transaction requested, but there is no transaction in progress!'); + } + if(this._currentTransaction.hasTransformations()) { + this.undoStack.push(this._currentTransaction); + } + 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; + }, + + 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(); + return toret; }, getNodeByPath: function(path) { @@ -452,23 +606,42 @@ $.extend(Document.prototype, Backbone.Events, { toret = toret.contents()[idx]; }); return toret; + }, + + _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}); } }); -var defineDocumentProperties = function(doc, $document) { - Object.defineProperty(doc, 'root', {get: function() { - return doc.createDocumentNode($document[0]); - }, configurable: true}); - Object.defineProperty(doc, 'dom', {get: function() { - return $document[0]; - }, configurable: true}); +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; + } +}); return { documentFromXML: function(xml) { var doc = new Document(xml); - doc.registerExtension(coreTransformations); return doc; },