From 9831076c8f7385dffb533e0327cc7dd7c9f1ef92 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Mon, 14 Apr 2014 11:24:12 +0200 Subject: [PATCH 01/16] editor: actions implementation --- .../modules/documentCanvas/canvas/canvas.js | 122 ++++++- .../documentCanvas/canvas/wlxmlListener.js | 4 + src/editor/modules/documentCanvas/commands.js | 338 ------------------ .../modules/documentCanvas/documentCanvas.js | 35 +- .../modules/documentToolbar/actionView.js | 139 +++++++ .../documentToolbar/documentToolbar.js | 164 +++++---- .../documentToolbar/documentToolbar.less | 10 +- .../modules/documentToolbar/template.html | 36 +- .../documentToolbar/templates/actionView.html | 1 + .../templates/actionViewButton.html | 7 + .../templates/actionViewSelection.html | 6 + .../modules/metadataEditor/metadataEditor.js | 1 + .../nodeBreadCrumbs/nodeBreadCrumbs.js | 6 +- .../modules/nodeBreadCrumbs/template.html | 2 +- .../modules/nodeFamilyTree/nodeFamilyTree.js | 90 ++--- src/editor/modules/nodePane/nodePane.js | 47 ++- src/editor/modules/rng/rng.js | 53 +-- src/editor/plugins/core/core.js | 235 +++++++++++- src/editor/plugins/core/footnote.js | 84 +++++ src/editor/plugins/core/lists.js | 121 +++++++ src/editor/plugins/core/switch.js | 67 ++++ src/editor/plugins/core/templates.js | 47 +++ tests/main.js | 6 + 23 files changed, 1078 insertions(+), 543 deletions(-) delete mode 100644 src/editor/modules/documentCanvas/commands.js create mode 100644 src/editor/modules/documentToolbar/actionView.js create mode 100644 src/editor/modules/documentToolbar/templates/actionView.html create mode 100644 src/editor/modules/documentToolbar/templates/actionViewButton.html create mode 100644 src/editor/modules/documentToolbar/templates/actionViewSelection.html create mode 100644 src/editor/plugins/core/footnote.js create mode 100644 src/editor/plugins/core/lists.js create mode 100644 src/editor/plugins/core/switch.js create mode 100644 src/editor/plugins/core/templates.js diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index b217d18..96e3bea 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -49,17 +49,16 @@ $.extend(TextHandler.prototype, { }); -var Canvas = function(wlxmlDocument, publisher) { +var Canvas = function(wlxmlDocument) { this.eventBus = _.extend({}, Backbone.Events); this.wrapper = $('
').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); } }; diff --git a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js index f932ea0..7d19e94 100644 --- a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js +++ b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js @@ -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 index 85831d8..0000000 --- a/src/editor/modules/documentCanvas/commands.js +++ /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 diff --git a/src/editor/modules/documentCanvas/documentCanvas.js b/src/editor/modules/documentCanvas/documentCanvas.js index 6015089..3e6a209 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.js +++ b/src/editor/modules/documentCanvas/documentCanvas.js @@ -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 index 0000000..ddef396 --- /dev/null +++ b/src/editor/modules/documentToolbar/actionView.js @@ -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 = $('').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('
'); + button.after('
'); + } + + 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 diff --git a/src/editor/modules/documentToolbar/documentToolbar.js b/src/editor/modules/documentToolbar/documentToolbar.js index 33f95a6..82b6dca 100644 --- a/src/editor/modules/documentToolbar/documentToolbar.js +++ b/src/editor/modules/documentToolbar/documentToolbar.js @@ -1,87 +1,127 @@ -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.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); } }; diff --git a/src/editor/modules/documentToolbar/documentToolbar.less b/src/editor/modules/documentToolbar/documentToolbar.less index 559afde..4b35b12 100644 --- a/src/editor/modules/documentToolbar/documentToolbar.less +++ b/src/editor/modules/documentToolbar/documentToolbar.less @@ -21,7 +21,15 @@ 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; + } } } diff --git a/src/editor/modules/documentToolbar/template.html b/src/editor/modules/documentToolbar/template.html index 2e14a89..1c013da 100644 --- a/src/editor/modules/documentToolbar/template.html +++ b/src/editor/modules/documentToolbar/template.html @@ -1,35 +1 @@ -
-
- - -
- -
- - - -
- -
- - - -
- - <% if(templates) { %> -
- - -
- <% } %> - -
- -
- -
-
\ No newline at end of file +
diff --git a/src/editor/modules/documentToolbar/templates/actionView.html b/src/editor/modules/documentToolbar/templates/actionView.html new file mode 100644 index 0000000..f5ca649 --- /dev/null +++ b/src/editor/modules/documentToolbar/templates/actionView.html @@ -0,0 +1 @@ +
\ 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 index 0000000..506a291 --- /dev/null +++ b/src/editor/modules/documentToolbar/templates/actionViewButton.html @@ -0,0 +1,7 @@ + \ 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 index 0000000..c09ed2d --- /dev/null +++ b/src/editor/modules/documentToolbar/templates/actionViewSelection.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/editor/modules/metadataEditor/metadataEditor.js b/src/editor/modules/metadataEditor/metadataEditor.js index 118571e..b9021bc 100644 --- a/src/editor/modules/metadataEditor/metadataEditor.js +++ b/src/editor/modules/metadataEditor/metadataEditor.js @@ -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; diff --git a/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js b/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js index 55f4fab..188aa6e 100644 --- a/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js +++ b/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js @@ -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) { diff --git a/src/editor/modules/nodeBreadCrumbs/template.html b/src/editor/modules/nodeBreadCrumbs/template.html index 2f96b2d..062fb1e 100644 --- a/src/editor/modules/nodeBreadCrumbs/template.html +++ b/src/editor/modules/nodeBreadCrumbs/template.html @@ -5,6 +5,6 @@
  • <%= utils.getTagLabel(parents[i].getTagName()) %><% if(parents[i].getClass()) { %>.<%= utils.getClassLabel(parents[i].getClass()) %> <% } %>/
  • <% } %>
  • <%= utils.getTagLabel(node.getTagName()) %><% if(node.getClass()) { %>.<%= utils.getClassLabel(node.getClass()) %> <% } %>
  • - <% } %> + <% } else { %>   <% } %>
    \ No newline at end of file diff --git a/src/editor/modules/nodeFamilyTree/nodeFamilyTree.js b/src/editor/modules/nodeFamilyTree/nodeFamilyTree.js index e2a4eb8..bda562f 100644 --- a/src/editor/modules/nodeFamilyTree/nodeFamilyTree.js +++ b/src/editor/modules/nodeFamilyTree/nodeFamilyTree.js @@ -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 = ''; - } - 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 = ''; + } + 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); } }, diff --git a/src/editor/modules/nodePane/nodePane.js b/src/editor/modules/nodePane/nodePane.js index 02ae2cc..f7e6694 100644 --- a/src/editor/modules/nodePane/nodePane.js +++ b/src/editor/modules/nodePane/nodePane.js @@ -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; } }; diff --git a/src/editor/modules/rng/rng.js b/src/editor/modules/rng/rng.js index b12c19f..e15c94b 100644 --- a/src/editor/modules/rng/rng.js +++ b/src/editor/modules/rng/rng.js @@ -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) { diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index d11bf73..d0a376d 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -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 index 0000000..2fa314a --- /dev/null +++ b/src/editor/plugins/core/footnote.js @@ -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 index 0000000..3cf1192 --- /dev/null +++ b/src/editor/plugins/core/lists.js @@ -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 index 0000000..2403c2b --- /dev/null +++ b/src/editor/plugins/core/switch.js @@ -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 index 0000000..2b1f730 --- /dev/null +++ b/src/editor/plugins/core/templates.js @@ -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 diff --git a/tests/main.js b/tests/main.js index c3da0df..e2028fa 100644 --- a/tests/main.js +++ b/tests/main.js @@ -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, -- 2.20.1 From d69dca1062b6c79b302512839c63853812e44cd8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Mon, 17 Feb 2014 10:37:43 +0100 Subject: [PATCH 02/16] editor: bottom panel --- src/editor/modules/rng/mainLayout.html | 1 + src/editor/modules/rng/mainLayout.less | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/editor/modules/rng/mainLayout.html b/src/editor/modules/rng/mainLayout.html index 0cccb9c..825ae38 100644 --- a/src/editor/modules/rng/mainLayout.html +++ b/src/editor/modules/rng/mainLayout.html @@ -5,4 +5,5 @@
    +
    \ No newline at end of file diff --git a/src/editor/modules/rng/mainLayout.less b/src/editor/modules/rng/mainLayout.less index f957f0a..c1a7e8e 100644 --- a/src/editor/modules/rng/mainLayout.less +++ b/src/editor/modules/rng/mainLayout.less @@ -23,7 +23,7 @@ top: 15px; left:0; right:0; - bottom:0; + bottom:20px; z-index: 1; > .rng-view-tabs { @@ -40,4 +40,10 @@ } } + [fnpjs-place="bottomPanel"] { + position: absolute; + bottom:0; + height: 20px; + width:100% + } } \ No newline at end of file -- 2.20.1 From c11ad12846df3142b19a343144fe1d2a82b97569 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Thu, 17 Apr 2014 11:44:03 +0200 Subject: [PATCH 03/16] editor: status bar --- src/editor/modules.js | 1 + src/editor/modules/rng/rng.js | 27 +++++++++++-- src/editor/modules/statusBar/statusBar.html | 1 + src/editor/modules/statusBar/statusBar.js | 42 +++++++++++++++++++++ src/editor/modules/statusBar/statusBar.less | 8 ++++ src/editor/styles/main.less | 1 + 6 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/editor/modules/statusBar/statusBar.html create mode 100644 src/editor/modules/statusBar/statusBar.js create mode 100644 src/editor/modules/statusBar/statusBar.less diff --git a/src/editor/modules.js b/src/editor/modules.js index f7c5f6e..6b92a5e 100644 --- a/src/editor/modules.js +++ b/src/editor/modules.js @@ -10,6 +10,7 @@ define(function(require) { data: require('modules/data/data'), rng: require('modules/rng/rng'), mainBar: require('modules/mainBar/mainBar'), + statusBar: require('modules/statusBar/statusBar'), indicator: require('modules/indicator/indicator'), sourceEditor: require('modules/sourceEditor/sourceEditor'), diff --git a/src/editor/modules/rng/rng.js b/src/editor/modules/rng/rng.js index e15c94b..6a3271b 100644 --- a/src/editor/modules/rng/rng.js +++ b/src/editor/modules/rng/rng.js @@ -107,7 +107,7 @@ return function(sandbox) { sandbox.getModule('mainBar').setCommandEnabled('drop-draft', usingDraft); sandbox.getModule('mainBar').setCommandEnabled('save', usingDraft); - _.each(['sourceEditor', 'documentCanvas', 'documentToolbar', 'metadataEditor', 'nodeBreadCrumbs', 'mainBar', 'indicator', 'documentHistory', 'diffViewer'], function(moduleName) { + _.each(['sourceEditor', 'documentCanvas', 'documentToolbar', 'metadataEditor', 'nodeBreadCrumbs', 'mainBar', 'indicator', 'documentHistory', 'diffViewer', 'statusBar'], function(moduleName) { sandbox.getModule(moduleName).start(); }); @@ -294,6 +294,21 @@ return function(sandbox) { } }; + eventHandlers.statusBar = { + ready: function() { + views.mainLayout.setView('bottomPanel', sandbox.getModule('statusBar').getView()); + } + }; + + eventHandlers.__all__ = { + actionHovered: function(action) { + sandbox.getModule('statusBar').showAction(action); + }, + actionOff: function() { + sandbox.getModule('statusBar').clearAction(); + } + }; + window.addEventListener('beforeunload', function(event) { var txt = gettext('Do you really want to exit?'); if(documentIsDirty) { @@ -319,10 +334,16 @@ return function(sandbox) { if(eventHandlers[moduleName] && eventHandlers[moduleName][eventName]) { logger.debug('Handling event ' + eventRepr); eventHandlers[moduleName][eventName].apply(eventHandlers, args); - } else { - logger.warning('No event handler for ' + eventRepr); + return; + } + + if(eventHandlers.__all__[eventName]) { + logger.debug('Handling event ' + eventRepr); + eventHandlers.__all__[eventName].apply(eventHandlers.__all__, args); + return; } + logger.warning('No event handler for ' + eventRepr); } }; }; diff --git a/src/editor/modules/statusBar/statusBar.html b/src/editor/modules/statusBar/statusBar.html new file mode 100644 index 0000000..0473d59 --- /dev/null +++ b/src/editor/modules/statusBar/statusBar.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/src/editor/modules/statusBar/statusBar.js b/src/editor/modules/statusBar/statusBar.js new file mode 100644 index 0000000..5ad7c6b --- /dev/null +++ b/src/editor/modules/statusBar/statusBar.js @@ -0,0 +1,42 @@ +define(function(require) { + +'use strict'; +/* globals gettext */ + +var $ = require('libs/jquery'), + template = require('libs/text!modules/statusBar/statusBar.html'), + logging = require('fnpjs/logging/logging'); + +var logger = logging.getLogger('statusBar'); + +return function(sandbox){ + + var view = $(template); + + return { + start: function() { + return sandbox.publish('ready'); + }, + getView: function() { + return view; + }, + showAction: function(action) { + var state = action.getState(), + description = state.description; + if(!description) { + description = state.allowed ? gettext('Undescribed action') : gettext('Action not allowed'); + logger.info('Undescribed action: ' + action.name); + } + view.text(description); + if(!state.allowed) { + view.prepend('!'); + } + }, + clearAction: function() { + view.text(''); + } + }; + +}; + +}); \ No newline at end of file diff --git a/src/editor/modules/statusBar/statusBar.less b/src/editor/modules/statusBar/statusBar.less new file mode 100644 index 0000000..cbaa2f5 --- /dev/null +++ b/src/editor/modules/statusBar/statusBar.less @@ -0,0 +1,8 @@ +#rng-module-statusBar { + border-width: 1px 0 0 0; + border-style: solid; + border-color: #ddd; + font-size: 0.8em; + height: 100%; + padding: 5px; +} \ No newline at end of file diff --git a/src/editor/styles/main.less b/src/editor/styles/main.less index 82bf8d5..411a2aa 100644 --- a/src/editor/styles/main.less +++ b/src/editor/styles/main.less @@ -15,3 +15,4 @@ @import '../modules/nodeFamilyTree/nodeFamilyTree.less'; @import '../modules/metadataEditor/metadataEditor.less'; @import '../modules/diffViewer/diffViewer.less'; +@import '../modules/statusBar/statusBar.less'; -- 2.20.1 From 105a4e9cdd9d1dffdff6411e7b7e83ff507c68e6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Thu, 10 Apr 2014 16:43:51 +0200 Subject: [PATCH 04/16] editor: undo/redo action description contains information on what would be undone/redone --- src/editor/modules/documentCanvas/canvas/canvas.js | 10 ++++++++-- src/editor/modules/metadataEditor/metadataEditor.js | 13 ++++++++++--- src/editor/plugins/core/core.js | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index 96e3bea..d58ea7b 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -10,7 +10,7 @@ define([ ], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener) { 'use strict'; -/* global document:false, window:false, Node:false */ +/* global document:false, window:false, Node:false, gettext */ var logger = logging.getLogger('canvas'); @@ -42,7 +42,13 @@ $.extend(TextHandler.prototype, { }, setText: function(text, node) { //this.canvas.wlxmlDocument.transform('setText', {node:node, text: text}); - node.setText(text); + node.document.transaction(function() { + node.setText(text); + }, { + metadata:{ + description: gettext('Changing text') + } + }); } diff --git a/src/editor/modules/metadataEditor/metadataEditor.js b/src/editor/modules/metadataEditor/metadataEditor.js index b9021bc..1088247 100644 --- a/src/editor/modules/metadataEditor/metadataEditor.js +++ b/src/editor/modules/metadataEditor/metadataEditor.js @@ -7,6 +7,7 @@ define([ ], function($, _, mainTemplate, itemTemplate, OpenSelectView) { 'use strict'; +/* globals gettext */ return function(sandbox) { @@ -41,11 +42,15 @@ return function(sandbox) { this.node.find('.rng-module-metadataEditor-addBtn').click(function() { adding = true; - currentNode.getMetadata().add('',''); + currentNode.document.transaction(function() { + currentNode.getMetadata().add('',''); + }, this, gettext('Add metadata row')); }); this.metaTable.on('click', '.rng-visualEditor-metaRemoveBtn', function(e) { - $(e.target).closest('tr').data('row').remove(); + currentNode.document.transaction(function() { + $(e.target).closest('tr').data('row').remove(); + }, this, gettext('Remove metadata row')); }); this.metaTable.on('keydown', '[contenteditable]', function(e) { @@ -70,7 +75,9 @@ return function(sandbox) { row = editable.parents('tr').data('row'), isKey = _.last(editable.attr('class').split('-')) === 'metaItemKey', method = isKey ? 'setKey' : 'setValue'; - row[method](toSet); + row.metadata.node.document.transaction(function() { + row[method](toSet); + }, this, gettext('Metadata edit')); } }, 500)); }, diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index d0a376d..a27ec9e 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -62,6 +62,9 @@ var undoRedoAction = function(dir) { 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'); + if(allowed) { + desc += ': ' + (_.last(params.document[dir+'Stack']).metadata || gettext('unknown operation')); + } return { allowed: allowed, description: allowed ? desc : descEmpty -- 2.20.1 From f93de3e03a720d2b0373ed31977aeb4c47c7fb69 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 18 Feb 2014 15:43:05 +0100 Subject: [PATCH 05/16] smartxml: DocumentNode.isInDocument syntactic sugar --- src/smartxml/smartxml.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index 08897cc..8924e7f 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -79,6 +79,10 @@ $.extend(DocumentNode.prototype, { return this.document.root.sameNode(this); }, + isInDocument: function() { + return this.document.containsNode(this); + }, + isSiblingOf: function(node) { return node && this.parent().sameNode(node.parent()); }, -- 2.20.1 From 1067bdcc2f6038b6ffd6a9e3100b0ff75eeb672a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 18 Feb 2014 16:05:07 +0100 Subject: [PATCH 06/16] smartxml: TextNodes don't contain other nodes - let them state that --- src/smartxml/smartxml.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index 8924e7f..e2cdc9b 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -247,6 +247,11 @@ $.extend(TextNode.prototype, { return this.nativeNode.data; }, + + containsNode: function() { + return false; + }, + triggerTextChangeEvent: function() { var event = new events.ChangeEvent('nodeTextChange', {node: this}); this.document.trigger('change', event); -- 2.20.1 From ea854b3eae2b03d3eab4f9b937798024648fc8ed Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 18 Feb 2014 16:10:58 +0100 Subject: [PATCH 07/16] editor: fix - better handle node removal in various views --- src/editor/modules/metadataEditor/metadataEditor.js | 2 +- src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js | 11 ++++++++++- src/editor/modules/nodePane/nodePane.js | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/editor/modules/metadataEditor/metadataEditor.js b/src/editor/modules/metadataEditor/metadataEditor.js index 1088247..70df62e 100644 --- a/src/editor/modules/metadataEditor/metadataEditor.js +++ b/src/editor/modules/metadataEditor/metadataEditor.js @@ -191,7 +191,7 @@ return function(sandbox) { if(event.type === 'metadataRemoved' && event.meta.node.sameNode(currentNode)) { view.removeMetadataRow(event.meta.row); } - if(event.type === 'nodeDetached' && event.meta.node.sameNode(currentNode)) { + if(event.type === 'nodeDetached' && event.meta.node.containsNode(currentNode)) { view.setMetadata(null); } }); diff --git a/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js b/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js index 188aa6e..0540542 100644 --- a/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js +++ b/src/editor/modules/nodeBreadCrumbs/nodeBreadCrumbs.js @@ -8,7 +8,8 @@ define([ return function(sandbox) { - var template = _.template(templateSrc); + var template = _.template(templateSrc), + listens = false; var view = { dom: $('
    ' + template({node:null, parents: null}) + '
    '), @@ -58,6 +59,14 @@ return function(sandbox) { start: function() { sandbox.publish('ready'); }, getView: function() { return view.dom; }, setNodeElement: function(nodeElement) { + if(!listens && nodeElement) { + nodeElement.document.on('change', function() { + if(view.currentNodeElement && !view.currentNodeElement.isInDocument()) { + view.setNodeElement(null); + } + }); + listens = true; + } view.setNodeElement(nodeElement); }, highlightNode: function(id) { view.highlightNode(id); }, diff --git a/src/editor/modules/nodePane/nodePane.js b/src/editor/modules/nodePane/nodePane.js index f7e6694..85a6fdd 100644 --- a/src/editor/modules/nodePane/nodePane.js +++ b/src/editor/modules/nodePane/nodePane.js @@ -35,6 +35,9 @@ return function(sandbox) { var module = this; if(!listens) { wlxmlNodeElement.document.on('change', function(event) { + if(currentNode && !currentNode.isInDocument()) { + module.setNodeElement(null); + } if(event.type === 'nodeAttrChange' && event.meta.node.sameNode(currentNode)) { module.setNodeElement(currentNode); } -- 2.20.1 From 42062ddcb82f2d372faafbe7bb1f9a27436f0bbd Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Wed, 19 Feb 2014 13:02:16 +0100 Subject: [PATCH 08/16] editor: additional descriptions for some transactions --- .../modules/documentCanvas/canvas/keyboard.js | 14 +++++++++----- src/editor/modules/nodePane/nodePane.js | 13 ++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/editor/modules/documentCanvas/canvas/keyboard.js b/src/editor/modules/documentCanvas/canvas/keyboard.js index 383ca18..9be3dae 100644 --- a/src/editor/modules/documentCanvas/canvas/keyboard.js +++ b/src/editor/modules/documentCanvas/canvas/keyboard.js @@ -4,7 +4,7 @@ define([ ], function(documentElement, utils) { 'use strict'; - +/* globals gettext */ var KEYS = { ENTER: 13, @@ -50,7 +50,7 @@ handlers.push({key: KEYS.ENTER, if(Object.keys(cursor.getPosition()).length === 0) { var currentElement = canvas.getCurrentNodeElement(); if(currentElement) { - canvas.wlxmlDocument.startTransaction(); + canvas.wlxmlDocument.startTransaction(gettext('Splitting text')); added = currentElement.wlxmlNode.after({ tagName: currentElement.getWlxmlTag() || 'div', attrs: {'class': currentElement.getWlxmlClass() || 'p'} @@ -68,7 +68,7 @@ handlers.push({key: KEYS.ENTER, element = element.parent(); } - canvas.wlxmlDocument.startTransaction(); + canvas.wlxmlDocument.startTransaction(gettext('Splitting text')); added = element.wlxmlNode.after( {tagName: element.getWlxmlTag() || 'div', attrs: {'class': element.getWlxmlClass() || 'p'}} ); @@ -100,9 +100,13 @@ handlers.push({key: KEYS.ENTER, // goto = nodes.second; // gotoOptions = {caretTo: 'start'}; // } + var node = position.element.wlxmlNode, + result, goto, gotoOptions; + + node.document.transaction(function() { + result = position.element.wlxmlNode.breakContent({offset: position.offset}); + }, this, gettext('Splitting text')); - var result = position.element.wlxmlNode.breakContent({offset: position.offset}), - goto, gotoOptions; if(result.emptyText) { goto = result.emptyText; gotoOptions = {}; diff --git a/src/editor/modules/nodePane/nodePane.js b/src/editor/modules/nodePane/nodePane.js index 85a6fdd..25e5ddb 100644 --- a/src/editor/modules/nodePane/nodePane.js +++ b/src/editor/modules/nodePane/nodePane.js @@ -12,13 +12,20 @@ return function(sandbox) { var view = $(_.template(templateSrc)({utils: wlxmlUtils})), listens = false, - currentNode; + currentNode, + msgs = { + Tag: gettext('Tag editing'), + Class: gettext('Class editing') + }; view.on('change', 'select', function(e) { var target = $(e.target); var attr = target.attr('class').split('-')[3] === 'tagSelect' ? 'Tag' : 'Class', - value = target.val().replace(/-/g, '.'); - currentNode['set' + attr](value); + value = target.val().replace(/-/g, '.'), + oldValue = attr === 'Tag' ? currentNode.getTagName() : currentNode.getClass(); + currentNode.document.transaction(function() { + currentNode['set' + attr](value); + }, this, msgs[attr] + ': ' + oldValue + ' -> ' + value); }); -- 2.20.1 From d0bf0dec6f6c72fb6dd3bccccbabf858c71368a0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Mon, 14 Apr 2014 15:49:00 +0200 Subject: [PATCH 09/16] fnpjs: Action - refactor update*Params --- src/fnpjs/actions.js | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/fnpjs/actions.js b/src/fnpjs/actions.js index 8b002bf..d369ff0 100644 --- a/src/fnpjs/actions.js +++ b/src/fnpjs/actions.js @@ -19,14 +19,14 @@ _.extend(Action.prototype, Backbone.Events, { getPluginName: function() { return this.fqName.split('.')[0]; }, - updateContextParam: function(contextName, value) { + updateParam: function(filter, value) { var changed = false; _.pairs(this.definition.params).forEach(function(pair) { var paramName = pair[0], paramDesc = pair[1]; - if(paramDesc.type === 'context' && paramDesc.name === contextName) { - this.params[paramName] = value; - changed = true; + if(filter(paramDesc, paramName)) { + this.params[paramName] = value; + changed = true; } }.bind(this)); if(changed) { @@ -34,30 +34,20 @@ _.extend(Action.prototype, Backbone.Events, { this.trigger('paramsChanged'); } }, + updateContextParam: function(contextName, value) { + this.updateParam(function(paramDesc) { + return paramDesc.type === 'context' && paramDesc.name === contextName; + }, value); + }, updateKeyParam: function(keyName, toggled) { - var changed = false; - _.pairs(this.definition.params).forEach(function(pair) { - var paramName = pair[0], - paramDesc = pair[1]; - if(paramDesc.type === 'key' && paramDesc.key === keyName) { - this.params[paramName] = toggled; - changed = true; - } - }.bind(this)); - - if(changed) { - this._cache = null; - this.trigger('paramsChanged'); - } + this.updateParam(function(paramDesc) { + return paramDesc.type === 'key' && paramDesc.key === keyName; + }, toggled); }, updateWidgetParam: function(name, value) { - var paramDesc = this.definition.params[name]; - if(paramDesc.type === 'context' || paramDesc.type === 'key') { - throw new Error(''); - } - this.params[name] = value; - this._cache = null; - this.trigger('paramsChanged'); + this.updateParam(function(paramDesc, paramName) { + return !_.contains(['context', 'key'], paramDesc.type) && paramName === name; + }, value); }, getState: function() { var gotState; -- 2.20.1 From 30cd9b94ad82e3c3e1551fb8688eca6144f4a0f6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Mon, 14 Apr 2014 16:33:26 +0200 Subject: [PATCH 10/16] fnpjs: actions - handle exception in action.getState gracefully --- src/fnpjs/actions.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/fnpjs/actions.js b/src/fnpjs/actions.js index d369ff0..c49373a 100644 --- a/src/fnpjs/actions.js +++ b/src/fnpjs/actions.js @@ -3,7 +3,11 @@ define(function(require) { 'use strict'; var _ = require('libs/underscore'), - Backbone = require('libs/backbone'); + Backbone = require('libs/backbone'), + logging = require('fnpjs/logging/logging'); + +var logger = logging.getLogger('fnpjs.actions'); + var Action = function(fqName, definition, config, appObject) { this.fqName = fqName; @@ -52,7 +56,12 @@ _.extend(Action.prototype, Backbone.Events, { getState: function() { var gotState; if(!this._cache) { - gotState = this.definition.getState.call(this, this.params); + try { + gotState = this.definition.getState.call(this, this.params); + } catch(e) { + logger.exception(e); + return; + } if(typeof gotState === 'boolean') { gotState = {allowed: gotState}; } -- 2.20.1 From 4a081f65f754c79a437d983ee1a22c60f97ee784 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Mon, 14 Apr 2014 16:34:09 +0200 Subject: [PATCH 11/16] editor: handle undefined action state --- src/editor/modules/documentToolbar/actionView.js | 7 +++++++ src/editor/modules/statusBar/statusBar.js | 14 +++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/editor/modules/documentToolbar/actionView.js b/src/editor/modules/documentToolbar/actionView.js index ddef396..114eb32 100644 --- a/src/editor/modules/documentToolbar/actionView.js +++ b/src/editor/modules/documentToolbar/actionView.js @@ -1,6 +1,7 @@ define(function(require) { 'use strict'; +/* globals gettext */ var $ = require('libs/jquery'), Backbone = require('libs/backbone'), @@ -44,6 +45,12 @@ var ActionView = Backbone.View.extend({ var actionState = this.action.getState(); + if(!actionState) { + this.$el.html(buttonTemplate({label: gettext('error :('), iconName:''})); + this._button().attr('disabled', true); + return; + } + var templateContext = { label: actionState.label || '?', iconName: (iconExists(actionState.icon)) ? actionState.icon : null, diff --git a/src/editor/modules/statusBar/statusBar.js b/src/editor/modules/statusBar/statusBar.js index 5ad7c6b..0f08c59 100644 --- a/src/editor/modules/statusBar/statusBar.js +++ b/src/editor/modules/statusBar/statusBar.js @@ -22,11 +22,19 @@ return function(sandbox){ }, showAction: function(action) { var state = action.getState(), + description; + + if(!state) { + description = gettext('error :('); + logger.error('Got undefined action state: ' + action.name); + } else { description = state.description; - if(!description) { - description = state.allowed ? gettext('Undescribed action') : gettext('Action not allowed'); - logger.info('Undescribed action: ' + action.name); + if(!description) { + description = state.allowed ? gettext('Undescribed action') : gettext('Action not allowed'); + logger.info('Undescribed action: ' + action.name); + } } + view.text(description); if(!state.allowed) { view.prepend('!'); -- 2.20.1 From 68c9d615c3b5f4d6fefcd721f0315fde89fc3328 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Thu, 17 Apr 2014 12:36:13 +0200 Subject: [PATCH 12/16] template get state fix --- src/editor/plugins/core/templates.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/editor/plugins/core/templates.js b/src/editor/plugins/core/templates.js index 2b1f730..1114258 100644 --- a/src/editor/plugins/core/templates.js +++ b/src/editor/plugins/core/templates.js @@ -13,12 +13,7 @@ var insertTemplateAction = { }, 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); - } + icon: 'core.plus' }, getState: function(params) { if(!(params.template && params.template.id)) { @@ -26,15 +21,24 @@ var insertTemplateAction = { 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') + } else if( + !params.fragment || !params.fragment.isValid() || + !(params.fragment instanceof params.fragment.NodeFragment) || + params.fragment.node.getNearestElementNode().isRoot() + ) { + 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()]) + description: interpolate(gettext('Insert template %s after %s'), [params.template.name, params.fragment.node.getNearestElementNode().getTagName()]), + execute: function(params) { + var node = params.fragment.node.getNearestElementNode(); + var toAdd = node.document.createDocumentNode(params.template.content); + node.after(toAdd); + } }; } }; -- 2.20.1 From e6993e4bfe0e5fd38ccf554da489b5af3625c2fb Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Thu, 17 Apr 2014 12:47:52 +0200 Subject: [PATCH 13/16] editor: Setting descriptions for transactions performed in actions --- src/editor/plugins/core/core.js | 35 ++++++++++++++++++++++----- src/editor/plugins/core/lists.js | 36 +++++++++++++++++++++++----- src/editor/plugins/core/switch.js | 10 ++++++-- src/editor/plugins/core/templates.js | 16 ++++++++++--- 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index a27ec9e..3465046 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -63,7 +63,10 @@ var undoRedoAction = function(dir) { desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'), descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo'); if(allowed) { - desc += ': ' + (_.last(params.document[dir+'Stack']).metadata || gettext('unknown operation')); + var metadata = _.last(params.document[dir+'Stack']).metadata; + if(metadata) { + desc += ': ' + (metadata.description || gettext('unknown operation')); + } } return { allowed: allowed, @@ -89,7 +92,8 @@ var commentAction = { icon: 'comment', execute: function(params, editor) { /* globals Node */ - var node = params.fragment.node; + var node = params.fragment.node, + action = this; if(node.nodeType === Node.TEXT_NODE) { node = node.parent(); } @@ -117,6 +121,10 @@ var commentAction = { var metadata = comment.getMetadata(); metadata.add({key: 'creator', value: creator}); metadata.add({key: 'date', value: dt}); + }, { + metadata: { + description: action.getState().description + } }); }, }, @@ -159,6 +167,7 @@ var createWrapTextAction = function(createParams) { return _.extend(state, { allowed: true, + description: createParams.description, execute: function(params) { params.fragment.document.transaction(function() { var parent = params.fragment.startNode.parent(); @@ -168,6 +177,10 @@ var createWrapTextAction = function(createParams) { offsetEnd: params.fragment.endOffset, textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()] }); + }, { + metadata: { + description: createParams.description + } }); } }); @@ -185,7 +198,8 @@ var createLinkFromSelection = function(params) { fields: [ {label: gettext('Link'), name: 'href', type: 'input'} ] - }); + }), + action = this; dialog.on('execute', function(event) { doc.transaction(function() { @@ -198,6 +212,10 @@ var createLinkFromSelection = function(params) { span.setAttr('href', event.formData.href); event.success(); return span; + }, { + metadata: { + description: action.getState().description + } }); }); dialog.show(); @@ -213,12 +231,17 @@ var editLink = function(params) { fields: [ {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')} ] - }); + }), + action = this; dialog.on('execute', function(event) { doc.transaction(function() { link.setAttr('href', event.formData.href); event.success(); + }, { + metadata: { + description: action.getState().description + } }); }); dialog.show(); @@ -262,8 +285,8 @@ plugin.actions = [ undoRedoAction('undo'), undoRedoAction('redo'), commentAction, - createWrapTextAction({name: 'emphasis', klass: 'emp'}), - createWrapTextAction({name: 'cite', klass: 'cite'}), + createWrapTextAction({name: 'emphasis', klass: 'emp', description: gettext('Mark as emphasized')}), + createWrapTextAction({name: 'cite', klass: 'cite', description: gettext('Mark as citation')}), linkAction ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions); diff --git a/src/editor/plugins/core/lists.js b/src/editor/plugins/core/lists.js index 3cf1192..a8dc802 100644 --- a/src/editor/plugins/core/lists.js +++ b/src/editor/plugins/core/lists.js @@ -34,30 +34,54 @@ var toggleListAction = function(type) { var execute = { add: function(params) { var boundries = getBoundriesForAList(params.fragment), - listParams = {klass: type === 'Bullet' ? 'list' : 'list.enum'}; + listParams = {klass: type === 'Bullet' ? 'list' : 'list.enum'}, + action = this; + if(boundries && boundries.node1) { listParams.node1 = boundries.node1; listParams.node2 = boundries.node2; - boundries.node1.document.createList(listParams); + boundries.node1.document.transaction(function() { + boundries.node1.document.createList(listParams); + }, { + metadata: { + description: action.getState().description + } + }); } else { throw new Error('Invalid boundries'); } }, remove: function(params) { /* globals Node */ - var current = params.fragment.node; + var current = params.fragment.node, + action = this; var toSearch = current.nodeType === Node.ELEMENT_NODE ? [current] : []; toSearch = toSearch.concat(current.parents()); toSearch.some(function(node) { if(node.is('list')) { - node.object.extractListItems(); + node.document.transaction(function() { + node.object.extractListItems(); + }, { + metadata: { + description: action.getState().description + } + }); + return true; // break } - }); + }.bind(this)); }, changeType: function(params) { - params.fragment.node.getParent('list').setClass(type === 'Bullet' ? 'list' : 'list.enum'); + var node = params.fragment.node, + action = this; + node.document.transaction(function() { + node.getParent('list').setClass(type === 'Bullet' ? 'list' : 'list.enum'); + }, { + metadata: { + description: action.getState().description + } + }); } }; diff --git a/src/editor/plugins/core/switch.js b/src/editor/plugins/core/switch.js index 2403c2b..2730761 100644 --- a/src/editor/plugins/core/switch.js +++ b/src/editor/plugins/core/switch.js @@ -16,7 +16,8 @@ var createSwitchAction = function(createParams) { var state = { label: this.config.label }, - f = params.fragment; + f = params.fragment, + description; if( @@ -37,10 +38,11 @@ var createSwitchAction = function(createParams) { toSwitch = toSwitch.getParent(createParams.from); } + description = 'Switch to ' + createParams.to.name; return _.extend(state, { allowed: !!toSwitch, toggled: alreadyInTarget, - description: 'Switch to ' + createParams.to.name, + description: description, execute: alreadyInTarget ? function() {} : function() { f.document.transaction(function() { if(createParams.to.tagName) { @@ -49,6 +51,10 @@ var createSwitchAction = function(createParams) { if(!_.isUndefined(createParams.to.klass)) { toSwitch.setClass(createParams.to.klass); } + }, { + metadata: { + description: description + } }); } }); diff --git a/src/editor/plugins/core/templates.js b/src/editor/plugins/core/templates.js index 1114258..69e30f9 100644 --- a/src/editor/plugins/core/templates.js +++ b/src/editor/plugins/core/templates.js @@ -16,6 +16,8 @@ var insertTemplateAction = { icon: 'core.plus' }, getState: function(params) { + var description; + if(!(params.template && params.template.id)) { return { allowed: false, @@ -31,13 +33,21 @@ var insertTemplateAction = { description: gettext('Wrong node selected') }; } + + description = interpolate(gettext('Insert template %s after %s'), [params.template.name, params.fragment.node.getNearestElementNode().getTagName()]); return { allowed: true, - description: interpolate(gettext('Insert template %s after %s'), [params.template.name, params.fragment.node.getNearestElementNode().getTagName()]), + description: description, execute: function(params) { var node = params.fragment.node.getNearestElementNode(); - var toAdd = node.document.createDocumentNode(params.template.content); - node.after(toAdd); + node.document.transaction(function() { + var toAdd = node.document.createDocumentNode(params.template.content); + node.after(toAdd); + }, { + metadata: { + description: description + } + }); } }; } -- 2.20.1 From 07d6b62cad891ce2a51a60f81a6af00b73f3b9fe Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 22 Apr 2014 12:33:55 +0200 Subject: [PATCH 14/16] fnpjs: actions - action returns via callback This allows asynchronous action execution --- src/fnpjs/actions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fnpjs/actions.js b/src/fnpjs/actions.js index c49373a..f380ed6 100644 --- a/src/fnpjs/actions.js +++ b/src/fnpjs/actions.js @@ -72,10 +72,11 @@ _.extend(Action.prototype, Backbone.Events, { } return this._cache; }, - execute: function() { + execute: function(callback) { var state = this.getState(); + callback = callback || function() {}; if(state.allowed) { - return state.execute.call(this, this.params, this.appObject); + return state.execute.call(this, callback, this.params, this.appObject); } throw new Error('Execution not allowed'); } -- 2.20.1 From bec14a088e45d21ac12f9c5e852dd0b7e559d680 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 22 Apr 2014 12:35:16 +0200 Subject: [PATCH 15/16] editor: actions returns via callback --- .../modules/documentToolbar/actionView.js | 5 ++-- src/editor/plugins/core/core.js | 23 +++++++++++-------- src/editor/plugins/core/lists.js | 15 +++++++----- src/editor/plugins/core/switch.js | 5 ++-- src/editor/plugins/core/templates.js | 5 ++-- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/editor/modules/documentToolbar/actionView.js b/src/editor/modules/documentToolbar/actionView.js index 114eb32..74db332 100644 --- a/src/editor/modules/documentToolbar/actionView.js +++ b/src/editor/modules/documentToolbar/actionView.js @@ -108,8 +108,9 @@ var ActionView = Backbone.View.extend({ this.trigger('mousedown'); }, onExecute: function() { - var ret = this.action.execute(); - this.trigger('actionExecuted', this.action, ret); + this.action.execute(function(ret) { + this.trigger('actionExecuted', this.action, ret); + }.bind(this)); }, onSelectionChange: function(e) { var select = $(e.target), diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index 3465046..4c2f4c0 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -54,8 +54,9 @@ var undoRedoAction = function(dir) { label: dir === 'undo' ? '<-' : '->', icon: 'share-alt', iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '', - execute: function(params) { + execute: function(callback, params) { params.document[dir](); + callback(); }, }, getState: function(params) { @@ -90,7 +91,7 @@ var commentAction = { }, stateDefaults: { icon: 'comment', - execute: function(params, editor) { + execute: function(callback, params, editor) { /* globals Node */ var node = params.fragment.node, action = this; @@ -124,7 +125,8 @@ var commentAction = { }, { metadata: { description: action.getState().description - } + }, + success: callback }); }, }, @@ -168,7 +170,7 @@ var createWrapTextAction = function(createParams) { return _.extend(state, { allowed: true, description: createParams.description, - execute: function(params) { + execute: function(callback, params) { params.fragment.document.transaction(function() { var parent = params.fragment.startNode.parent(); return parent.wrapText({ @@ -180,7 +182,8 @@ var createWrapTextAction = function(createParams) { }, { metadata: { description: createParams.description - } + }, + success: callback }); } }); @@ -189,7 +192,7 @@ var createWrapTextAction = function(createParams) { }; -var createLinkFromSelection = function(params) { +var createLinkFromSelection = function(callback, params) { var doc = params.fragment.document, dialog = Dialog.create({ title: gettext('Create link'), @@ -215,13 +218,14 @@ var createLinkFromSelection = function(params) { }, { metadata: { description: action.getState().description - } + }, + success: callback }); }); dialog.show(); }; -var editLink = function(params) { +var editLink = function(callback, params) { var doc = params.fragment.document, link = params.fragment.node.getParent('link'), dialog = Dialog.create({ @@ -241,7 +245,8 @@ var editLink = function(params) { }, { metadata: { description: action.getState().description - } + }, + success: callback }); }); dialog.show(); diff --git a/src/editor/plugins/core/lists.js b/src/editor/plugins/core/lists.js index a8dc802..911a1f2 100644 --- a/src/editor/plugins/core/lists.js +++ b/src/editor/plugins/core/lists.js @@ -32,7 +32,7 @@ var countItems = function(boundries) { var toggleListAction = function(type) { var execute = { - add: function(params) { + add: function(callback, params) { var boundries = getBoundriesForAList(params.fragment), listParams = {klass: type === 'Bullet' ? 'list' : 'list.enum'}, action = this; @@ -45,13 +45,14 @@ var toggleListAction = function(type) { }, { metadata: { description: action.getState().description - } + }, + success: callback }); } else { throw new Error('Invalid boundries'); } }, - remove: function(params) { + remove: function(callback, params) { /* globals Node */ var current = params.fragment.node, action = this; @@ -65,14 +66,15 @@ var toggleListAction = function(type) { }, { metadata: { description: action.getState().description - } + }, + success: callback }); return true; // break } }.bind(this)); }, - changeType: function(params) { + changeType: function(callback, params) { var node = params.fragment.node, action = this; node.document.transaction(function() { @@ -80,7 +82,8 @@ var toggleListAction = function(type) { }, { metadata: { description: action.getState().description - } + }, + success: callback }); } }; diff --git a/src/editor/plugins/core/switch.js b/src/editor/plugins/core/switch.js index 2730761..57b28b9 100644 --- a/src/editor/plugins/core/switch.js +++ b/src/editor/plugins/core/switch.js @@ -43,7 +43,7 @@ var createSwitchAction = function(createParams) { allowed: !!toSwitch, toggled: alreadyInTarget, description: description, - execute: alreadyInTarget ? function() {} : function() { + execute: alreadyInTarget ? function() {} : function(callback) { f.document.transaction(function() { if(createParams.to.tagName) { toSwitch = toSwitch.setTag(createParams.to.tagName); @@ -54,7 +54,8 @@ var createSwitchAction = function(createParams) { }, { metadata: { description: description - } + }, + success: callback }); } }); diff --git a/src/editor/plugins/core/templates.js b/src/editor/plugins/core/templates.js index 69e30f9..7d6d74f 100644 --- a/src/editor/plugins/core/templates.js +++ b/src/editor/plugins/core/templates.js @@ -38,7 +38,7 @@ var insertTemplateAction = { return { allowed: true, description: description, - execute: function(params) { + execute: function(callback, params) { var node = params.fragment.node.getNearestElementNode(); node.document.transaction(function() { var toAdd = node.document.createDocumentNode(params.template.content); @@ -46,7 +46,8 @@ var insertTemplateAction = { }, { metadata: { description: description - } + }, + success: callback }); } }; -- 2.20.1 From 471df249233a0064cdd3c4efe890536d8b304037 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 22 Apr 2014 12:47:12 +0200 Subject: [PATCH 16/16] editor: return fragments from actions, set this fragment on canvas if possible --- .../modules/documentCanvas/documentCanvas.js | 13 +++++++++- src/editor/plugins/core/core.js | 8 +++--- src/editor/plugins/core/lists.js | 7 +++++- src/editor/plugins/core/switch.js | 25 +++++++++++++++---- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/editor/modules/documentCanvas/documentCanvas.js b/src/editor/modules/documentCanvas/documentCanvas.js index 3e6a209..9a88b75 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.js +++ b/src/editor/modules/documentCanvas/documentCanvas.js @@ -3,11 +3,15 @@ define([ 'libs/jquery', 'libs/underscore', +'fnpjs/logging/logging', './canvas/canvas', -'libs/text!./template.html'], function($, _, canvas3, template) { +'libs/text!./template.html'], function($, _, logging, canvas3, template) { 'use strict'; + +var logger = logging.getLogger('documentCanvas'); + return function(sandbox) { var canvas = canvas3.fromXMLDocument(null); @@ -72,6 +76,13 @@ return function(sandbox) { canvas.setCurrentElement(node); }, onAfterActionExecuted: function(action, ret) { + if(ret && ret instanceof canvas.wlxmlDocument.CaretFragment && ret.isValid()) { + logger.debug('The action returned a valid fragment'); + canvas.setCurrentElement(ret.node, {caretTo: ret.offset}); + return; + } + logger.debug('No valid fragment returned from the action'); + (actionHandlers[action.getPluginName()] || []).forEach(function(handler) { handler(canvas, action, ret); }); diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index 4c2f4c0..5d3a57f 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -206,15 +206,16 @@ var createLinkFromSelection = function(callback, params) { dialog.on('execute', function(event) { doc.transaction(function() { - var span = params.fragment.startNode.parent().wrapText({ + var span = action.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()] - }); + }), + doc = params.fragment.document; span.setAttr('href', event.formData.href); event.success(); - return span; + return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0}); }, { metadata: { description: action.getState().description @@ -242,6 +243,7 @@ var editLink = function(callback, params) { doc.transaction(function() { link.setAttr('href', event.formData.href); event.success(); + return params.fragment; }, { metadata: { description: action.getState().description diff --git a/src/editor/plugins/core/lists.js b/src/editor/plugins/core/lists.js index 911a1f2..427673e 100644 --- a/src/editor/plugins/core/lists.js +++ b/src/editor/plugins/core/lists.js @@ -41,7 +41,12 @@ var toggleListAction = function(type) { listParams.node1 = boundries.node1; listParams.node2 = boundries.node2; boundries.node1.document.transaction(function() { - boundries.node1.document.createList(listParams); + var list = boundries.node1.document.createList(listParams), + item1 = list.object.getItem(0), + text = item1 ? item1.contents()[0] : undefined, // + doc = boundries.node1.document; + + return doc.createFragment(doc.CaretFragment, {node: text, offset:0}); }, { metadata: { description: action.getState().description diff --git a/src/editor/plugins/core/switch.js b/src/editor/plugins/core/switch.js index 57b28b9..9b34721 100644 --- a/src/editor/plugins/core/switch.js +++ b/src/editor/plugins/core/switch.js @@ -32,7 +32,8 @@ var createSwitchAction = function(createParams) { var node = f instanceof f.CaretFragment ? f.node.parent() : f.getCommonParent(), alreadyInTarget = node.isInside(createParams.to), - toSwitch = node; + toSwitch = node, + textNodePath = (f.node || f.startNode).getPath(); if(!toSwitch.is(createParams.from)) { toSwitch = toSwitch.getParent(createParams.from); @@ -51,6 +52,7 @@ var createSwitchAction = function(createParams) { if(!_.isUndefined(createParams.to.klass)) { toSwitch.setClass(createParams.to.klass); } + return f.document.createFragment(f.CaretFragment, {node: f.document.getNodeByPath(textNodePath), offset: f.offset}); }, { metadata: { description: description @@ -63,12 +65,25 @@ var createSwitchAction = function(createParams) { }; }; +var headerAction = createSwitchAction({name: 'switchToHeader', from: {tagName: 'div', klass: 'p'}, to: {tagName: 'header', klass: '', name: gettext('header')}}), + paragraphAction = createSwitchAction({name: 'switchToParagraph', from: {tagName: 'header'}, to: {tagName: 'div', klass: 'p', name: gettext('paragraf')}}); 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')}}) - ] + actions: [headerAction, paragraphAction], + canvasActionHandler: { + handles: [headerAction, paragraphAction], + // handle: function(canvas, action, ret) { + // var params = {}, + // f; + // if(ret && ret.node2) { + // f = ret.oldFragment; + // if(f && f instanceof f.CaretFragment) { + // params.caretTo = f.offset; + // } + // canvas.setCurrentElement(ret.node2, params); + // } + // } + } }; }); \ No newline at end of file -- 2.20.1