Merge in metadata support into master
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Fri, 17 Jan 2014 14:29:59 +0000 (15:29 +0100)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Fri, 17 Jan 2014 14:29:59 +0000 (15:29 +0100)
Creating/editing/deleting metadata on arbitrary nodes

src/editor/modules/metadataEditor/metadataEditor.js
src/editor/modules/metadataEditor/transformations.js [deleted file]
src/editor/modules/rng/rng.js
src/smartxml/smartxml.js
src/smartxml/smartxml.test.js
src/wlxml/extensions/metadata/metadata.js [new file with mode: 0644]
src/wlxml/extensions/metadata/metadata.test.js [new file with mode: 0644]
src/wlxml/wlxml.js
src/wlxml/wlxml.test.js

index 623dee9..64a5c44 100644 (file)
@@ -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 = $('<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();
         }
-        
     };
 };
 
diff --git a/src/editor/modules/metadataEditor/transformations.js b/src/editor/modules/metadataEditor/transformations.js
deleted file mode 100644 (file)
index 4bb3fdc..0000000
+++ /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 = $('<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
index fc97f99..665d3a9 100644 (file)
@@ -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);
index acbe04d..d168e6d 100644 (file)
@@ -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, {
index 57f11cc..0883ba7 100644 (file)
@@ -86,6 +86,31 @@ describe('smartxml', function() {
             });
         });
 
+        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,
diff --git a/src/wlxml/extensions/metadata/metadata.js b/src/wlxml/extensions/metadata/metadata.js
new file mode 100644 (file)
index 0000000..b30b94c
--- /dev/null
@@ -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 (file)
index 0000000..422a371
--- /dev/null
@@ -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('<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
index 820b95f..fe0636d 100644 (file)
@@ -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 = $('<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;
     }
 });
@@ -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) {
index 516b47c..18a7332 100644 (file)
@@ -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