editor: Bring back metadata editor as a modal dialog
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Fri, 11 Jul 2014 12:27:58 +0000 (14:27 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Fri, 11 Jul 2014 12:27:58 +0000 (14:27 +0200)
src/editor/plugins/core/core.js
src/editor/plugins/core/core.less
src/editor/plugins/core/metadataEditor/action.js [new file with mode: 0644]
src/editor/plugins/core/metadataEditor/metadataEditor.js [new file with mode: 0644]
src/editor/plugins/core/metadataEditor/metadataEditor.less [new file with mode: 0644]
src/editor/plugins/core/metadataEditor/templates/item.html [new file with mode: 0644]
src/editor/plugins/core/metadataEditor/templates/main.html [new file with mode: 0644]
src/editor/plugins/core/metadataEditor/view.js [new file with mode: 0644]
src/editor/views/dialog/dialog.html
src/editor/views/dialog/dialog.js

index 450f147..9445682 100644 (file)
@@ -10,7 +10,8 @@ var _ = require('libs/underscore'),
     lists = require('plugins/core/lists'),
     plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}}},
     Dialog = require('views/dialog/dialog'),
-    canvasElements = require('plugins/core/canvasElements');
+    canvasElements = require('plugins/core/canvasElements'),
+    metadataEditor = require('./metadataEditor/metadataEditor');
 
 
 
@@ -389,6 +390,7 @@ var linkAction = {
     }
 };
 
+var metadataParams = {};
 
 plugin.actions = [
     undoRedoAction('undo'),
@@ -396,7 +398,8 @@ plugin.actions = [
     commentAction,
     createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
     createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
-    linkAction
+    linkAction,
+    metadataEditor.action(metadataParams)
 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions);
 
 
@@ -404,6 +407,15 @@ plugin.actions = [
 plugin.config = function(config) {
     // templates.actions[0].config(config.templates);
     templates.actions[0].params.template.options = config.templates;
+    metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
+        if(configRow1.key < configRow2.key) {
+            return -1;
+        }
+        if(configRow1.key > configRow2.key) {
+            return 1;
+        }
+        return 0;
+    });
 };
 
 plugin.canvasElements = canvasElements;
index 14c179e..3d172ae 100644 (file)
@@ -1,2 +1,3 @@
 @import 'canvasElements.less';
 @import 'links/links.less';
