define([
'libs/jquery',
'libs/underscore',
-'./transformations',
'libs/text!./templates/main.html',
'libs/text!./templates/item.html'
-], function($, _, transformations, mainTemplate, itemTemplate) {
+], function($, _, mainTemplate, itemTemplate) {
'use strict';
return function(sandbox) {
+ var currentNode,
+ adding = false;
var view = {
node: $(_.template(mainTemplate)()),
var metaTable = this.metaTable = this.node.find('table');
this.node.find('.rng-module-metadataEditor-addBtn').click(function() {
- var newRow = view._addMetaRow('', '');
- $(newRow.find('td div')[0]).focus();
- sandbox.publish('metadataChanged', view.getMetadata());
+ adding = true;
+ currentNode.getMetadata().add('','');
});
this.metaTable.on('click', '.rng-visualEditor-metaRemoveBtn', function(e) {
- $(e.target).closest('tr').remove();
- sandbox.publish('metadataChanged', view.getMetadata());
+ $(e.target).closest('tr').data('row').remove();
});
this.metaTable.on('keydown', '[contenteditable]', function(e) {
}
});
-
- var onKeyUp = function(e) {
+ this.metaTable.on('keyup', '[contenteditable]', _.throttle(function(e) {
if(e.which !== 13) {
- sandbox.publish('metadataChanged', view.getMetadata());
+ var editable = $(e.target),
+ toSet = editable.text(),
+ row = editable.parents('tr').data('row'),
+ isKey = _.last(editable.attr('class').split('-')) === 'metaItemKey',
+ method = isKey ? 'setKey' : 'setValue';
+ row[method](toSet);
}
- };
- this.metaTable.on('keyup', '[contenteditable]', _.throttle(onKeyUp, 500));
- },
- getMetadata: function() {
- var toret = {};
- this.node.find('tr').each(function() {
- var inputs = $(this).find('td [contenteditable]');
- var key = $(inputs[0]).text();
- var value = $(inputs[1]).text();
- toret[key] = value;
- });
- return toret;
+ }, 500));
},
- setMetadata: function(metadata) {
- var view = this;
+ setMetadata: function(node) {
+ var view = this,
+ metadata = node.getMetadata();
this.metaTable.find('tr').remove();
- _.each(_.keys(metadata), function(key) {
- view._addMetaRow(key, metadata[key]);
+ metadata.forEach(function(row) {
+ view.addMetadataRow(row);
});
},
- _addMetaRow: function(key, value) {
- var newRow = $(_.template(itemTemplate)({key: key || '', value: value || ''}));
+ addMetadataRow: function(row) {
+ var newRow = $(_.template(itemTemplate)({key: row.getKey() || '', value: row.getValue() || ''}));
newRow.appendTo(this.metaTable);
+ newRow.data('row', row);
+ if(adding) {
+ $(newRow.find('td div')[0]).focus();
+ adding = false;
+ }
return newRow;
+ },
+ updateMetadataRow: function(row) {
+ this._getRowTr(row, function(tr) {
+ var tds = tr.find('td > div'),
+ keyTd = $(tds[0]),
+ valueTd = $(tds[1]);
+
+ if(keyTd.text() !== row.getKey()) {
+ keyTd.text(row.getKey());
+ }
+ if(valueTd.text() !== row.getValue()) {
+ valueTd.text(row.getValue());
+ }
+ });
+ },
+ removeMetadataRow: function(row) {
+ this._getRowTr(row, function(tr) {
+ tr.remove();
+ });
+ },
+ _getRowTr: function(row, callback) {
+ this.metaTable.find('tr').each(function() {
+ var tr = $(this);
+ if(tr.data('row') === row) {
+ callback(tr);
+ return false;
+ }
+ });
}
};
start: function() {
sandbox.publish('ready');
},
- setDocument: function(xml) {
- view.setMetadata(transformations.getMetadata(xml));
- sandbox.publish('metadataSet');
+ setDocument: function(document) {
+ document.on('change', function(event) {
+ if(event.type === 'metadataAdded' && event.meta.node.sameNode(currentNode)) {
+ view.addMetadataRow(event.meta.row);
+ }
+ if(event.type === 'metadataChanged' && event.meta.node.sameNode(currentNode)) {
+ view.updateMetadataRow(event.meta.row);
+ }
+ if(event.type === 'metadataRemoved' && event.meta.node.sameNode(currentNode)) {
+ view.removeMetadataRow(event.meta.row);
+ }
+ });
},
- getMetadata: function() {
- return transformations.getXML(view.getMetadata());
+ setNodeElement: function(node) {
+ if(currentNode && currentNode.sameNode(node)) {
+ return;
+ }
+ currentNode = node;
+ view.setMetadata(node);
},
getView: function() {
return view.node;
- },
- attachMetadata: function(document) {
- var toret = $('<div>');
- toret.append($(document));
- var meta = $('<metadata></metadata>\n').append(transformations.getXML(view.getMetadata()));
-
- var metadata = toret.find('metadata');
- if(metadata.length === 0) {
- var section = toret.find('section');
- section = section.length ? $(section[0]) : null;
- if(section) {
- section.prepend(meta);
- }
- } else {
- metadata.replaceWith(meta);
- }
- return toret.html();
}
-
};
};
+++ /dev/null
-define(['libs/jquery', 'libs/underscore'], function($, _) {
-
- 'use strict';
-
- return {
- getMetadata: function(xml) {
- var toret = {};
- $(xml).find('metadata').children().each(function() {
- var node = $(this);
- toret[this.nodeName.split(':')[1].toLowerCase()] = node.text();
- });
- return toret;
- },
- getXML: function(metadata) {
- var meta = $('<metadata></metadata>\n');
- _.each(_.keys(metadata), function(key) {
- meta.append('\n\t<dc:'+key+'>' + metadata[key] + '</dc:'+key+'>');
- });
- meta.append('\n');
- /* globals vkbeautify */
- return vkbeautify.xml(meta.html());
- }
- };
-
-});
\ No newline at end of file
sandbox.getModule('nodeFamilyTree').setElement(nodeElement);
sandbox.getModule('nodeBreadCrumbs').setNodeElement(nodeElement);
sandbox.getModule('documentToolbar').setNodeElement(nodeElement);
+ sandbox.getModule('metadataEditor').setNodeElement(nodeElement);
},
updateCurrentTextElement: function(textElement) {
sandbox.getModule('nodeFamilyTree').setElement(textElement);
},
clone: function() {
- var clone = this._$.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]);
},
};
-var Document = function(xml) {
- this.loadXML(xml);
+var Document = function(xml, extensions) {
this.undoStack = [];
this.redoStack = [];
this._transactionStack = [];
this._elementNodeTransformations = {};
this.registerExtension(coreTransformations);
+
+ (extensions || []).forEach(function(extension) {
+ this.registerExtension(extension);
+ }.bind(this));
+ this.loadXML(xml);
};
$.extend(Document.prototype, Backbone.Events, {
});
});
+ it('can be cloned with its contents and its contents data', function() {
+ var doc = getDocumentFromXML('<root><div></div></root>'),
+ root = doc.root,
+ div = root.contents()[0];
+
+ var ClonableObject = function(arg) {
+ this.arg = arg;
+ };
+ ClonableObject.prototype.clone = function() {
+ return new ClonableObject(this.arg);
+ };
+
+ div.setData('key', 'value');
+ div.setData('clonableObject', new ClonableObject('test'));
+
+ var rootClone = root.clone(),
+ divClone = rootClone.contents()[0],
+ stringClone = divClone.getData('key'),
+ objClone = divClone.getData('clonableObject');
+
+ expect(stringClone).to.equal('value');
+ expect(objClone.arg).to.equal('test', 'clonable object got copied');
+ expect(objClone !== div.getData('clonableObject')).to.be.equal(true, 'copy of the clonable object is a new object');
+ });
+
it('knows its path in the document tree', function() {
var doc = getDocumentFromXML('<root><a><b><c></c>text</b></a></root>'),
root = doc.root,
--- /dev/null
+define(function(require) {
+
+'use strict';
+
+var _ = require('libs/underscore'),
+ smartxmlTransformations = require('smartxml/transformations'),
+ metadataKey = 'wlxml.metadata';
+
+
+var Row = function(key, value, metadata) {
+ this.key = key || '';
+ this.value = value || '';
+ this.metadata = metadata;
+};
+
+_.extend(Row.prototype, {
+ ChangeProperty: smartxmlTransformations.createContextTransformation({
+ impl: function(t, rowIndex, propName, value) {
+ var row = this.getMetadata().at(rowIndex);
+ t.rowIndex = rowIndex;
+ t.propName = propName;
+ t.oldValue = row[propName];
+ row[propName] = value;
+ this.triggerChangeEvent('metadataChanged', {row:row});
+ },
+ undo: function(t) {
+ var row = this.getMetadata().at(t.rowIndex);
+ row[t.propName] = t.oldValue;
+ this.triggerChangeEvent('metadataChanged', {row:row});
+ }
+ }),
+
+ setKey: function(key) {
+ return this.metadata.node.transform(this.ChangeProperty, [this.getIndex(), 'key', key]);
+ },
+ getKey: function() {
+ return this.key;
+ },
+ setValue: function(value) {
+ return this.metadata.node.transform(this.ChangeProperty, [this.getIndex(), 'value', value]);
+ },
+ getValue: function() {
+ return this.value;
+ },
+ remove: function() {
+ this.metadata.remove(this);
+ },
+ getIndex: function() {
+ return this.metadata.indexOf(this);
+ }
+});
+
+
+var Metadata = function(node) {
+ this._rows = [];
+ Object.defineProperty(this, 'length', {
+ get: function() {
+ return this._rows.length;
+ }
+ });
+ this.node = node;
+};
+
+_.extend(Metadata.prototype, {
+ Add: smartxmlTransformations.createContextTransformation({
+ impl: function(t, rowDesc) {
+ var metadata = this.getMetadata(),
+ row = new Row(rowDesc.key, rowDesc.value, metadata);
+ metadata._rows.push(row);
+ t.rowIdx = row.getIndex();
+ this.triggerChangeEvent('metadataAdded', {row: row});
+ return row;
+ },
+ undo: function(t) {
+ this.getMetadata().at(t.rowIdx).remove();
+ }
+ }),
+
+ Remove: smartxmlTransformations.createContextTransformation({
+ impl: function(t, rowIdx) {
+ var metadata = this.getMetadata();
+ t.rowIdx = rowIdx;
+ t.row = metadata.at(rowIdx);
+ metadata._rows.splice(rowIdx, 1);
+ this.triggerChangeEvent('metadataRemoved', {row: t.row});
+ },
+ undo: function(t) {
+ var metadata = this.getMetadata();
+ metadata._rows.splice(t.rowIdx, 0, new Row(t.row.getKey(), t.row.getValue(), metadata));
+ }
+ }),
+
+ forEach: function(callback) {
+ return this._rows.forEach(callback);
+ },
+ add: function(rowDesc, options) {
+ var row;
+ options = _.extend({undoable: true}, options);
+ if(options.undoable) {
+ return this.node.transform(this.Add, [rowDesc]);
+ } else {
+ row = new Row(rowDesc.key, rowDesc.value, this);
+ this._rows.push(row);
+ return row;
+ }
+ },
+ at: function(idx) {
+ return this._rows[idx];
+ },
+ indexOf: function(row) {
+ var idx = this._rows.indexOf(row);
+ if(idx !== -1) {
+ return idx;
+ }
+ return undefined;
+ },
+ remove: function(row) {
+ var idx = this.indexOf(row);
+ if(typeof idx !== 'undefined') {
+ this.node.transform(this.Remove, [idx]);
+ }
+ },
+ clone: function(node) {
+ var clone = new Metadata(node);
+ this._rows.forEach(function(row) {
+ clone._rows.push(new Row(row.getKey(), row.getValue(), clone));
+ });
+ return clone;
+ }
+});
+
+
+return {
+ elementNode: {
+ methods: {
+ getMetadata: function() {
+ if(!this.getData(metadataKey)) {
+ this.setData(metadataKey, new Metadata(this));
+ }
+ return this.getData(metadataKey);
+ }
+ }
+ }
+};
+
+});
+
--- /dev/null
+define(function(require) {
+
+'use strict';
+
+/* jshint multistr:true */
+/* globals describe, it */
+
+var chai = require('libs/chai'),
+ wlxml = require('wlxml/wlxml'),
+ expect = chai.expect,
+ $ = require('libs/jquery');
+
+
+var getDocumentFromXML = function(xml, options) {
+ return wlxml.WLXMLDocumentFromXML(xml, options || {});
+};
+
+
+describe('Metadata API', function() {
+ it('returns empty metadata for node without metadata', function() {
+ var doc = getDocumentFromXML('<section></section>');
+ expect(doc.root.getMetadata().length).to.equal(0);
+ });
+ it('allows to set metadata on an element node', function() {
+ var doc = getDocumentFromXML('<section></section>'),
+ metadata = doc.root.getMetadata();
+
+ var row = metadata.add({key: 'key', value: 'value'});
+ expect(metadata.length).to.equal(1);
+ expect(metadata.at(0)).to.equal(row);
+ expect(row.getKey()).to.equal('key');
+ expect(row.getValue()).to.equal('value');
+ });
+ it('allows to remove specific metadata row', function() {
+ var doc = getDocumentFromXML('<section><metadata><dc:key>value</dc:key><dc:key>value</dc:key></metadata></section>'),
+ metadata = doc.root.getMetadata();
+
+ expect(metadata.length).to.equal(2);
+ metadata.at(0).remove();
+ expect(metadata.length).to.equal(1);
+ expect(metadata.at(0).getValue()).to.equal('value');
+ });
+ it('reads node\'s metadata from source of its metadata child node', function() {
+ var doc = getDocumentFromXML('<section><metadata><dc:key>value</dc:key></metadata></section>'),
+ metadata = doc.root.getMetadata();
+ expect(metadata.length).to.equal(1);
+ expect(metadata.at(0).getKey()).to.equal('key');
+ expect(metadata.at(0).getValue()).to.equal('value');
+ });
+
+ it('serializes node\'s metadata to its metadata child node', function() {
+ var doc = getDocumentFromXML('<section></section>');
+
+ doc.root.getMetadata().add({key: 'key', value: 'value'});
+
+ var metadataNodes = $(doc.toXML()).children('metadata'),
+ keyNodes = metadataNodes.children();
+
+ expect(metadataNodes).to.have.length(1);
+ expect(keyNodes).to.have.length(1);
+ expect(keyNodes[0].tagName.toLowerCase()).to.equal('dc:key');
+ expect($(keyNodes[0]).text()).to.equal('value');
+ });
+
+ describe('Hiding metadata nodes from document api', function() {
+ it('hides metadata nodes from document api', function() {
+ var doc = getDocumentFromXML('<section><div></div><metadata><dc:key>value</dc:key></metadata><div></div></section>'),
+ rootContents = doc.root.contents();
+ expect(rootContents).to.have.length(2);
+ expect(rootContents[0].getTagName()).to.equal('div');
+ expect(rootContents[1].getTagName()).to.equal('div');
+ expect(rootContents[0].next().sameNode(rootContents[1])).to.equal(true);
+ expect(rootContents[1].prev().sameNode(rootContents[0])).to.equal(true);
+ });
+
+ it('merges adjacent text nodes', function() {
+ var doc = getDocumentFromXML('<section>Alice<metadata></metadata> has a cat</section>'),
+ contents = doc.root.contents();
+ expect(contents.length).to.equal(1);
+ expect(contents[0].getText()).to.equal('Alice has a cat');
+ });
+ });
+
+ describe('undo', function() {
+ it('undoes adding metadata', function() {
+ var doc = getDocumentFromXML('<section></section>'),
+ metadata = doc.root.getMetadata();
+ metadata.add({key: 'k', value: 'v'});
+ doc.undo();
+ expect(metadata.length).to.equal(0);
+ doc.redo();
+ expect(metadata.length).to.equal(1);
+ expect(metadata.at(0).getValue()).to.equal('v');
+ });
+ it('undoes changing metadata key', function() {
+ var doc = getDocumentFromXML('<section><metadata><dc:key>value</dc:key></metadata></section>'),
+ metadata = doc.root.getMetadata(),
+ row = metadata.at(0);
+
+ row.setKey('key2');
+ doc.undo();
+ expect(row.getKey()).to.equal('key');
+ doc.redo();
+ expect(row.getKey()).to.equal('key2');
+ });
+ it('undoes changing metadata value', function() {
+ var doc = getDocumentFromXML('<section><metadata><dc:key>value</dc:key></metadata></section>'),
+ metadata = doc.root.getMetadata(),
+ row = metadata.at(0);
+
+ row.setValue('value2');
+ doc.undo();
+ expect(row.getValue()).to.equal('value');
+ doc.redo();
+ expect(row.getValue()).to.equal('value2');
+ });
+ it('undoes removing metadata', function() {
+ var doc = getDocumentFromXML('<section><metadata><dc:key>value</dc:key></metadata></section>'),
+ metadata = doc.root.getMetadata(),
+ row = metadata.at(0);
+
+ row.remove();
+ doc.undo();
+ expect(metadata.length).to.equal(1, 'undo brought back metadata');
+ doc.redo();
+ expect(metadata.length).to.equal(0, 'redo removed metadata');
+ });
+ });
+
+
+
+});
+
+
+});
\ No newline at end of file
'libs/jquery',
'libs/underscore',
'smartxml/smartxml',
- 'smartxml/transformations'
-], function($, _, smartxml, transformations) {
+ 'smartxml/transformations',
+ 'wlxml/extensions/metadata/metadata'
+], function($, _, smartxml, transformations, metadataExtension) {
'use strict';
},
_getXMLDOMToDump: function() {
- var DOM = this._$.clone(true, true);
+ var DOM = this._$.clone(true, true),
+ doc = this.document;
DOM.find('*').addBack().each(function() {
var el = $(this),
data = el.data();
- var txt;
+ var txt, documentNode, metaNode;
if(data[formatter_prefix+ 'orig_before']) {
txt = idx > 0 && contents[idx-1].nodeType === Node.TEXT_NODE ? contents[idx-1] : null;
el.append(data[formatter_prefix+ 'orig_end']);
}
}
+
+
+ if(this.nodeType === Node.ELEMENT_NODE) {
+ documentNode = doc.createDocumentNode(this);
+ metaNode = $('<metadata>');
+ documentNode.getMetadata().forEach(function(row) {
+ metaNode.append('<dc:'+ row.key + '>' + row.value + '</dc:' + row.key + '>');
+ });
+ if(metaNode.children().length) {
+ $(this).prepend(metaNode);
+ }
+ }
+
});
+
+
return DOM;
}
});
WLXMLDocumentNode.prototype = Object.create(smartxml.DocumentNode.prototype);
var WLXMLDocument = function(xml, options) {
- smartxml.Document.call(this, xml);
- this.options = options;
-
this.classMethods = {};
this.classTransformations = {};
+ smartxml.Document.call(this, xml, [metadataExtension]);
+ this.options = options;
};
var formatter_prefix = '_wlxml_formatter_';
},
normalizeXML: function(nativeNode) {
+ var doc = this,
+ prefixLength = 'dc:'.length;
+
+ $(nativeNode).find('metadata').each(function() {
+ var metadataNode = $(this),
+ owner = doc.createDocumentNode(metadataNode.parent()[0]),
+ metadata = owner.getMetadata();
+
+ metadataNode.children().each(function() {
+ metadata.add({key: (this.tagName).toLowerCase().substr(prefixLength), value: $(this).text()}, {undoable: false});
+ });
+ metadataNode.remove();
+ });
+ nativeNode.normalize();
+
$(nativeNode).find(':not(iframe)').addBack().contents()
.filter(function() {return this.nodeType === Node.TEXT_NODE;})
.each(function() {
/* globals document */
el.replaceWith(document.createTextNode(text.transformed));
});
+
+
},
registerClassTransformation: function(Transformation, className) {
define([
+ 'libs/jquery',
'libs/chai',
'./wlxml.js'
-], function(chai, wlxml) {
+], function($, chai, wlxml) {
'use strict';
expect(testClassNode.object.testTransformation().sameNode(testClassNode)).to.equal(true, '1');
expect(testClassNode.object.testTransformation2().sameNode(testClassNode)).to.equal(true, '1');
});
-
-
});
-
});
});
\ No newline at end of file