editor: actions implementation
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Mon, 14 Apr 2014 09:24:12 +0000 (11:24 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Wed, 23 Apr 2014 11:05:04 +0000 (13:05 +0200)
23 files changed:
src/editor/modules/documentCanvas/canvas/canvas.js
src/editor/modules/documentCanvas/canvas/wlxmlListener.js
src/editor/modules/documentCanvas/commands.js [deleted file]
src/editor/modules/documentCanvas/documentCanvas.js
src/editor/modules/documentToolbar/actionView.js [new file with mode: 0644]
src/editor/modules/documentToolbar/documentToolbar.js
src/editor/modules/documentToolbar/documentToolbar.less
src/editor/modules/documentToolbar/template.html
src/editor/modules/documentToolbar/templates/actionView.html [new file with mode: 0644]
src/editor/modules/documentToolbar/templates/actionViewButton.html [new file with mode: 0644]
src/editor/modules/documentToolbar/templates/actionViewSelection.html [new file with mode: 0644]
src/editor/modules/metadataEditor/metadataEditor.js
src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js
src/editor/modules/nodeBreadCrumbs/template.html
src/editor/modules/nodeFamilyTree/nodeFamilyTree.js
src/editor/modules/nodePane/nodePane.js
src/editor/modules/rng/rng.js
src/editor/plugins/core/core.js
src/editor/plugins/core/footnote.js [new file with mode: 0644]
src/editor/plugins/core/lists.js [new file with mode: 0644]
src/editor/plugins/core/switch.js [new file with mode: 0644]
src/editor/plugins/core/templates.js [new file with mode: 0644]
tests/main.js

index b217d18..96e3bea 100644 (file)
@@ -49,17 +49,16 @@ $.extend(TextHandler.prototype, {
 });
 
 
-var Canvas = function(wlxmlDocument, publisher) {
+var Canvas = function(wlxmlDocument) {
     this.eventBus = _.extend({}, Backbone.Events);
     this.wrapper = $('<div>').addClass('canvas-wrapper').attr('contenteditable', true);
     this.wlxmlListener = wlxmlListener.create(this);
     this.loadWlxmlDocument(wlxmlDocument);
     this.setupEventHandling();
-    this.publisher = publisher ? publisher : function() {};
     this.textHandler = new TextHandler(this);
 };
 
-$.extend(Canvas.prototype, {
+$.extend(Canvas.prototype, Backbone.Events, {
 
     loadWlxmlDocument: function(wlxmlDocument) {
         if(!wlxmlDocument) {
@@ -104,9 +103,14 @@ $.extend(Canvas.prototype, {
 
     setupEventHandling: function() {
         var canvas = this;
+
         this.wrapper.on('keyup keydown keypress', function(e) {
-            keyboard.handleKey(e, this);
-        }.bind(this));
+            keyboard.handleKey(e, canvas);
+        });
+
+        this.wrapper.on('mouseup', function() {
+            canvas.triggerSelectionChanged();
+        });
 
         var mouseDown;
         this.wrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) {
@@ -244,6 +248,14 @@ $.extend(Canvas.prototype, {
         return element.dom().parents().index(this.wrapper) !== -1;
     },
 
+    triggerSelectionChanged: function() {
+        this.trigger('selectionChanged', this.getSelection());
+    },
+
+    getSelection: function() {
+        return new Selection(this);
+    },
+
     setCurrentElement: function(element, params) {
         if(!element) {
             logger.debug('Invalid element passed to setCurrentElement: ' + element);
@@ -299,18 +311,14 @@ $.extend(Canvas.prototype, {
             if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element)) {
                 this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
             }
-            if(!(textElementToLand.sameNode(currentTextElement))) {
-                this.publisher('currentTextElementSet', textElementToLand.wlxmlNode);
-            }
         } else {
             document.getSelection().removeAllRanges();
         }
 
         if(!(currentNodeElement && currentNodeElement.sameNode(nodeElementToLand))) {
             _markAsCurrent(nodeElementToLand);
-
-            this.publisher('currentNodeElementSet', nodeElementToLand.wlxmlNode);
         }
+        this.triggerSelectionChanged();
     },
 
     _moveCaretToTextElement: function(element, where) {
@@ -320,7 +328,7 @@ $.extend(Canvas.prototype, {
         if(typeof where !== 'number') {
             range.selectNodeContents(node);
         } else {
-            range.setStart(node, where);
+            range.setStart(node, Math.min(node.data.length, where));
         }
         
         if(where !== 'whole') {
@@ -341,15 +349,103 @@ $.extend(Canvas.prototype, {
         if(position.element) {
             this._moveCaretToTextElement(position.element, position.offset);
         }
+    },
+
+    findCanvasElement: function(node) {
+        return utils.findCanvasElement(node);
+    },
+
+    toggleGrid: function() {
+        this.wrapper.toggleClass('grid-on');
+        this.trigger('changed');
+    },
+    isGridToggled: function() {
+        return this.wrapper.hasClass('grid-on');
     }
 });
 
 
+var isText = function(node) {
+    return node && node.nodeType === Node.TEXT_NODE && $(node.parentNode).is('[document-text-element]');
+};
+
+var Selection = function(canvas) {
+    this.canvas = canvas;
+    var nativeSelection = this.nativeSelection = window.getSelection();
+    Object.defineProperty(this, 'type', {
+        get: function() {
+            if(nativeSelection.focusNode) {
+                if(nativeSelection.isCollapsed && isText(nativeSelection.focusNode)) {
+                    return 'caret';
+                }
+                if(isText(nativeSelection.focusNode) && isText(nativeSelection.anchorNode)) {
+                    return 'textSelection';
+                }
+            }
+            if(canvas.getCurrentNodeElement()) {
+                return 'node';
+            }
+        }
+    });
+};
+
+$.extend(Selection.prototype, {
+    toDocumentFragment: function() {
+        var doc = this.canvas.wlxmlDocument,
+            anchorElement = this.canvas.getDocumentElement(this.nativeSelection.anchorNode),
+            focusElement = this.canvas.getDocumentElement(this.nativeSelection.focusNode),
+            anchorNode = anchorElement ? anchorElement.wlxmlNode : null,
+            focusNode = focusElement ? focusElement.wlxmlNode : null;
+        if(this.type === 'caret') {
+            return doc.createFragment(doc.CaretFragment, {node: anchorNode, offset: this.nativeSelection.anchorOffset});
+        }
+        if(this.type === 'textSelection') {
+            if(anchorNode.isSiblingOf(focusNode)) {
+                return doc.createFragment(doc.TextRangeFragment, {
+                    node1: anchorNode,
+                    offset1: this.nativeSelection.anchorOffset,
+                    node2: focusNode,
+                    offset2: this.nativeSelection.focusOffset,
+                });
+            }
+            else {
+                var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode});
+                return doc.createFragment(doc.RangeFragment, {
+                    node1: siblingParents.node1,
+                    node2: siblingParents.node2
+                });
+            }
+        }
+        if(this.type === 'node') {
+            return doc.createFragment(doc.NodeFragment, {node: this.canvas.getCurrentNodeElement().wlxmlNode});
+        }
+    },
+    sameAs: function(other) {
+        void(other);
+    }
+});
+
 var Cursor = function(canvas) {
     this.canvas = canvas;
+    this.selection = window.getSelection();
 };
 
 $.extend(Cursor.prototype, {
+    sameAs: function(other) {
+        var same = true;
+        if(!other) {
+            return false;
+        }
+
+        ['focusNode', 'focusOffset', 'anchorNode', 'anchorOffset'].some(function(prop) {
+            same = same && this.selection[prop] === other.selection[prop];
+            if(!same) {
+                return true; // break
+            }
+        }.bind(this));
+
+        return same;
+    },
     isSelecting: function() {
         var selection = window.getSelection();
         return !selection.isCollapsed;
@@ -456,8 +552,8 @@ $.extend(Cursor.prototype, {
 });
 
 return {
-    fromXMLDocument: function(wlxmlDocument, publisher) {
-        return new Canvas(wlxmlDocument, publisher);
+    fromXMLDocument: function(wlxmlDocument) {
+        return new Canvas(wlxmlDocument);
     }
 };
 
index f932ea0..7d19e94 100644 (file)
@@ -23,6 +23,10 @@ $.extend(Listener.prototype, {
             }
         }, this);
 
+        wlxmlDocument.on('operationEnd', function() {
+            this.canvas.triggerSelectionChanged();
+        }, this);
+
         wlxmlDocument.on('contentSet', function() {
             this.canvas.loadWlxmlDocument(wlxmlDocument);
         }, this);
diff --git a/src/editor/modules/documentCanvas/commands.js b/src/editor/modules/documentCanvas/commands.js
deleted file mode 100644 (file)
index 85831d8..0000000
+++ /dev/null
@@ -1,338 +0,0 @@
-define([
-'./canvas/utils',
-'views/dialog/dialog',
-'fnpjs/datetime'
-], function(utils, Dialog, datetime) {
-    
-'use strict';
-/* globals gettext */
-
-
-var gridToggled = false;
-
-var commands = {
-    _cmds: {},
-    register: function(name, command) {
-        this._cmds[name] = command;
-    },
-
-    run: function(name, params, canvas, user) {
-        return this._cmds[name](canvas, params, user);
-    }
-};
-
-commands.register('undo', function(canvas) {
-    var doc = canvas.wlxmlDocument;
-
-    doc.undo();
-});
-
-commands.register('redo', function(canvas) {
-    var doc = canvas.wlxmlDocument;
-
-    doc.redo();
-});
-
-commands.register('remove-node', function(canvas) {
-    canvas.getCurrentNodeElement().wlxmlNode.detach();
-});
-
-commands.register('unwrap-node', function(canvas) {
-    var cursor = canvas.getCursor(),
-        selectionStart = cursor.getSelectionStart(),
-        selectionEnd = cursor.getSelectionEnd(),
-        parent1 = selectionStart.element.parent() || undefined,
-        parent2 = selectionEnd.element.parent() || undefined;
-
-    var selectionAnchor = cursor.getSelectionAnchor(),
-        node1 = parent1.wlxmlNode,
-        node2 = parent2.wlxmlNode,
-        doc = node1.document;
-    if(doc.areItemsOfSameList({node1: node1, node2: node2})) {
-        doc.extractItems({item1: node1, item2: node2});
-        canvas.setCurrentElement(selectionAnchor.element, {caretTo: selectionAnchor.offset});
-    } else if(!cursor.isSelecting()) {
-        var nodeToUnwrap = cursor.getPosition().element.wlxmlNode,
-            parentNode = nodeToUnwrap.unwrap();
-        if(parentNode) {
-            canvas.setCurrentElement(utils.findCanvasElement(parentNode));
-        }
-    }
-});
-
-commands.register('wrap-node', function(canvas) {
-    var cursor = canvas.getCursor(),
-        selectionStart = cursor.getSelectionStart(),
-        selectionEnd = cursor.getSelectionEnd(),
-        parent1 = selectionStart.element.parent() || undefined,
-        parent2 = selectionEnd.element.parent() || undefined;
-
-    var node1 = parent1.wlxmlNode,
-        node2 = parent2.wlxmlNode,
-        doc = node1.document;
-
-    if(doc.areItemsOfSameList({node1: node1, node2: node2})) {
-        doc.createList({node1: node1, node2: node2});
-    }
-});
-
-commands.register('list', function(canvas, params) {
-    void(params);
-    var cursor = canvas.getCursor(),
-        selectionStart = cursor.getSelectionStart(),
-        selectionEnd = cursor.getSelectionEnd(),
-        parent1 = selectionStart.element.parent() || undefined,
-        parent2 = selectionEnd.element.parent() || undefined,
-        selectionFocus = cursor.getSelectionFocus(),
-        node1 = parent1.wlxmlNode,
-        node2 = parent2.wlxmlNode,
-        doc = node1.document;
-    
-    if(cursor.isSelecting()) {
-        doc.transaction(function() {
-            doc.createList({node1: node1, node2: node2, klass: params.meta === 'num' ? 'list.enum' : 'list'});
-        }, {
-            success: function() {
-                canvas.setCurrentElement(selectionFocus.element, {caretTo: selectionFocus.offset});
-            }
-        });
-    } else {
-        var list;
-        if(node1.isInside('list')) {
-            list = node1.getParent('list');
-            if((params.meta === 'num' && list.getClass() === 'list.enum') || params.meta !== 'num' && list.getClass() === 'list') {
-                list.object.extractAllItems();
-            } else {
-                list.setClass(params.meta === 'num' ? 'list.enum' : 'list');
-            }
-        }
-    }
-});
-
-commands.register('toggle-grid', function(canvas, params) {
-    canvas.doc().dom().parent().toggleClass('grid-on', params.toggle);
-    gridToggled = params.toggle;
-});
-
-commands.register('newNodeRequested', function(canvas, params, user) {
-    var cursor = canvas.getCursor(),
-        selectionStart = cursor.getSelectionStart(),
-        selectionEnd = cursor.getSelectionEnd(),
-        wlxmlNode, caretTo, wrapperCanvasElement;
-
-    var insertNode = function(insertion, callback) {
-        var doc = canvas.wlxmlDocument,
-            metadata, creator, dialog;
-
-        var execCallback = function(node) {
-            if(callback) {
-                callback(node);
-            }
-        };
-
-        if(params.wlxmlTag === 'aside' && params.wlxmlClass === 'comment') {
-            doc.transaction(function() {
-                var node = insertion();
-                if(user) {
-                    creator = user.name;
-                    if(user.email) {
-                        creator += ' (' + user.email + ')';
-                    }
-                } else {
-                    creator = 'anonymous';
-                }
-
-                metadata = node.getMetadata();
-                metadata.add({key: 'creator', value: creator});
-                metadata.add({key: 'date', value: datetime.currentStrfmt()});
-                return node;
-            }, {
-                success: execCallback
-            });
-        } else if(params.wlxmlClass === 'link') {
-            dialog = Dialog.create({
-                title: gettext('Create link'),
-                executeButtonText: gettext('Apply'),
-                cancelButtonText: gettext('Cancel'),
-                fields: [
-                    {label: gettext('Link'), name: 'href', type: 'input'}
-                ]
-            });
-            dialog.on('execute', function(event) {
-                doc.transaction(function() {
-                    var node = insertion();
-                    node.setAttr('href', event.formData.href);
-                    event.success();
-                    return node;
-                }, {
-                    success: execCallback
-                });
-            });
-            dialog.show();
-        } else {
-            doc.transaction(function() {
-                return insertion();
-            }, {success: execCallback});
-        }
-    };
-
-    if(cursor.isSelecting()) {
-        if(cursor.isSelectingSiblings()) {
-            if(cursor.isSelectingWithinElement()) {
-                wlxmlNode = selectionStart.element.wlxmlNode;
-                caretTo = selectionStart.offset < selectionEnd.offset ? 'start' : 'end';
-
-                insertNode(
-                    function() {
-                        return wlxmlNode.wrapWith({tagName: params.wlxmlTag, attrs: {'class': params.wlxmlClass}, start: selectionStart.offset, end: selectionEnd.offset});
-                    },
-                    function(wrapper) {
-                        wrapperCanvasElement = utils.findCanvasElement(wrapper);
-                        canvas.setCurrentElement(wrapperCanvasElement.children()[0], {caretTo: caretTo});
-                    }
-                );
-            }
-            else {
-                wlxmlNode = selectionStart.element.wlxmlNode.parent();
-                caretTo = selectionStart.element.sameNode(cursor.getSelectionAnchor().element) ? 'end' : 'start';
-
-                insertNode(
-                    function() {
-                        return wlxmlNode.wrapText({
-                            _with: {tagName: params.wlxmlTag, attrs: {'class': params.wlxmlClass}},
-                            offsetStart: selectionStart.offset,
-                            offsetEnd: selectionEnd.offset,
-                            textNodeIdx: [wlxmlNode.indexOf(selectionStart.element.wlxmlNode), wlxmlNode.indexOf(selectionEnd.element.wlxmlNode)] //parent.childIndex(selectionEnd.element)]
-                        });
-                    },
-                    function(wrapper) {
-                        wrapperCanvasElement = utils.findCanvasElement(wrapper);
-                        canvas.setCurrentElement(wrapperCanvasElement.children()[caretTo === 0 ? 0 : wrapperCanvasElement.children().length - 1], {caretTo: caretTo});
-                    }
-                );
-            }
-        } else {
-            var node1 = selectionStart.element.wlxmlNode,
-                node2 = selectionEnd.element.wlxmlNode,
-                siblingParents = canvas.wlxmlDocument.getSiblingParents({node1: node1, node2: node2});
-
-            if(siblingParents) {
-                insertNode(
-                    function() {
-                        return canvas.wlxmlDocument.wrapNodes({
-                            node1: siblingParents.node1,
-                            node2: siblingParents.node2,
-                            _with: {tagName: params.wlxmlTag, attrs: {'class': params.wlxmlClass}}
-                        });
-                    }
-                );
-            }
-        }
-    } else if(canvas.getCurrentNodeElement()) {
-        wlxmlNode = canvas.getCurrentNodeElement().wlxmlNode;
-
-        var linkFound = [wlxmlNode].concat(wlxmlNode.parents()).some(function(node) {
-            if(node.getClass() === 'link') {
-                var dialog = Dialog.create({
-                    title: gettext('Edit link'),
-                    executeButtonText: gettext('Apply'),
-                    cancelButtonText: gettext('Cancel'),
-                    fields: [
-                        {label: gettext('Link'), name: 'href', type: 'input', initialValue: node.getAttr('href')},
-                    ]
-                });
-                dialog.on('execute', function(event) {
-                    canvas.wlxmlDocument.transaction(function() {
-                        node.setAttr('href', event.formData.href);
-                        event.success();
-                    });
-                });
-                dialog.show();
-                return true;
-            }
-        });
-        if(linkFound) {
-            return;
-        }
-
-        if(params.ctrlKey) {
-            insertNode(
-                function() {
-                    return wlxmlNode.wrapWith({tagName: params.wlxmlTag, attrs: {'class': params.wlxmlClass}});
-                },
-                function(wrapper) {
-                    canvas.setCurrentElement(utils.findCanvasElement(wrapper));
-                }
-            );
-        } else {
-            insertNode(
-                function() {
-                    var node = wlxmlNode.after({tagName: params.wlxmlTag, attrs: {'class': params.wlxmlClass}});
-                    node.append({text:''});
-                    return node;
-                }, function(wrapper) {
-                    canvas.setCurrentElement(utils.findCanvasElement(wrapper));
-                }
-            );
-        }
-    }
-});
-
-commands.register('footnote', function(canvas, params) {
-    void(params);
-    var cursor = canvas.getCursor(),
-        position = cursor.getPosition(),
-        asideNode, asideElement, node;
-        
-
-    if(cursor.isSelectingWithinElement()) {
-        asideNode = position.element.wlxmlNode.wrapWith({tagName: 'aside', attrs:{'class': 'footnote'}, start: cursor.getSelectionStart().offset, end: cursor.getSelectionEnd().offset});
-    } else {
-        node = position.element.wlxmlNode;
-        node.document.transaction(function() {
-            asideNode = node.divideWithElementNode({tagName: 'aside', attrs:{'class': 'footnote'}}, {offset: position.offset});
-            asideNode.append({text: ''});
-        });
-    }
-
-    asideElement = utils.findCanvasElement(asideNode);
-    asideElement.toggle(true);
-    canvas.setCurrentElement(asideElement);
-});
-
-commands.register('take-away-node', function(canvas) {
-    var position = canvas.getCursor().getPosition(),
-        element = position.element,
-        nodeElement = element ? element.parent() : canvas.getCurrentNodeElement();
-
-    if(!nodeElement || !(nodeElement.parent())) {
-        return;
-    }
-
-    var range = nodeElement.wlxmlNode.unwrapContent();
-
-    if(element) {
-        var elementIsFirstChild = nodeElement.childIndex(element);
-        if(element.bound()) {
-            canvas.setCurrentElement(element, {caretTo: position.offset});
-        } else {
-            if(elementIsFirstChild) {
-                canvas.setCurrentElement(utils.findCanvasElement(range.element1), {caretTo: 'end'});
-            } else {
-                canvas.setCurrentElement(utils.findCanvasElement(range.element2), {caretTo: 'end'});
-            }
-        }
-    } else {
-        canvas.setCurrentElement(utils.findCanvasElement(range.element1), {caretTo: 'start'});
-    }
-
-});
-
-
-return {
-    run: function(name, params, canvas, user) {
-        return commands.run(name, params, canvas, user);
-    }
-};
-
-});
\ No newline at end of file
index 6015089..3e6a209 100644 (file)
@@ -2,20 +2,26 @@
 
 define([
 'libs/jquery',
+'libs/underscore',
 './canvas/canvas',
-'./commands',
-'libs/text!./template.html'], function($, canvas3, commands, template) {
+'libs/text!./template.html'], function($, _, canvas3, template) {
 
 'use strict';
 
 return function(sandbox) {
 
-    var canvas = canvas3.fromXMLDocument(null, sandbox.publish);
+    var canvas = canvas3.fromXMLDocument(null);
     var canvasWrapper = $(template);
     var shownAlready = false;
     var scrollbarPosition = 0,
+        actionHandlers = {},
         cursorPosition;
+        
     
+    canvas.on('selectionChanged', function(selection) {
+        sandbox.publish('selectionChanged', selection);
+    });
+
     canvasWrapper.onShow = function() {
         if(!shownAlready) {
             shownAlready = true;
@@ -33,10 +39,25 @@ return function(sandbox) {
 
     /* public api */
     return {
-        start: function() { sandbox.publish('ready'); },
+        start: function() {
+            sandbox.getPlugins().forEach(function(plugin) {
+                var handlers;
+                if(plugin.canvas) {
+                    handlers = plugin.canvas.actionHandlers;
+                    if(handlers && !_.isArray(handlers)) {
+                        handlers = [handlers];
+                    }
+                    actionHandlers[plugin.name] = handlers;
+                }
+            });
+            sandbox.publish('ready');
+        },
         getView: function() {
             return canvasWrapper;
         },
+        getCanvas: function() {
+            return canvas;
+        },
         setDocument: function(wlxmlDocument) {
             canvas.loadWlxmlDocument(wlxmlDocument);
             canvasWrapper.find('#rng-module-documentCanvas-content').empty().append(canvas.view());
@@ -50,8 +71,10 @@ return function(sandbox) {
         jumpToElement: function(node) {
             canvas.setCurrentElement(node);
         },
-        command: function(command, params) {
-            commands.run(command, params, canvas, sandbox.getConfig().user);
+        onAfterActionExecuted: function(action, ret) {
+            (actionHandlers[action.getPluginName()] || []).forEach(function(handler) {
+                handler(canvas, action, ret);
+            });
         }
     };
     
diff --git a/src/editor/modules/documentToolbar/actionView.js b/src/editor/modules/documentToolbar/actionView.js
new file mode 100644 (file)
index 0000000..ddef396
--- /dev/null
@@ -0,0 +1,139 @@
+define(function(require) {
+    
+'use strict';
+
+var $ = require('libs/jquery'),
+    Backbone = require('libs/backbone'),
+    _ = require('libs/underscore'),
+    viewTemplate = require('libs/text!modules/documentToolbar/templates/actionView.html'),
+    buttonTemplate = require('libs/text!modules/documentToolbar/templates/actionViewButton.html'),
+    selectionTemplate = require('libs/text!modules/documentToolbar/templates/actionViewSelection.html');
+
+
+viewTemplate = _.template(viewTemplate);
+buttonTemplate = _.template(buttonTemplate);
+selectionTemplate = _.template(selectionTemplate);
+
+var iconExists = function(iconName) {
+    /* globals window */
+    var el = $('<i>').addClass('icon-' + iconName);
+    $('body').append(el);
+    var style = window.getComputedStyle(el[0]);
+    var toret = /glyphicons/.test(style.backgroundImage) && !/14px 14px/.test(style.backgroundPosition);
+    el.remove();
+    return toret;
+};
+
+var ActionView = Backbone.View.extend({
+    events: {
+        'mousedown .btn': 'onMouseDown',
+        'click .btn': 'onExecute',
+        'change select': 'onSelectionChange',
+        'mouseenter': 'onMouseEnter',
+        'mouseleave': 'onMouseLeave'
+    },
+    initialize: function() {
+        this.action = this.options.action;
+        this.action.on('paramsChanged', function() {
+            this.render();
+        }, this);
+        this.setElement(viewTemplate());
+    },
+    render: function() {
+        /* globals document */
+
+        var actionState = this.action.getState();
+
+        var templateContext = {
+            label: actionState.label || '?',
+            iconName: (iconExists(actionState.icon)) ? actionState.icon : null,
+            iconStyle: actionState.iconStyle
+        },
+            hovered = document.querySelectorAll(':hover'),
+            hovers = false,
+            button = this._button();
+           
+        if(hovered.length && _.last(hovered) === button[0]) {
+            hovers = true;
+        }
+
+        this.$el.empty();
+        _.pairs(this.action.definition.params).forEach(function(pair) {
+            var paramName = pair[0],
+                paramDesc = pair[1],
+                widget;
+            if(paramDesc.type === 'select') {
+                widget = $(selectionTemplate({
+                    paramName: paramName,
+                    options: paramDesc.options
+                }));
+                if(this.action.params[paramName]) {
+                    widget.find('option[value=' + this.action.params[paramName].id + ']').attr('selected', true);
+                }
+                this.$el.append(widget);
+            }
+        }.bind(this));
+
+        this.$el.append(buttonTemplate(templateContext));
+        button = this._button();
+        
+        if(!actionState.allowed) {
+            button.attr('disabled', true);
+            button.wrap('<div style="position: relative;">');
+            button.after('<div style="position: absolute; top:0; bottom:0; left:0; right: 0"></div>');
+        }
+        
+        if(actionState.toggled !== undefined) {
+            button.toggleClass('active', actionState.toggled);
+        }
+
+        if(hovers) {
+            this.trigger('hover');
+        }
+    },
+    onMouseEnter: function() {
+        this.trigger('hover');
+    },
+    onMouseLeave: function() {
+        this.trigger('leave');
+    },
+    onMouseDown: function() {
+        this.trigger('mousedown');
+    },
+    onExecute: function() {
+        var ret = this.action.execute();
+        this.trigger('actionExecuted', this.action, ret);
+    },
+    onSelectionChange: function(e) {
+        var select = $(e.target),
+            paramName = select.attr('param');
+
+        this.action.definition.params[paramName].options.some(function(option) {
+            if(option.id.toString() === select.val()) {
+                this.action.updateWidgetParam(paramName, option);
+                return true; // break
+            }
+        }.bind(this));
+    },
+    _button: function() {
+        return this.$el.find('button');
+    }
+});
+
+var create = function(action) {
+    var view = new ActionView({action:action});
+    view.render();
+
+    return {
+        on: function() {
+            view.on.apply(view, Array.prototype.slice.call(arguments, 0));
+        },
+        dom: view.$el,
+    };
+};
+
+return {
+    create: create
+};
+
+});
\ No newline at end of file
index 33f95a6..82b6dca 100644 (file)
-define(['libs/jquery', 'libs/underscore', 'utils/wlxml', 'libs/text!./template.html'], function($, _, wlxmlUtils, template) {
+define(['libs/jquery', 'libs/underscore', 'modules/documentToolbar/actionView', 'libs/text!./template.html'], function($, _, actionView, template) {
 
 'use strict';
-/* globals Node */
+
 
 return function(sandbox) {
     
-    var documentTemplates = sandbox.getBootstrappedData(),
-        currentNode;
+    var addedActions = [],
+        contextParams = {},
+        contextDefer = {},
+        duringClick = false,
+        document, canvas;
 
     var view = {
-        node: $(_.template(template)({wlxmlUtils: wlxmlUtils, templates: documentTemplates})),
-        setup: function() {
-            var view = this;
+        node: $(_.template(template)()),
+        getOption: function(option) {
+            return this.node.find('.rng-module-documentToolbar-toolbarOption[data-option=' + option +']').val();
+        },
+        addAction: function(group, actionDescription) {
+            var action = sandbox.createAction(actionDescription.actionName, actionDescription.actionConfig),
+                view;
+            addedActions.push(action);
+            view = actionView.create(action);
             
-            this.node.find('button').click(function(e) {
-                e.stopPropagation();
-
-                var btn = $(e.currentTarget),
-                    btnName = btn.attr('data-name'),
-                    meta = btn.attr('data-meta'),
-                    params = {},
-                    command = btnName;
-
-                if(myHandlers[btnName]) {
-                    myHandlers[btnName](btn);
-                } else {
-                    if(btn.attr('data-btn-type') === 'toggle') {
-                        command = 'toggle-' + command;
-                        btn.toggleClass('active');
-                        params.toggle = btn.hasClass('active');
-                    }
-
-                    if(btnName === 'new-node') {
-                        command = 'newNodeRequested';
-                        params.wlxmlTag = view.getOption('newTag-tag');
-                        params.wlxmlClass = view.getOption('newTag-class');
-                        if(meta) {
-                            var split = meta.split('/');
-                            params.wlxmlTag = split[0];
-                            params.wlxmlClass = split[1];
-                        }
-                    } else {
-                        params.meta = meta;
-                    }
+            _.pairs(contextParams).forEach(function(pair) {
+                var name = pair[0],
+                    value = pair[1];
+                action.updateContextParam(name, value);
+            });
 
-                    if(command === 'undo' || command === 'redo') {
-                        params.callback = function(disable) {
-                            btn.attr('disabled', !disable);
-                        };
+            group.append(view.dom);
+            view.on('actionExecuted', function(action, ret) {
+                sandbox.publish('actionExecuted', action, ret);
+                duringClick = false;
+                _.pairs(contextDefer).forEach(function(pair) {
+                    var what = pair[0],
+                        deferred = pair[1];
+                    if(deferred) {
+                        refreshContextParam(what);
                     }
+                });
+            });
+            view.on('mousedown', function() {
+                duringClick = true;
+            });
 
-                    _.extend(params, {ctrlKey: e.ctrlKey});
-
-                    sandbox.publish('command', command, params);
-                }
+            view.on('hover', function() {
+                sandbox.publish('actionHovered', action);
+            });
+            view.on('leave', function() {
+                sandbox.publish('actionOff', action);
             });
         },
-        getOption: function(option) {
-            return this.node.find('.rng-module-documentToolbar-toolbarOption[data-option=' + option +']').val();
+        addActionsGroup: function() {
+            var div = $('<div>');
+            div.addClass('rng-module-documentToolbar-toolbarGroup');
+            this.node.append(div);
+            return div;
         }
     };
     
-    var myHandlers = {
-        templatesBtn: function() {
-            if(currentNode && currentNode.nodeType === Node.ELEMENT_NODE) {
-                var templateId = parseInt(view.node.find('[data-name=templates-select]').val(), 10);
-                documentTemplates.forEach(function(template) {
-                    if(template.id === templateId) {
-                        var toAdd = currentNode.document.createDocumentNode(template.content);
-                        currentNode.after(toAdd);
-                    }
-                });
-            }
+    var setContextParam = function(what, ctx) {
+        contextParams[what] = ctx;
+        if(duringClick) {
+            contextDefer[what] = true;
+        } else {
+            refreshContextParam(what);
         }
     };
 
-    view.setup();
+    var refreshContextParam = function(what) {
+        addedActions.forEach(function(action) {
+            action.updateContextParam(what, contextParams[what]);
+        });
+    };
+
+    sandbox.registerKeyHandler('keydown', function(e) {
+        if(e.keyCode === 17) {
+            addedActions.forEach(function(action) {
+                action.updateKeyParam('ctrl', true);
+            });
+        }
+    });
+    sandbox.registerKeyHandler('keyup', function(e) {
+        if(e.keyCode === 17) {
+            addedActions.forEach(function(action) {
+                action.updateKeyParam('ctrl', false);
+            });
+        }
+    });
 
     return {
-        start: function() { sandbox.publish('ready'); },
+        start: function() {
+            var config = sandbox.getConfig().toolbar || {};
+            config.forEach(function(actionsGroup) {
+                var group = view.addActionsGroup();
+                actionsGroup.forEach(function(actionDescription) {
+                    if(typeof actionDescription === 'string') {
+                        actionDescription = {actionName: actionDescription, actionConfig: {}};
+                    }
+                    view.addAction(group, actionDescription);
+                });
+            });
+            sandbox.publish('ready');
+        },
         getView: function() { return view.node; },
-        setNodeElement: function(node) {
-            currentNode = node;
+        setDocumentFragment: function(fragment) {
+            if(!document) {
+                document = fragment.document;
+                document.on('operationEnd', function() {
+                    setContextParam('document', document);
+                });
+            }
+            setContextParam('fragment', fragment);
+
+        },
+        setCanvas: function(_canvas) {
+            setContextParam('canvas', _canvas);
+            if(!canvas) {
+                canvas = _canvas;
+                canvas.on('changed', function() {
+                    setContextParam('canvas', _canvas);
+                });
+            }
         },
         getOption: function(option) { return view.getOption(option); }
     };
index 559afde..4b35b12 100644 (file)
         border-color: #ddd;
         padding: 0 8px 0 0;
         margin: 0 8px 0 0;
-        float:left;
+        display: inline-block;
+    }
+
+    .toolbar-widget {
+        display: inline-block;
+        margin: 0 5px;
+        div {
+            display: inline-block;
+        }
     }
     
 }
index 2e14a89..1c013da 100644 (file)
@@ -1,35 +1 @@
-<div class="rng-module-documentToolbar">
-    <div class="rng-module-documentToolbar-toolbarGroup">
-        <button data-name="undo" data-btn-type="cmd" data-meta="bullets" class="btn btn-mini"><i class="icon-share-alt" style="-webkit-transform: scale(-1,1); transform: scale(-1, 1);"></i></button>
-        <button data-name="redo" data-btn-type="cmd" class="btn btn-mini"><i class="icon-share-alt"></i></button>
-    </div>
-
-    <div class="rng-module-documentToolbar-toolbarGroup">
-        <button data-name="new-node" data-meta="header/" data-btn-type="cmd" class="btn btn-mini">nagłówek</button>
-        <button data-name="list" data-btn-type="cmd" data-meta="bullets" class="btn btn-mini">lista pkt</button>
-        <button data-name="list" data-btn-type="cmd" data-meta="num" class="btn btn-mini">lista num</button>
-    </div>
-
-    <div class="rng-module-documentToolbar-toolbarGroup">
-        <button data-name="new-node" data-meta="span/emp" data-btn-type="cmd" class="btn btn-mini">wyróżnienie</button>
-        <button data-name="new-node" data-meta="span/cite" data-btn-type="cmd" class="btn btn-mini">cytat</i></button>
-        <button data-name="new-node" data-meta="span/link" data-btn-type="cmd" class="btn btn-mini">link</i></button>
-    </div>
-
-    <% if(templates) { %>
-    <div class="rng-module-documentToolbar-toolbarGroup">
-        <select data-name="templates-select">
-        <% templates.forEach(function(template) { %>
-            <option value="<%= template.id %>"><%= template.name %></option>
-        <% }); %>
-        </select>
-        <button class="btn btn-mini" data-name="templatesBtn">+</button>
-    </div>
-    <% } %>
-
-    <div class="rng-module-documentToolbar-toolbarGroup">
-        <button data-name="new-node" data-meta="aside/comment" data-btn-type="cmd" class="btn btn-mini"><i class="icon-comment"></i></button>
-    </div>
-
-    <div style="clear: both;"></div>
-</div>
\ No newline at end of file
+<div class="rng-module-documentToolbar"></div>
diff --git a/src/editor/modules/documentToolbar/templates/actionView.html b/src/editor/modules/documentToolbar/templates/actionView.html
new file mode 100644 (file)
index 0000000..f5ca649
--- /dev/null
@@ -0,0 +1 @@
+<div class="toolbar-widget"></div>
\ No newline at end of file
diff --git a/src/editor/modules/documentToolbar/templates/actionViewButton.html b/src/editor/modules/documentToolbar/templates/actionViewButton.html
new file mode 100644 (file)
index 0000000..506a291
--- /dev/null
@@ -0,0 +1,7 @@
+<button class="btn btn-mini">
+<% if(iconName) { %>
+    <i class="icon-<%= iconName %>" style="<%= iconStyle || '' %>"></i>
+<% } else { %>
+    <%= label %>
+<% } %>
+</button>
\ No newline at end of file
diff --git a/src/editor/modules/documentToolbar/templates/actionViewSelection.html b/src/editor/modules/documentToolbar/templates/actionViewSelection.html
new file mode 100644 (file)
index 0000000..c09ed2d
--- /dev/null
@@ -0,0 +1,6 @@
+<select param="<%= paramName %>">
+    <option value=""></option>
+    <% options.forEach(function(option) { %>
+        <option value="<%= option.id %>"><%= option.name %></option>
+    <% }); %>
+</select>
\ No newline at end of file
index 118571e..b9021bc 100644 (file)
@@ -77,6 +77,7 @@ return function(sandbox) {
         clear: function() {
         },
         setMetadata: function(node) {
+            this.node.find('.rng-module-metadataEditor-addBtn').attr('disabled', !node);
             if(!node) {
                 this.metaTable.html('');
                 return;
index 55f4fab..188aa6e 100644 (file)
@@ -31,7 +31,11 @@ return function(sandbox) {
         setNodeElement: function(nodeElement) {
             this.dom.empty();
             this.currentNodeElement = nodeElement;
-            var parents = nodeElement.parents();
+            var parents;
+            if(nodeElement) {
+                parents = nodeElement.parents();
+            }
+
             this.dom.html(template({node: nodeElement, parents: parents, utils: wlxmlUtils}));
 
             this.dom.find('li > a[href="#"]').each(function(idx, a) {
index 2f96b2d..062fb1e 100644 (file)
@@ -5,6 +5,6 @@
             <li><a href="#"> <%= utils.getTagLabel(parents[i].getTagName()) %><% if(parents[i].getClass()) { %>.<%= utils.getClassLabel(parents[i].getClass()) %> <% } %></a><span class="divider">/</span></li>
         <% } %>
             <li class="active"><%= utils.getTagLabel(node.getTagName()) %><% if(node.getClass()) { %>.<%= utils.getClassLabel(node.getClass()) %> <% } %></span></li>
-    <% } %>
+    <% } else { %> &nbsp; <% } %>
     </ul>
 </div>
\ No newline at end of file
index e2a4eb8..bda562f 100644 (file)
@@ -29,7 +29,7 @@ return function(sandbox) {
         listens = true;
         document.on('change', function(event) {
             if(event.type === 'nodeTextChange' && event.meta.node.parent().sameNode(view.currentNodeElement)) {
-                view.setElement();
+                view.setElement(view.currentNodeElement);
             }
         }, this);
     };
@@ -52,50 +52,54 @@ return function(sandbox) {
             });
         },
         setElement: function(element) {
-            element = element || this.currentNodeElement;
-            var textElement = element.getText ? element : null,
-                nodeElement = element.getText ? element.parent() : element, // TODO: better type detection
-                nodeElementParent = nodeElement.parent(),
-                parent;
-            
-            this.currentNodeElement = nodeElement;
-            items = [];
+            var contents = [],
+                parent, nodeElementParent;
 
-            if(nodeElementParent) {
-                items.push(nodeElementParent);
-                parent = {
-                    id: items.length - 1,
-                    repr: wlxmlUtils.getTagLabel(nodeElementParent.getTagName()) + (nodeElementParent.getClass() ? ' / ' + wlxmlUtils.getClassLabel(nodeElementParent.getClass()) : '')
-                };
+            if(element) {
+                element = element || this.currentNodeElement;
+                var textElement = element.getText ? element : null,
+                    nodeElement = element.getText ? element.parent() : element, // TODO: better type detection
+                    items;
                 
-            }
-        
-            var nodeContents = nodeElement.contents(),
-                contents = [];
-            nodeContents.forEach(function(child) {
-                if(child.getText) {
-                    var text = child.getText();
-                    if(!text) {
-                        text = '<pusty tekst>';
-                    }
-                    else {
-                        if(text.length > 13) {
-                            text = text.substr(0,13) + '...';
+                this.currentNodeElement = nodeElement;
+                items = [];
+                nodeElementParent = nodeElement.parent();
+
+                if(nodeElementParent) {
+                    items.push(nodeElementParent);
+                    parent = {
+                        id: items.length - 1,
+                        repr: wlxmlUtils.getTagLabel(nodeElementParent.getTagName()) + (nodeElementParent.getClass() ? ' / ' + wlxmlUtils.getClassLabel(nodeElementParent.getClass()) : '')
+                    };
+                    
+                }
+            
+                var nodeContents = nodeElement.contents();
+                nodeContents.forEach(function(child) {
+                    if(child.getText) {
+                        var text = child.getText();
+                        if(!text) {
+                            text = '<pusty tekst>';
+                        }
+                        else {
+                            if(text.length > 13) {
+                                text = text.substr(0,13) + '...';
+                            }
+                            text = '"' + text + '"';
                         }
-                        text = '"' + text + '"';
+                        contents.push({
+                            id: items.length,
+                            repr: _.escape(text), bold: child.sameNode(textElement)
+                        });
+                    } else {
+                        contents.push({
+                            id: items.length,
+                            repr: wlxmlUtils.getTagLabel(child.getTagName()) + (child.getClass() ? ' / ' + wlxmlUtils.getClassLabel(child.getClass()) : '')
+                        });
                     }
-                    contents.push({
-                        id: items.length,
-                        repr: _.escape(text), bold: child.sameNode(textElement)
-                    });
-                } else {
-                    contents.push({
-                        id: items.length,
-                        repr: wlxmlUtils.getTagLabel(child.getTagName()) + (child.getClass() ? ' / ' + wlxmlUtils.getClassLabel(child.getClass()) : '')
-                    });
-                }
-                items.push(child);
-            });
+                    items.push(child);
+                });
+            }
             this.dom.empty();
             this.dom.append($(template({parent: parent, contents: contents})));
 
@@ -123,10 +127,10 @@ return function(sandbox) {
             sandbox.publish('ready');
         },
         setElement: function(element) {
-            if(!listens) {
+            if(!listens && element) {
                 startListening(element.document);
             }
-            if(!(element.sameNode(view.currentNodeElement))) {
+            if(!element || !(element.sameNode(view.currentNodeElement))) {
                 view.setElement(element);
             }
         },
index 02ae2cc..f7e6694 100644 (file)
@@ -11,6 +11,7 @@ define([
 return function(sandbox) {
     
     var view = $(_.template(templateSrc)({utils: wlxmlUtils})),
+        listens = false,
         currentNode;
     
     view.on('change', 'select', function(e) {
@@ -19,6 +20,8 @@ return function(sandbox) {
             value = target.val().replace(/-/g, '.');
         currentNode['set' + attr](value);
     });
+
+
    
     return {
         start: function() {
@@ -28,28 +31,34 @@ return function(sandbox) {
             return view;
         },
         setNodeElement: function(wlxmlNodeElement) {
-            var module = this;
-            if(!currentNode) {
-                wlxmlNodeElement.document.on('change', function(event) {
-                    if(event.type === 'nodeAttrChange' && event.meta.node.sameNode(currentNode)) {
-                        module.setNodeElement(currentNode);
-                    }
-                });
-            }
+            if(wlxmlNodeElement) {
+                var module = this;
+                if(!listens) {
+                    wlxmlNodeElement.document.on('change', function(event) {
+                        if(event.type === 'nodeAttrChange' && event.meta.node.sameNode(currentNode)) {
+                            module.setNodeElement(currentNode);
+                        }
+                    });
+                    listens = true;
+                }
 
-            view.find('.rng-module-nodePane-tagSelect').val(wlxmlNodeElement.getTagName());
+                view.find('.rng-module-nodePane-tagSelect').attr('disabled', false).val(wlxmlNodeElement.getTagName());
 
-            var escapedClassName = (wlxmlNodeElement.getClass() || '').replace(/\./g, '-');
-            view.find('.rng-module-nodePane-classSelect').val(escapedClassName);
-
-            var attrs = _.extend(wlxmlNodeElement.getMetaAttributes(), wlxmlNodeElement.getOtherAttributes());
-            var widget = metaWidget.create({attrs:attrs});
-            widget.on('valueChanged', function(key, value) {
-                wlxmlNodeElement.setMetaAttribute(key, value);
-                //wlxmlNodeElement.setMetaAttribute(key, value);
-            });
-            view.find('.metaFields').empty().append(widget.el);
+                var escapedClassName = (wlxmlNodeElement.getClass() || '').replace(/\./g, '-');
+                view.find('.rng-module-nodePane-classSelect').attr('disabled', false).val(escapedClassName);
 
+                var attrs = _.extend(wlxmlNodeElement.getMetaAttributes(), wlxmlNodeElement.getOtherAttributes());
+                var widget = metaWidget.create({attrs:attrs});
+                widget.on('valueChanged', function(key, value) {
+                    wlxmlNodeElement.setMetaAttribute(key, value);
+                    //wlxmlNodeElement.setMetaAttribute(key, value);
+                });
+                view.find('.metaFields').empty().append(widget.el);
+            } else {
+                view.find('.rng-module-nodePane-tagSelect').attr('disabled', true).val('');
+                view.find('.rng-module-nodePane-classSelect').attr('disabled', true).val('');
+                view.find('.metaFields').empty();
+            }
             currentNode = wlxmlNodeElement;
         }
     };
index b12c19f..e15c94b 100644 (file)
@@ -42,16 +42,25 @@ return function(sandbox) {
         jumpToDocumentElement: function(element) {
             sandbox.getModule('documentCanvas').jumpToElement(element);
         },
-        updateCurrentNodeElement: function(nodeElement) {
-            sandbox.getModule('nodePane').setNodeElement(nodeElement);
-            sandbox.getModule('nodeFamilyTree').setElement(nodeElement);
-            sandbox.getModule('nodeBreadCrumbs').setNodeElement(nodeElement);
-            sandbox.getModule('documentToolbar').setNodeElement(nodeElement);
-            sandbox.getModule('metadataEditor').setNodeElement(nodeElement);
+        refreshCanvasSelection: function(selection) {
+            var fragment = selection.toDocumentFragment(),
+                elementParent;
+            
+            sandbox.getModule('documentToolbar').setDocumentFragment(fragment);
+            
+            if(fragment && fragment.node) {
+                elementParent = fragment.node.getNearestElementNode();
+                sandbox.getModule('nodePane').setNodeElement(elementParent);
+                sandbox.getModule('nodeFamilyTree').setElement(fragment.node);
+                sandbox.getModule('nodeBreadCrumbs').setNodeElement(elementParent);
+                sandbox.getModule('metadataEditor').setNodeElement(elementParent);
+            } else {
+                sandbox.getModule('nodePane').setNodeElement(null);
+                sandbox.getModule('nodeFamilyTree').setElement(null);
+                sandbox.getModule('nodeBreadCrumbs').setNodeElement(null);
+                sandbox.getModule('metadataEditor').setNodeElement(null);
+            }
         },
-        updateCurrentTextElement: function(textElement) {
-            sandbox.getModule('nodeFamilyTree').setElement(textElement);
-        }
     };
     
 
@@ -196,24 +205,16 @@ return function(sandbox) {
             views.visualEditing.setView('leftColumn', sandbox.getModule('documentCanvas').getView());
         },
         
-        currentTextElementSet: function(textElement) {
-            commands.updateCurrentTextElement(textElement);
-        },
-
-        currentNodeElementSet: function(nodeElement) {
-            commands.updateCurrentNodeElement(nodeElement);
-        },
-        
-        currentNodeElementChanged: function(nodeElement) {
-            commands.updateCurrentNodeElement(nodeElement);
-        },
-
         nodeHovered: function(canvasNode) {
             commands.highlightDocumentNode(canvasNode);
         },
         
         nodeBlured: function(canvasNode) {
             commands.dimDocumentNode(canvasNode);
+        },
+
+        selectionChanged: function(selection) {
+            commands.refreshCanvasSelection(selection);
         }
     };
 
@@ -248,9 +249,10 @@ return function(sandbox) {
     eventHandlers.documentToolbar = {
         ready: function() {
             views.visualEditing.setView('toolbar', sandbox.getModule('documentToolbar').getView());
+            sandbox.getModule('documentToolbar').setCanvas(sandbox.getModule('documentCanvas').getCanvas());
         },
-        command: function(cmd, params) {
-            sandbox.getModule('documentCanvas').command(cmd, params);
+        actionExecuted: function(action, ret) {
+            sandbox.getModule('documentCanvas').onAfterActionExecuted(action, ret);
         }
     };
     
@@ -305,6 +307,11 @@ return function(sandbox) {
     
     return {
         start: function() {
+            sandbox.registerActionsAppObject({
+                getUser: function() {
+                    return sandbox.getConfig().user;
+                }
+            });
             sandbox.getModule('data').start();
         },
         handleEvent: function(moduleName, eventName, args) {
index d11bf73..d0a376d 100644 (file)
@@ -1,9 +1,15 @@
 define(function(require) {
     
 'use strict';
+/* globals gettext */
 
 var _ = require('libs/underscore'),
-    plugin = {documentExtension: {textNode: {}}};
+    templates = require('plugins/core/templates'),
+    footnote = require('plugins/core/footnote'),
+    switchTo = require('plugins/core/switch'),
+    lists = require('plugins/core/lists'),
+    plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}}},
+    Dialog = require('views/dialog/dialog');
 
 
 plugin.documentExtension.textNode.transformations = {
@@ -38,6 +44,233 @@ plugin.documentExtension.textNode.transformations = {
     }
 };
 
+var undoRedoAction = function(dir) {
+    return {
+        name: dir,
+        params: {
+            document: {type: 'context', name: 'document'},
+        },
+        stateDefaults: {
+            label: dir === 'undo' ? '<-' : '->',
+            icon: 'share-alt',
+            iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
+            execute: function(params) {
+                params.document[dir]();
+            },
+        },
+        getState: function(params) {
+            var allowed = params.document && !!(params.document[dir+'Stack'].length),
+                desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
+                descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
+            return {
+                allowed: allowed,
+                description: allowed ? desc : descEmpty
+            };
+        }
+    };
+};
+
+var pad = function(number) {
+    if(number < 10) {
+        number = '0' + number;
+    }
+    return number;
+};
+
+var commentAction = {
+    name: 'comment',
+    params: {
+        fragment: {type: 'context', name: 'fragment'}
+    },
+    stateDefaults: {
+        icon: 'comment',
+        execute: function(params, editor) {
+            /* globals Node */
+            var node = params.fragment.node;
+            if(node.nodeType === Node.TEXT_NODE) {
+                node = node.parent();
+            }
+            node.document.transaction(function() {
+                var comment =  node.after({tagName: 'aside', attrs: {'class': 'comment'}});
+                comment.append({text:''});
+                var user = editor.getUser(), creator;
+                if(user) {
+                    creator = user.name;
+                    if(user.email) {
+                        creator += ' (' + user.email + ')';
+                    }
+                } else {
+                    creator = 'anonymous';
+                }
+
+                var currentDate = new Date(),
+                    dt = pad(currentDate.getDate()) + '-' +
+                                pad((currentDate.getMonth() + 1))  + '-' +
+                                pad(currentDate.getFullYear()) + ' ' +
+                                pad(currentDate.getHours()) + ':' +
+                                pad(currentDate.getMinutes()) + ':' +
+                                pad(currentDate.getSeconds());
+
+                var metadata = comment.getMetadata();
+                metadata.add({key: 'creator', value: creator});
+                metadata.add({key: 'date', value: dt});
+            });
+        },
+    },
+    getState: function(params) {
+        var state = {
+            allowed: params.fragment && params.fragment.isValid() &&
+                        params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
+        };
+        if(state.allowed) {
+            state.description = gettext('Insert comment after current node');
+        }
+        return state;
+    }
+};
+
+
+var createWrapTextAction = function(createParams) {
+    return {
+        name: createParams.name,
+        params: {
+            fragment: {type: 'context', name: 'fragment'},
+        },
+        getState: function(params) {
+            var state = {
+                    label: this.config.label
+                },
+                parent;
+            
+            if(
+                !params.fragment || !params.fragment.isValid() ||
+                !(params.fragment instanceof params.fragment.TextRangeFragment) ||
+                !params.fragment.hasSiblingBoundries()) {
+                    return _.extend(state, {allowed: false});
+            }
+            
+            parent = params.fragment.startNode.parent();
+            if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
+                return _.extend(state, {allowed: false});
+            }
+
+            return _.extend(state, {
+                allowed: true,
+                execute: function(params) {
+                    params.fragment.document.transaction(function() {
+                        var parent = params.fragment.startNode.parent();
+                        return parent.wrapText({
+                            _with: {tagName: 'span', attrs: {'class': createParams.klass}},
+                            offsetStart: params.fragment.startOffset,
+                            offsetEnd: params.fragment.endOffset,
+                            textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
+                        });
+                    });
+                }
+            });
+        }
+    };
+};
+
+
+var createLinkFromSelection = function(params) {
+    var doc = params.fragment.document,
+        dialog = Dialog.create({
+            title: gettext('Create link'),
+            executeButtonText: gettext('Apply'),
+            cancelButtonText: gettext('Cancel'),
+            fields: [
+                {label: gettext('Link'), name: 'href', type: 'input'}
+            ]
+        });
+    
+    dialog.on('execute', function(event) {
+        doc.transaction(function() {
+            var span =  params.fragment.startNode.parent().wrapText({
+                _with: {tagName: 'span', attrs: {'class': 'link'}},
+                offsetStart: params.fragment.startOffset,
+                offsetEnd: params.fragment.endOffset,
+                textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
+            });
+            span.setAttr('href', event.formData.href);
+            event.success();
+            return span;
+        });
+    });
+    dialog.show();
+};
+
+var editLink = function(params) {
+    var doc = params.fragment.document,
+        link = params.fragment.node.getParent('link'),
+        dialog = Dialog.create({
+            title: gettext('Edit link'),
+            executeButtonText: gettext('Apply'),
+            cancelButtonText: gettext('Cancel'),
+            fields: [
+                {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
+            ]
+        });
+    
+    dialog.on('execute', function(event) {
+        doc.transaction(function() {
+            link.setAttr('href', event.formData.href);
+            event.success();
+        });
+    });
+    dialog.show();
+};
+
+var linkAction = {
+    name: 'link',
+    params: {
+        fragment: {type: 'context', name: 'fragment'}
+    },
+    stateDefaults: {
+        label: gettext('link')
+    },
+    getState: function(params) {
+        if(!params.fragment || !params.fragment.isValid()) {
+            return {allowed: false};
+        }
+
+        if(params.fragment instanceof params.fragment.TextRangeFragment) {
+            if(!params.fragment.hasSiblingBoundries() || params.fragment.startNode.parent().is('link')) {
+                return {allowed: false};
+            }
+            return {
+                allowed: true,
+                description: gettext('Create link from selection'),
+                execute: createLinkFromSelection
+            };
+        }
+
+        if(params.fragment instanceof params.fragment.CaretFragment) {
+            if(params.fragment.node.isInside('link')) {
+                return {allowed: true, toggled: true, execute: editLink};
+            }
+        }
+        return {allowed: false};
+    }
+};
+
+
+plugin.actions = [
+    undoRedoAction('undo'),
+    undoRedoAction('redo'),
+    commentAction,
+    createWrapTextAction({name: 'emphasis', klass: 'emp'}),
+    createWrapTextAction({name: 'cite', klass: 'cite'}),
+    linkAction
+].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions);
+
+
+
+plugin.config = function(config) {
+    // templates.actions[0].config(config.templates);
+    templates.actions[0].params.template.options = config.templates;
+};
+
 return plugin;
 
 });
\ No newline at end of file
diff --git a/src/editor/plugins/core/footnote.js b/src/editor/plugins/core/footnote.js
new file mode 100644 (file)
index 0000000..2fa314a
--- /dev/null
@@ -0,0 +1,84 @@
+define(function() {
+    
+'use strict';
+/* globals gettext */
+
+var footnoteExecute = {
+    selecting: function(params) {
+        var parent = params.fragment.startNode.parent();
+        return parent.wrapText({
+            _with: {tagName: 'aside', attrs: {'class': 'footnote'}},
+            offsetStart: params.fragment.startOffset,
+            offsetEnd: params.fragment.endOffset,
+            textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
+        });
+    },
+    afterCursor: function(params) {
+        var node = params.fragment.node,
+            asideNode;
+        node.document.transaction(function() {
+            asideNode = node.divideWithElementNode({tagName: 'aside', attrs:{'class': 'footnote'}}, {offset: params.fragment.offset});
+            asideNode.append({text: ''});
+        });
+        return asideNode;
+    },
+    afterNode: function(params) {
+        var node = params.fragment.node,
+            asideNode;
+        node.document.transaction(function() {
+            asideNode = node.after({tagName: 'aside', attrs:{'class': 'footnote'}}, {offset: params.fragment.offset});
+            asideNode.append({text: ''});
+        });
+        return asideNode;
+    }
+};
+
+var footnoteAction =  {
+    name: 'footnote',
+    params: {
+        fragment: {type: 'context', name: 'fragment'}
+    },
+    stateDefaults: {
+        icon: 'asterisk'
+    },
+    getState: function(params) {
+        if(!params.fragment || !params.fragment.isValid()) {
+            return {allowed: false};
+        }
+        if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundries()) {
+            return {
+                allowed: true,
+                description: gettext('Create footnote from selection'),
+                execute: footnoteExecute.selecting
+            };
+        }
+        if(params.fragment instanceof params.fragment.CaretFragment) {
+            return {
+                allowed: true,
+                description: gettext('Insert footnote after cursor'),
+                execute: footnoteExecute.afterCursor
+            };
+        }
+        if(params.fragment instanceof params.fragment.NodeFragment) {
+            if(params.fragment.node.isRoot()) {
+                return {
+                    allowed: false,
+                    description: gettext('Cannot insert footnote after root node')
+                };
+            }
+            return {
+                allowed: true,
+                description: gettext('Insert footnote after node'),
+                execute: footnoteExecute.afterNode
+            };
+        }
+        return false;
+    }
+};
+
+
+return {
+    actions: [footnoteAction],
+};
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/lists.js b/src/editor/plugins/core/lists.js
new file mode 100644 (file)
index 0000000..3cf1192
--- /dev/null
@@ -0,0 +1,121 @@
+define(function() {
+    
+'use strict';
+/* globals gettext, interpolate */
+
+
+var getBoundriesForAList = function(fragment) {
+    var node;
+
+    if(fragment instanceof fragment.RangeFragment && fragment.hasSiblingBoundries()) {
+        return fragment.boundriesSiblingParents();
+    }
+    if(fragment instanceof fragment.NodeFragment) {
+        node = fragment.node.getNearestElementNode();
+        return {
+            node1: node,
+            node2: node
+        };
+    }
+};
+
+var countItems = function(boundries) {
+    var ptr = boundries.node1,
+        c = 1;
+    while(ptr && !ptr.sameNode(boundries.node2)) {
+        c++;
+        ptr = ptr.next();
+    }
+    return c;
+};
+
+var toggleListAction = function(type) {
+    
+    var execute = {
+        add: function(params) {
+            var boundries = getBoundriesForAList(params.fragment),
+                listParams = {klass: type === 'Bullet' ? 'list' : 'list.enum'};
+            if(boundries && boundries.node1) {
+                listParams.node1 = boundries.node1;
+                listParams.node2 = boundries.node2;
+                boundries.node1.document.createList(listParams);
+            } else {
+                throw new Error('Invalid boundries');
+            }
+        },
+        remove: function(params) {
+            /* globals Node */
+            var current = params.fragment.node;
+
+            var toSearch = current.nodeType === Node.ELEMENT_NODE ? [current] : [];
+            toSearch = toSearch.concat(current.parents());
+            toSearch.some(function(node) {
+                if(node.is('list')) {
+                    node.object.extractListItems();
+                    return true; // break
+                }
+            });
+        },
+        changeType: function(params) {
+            params.fragment.node.getParent('list').setClass(type === 'Bullet' ? 'list' : 'list.enum');
+        }
+    };
+
+    var isToggled = function(params) {
+        if(params.fragment && params.fragment.node && params.fragment.node.isInside('list')) {
+            var list = params.fragment.node.getParent('list');
+            return list.getClass() === (type === 'Bullet' ? 'list' : 'list.enum');
+        }
+        return false;
+    };
+
+
+    return {
+        name: 'toggle' + type + 'List',
+        context: ['fragment'],
+        params: {
+            fragment: {type: 'context', name: 'fragment'}
+        },
+        stateDefaults: {
+            label: type === 'Bullet' ? gettext('bull. list') : gettext('num. list')
+        },
+        getState: function(params) {
+            if(!params.fragment || !params.fragment.isValid()) {
+                return false;
+            }
+
+            if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside('list')) {
+                var list = params.fragment.node.getParent('list');
+                if((list.getClass() === 'list' && type === 'Enum') || (list.getClass() === 'list.enum' && type === 'Bullet')) {
+                    return {
+                        allowed: true,
+                        description: interpolate(gettext('Change list type to %s'), [type]),
+                        execute: execute.changeType
+                    };
+                }
+                return {
+                    allowed: true,
+                    toggled: isToggled(params),
+                    description: gettext('Remove list'),
+                    execute: execute.remove
+                };
+
+            }
+            var boundries = getBoundriesForAList(params.fragment);
+            if(boundries) {
+                return {
+                    allowed: true,
+                    description: interpolate(gettext('Make %s fragment(s) into list'), [countItems(getBoundriesForAList(params.fragment))]),
+                    execute: execute.add
+                };
+            }
+        }
+    };
+};
+
+
+return {
+    actions: [toggleListAction('Bullet'), toggleListAction('Enum')]
+};
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/switch.js b/src/editor/plugins/core/switch.js
new file mode 100644 (file)
index 0000000..2403c2b
--- /dev/null
@@ -0,0 +1,67 @@
+define(function(require) {
+    
+'use strict';
+/* globals gettext */
+
+var _ = require('libs/underscore');
+
+
+var createSwitchAction = function(createParams) {
+    return  {
+        name: createParams.name,
+        params: {
+            fragment: {type: 'context', name: 'fragment'},
+        },
+        getState: function(params) {
+            var state = {
+                    label: this.config.label
+                },
+                f = params.fragment;
+
+
+            if(
+                !(f && f.isValid()) ||
+                !((f instanceof f.CaretFragment) || (f instanceof f.TextRangeFragment && f.getCommonParent()))
+            ) {
+                return _.extend(state, {
+                    allowed: false,
+                    description: 'wrong or no selection'
+                });
+            }
+
+            var node = f instanceof f.CaretFragment ? f.node.parent() : f.getCommonParent(),
+                alreadyInTarget = node.isInside(createParams.to),
+                toSwitch = node;
+
+            if(!toSwitch.is(createParams.from)) {
+                toSwitch = toSwitch.getParent(createParams.from);
+            }
+
+            return _.extend(state, {
+                allowed: !!toSwitch,
+                toggled: alreadyInTarget,
+                description: 'Switch to ' + createParams.to.name,
+                execute: alreadyInTarget ? function() {} : function() {
+                    f.document.transaction(function() {
+                        if(createParams.to.tagName) {
+                            toSwitch = toSwitch.setTag(createParams.to.tagName);
+                        }
+                        if(!_.isUndefined(createParams.to.klass)) {
+                            toSwitch.setClass(createParams.to.klass);
+                        }
+                    });
+                }
+            });
+        }
+    };
+};
+
+
+return {
+    actions: [
+        createSwitchAction({name: 'switchToHeader', from: {tagName: 'div', klass: 'p'}, to: {tagName: 'header', klass: '', name: gettext('header')}}),
+        createSwitchAction({name: 'switchToParagraph', from: {tagName: 'header'}, to: {tagName: 'div', klass: 'p', name: gettext('paragraf')}})
+    ]
+};
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/templates.js b/src/editor/plugins/core/templates.js
new file mode 100644 (file)
index 0000000..2b1f730
--- /dev/null
@@ -0,0 +1,47 @@
+define(function() {
+    
+'use strict';
+/* globals gettext, interpolate */
+
+
+var insertTemplateAction = {
+    name: 'template',
+    params: {
+        fragment: {type: 'context', name: 'fragment'},
+        template: {type: 'select', options: []},
+        ctrl: {type: 'key', key: 'ctrl'}
+    },
+    stateDefaults: {
+        label: '+',
+        icon: 'core.plus',
+        execute: function(params) {
+            var node = params.fragment.node.getNearestElementNode();
+            var toAdd = node.document.createDocumentNode(params.template.content);
+            node.after(toAdd);
+        }
+    },
+    getState: function(params) {
+        if(!(params.template && params.template.id)) {
+            return {
+                allowed: false,
+                description: gettext('No template selected')
+            };
+        } else if(!params.fragment || !params.fragment.isValid() || !(params.fragment instanceof params.fragment.NodeFragment)) {
+            return {
+                allowed: false,
+                description: gettext('Wrong node selected')
+            };
+        }
+        return {
+            allowed: true,
+            description: interpolate(gettext('Insert template %s after %s'), [params.template.name, params.fragment.node.getNearestElementNode().getTagName()])
+        };
+    }
+};
+
+
+return {
+    actions: [insertTemplateAction]
+};
+
+});
\ No newline at end of file
index c3da0df..e2028fa 100644 (file)
@@ -6,6 +6,12 @@
       return (/\.test\.js$/).test(file);
     });
 
+    /* globals window */
+    // This installs noop i18n functions so that tests can work with i18nized code
+    window.gettext = window.interpolate = function() {
+        return Array.prototype.slice.call(arguments, 0);
+    };
+
     require({
       baseUrl: '/base/src/editor',
       deps: tests,