From: Aleksander Ɓukasz Date: Fri, 17 Jan 2014 14:29:59 +0000 (+0100) Subject: Merge in metadata support into master X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/fcecb8e0ed56cd955b1d2bcbc12e2675f40f0243?hp=e1dee375c870a1771d87701c497b8c1734be6b8a Merge in metadata support into master Creating/editing/deleting metadata on arbitrary nodes --- diff --git a/src/editor/modules/metadataEditor/metadataEditor.js b/src/editor/modules/metadataEditor/metadataEditor.js index 623dee9..64a5c44 100644 --- a/src/editor/modules/metadataEditor/metadataEditor.js +++ b/src/editor/modules/metadataEditor/metadataEditor.js @@ -1,15 +1,16 @@ 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)()), @@ -18,14 +19,12 @@ return function(sandbox) { 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) { @@ -43,35 +42,62 @@ return function(sandbox) { } }); - - 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; + } + }); } }; @@ -81,34 +107,29 @@ return function(sandbox) { 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 = $('
'); - toret.append($(document)); - var meta = $('\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(); } - }; }; diff --git a/src/editor/modules/metadataEditor/transformations.js b/src/editor/modules/metadataEditor/transformations.js deleted file mode 100644 index 4bb3fdc..0000000 --- a/src/editor/modules/metadataEditor/transformations.js +++ /dev/null @@ -1,25 +0,0 @@ -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 = $('\n'); - _.each(_.keys(metadata), function(key) { - meta.append('\n\t' + metadata[key] + ''); - }); - meta.append('\n'); - /* globals vkbeautify */ - return vkbeautify.xml(meta.html()); - } - }; - -}); \ No newline at end of file diff --git a/src/editor/modules/rng/rng.js b/src/editor/modules/rng/rng.js index fc97f99..665d3a9 100644 --- a/src/editor/modules/rng/rng.js +++ b/src/editor/modules/rng/rng.js @@ -43,6 +43,7 @@ return function(sandbox) { 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); diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index acbe04d..d168e6d 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -33,7 +33,20 @@ $.extend(DocumentNode.prototype, { }, 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]); }, @@ -250,8 +263,7 @@ var registerMethod = function(methodName, method, target) { }; -var Document = function(xml) { - this.loadXML(xml); +var Document = function(xml, extensions) { this.undoStack = []; this.redoStack = []; this._transactionStack = []; @@ -265,6 +277,11 @@ var Document = function(xml) { this._elementNodeTransformations = {}; this.registerExtension(coreTransformations); + + (extensions || []).forEach(function(extension) { + this.registerExtension(extension); + }.bind(this)); + this.loadXML(xml); }; $.extend(Document.prototype, Backbone.Events, { diff --git a/src/smartxml/smartxml.test.js b/src/smartxml/smartxml.test.js index 57f11cc..0883ba7 100644 --- a/src/smartxml/smartxml.test.js +++ b/src/smartxml/smartxml.test.js @@ -86,6 +86,31 @@ describe('smartxml', function() { }); }); + it('can be cloned with its contents and its contents data', function() { + var doc = getDocumentFromXML('
'), + 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('text'), root = doc.root, diff --git a/src/wlxml/extensions/metadata/metadata.js b/src/wlxml/extensions/metadata/metadata.js new file mode 100644 index 0000000..b30b94c --- /dev/null +++ b/src/wlxml/extensions/metadata/metadata.js @@ -0,0 +1,147 @@ +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); + } + } + } +}; + +}); + diff --git a/src/wlxml/extensions/metadata/metadata.test.js b/src/wlxml/extensions/metadata/metadata.test.js new file mode 100644 index 0000000..422a371 --- /dev/null +++ b/src/wlxml/extensions/metadata/metadata.test.js @@ -0,0 +1,135 @@ +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('
'); + expect(doc.root.getMetadata().length).to.equal(0); + }); + it('allows to set metadata on an element node', function() { + var doc = getDocumentFromXML('
'), + 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('
valuevalue
'), + 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('
value
'), + 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('
'); + + 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('
value
'), + 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('
Alice has a cat
'), + 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('
'), + 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('
value
'), + 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('
value
'), + 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('
value
'), + 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 diff --git a/src/wlxml/wlxml.js b/src/wlxml/wlxml.js index 820b95f..fe0636d 100644 --- a/src/wlxml/wlxml.js +++ b/src/wlxml/wlxml.js @@ -2,8 +2,9 @@ define([ 'libs/jquery', 'libs/underscore', 'smartxml/smartxml', - 'smartxml/transformations' -], function($, _, smartxml, transformations) { + 'smartxml/transformations', + 'wlxml/extensions/metadata/metadata' +], function($, _, smartxml, transformations, metadataExtension) { 'use strict'; @@ -79,7 +80,8 @@ $.extend(WLXMLElementNode.prototype, smartxml.ElementNode.prototype, { }, _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), @@ -89,7 +91,7 @@ $.extend(WLXMLElementNode.prototype, smartxml.ElementNode.prototype, { 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; @@ -119,8 +121,23 @@ $.extend(WLXMLElementNode.prototype, smartxml.ElementNode.prototype, { el.append(data[formatter_prefix+ 'orig_end']); } } + + + if(this.nodeType === Node.ELEMENT_NODE) { + documentNode = doc.createDocumentNode(this); + metaNode = $(''); + documentNode.getMetadata().forEach(function(row) { + metaNode.append('' + row.value + ''); + }); + if(metaNode.children().length) { + $(this).prepend(metaNode); + } + } + }); + + return DOM; } }); @@ -143,11 +160,10 @@ var WLXMLDocumentNode = function() { 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_'; @@ -162,6 +178,21 @@ $.extend(WLXMLDocument.prototype, { }, 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() { @@ -244,6 +275,8 @@ $.extend(WLXMLDocument.prototype, { /* globals document */ el.replaceWith(document.createTextNode(text.transformed)); }); + + }, registerClassTransformation: function(Transformation, className) { diff --git a/src/wlxml/wlxml.test.js b/src/wlxml/wlxml.test.js index 516b47c..18a7332 100644 --- a/src/wlxml/wlxml.test.js +++ b/src/wlxml/wlxml.test.js @@ -1,7 +1,8 @@ define([ + 'libs/jquery', 'libs/chai', './wlxml.js' -], function(chai, wlxml) { +], function($, chai, wlxml) { 'use strict'; @@ -291,10 +292,7 @@ describe('WLXMLDocument', function() { 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