From: Aleksander Łukasz Date: Wed, 23 Apr 2014 13:53:54 +0000 (+0200) Subject: Merging in actions branch X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/2687ec6cbbe8101a4faa232c79f5c1321dcebe6d?hp=732705c82a02743c942fd792e5b4438b045cf9f1 Merging in actions branch - actions api - new rules for canvas & toolbar behavior --- 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/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index b217d18..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,24 +42,29 @@ $.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') + } + }); } }); -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 +109,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 +254,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 +317,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 +334,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 +355,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 +558,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/keyboard.js b/src/editor/modules/documentCanvas/canvas/keyboard.js index 383ca18..fc8be1e 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, @@ -44,20 +44,27 @@ handlers.push({key: KEYS.ENTER, event.preventDefault(); var cursor = canvas.getCursor(), position = cursor.getPosition(), - element = position.element, - added; + element = position.element; if(Object.keys(cursor.getPosition()).length === 0) { var currentElement = canvas.getCurrentNodeElement(); if(currentElement) { - canvas.wlxmlDocument.startTransaction(); - added = currentElement.wlxmlNode.after({ - tagName: currentElement.getWlxmlTag() || 'div', - attrs: {'class': currentElement.getWlxmlClass() || 'p'} + canvas.wlxmlDocument.transaction(function() { + var added = currentElement.wlxmlNode.after({ + tagName: currentElement.getWlxmlTag() || 'div', + attrs: {'class': currentElement.getWlxmlClass() || 'p'} + }); + added.append({text:''}); + return added; + }, { + metadata: { + description: gettext('Splitting text') + }, + success: function(ret) { + canvas.setCurrentElement(utils.findCanvasElement(ret), {caretTo: 'start'}); + } }); - added.append({text:''}); - canvas.wlxmlDocument.endTransaction(); - canvas.setCurrentElement(utils.findCanvasElement(added), {caretTo: 'start'}); + } return; } @@ -68,13 +75,20 @@ handlers.push({key: KEYS.ENTER, element = element.parent(); } - canvas.wlxmlDocument.startTransaction(); - added = element.wlxmlNode.after( - {tagName: element.getWlxmlTag() || 'div', attrs: {'class': element.getWlxmlClass() || 'p'}} - ); - added.append({text: ''}); - canvas.wlxmlDocument.endTransaction(); - canvas.setCurrentElement(utils.findCanvasElement(added), {caretTo: 'start'}); + canvas.wlxmlDocument.transaction(function() { + var added = element.wlxmlNode.after( + {tagName: element.getWlxmlTag() || 'div', attrs: {'class': element.getWlxmlClass() || 'p'}} + ); + added.append({text: ''}); + return added; + }, { + metadata: { + description: gettext('Splitting text') + }, + success: function(ret) { + canvas.setCurrentElement(utils.findCanvasElement(ret), {caretTo: 'start'}); + } + }); } else { @@ -100,9 +114,17 @@ 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}); + }, { + metadata: { + description: gettext('Splitting text') + } + }); - var result = position.element.wlxmlNode.breakContent({offset: position.offset}), - goto, gotoOptions; if(result.emptyText) { goto = result.emptyText; gotoOptions = {}; @@ -280,6 +302,10 @@ handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE], } event.preventDefault(); } + }, { + metadata: { + description: gettext('Remove text') + } }); } }); 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..9a88b75 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.js +++ b/src/editor/modules/documentCanvas/documentCanvas.js @@ -2,20 +2,30 @@ define([ 'libs/jquery', +'libs/underscore', +'fnpjs/logging/logging', './canvas/canvas', -'./commands', -'libs/text!./template.html'], function($, canvas3, commands, template) { +'libs/text!./template.html'], function($, _, logging, canvas3, template) { 'use strict'; + +var logger = logging.getLogger('documentCanvas'); + 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 +43,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 +75,17 @@ 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) { + 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/modules/documentToolbar/actionView.js b/src/editor/modules/documentToolbar/actionView.js new file mode 100644 index 0000000..74db332 --- /dev/null +++ b/src/editor/modules/documentToolbar/actionView.js @@ -0,0 +1,147 @@ +define(function(require) { + +'use strict'; +/* globals gettext */ + +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(); + + 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, + 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() { + this.action.execute(function(ret) { + this.trigger('actionExecuted', this.action, ret); + }.bind(this)); + }, + 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..b66734f 100644 --- a/src/editor/modules/documentToolbar/documentToolbar.js +++ b/src/editor/modules/documentToolbar/documentToolbar.js @@ -1,87 +1,106 @@ -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 = {}, + 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; - } - - if(command === 'undo' || command === 'redo') { - params.callback = function(disable) { - btn.attr('disabled', !disable); - }; - } + _.pairs(contextParams).forEach(function(pair) { + var name = pair[0], + value = pair[1]; + action.updateContextParam(name, value); + }); - _.extend(params, {ctrlKey: e.ctrlKey}); + group.append(view.dom); + view.on('actionExecuted', function(action, ret) { + sandbox.publish('actionExecuted', action, ret); + }); - 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; + addedActions.forEach(function(action) { + action.updateContextParam(what, contextParams[what]); + }); }; - view.setup(); + 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..70df62e 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,13 +75,16 @@ 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)); }, clear: function() { }, setMetadata: function(node) { + this.node.find('.rng-module-metadataEditor-addBtn').attr('disabled', !node); if(!node) { this.metaTable.html(''); return; @@ -183,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 55f4fab..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}) + '
'), @@ -31,7 +32,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) { @@ -54,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/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..25e5ddb 100644 --- a/src/editor/modules/nodePane/nodePane.js +++ b/src/editor/modules/nodePane/nodePane.js @@ -11,14 +11,24 @@ define([ return function(sandbox) { var view = $(_.template(templateSrc)({utils: wlxmlUtils})), - currentNode; + listens = false, + 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); }); + + return { start: function() { @@ -28,28 +38,37 @@ 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(currentNode && !currentNode.isInDocument()) { + module.setNodeElement(null); + } + 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/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 diff --git a/src/editor/modules/rng/rng.js b/src/editor/modules/rng/rng.js index b12c19f..6a3271b 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); - } }; @@ -98,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(); }); @@ -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); } }; @@ -292,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) { @@ -305,6 +322,11 @@ return function(sandbox) { return { start: function() { + sandbox.registerActionsAppObject({ + getUser: function() { + return sandbox.getConfig().user; + } + }); sandbox.getModule('data').start(); }, handleEvent: function(moduleName, eventName, args) { @@ -312,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..0f08c59 --- /dev/null +++ b/src/editor/modules/statusBar/statusBar.js @@ -0,0 +1,50 @@ +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; + + 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); + } + } + + 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/plugins/core/core.js b/src/editor/plugins/core/core.js index d11bf73..a780227 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,274 @@ 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(callback, params) { + params.document[dir](); + callback(); + }, + }, + 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'); + if(allowed) { + var metadata = _.last(params.document[dir+'Stack']).metadata; + if(metadata) { + desc += ': ' + (metadata.description || gettext('unknown operation')); + } + } + 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(callback, params, editor) { + /* globals Node */ + var node = params.fragment.node, + action = this; + 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}); + }, { + metadata: { + description: action.getState().description + }, + success: callback + }); + }, + }, + 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, + description: createParams.description, + execute: function(callback, params) { + params.fragment.document.transaction(function() { + var parent = params.fragment.startNode.parent(), + doc = params.fragment.document, + wrapper, lastTextNode; + + wrapper = 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()] + }); + + lastTextNode = wrapper.getLastTextNode(); + if(lastTextNode) { + return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length}); + } + }, { + metadata: { + description: createParams.description + }, + success: callback + }); + } + }); + } + }; +}; + + +var createLinkFromSelection = function(callback, 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'} + ] + }), + action = this; + + dialog.on('execute', function(event) { + doc.transaction(function() { + 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 doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0}); + }, { + metadata: { + description: action.getState().description + }, + success: callback + }); + }); + dialog.show(); +}; + +var editLink = function(callback, 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')} + ] + }), + action = this; + + dialog.on('execute', function(event) { + doc.transaction(function() { + link.setAttr('href', event.formData.href); + event.success(); + return params.fragment; + }, { + metadata: { + description: action.getState().description + }, + success: callback + }); + }); + 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', 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); + + + +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..427673e --- /dev/null +++ b/src/editor/plugins/core/lists.js @@ -0,0 +1,153 @@ +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(callback, params) { + var boundries = getBoundriesForAList(params.fragment), + listParams = {klass: type === 'Bullet' ? 'list' : 'list.enum'}, + action = this; + + if(boundries && boundries.node1) { + listParams.node1 = boundries.node1; + listParams.node2 = boundries.node2; + boundries.node1.document.transaction(function() { + 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 + }, + success: callback + }); + } else { + throw new Error('Invalid boundries'); + } + }, + remove: function(callback, params) { + /* globals 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.document.transaction(function() { + node.object.extractListItems(); + }, { + metadata: { + description: action.getState().description + }, + success: callback + }); + + return true; // break + } + }.bind(this)); + }, + changeType: function(callback, params) { + 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 + }, + success: callback + }); + } + }; + + 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..9b34721 --- /dev/null +++ b/src/editor/plugins/core/switch.js @@ -0,0 +1,89 @@ +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, + description; + + + 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, + textNodePath = (f.node || f.startNode).getPath(); + + if(!toSwitch.is(createParams.from)) { + toSwitch = toSwitch.getParent(createParams.from); + } + + description = 'Switch to ' + createParams.to.name; + return _.extend(state, { + allowed: !!toSwitch, + toggled: alreadyInTarget, + description: description, + execute: alreadyInTarget ? function() {} : function(callback) { + 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 f.document.createFragment(f.CaretFragment, {node: f.document.getNodeByPath(textNodePath), offset: f.offset}); + }, { + metadata: { + description: description + }, + success: callback + }); + } + }); + } + }; +}; + +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: [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 diff --git a/src/editor/plugins/core/templates.js b/src/editor/plugins/core/templates.js new file mode 100644 index 0000000..7d6d74f --- /dev/null +++ b/src/editor/plugins/core/templates.js @@ -0,0 +1,62 @@ +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' + }, + getState: function(params) { + var description; + + 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) || + params.fragment.node.getNearestElementNode().isRoot() + ) { + return { + allowed: false, + 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: description, + execute: function(callback, params) { + var node = params.fragment.node.getNearestElementNode(); + node.document.transaction(function() { + var toAdd = node.document.createDocumentNode(params.template.content); + node.after(toAdd); + }, { + metadata: { + description: description + }, + success: callback + }); + } + }; + } +}; + + +return { + actions: [insertTemplateAction] +}; + +}); \ 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'; diff --git a/src/fnpjs/actions.js b/src/fnpjs/actions.js new file mode 100644 index 0000000..f380ed6 --- /dev/null +++ b/src/fnpjs/actions.js @@ -0,0 +1,90 @@ +define(function(require) { + +'use strict'; + +var _ = require('libs/underscore'), + 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; + this.definition = definition; + this.config = config; + this._cache = null; + this.appObject = appObject; + + this.params = {}; +}; + +_.extend(Action.prototype, Backbone.Events, { + getPluginName: function() { + return this.fqName.split('.')[0]; + }, + updateParam: function(filter, value) { + var changed = false; + _.pairs(this.definition.params).forEach(function(pair) { + var paramName = pair[0], + paramDesc = pair[1]; + if(filter(paramDesc, paramName)) { + this.params[paramName] = value; + changed = true; + } + }.bind(this)); + if(changed) { + this._cache = null; + this.trigger('paramsChanged'); + } + }, + updateContextParam: function(contextName, value) { + this.updateParam(function(paramDesc) { + return paramDesc.type === 'context' && paramDesc.name === contextName; + }, value); + }, + updateKeyParam: function(keyName, toggled) { + this.updateParam(function(paramDesc) { + return paramDesc.type === 'key' && paramDesc.key === keyName; + }, toggled); + }, + updateWidgetParam: function(name, value) { + this.updateParam(function(paramDesc, paramName) { + return !_.contains(['context', 'key'], paramDesc.type) && paramName === name; + }, value); + }, + getState: function() { + var gotState; + if(!this._cache) { + try { + gotState = this.definition.getState.call(this, this.params); + } catch(e) { + logger.exception(e); + return; + } + if(typeof gotState === 'boolean') { + gotState = {allowed: gotState}; + } + this._cache = _.extend({}, this.definition.stateDefaults || {}, gotState); + } + if(this._cache === false) { + this._cache = {allowed: false}; + } + return this._cache; + }, + execute: function(callback) { + var state = this.getState(); + callback = callback || function() {}; + if(state.allowed) { + return state.execute.call(this, callback, this.params, this.appObject); + } + throw new Error('Execution not allowed'); + } +}); + + +return { + Action: Action +}; + +}); diff --git a/src/fnpjs/runner.js b/src/fnpjs/runner.js index 2f00170..6c28a61 100644 --- a/src/fnpjs/runner.js +++ b/src/fnpjs/runner.js @@ -1,4 +1,4 @@ -define(['libs/jquery', 'libs/underscore', 'fnpjs/logging/logging'], function($, _, logging) { +define(['libs/jquery', 'libs/underscore', 'fnpjs/logging/logging', 'fnpjs/actions'], function($, _, logging, actions) { 'use strict'; @@ -13,7 +13,9 @@ var Runner = function(app, modules) { moduleInstances = {}, eventListeners = [], plugins = [], - config; + actionDefinitions = {}, + config, + actionsAppObject; _.each(_.keys(modules || {}), function(moduleName) { if(_.contains(app.permissions[moduleName] || [], 'handleEvents')) { @@ -62,6 +64,24 @@ var Runner = function(app, modules) { this.getConfig = function() { return config; }; + + this.createAction = function(fqName, config) { + var definition = actionDefinitions[fqName]; + if(!definition) { + throw new Error('Invalid action: ' + fqName); + } + return new actions.Action(fqName, definition, config, actionsAppObject); + }; + + this.registerKeyHandler = function(eventName, handler) { + $('body').on(eventName, function(e) { + handler(e); + }); + }; + + this.registerActionsAppObject = function(_actionsAppObject) { + actionsAppObject = _actionsAppObject; + }; }; @@ -71,6 +91,10 @@ var Runner = function(app, modules) { this.registerPlugin = function(plugin) { plugins.push(plugin); + (plugin.actions || []).forEach(function(definition) { + var actionFqName = plugin.name + '.' + definition.name; + actionDefinitions[actionFqName] = definition; + }); }; this.start = function(_config) { @@ -83,6 +107,20 @@ var Runner = function(app, modules) { logging.setConfig(config.logging); } + _.pairs(config.plugins || {}).forEach(function(pair) { + var pluginName = pair[0], + pluginConfig = pair[1]; + + plugins.some(function(plugin) { + if(plugin.name === pluginName) { + if(_.isFunction(plugin.config)) { + plugin.config(pluginConfig); + } + return true; //break + } + }); + }); + app.initModules.forEach(function(moduleName) { getModuleInstance(moduleName).start(); }); diff --git a/src/smartxml/fragments.js b/src/smartxml/fragments.js new file mode 100644 index 0000000..9bb92cc --- /dev/null +++ b/src/smartxml/fragments.js @@ -0,0 +1,120 @@ +define(function(require) { + +'use strict'; + +var $ = require('libs/jquery'), + _ = require('libs/underscore'); + + +var Fragment = function(document) { + this.document = document; +}; +$.extend(Fragment.prototype, { + isValid: function() { + return false; + } +}); + + +var NodeFragment = function(document, params) { + Fragment.call(this, document); + this.node = params.node; +}; +NodeFragment.prototype = Object.create(Fragment.prototype); +$.extend(NodeFragment.prototype, { + isValid: function() { + return this.document.containsNode(this.node); + } +}); + + +var CaretFragment = function(document, params) { + NodeFragment.call(this, document, params); + this.offset = params.offset; + +}; +CaretFragment.prototype = Object.create(NodeFragment.prototype); +$.extend(CaretFragment.prototype, { + isValid: function() { + /* globals Node */ + return NodeFragment.prototype.isValid.call(this) && + this.node.nodeType === Node.TEXT_NODE && + _.isNumber(this.offset); + } +}); + + + +var RangeFragment = function(document, params) { + Fragment.call(this, document); + + if(params.node1.sameNode(params.node2)) { + this.startNode = this.endNode = params.node1; + } else { + /*jshint bitwise: false*/ + /* globals Node */ + var node1First = params.node1.nativeNode.compareDocumentPosition(params.node2.nativeNode) & Node.DOCUMENT_POSITION_FOLLOWING; + (node1First ? ['start', 'end'] : ['end','start']).forEach(function(prefix, idx) { + this[prefix + 'Node'] = params['node'+(idx+1)]; + }.bind(this)); + } +}; +RangeFragment.prototype = Object.create(Fragment.prototype); +$.extend(RangeFragment.prototype, { + isValid: function() { + return this.document.containsNode(this.startNode) && this.document.containsNode(this.endNode); + }, + hasSiblingBoundries: function() { + return this.isValid() && this.startNode.isSiblingOf(this.endNode); + }, + boundriesSiblingParents: function() { + return this.startNode.document.getSiblingParents({ + node1: this.startNode, + node2: this.endNode + }); + }, + getCommonParent: function() { + var siblingParents = this.boundriesSiblingParents(); + if(siblingParents) { + return siblingParents.node1.parent(); + } + }, +}); + +var TextRangeFragment = function(document, params) { + var orderChanged; + + RangeFragment.call(this, document, params); + + if(this.startNode.sameNode(this.endNode)) { + this.startOffset = Math.min(params.offset1, params.offset2); + this.endOffset = Math.max(params.offset1, params.offset2); + } else { + orderChanged = !params.node1.sameNode(this.startNode); + this.startOffset = orderChanged ? params.offset2 : params.offset1; + this.endOffset = orderChanged ? params.offset1 : params.offset2; + } +}; +TextRangeFragment.prototype = Object.create(RangeFragment.prototype); +$.extend(TextRangeFragment.prototype, { + isValid: function() { + return RangeFragment.prototype.isValid.call(this) && + _.isNumber(this.startOffset) && + _.isNumber(this.endOffset); + } +}); + +var FragmentTypes = { + Fragment: Fragment, + NodeFragment: NodeFragment, + CaretFragment: CaretFragment, + RangeFragment: RangeFragment, + TextRangeFragment: TextRangeFragment +}; +_.values(FragmentTypes).forEach(function(Type) { + $.extend(Type.prototype, FragmentTypes); +}); + +return FragmentTypes; + +}); \ No newline at end of file diff --git a/src/smartxml/fragments.test.js b/src/smartxml/fragments.test.js new file mode 100644 index 0000000..8cd6a58 --- /dev/null +++ b/src/smartxml/fragments.test.js @@ -0,0 +1,82 @@ +define(function(require) { + +'use strict'; +/* global describe, it */ +/* jshint expr:true */ + + +var chai = require('libs/chai'), + smartxml = require('./smartxml.js'); + + +var expect = chai.expect; + +var getDocumentFromXML = function(xml) { + return smartxml.documentFromXML(xml); +}; + +describe('Fragments API', function() { + describe('node fragment', function() { + it('describes a single node', function() { + var doc = getDocumentFromXML('
    Alice
    '); + + var fragment = doc.createFragment(doc.CaretFragment, {node: doc.root.contents()[0], offset: 1}); + + expect(fragment instanceof fragment.CaretFragment).to.be.true; + expect(fragment instanceof fragment.NodeFragment).to.be.true; + expect(fragment.node.getText()).to.equal('Alice'); + expect(fragment.offset).to.equal(1); + }); + }); + + describe('text range fragment', function() { + it('describes fragment of a text node', function() { + var doc = getDocumentFromXML('
    Alice
    '), + textNode = doc.root.contents()[0]; + + var fragment = doc.createFragment(doc.TextRangeFragment, { + node1: textNode, + offset1: 4, + node2: textNode, + offset2: 1 + }); + + expect(fragment instanceof fragment.TextRangeFragment).to.be.true; + expect(fragment instanceof fragment.RangeFragment).to.be.true; + expect(fragment.startNode.getText()).to.equal('Alice'); + expect(fragment.startOffset).to.equal(1); + expect(fragment.endNode.getText()).to.equal('Alice'); + expect(fragment.endOffset).to.equal(4); + }); + it('describes text spanning multiple nodes', function() { + var doc = getDocumentFromXML('
    Alice has a cat!
    '), + textNode1 = doc.root.contents()[0], + textNode2 = doc.root.contents()[2]; + + var fragment = doc.createFragment(doc.TextRangeFragment, { + node1: textNode2, + offset1: 4, + node2: textNode1, + offset2: 1 + }); + + expect(fragment instanceof fragment.TextRangeFragment).to.be.true; + expect(fragment.startNode.getText()).to.equal('Alice '); + expect(fragment.startOffset).to.equal(1); + expect(fragment.endNode.getText()).to.equal(' a cat!'); + expect(fragment.endOffset).to.equal(4); + }); + }); +}); + +}); \ No newline at end of file diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index 628163a..3ee49b7 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -4,8 +4,9 @@ define([ 'libs/backbone', 'smartxml/events', 'smartxml/transformations', - 'smartxml/core' -], function($, _, Backbone, events, transformations, coreTransformations) { + 'smartxml/core', + 'smartxml/fragments' +], function($, _, Backbone, events, transformations, coreTransformations, fragments) { 'use strict'; /* globals Node */ @@ -78,6 +79,14 @@ $.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()); + }, + sameNode: function(otherNode) { return !!(otherNode) && this.nativeNode === otherNode.nativeNode; }, @@ -143,6 +152,10 @@ $.extend(DocumentNode.prototype, { return 0; } return this.parent().indexOf(this); + }, + + getNearestElementNode: function() { + return this.nodeType === Node.ELEMENT_NODE ? this : this.parent(); } }); @@ -210,6 +223,22 @@ $.extend(ElementNode.prototype, { return node && (node.nativeNode === this.nativeNode || node._$.parents().index(this._$) !== -1); }, + getLastTextNode: function() { + var contents = this.contents(), + toret; + + contents.reverse().some(function(node) { + if(node.nodeType === Node.TEXT_NODE) { + toret = node; + return true; + } + toret = node.getLastTextNode(); + return !!toret; + }); + + return toret; + }, + toXML: function() { var wrapper = $('
    '); wrapper.append(this._getXMLDOMToDump()); @@ -234,6 +263,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); @@ -291,7 +325,7 @@ var Document = function(xml, extensions) { this.loadXML(xml); }; -$.extend(Document.prototype, Backbone.Events, { +$.extend(Document.prototype, Backbone.Events, fragments, { ElementNodeFactory: ElementNode, TextNodeFactory: TextNode, @@ -542,6 +576,7 @@ $.extend(Document.prototype, Backbone.Events, { this._undoInProgress = false; this.redoStack.push(transaction); + this.trigger('operationEnd'); } }, redo: function() { @@ -553,6 +588,8 @@ $.extend(Document.prototype, Backbone.Events, { }); this._transformationLevel--; this.undoStack.push(transaction); + this.trigger('operationEnd'); + } }, @@ -570,6 +607,7 @@ $.extend(Document.prototype, Backbone.Events, { } if(this._currentTransaction.hasTransformations()) { this.undoStack.push(this._currentTransaction); + this.trigger('operationEnd'); } this._currentTransaction = null; }, @@ -626,6 +664,13 @@ $.extend(Document.prototype, Backbone.Events, { } return $document[0]; }, configurable: true}); + }, + + createFragment: function(Type, params) { + if(!Type.prototype instanceof fragments.Fragment) { + throw new Error('Can\'t create a fragment: `Type` is not a valid Fragment'); + } + return new Type(this, params); } }); diff --git a/src/smartxml/smartxml.test.js b/src/smartxml/smartxml.test.js index 5d1ca3e..a994f05 100644 --- a/src/smartxml/smartxml.test.js +++ b/src/smartxml/smartxml.test.js @@ -219,6 +219,21 @@ describe('smartxml', function() { expect(event.meta.oldVal).to.equal('value1'); }); }); + + describe('Searching for the last child text node', function() { + [ + '
    xxx
    last
    ', + '
    last
    ', + '
    xxx
    last
    ' + ].forEach(function(xml, i) { + var example = 'example ' + i; + it('returns last child text node ' + example + ')', function() { + var doc = getDocumentFromXML(xml), + lastTextNode = doc.root.getLastTextNode(); + expect(lastTextNode.getText()).to.equal('last', example); + }); + }); + }); }); describe('Basic TextNode properties', function() { diff --git a/src/wlxml/extensions/list/list.js b/src/wlxml/extensions/list/list.js index 85ab751..29834ae 100644 --- a/src/wlxml/extensions/list/list.js +++ b/src/wlxml/extensions/list/list.js @@ -29,14 +29,29 @@ extension.wlxmlClass.list.transformations = { } }; +extension.wlxmlClass.list.transformations = { + extractListItems: function() { + var contents = this.contents(), + first = contents[0], + last; + if(contents.length) { + last = contents[contents.length-1]; + this.document.extractItems({ + item1: first, + item2: last + }); + } else { + this.detach(); + } + } +}; + extension.document.methods = { areItemsOfSameList: function(params) { return params.node1.parent().sameNode(params.node2.parent()) && params.node2.parent().is('list'); } }; - - extension.document.transformations.createList = { impl: function(params) { /* globals Node */ diff --git a/src/wlxml/wlxml.js b/src/wlxml/wlxml.js index 4e1ddc6..5194c19 100644 --- a/src/wlxml/wlxml.js +++ b/src/wlxml/wlxml.js @@ -12,16 +12,16 @@ define([ var WLXMLDocumentNodeMethods = { - isInside: function(klass) { - var parent = this.getParent(klass); + isInside: function(query) { + var parent = this.getParent(query); return !!parent; }, - getParent: function(klass) { + getParent: function(query) { /* globals Node */ var me = this.nodeType === Node.ELEMENT_NODE ? [this] : [], toret; me.concat(this.parents()).some(function(node) { - if(node.is(klass)) { + if(node.is(query)) { toret = node; return true; } @@ -78,8 +78,12 @@ $.extend(WLXMLElementNode.prototype, WLXMLDocumentNodeMethods, smartxml.ElementN return this.setAttr('class', klass); } }, - is: function(klass) { - return this.getClass().substr(0, klass.length) === klass; + is: function(query) { + if(typeof query === 'string') { + query = {klass: query}; + } + return (_.isUndefined(query.klass) || this.getClass().substr(0, query.klass.length) === query.klass) && + (_.isUndefined(query.tagName) || this.getTagName() === query.tagName); }, getMetaAttributes: function() { var toret = new AttributesList(), 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,