From: Aleksander Łukasz Date: Mon, 14 Apr 2014 09:24:12 +0000 (+0200) Subject: editor: actions implementation X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/9831076c8f7385dffb533e0327cc7dd7c9f1ef92 editor: actions implementation --- 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,