+@import 'metadataEditor/metadataEditor.less';
\ No newline at end of file
diff --git a/src/editor/plugins/core/metadataEditor/action.js b/src/editor/plugins/core/metadataEditor/action.js
new file mode 100644 (file)
index 0000000..9184eda
--- /dev/null
@@ -0,0 +1,42 @@
+define(function(require) {
+    
+'use strict';
+/* globals gettext */
+
+var Dialog = require('views/dialog/dialog'),
+    View = require('./view');
+
+
+return function(ctx) {
+    return {
+        name: 'showMetadataEditor',
+        params: {
+            doc: {type: 'context', name: 'document'}
+        },
+        stateDefaults: {
+            allowed: true,
+            label: gettext('Metadata'),
+            execute: function(callback, params) {
+                void(callback);
+                var dialog = Dialog.create({
+                        title: gettext('Document Metadata'),
+                        executeButtonText: gettext('Close'),
+                        cssClass: 'metadataEditor'
+                    }),
+                    view = new View(params.doc.root, ctx.config);
+                dialog.show();
+                dialog.setContentView(view.dom);
+                dialog.on('execute', function(e) {
+                    e.success();
+                });
+            }
+        },
+        getState: function(params) {
+            return {
+                allowed: !!params.doc
+            };
+        }
+    };
+};
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/metadataEditor/metadataEditor.js b/src/editor/plugins/core/metadataEditor/metadataEditor.js
new file mode 100644 (file)
index 0000000..aa0277b
--- /dev/null
@@ -0,0 +1,11 @@
+define(function(require) {
+    
+'use strict';
+
+var action = require('./action');
+
+return {
+    action: action
+};
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/metadataEditor/metadataEditor.less b/src/editor/plugins/core/metadataEditor/metadataEditor.less
new file mode 100644 (file)
index 0000000..a0f2b9f
--- /dev/null
@@ -0,0 +1,55 @@
+.metadataEditor .modal-body {
+    overflow: visible;
+}
+
+.rng-module-metadataEditor {
+
+    table {
+        table-layout:fixed;
+        margin-bottom:10px;
+
+        [contenteditable] {
+            cursor: text;
+            word-wrap: break-word;
+            min-height: 13px;
+        }
+
+        li:last-child {
+            border-bottom: none !important;
+        }
+
+        tr td {
+            vertical-align: middle;
+        }
+
+        tr td:nth-child(1){
+            width: 30%;
+        }
+
+        tr td:nth-child(2)  {
+            width:70%;
+        }
+
+        tr td:nth-child(3) {
+            width: 14px;
+        }
+    }
+
+    .rng-module-metadataEditor-addBtn {
+        float:right;
+        margin-right:6px;
+    }
+
+    .rng-visualEditor-metaRemoveBtn {
+        color: @red;
+    }
+
+    .btn {
+        padding:3px;
+        line-height:10px;
+    }
+
+    .drop-menu {
+        overflow: auto;
+    }
+}
\ No newline at end of file
diff --git a/src/editor/plugins/core/metadataEditor/templates/item.html b/src/editor/plugins/core/metadataEditor/templates/item.html
new file mode 100644 (file)
index 0000000..f170c5a
--- /dev/null
@@ -0,0 +1,5 @@
+<tr>
+    <td></td>
+    <td></td>
+    <td><a href="#" class="rng-visualEditor-metaRemoveBtn">x</a></td>
+</tr>
\ No newline at end of file
diff --git a/src/editor/plugins/core/metadataEditor/templates/main.html b/src/editor/plugins/core/metadataEditor/templates/main.html
new file mode 100644 (file)
index 0000000..cd4cc89
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="rng-module-metadataEditor">
+    <table class="table table-condensed table-bordered">
+    </table>
+    <button class="rng-module-metadataEditor-addBtn btn btn-mini btn-info">+</button></td>
+    <div style="clear: both"></div>
+</div>
\ No newline at end of file
diff --git a/src/editor/plugins/core/metadataEditor/view.js b/src/editor/plugins/core/metadataEditor/view.js
new file mode 100644 (file)
index 0000000..2857736
--- /dev/null
@@ -0,0 +1,202 @@
+define([
+'libs/jquery',
+'libs/underscore',
+'libs/text!./templates/main.html',
+'libs/text!./templates/item.html',
+'views/openSelect/openSelect'
+], function($, _, mainTemplate, itemTemplate, OpenSelectView) {
+
+'use strict';
+/* globals gettext */
+
+
+var View = function(node, metadataConfig) {
+    this.node = node;
+    this.metadataConfig = metadataConfig;
+    this.dom = $(_.template(mainTemplate)());
+    this.adding = false;
+
+    var metaTable = this.metaTable = this.dom.find('table');
+    
+    this.dom.find('.rng-module-metadataEditor-addBtn').click(function() {
+        this.adding = true;
+        this.node.document.transaction(function() {
+            this.node.getMetadata().add('','');
+        }.bind(this), {
+            metadata: {
+                description: gettext('Add metadata row')
+            }
+        });
+    }.bind(this));
+    
+    this.metaTable.on('click', '.rng-visualEditor-metaRemoveBtn', function(e) {
+        this.node.document.transaction(function() {
+            $(e.target).closest('tr').data('row').remove();
+        }, {
+            metadata: {
+                description: gettext('Remove metadata row')
+            }
+        });
+    }.bind(this));
+    
+    this.metaTable.on('keydown', '[contenteditable]', function(e) {
+        /* globals document */
+        if(e.which === 13) {
+            if($(document.activeElement).hasClass('rng-module-metadataEditor-metaItemKey')) {
+                metaTable.find('.rng-module-metadataEditor-metaItemValue').focus();
+            } else {
+                var input = $('<input>');
+                input.appendTo('body').focus();
+                this.dom.find('.rng-module-metadataEditor-addBtn').focus();
+                input.remove();
+            }
+            e.preventDefault();
+        }
+    }.bind(this));
+
+    this.metaTable.on('keyup', '[contenteditable]', _.throttle(function(e) {
+        if(e.which !== 13) {
+            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.metadata.node.document.transaction(function() {
+                row[method](toSet);
+            }, {
+                metadata: {
+                    description: gettext('Metadata edit')
+                }
+            });
+        }
+    }, 500));
+
+    this.setMetadata(this.node); //
+
+    this.node.document.on('change', this.handleEvent, this);
+};
+
+_.extend(View.prototype, {
+    close: function() {
+        this.node.document.off('change', this.handleEvent);
+    },
+    handleEvent: function(event) {
+        if(event.type === 'metadataAdded' && event.meta.node.sameNode(this.node)) {
+            this.addMetadataRow(event.meta.row);
+        }
+        if(event.type === 'metadataChanged' && event.meta.node.sameNode(this.node)) {
+            this.updateMetadataRow(event.meta.row);
+        }
+        if(event.type === 'metadataRemoved' && event.meta.node.sameNode(this.node)) {
+            this.removeMetadataRow(event.meta.row);
+        }
+        if(event.type === 'nodeDetached' && event.meta.node.containsNode(this.node)) {
+            this.setMetadata(null);
+        }
+    },
+    getValuesForKey: function(key) {
+        var toret = [];
+        this.metadataConfig.some(function(configRow) {
+            if(configRow.key === key) {
+                toret = configRow.values || [];
+                return true;
+            }
+        });
+        return toret;
+    },
+    setMetadata: function(node) {
+        this.dom.find('.rng-module-metadataEditor-addBtn').attr('disabled', !node);
+        if(!node) {
+            this.metaTable.html('');
+            return;
+        }
+        var view = this,
+            metadata = node.getMetadata();
+        this.metaTable.find('tr').remove();
+        metadata.forEach(function(row) {
+            view.addMetadataRow(row);
+        });
+    },
+    addMetadataRow: function(row) {
+        var newRow = $(_.template(itemTemplate)({key: row.getKey() || '', value: row.getValue() || ''}));
+        newRow.appendTo(this.metaTable);
+        newRow.data('row', row);
+
+        var keySelectView = new OpenSelectView({
+            value: row.getKey() || '',
+            inputTemplate: _.template('<div class="openInput rng-module-metadataEditor-metaItemKey" contentEditable="true"><%= value %></div>')({value: row.getKey() || '' }),
+            setInput: function(inputDOM, value) {
+                if(inputDOM.text() !== value) {
+                    inputDOM.text(value);
+                    row.setKey(value);
+                }
+                valueSelectView.clearItems();
+                this.getValuesForKey(value).forEach(function(value) {
+                    valueSelectView.addItem(value);
+                });
+            }.bind(this)
+        });
+        newRow.find('td:first').append(keySelectView.el).data('view', keySelectView);
+
+
+        var valueSelectView = new OpenSelectView({
+            value: row.getValue(),
+            inputTemplate: _.template('<div class="openInput rng-module-metadataEditor-metaItemValue" contentEditable="true"><%= value %></div>')({value: row.getValue() || '' }),
+            maxHeight: '300px',
+            maxWidth: '100px',
+            setInput: function(inputDOM, value) {
+                if(inputDOM.text() !== value) {
+                    inputDOM.text(value);
+                    row.setValue(value);
+                }
+            }
+        });
+        newRow.find('td:nth-child(2)').append(valueSelectView.el).data('view', valueSelectView);
+        
+
+        this.metadataConfig.forEach(function(configRow) {
+            keySelectView.addItem(configRow.key);
+            if(row.getKey() === configRow.key) {
+                (configRow.values || []).forEach(function(value) {
+                    valueSelectView.addItem(value);
+                });
+            }
+        });
+
+        if(this.adding) {
+            $(newRow.find('td div')[0]).focus();
+            this.adding = false;
+        }
+        return newRow;
+    },
+    updateMetadataRow: function(row) {
+        this._getRowTr(row, function(tr) {
+            var tds = tr.find('td'),
+                keyTd = $(tds[0]),
+                valueTd = $(tds[1]);
+
+            keyTd.data('view').setInput(row.getKey());
+            valueTd.data('view').setInput(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;
+            }
+        });
+    }
+});
+
+
+return View;
+
+
+});
\ No newline at end of file
index 1576831..7499cd1 100644 (file)
@@ -1,4 +1,4 @@
-<div class="rng-dialog modal hide static">
+<div class="rng-dialog modal hide static <%= cssClass %>">
     <div class="modal-header">
         <button type="button" class="close">&times;</button>
         <h1><%= title %></h1>
index dff0b6f..7d9f775 100644 (file)
@@ -28,7 +28,8 @@ define(function(require) {
         show: function() {
             this.setElement(this.template(_.extend({
                 executeButtonText: null,
-                cancelButtonText: null
+                cancelButtonText: null,
+                cssClass: ''
             }, this.options)));
 
             var body = this.$('.modal-body');
@@ -96,6 +97,10 @@ define(function(require) {
             this.$('.btn, button').toggleClass('disabled', !toggle);
             this.$('textarea').attr('disabled', !toggle);
             this.actionsDisabled = !toggle;
+        },
+        setContentView: function(view) {
+            var body = this.$('.modal-body');
+            body.append(view);
         }
     });