From: Aleksander Ɓukasz Date: Thu, 29 May 2014 07:38:58 +0000 (+0200) Subject: Merge in gutter comments X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/0c57fd826a58a217f499b5084c837fb8ef3f6d4f?hp=ab3a2987b2eaca2555a206ac8d4f73903aa9870f Merge in gutter comments --- diff --git a/src/editor/modules/documentCanvas/canvas/canvas.html b/src/editor/modules/documentCanvas/canvas/canvas.html new file mode 100644 index 0000000..d798fd2 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/canvas.html @@ -0,0 +1,5 @@ +
+
+
+
+
\ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index a057e05..05c109c 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -9,7 +9,10 @@ define([ 'modules/documentCanvas/canvas/wlxmlListener', 'modules/documentCanvas/canvas/elementsRegister', 'modules/documentCanvas/canvas/genericElement', -], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement) { +'modules/documentCanvas/canvas/nullElement', +'modules/documentCanvas/canvas/gutter', +'libs/text!./canvas.html' +], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement, nullElement, gutter, canvasTemplate) { 'use strict'; /* global document:false, window:false, Node:false, gettext */ @@ -57,8 +60,9 @@ $.extend(TextHandler.prototype, { }); -var Canvas = function(wlxmlDocument, elements) { - this.elementsRegister = new ElementsRegister(documentElement.DocumentNodeElement); +var Canvas = function(wlxmlDocument, elements, metadata) { + this.metadata = metadata || {}; + this.elementsRegister = new ElementsRegister(documentElement.DocumentNodeElement, nullElement); elements = [ {tag: 'section', klass: null, prototype: genericElement}, @@ -72,7 +76,15 @@ var Canvas = function(wlxmlDocument, elements) { this.elementsRegister.register(elementDesc); }.bind(this)); this.eventBus = _.extend({}, Backbone.Events); - this.wrapper = $('
').addClass('canvas-wrapper').attr('contenteditable', true); + + this.dom = $(canvasTemplate); + this.rootWrapper = this.dom.find('.root-wrapper'); + + + this.gutter = gutter.create(); + this.gutterView = new gutter.GutterView(this.gutter); + this.dom.find('.view-row').append(this.gutterView.dom); + this.wlxmlListener = wlxmlListener.create(this); this.loadWlxmlDocument(wlxmlDocument); this.setupEventHandling(); @@ -81,6 +93,10 @@ var Canvas = function(wlxmlDocument, elements) { $.extend(Canvas.prototype, Backbone.Events, { + getElementOffset: function(element) { + return element.dom.offset().top - this.dom.offset().top; + }, + loadWlxmlDocument: function(wlxmlDocument) { if(!wlxmlDocument) { return false; @@ -122,28 +138,30 @@ $.extend(Canvas.prototype, Backbone.Events, { }, reloadRoot: function() { + if(this.rootElement) { + this.rootElement.detach(); + } this.rootElement = this.createElement(this.wlxmlDocument.root); - this.wrapper.empty(); - this.wrapper.append(this.rootElement.dom); + this.rootWrapper.append(this.rootElement.dom); }, setupEventHandling: function() { var canvas = this; - this.wrapper.on('keyup keydown keypress', function(e) { + this.rootWrapper.on('keyup keydown keypress', function(e) { keyboard.handleKey(e, canvas); }); - this.wrapper.on('mouseup', function() { + this.rootWrapper.on('mouseup', function() { canvas.triggerSelectionChanged(); }); var mouseDown; - this.wrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) { + this.rootWrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) { mouseDown = e.target; }); - this.wrapper.on('click', '[document-node-element], [document-text-element]', function(e) { + this.rootWrapper.on('click', '[document-node-element], [document-text-element]', function(e) { e.stopPropagation(); if(e.originalEvent.detail === 3) { e.preventDefault(); @@ -155,7 +173,7 @@ $.extend(Canvas.prototype, Backbone.Events, { } }); - this.wrapper.on('paste', function(e) { + this.rootWrapper.on('paste', function(e) { e.preventDefault(); var clipboardData = e.originalEvent.clipboardData; @@ -191,7 +209,7 @@ $.extend(Canvas.prototype, Backbone.Events, { mutation.target.data = mutation.target.data.replace(utils.unicode.ZWS, ''); canvas._moveCaretToTextElement(canvas.getDocumentElement(mutation.target), 'end'); } - observer.observe(canvas.wrapper[0], config); + observer.observe(canvas.dom[0], config); var textElement = canvas.getDocumentElement(mutation.target), toSet = mutation.target.data !== utils.unicode.ZWS ? mutation.target.data : ''; @@ -205,11 +223,15 @@ $.extend(Canvas.prototype, Backbone.Events, { }); }); var config = { attributes: false, childList: false, characterData: true, subtree: true, characterDataOldValue: true}; - observer.observe(this.wrapper[0], config); + observer.observe(this.rootWrapper[0], config); - this.wrapper.on('mouseover', '[document-node-element], [document-text-element]', function(e) { - var el = canvas.getDocumentElement(e.currentTarget); + var hoverHandler = function(e) { + var el = canvas.getDocumentElement(e.currentTarget), + expose = { + mouseover: true, + mouseout: false + }; if(!el) { return; } @@ -217,19 +239,11 @@ $.extend(Canvas.prototype, Backbone.Events, { if(el instanceof documentElement.DocumentTextElement) { el = el.parent(); } - el.toggleLabel(true); - }); - this.wrapper.on('mouseout', '[document-node-element], [document-text-element]', function(e) { - var el = canvas.getDocumentElement(e.currentTarget); - if(!el) { - return; - } - e.stopPropagation(); - if(el instanceof documentElement.DocumentTextElement) { - el = el.parent(); - } - el.toggleLabel(false); - }); + el.updateState({exposed:expose[e.type]}); + }; + + this.rootWrapper.on('mouseover', '[document-node-element], [document-text-element]', hoverHandler); + this.rootWrapper.on('mouseout', '[document-node-element], [document-text-element]', hoverHandler); this.eventBus.on('elementToggled', function(toggle, element) { if(!toggle) { @@ -239,7 +253,7 @@ $.extend(Canvas.prototype, Backbone.Events, { }, view: function() { - return this.wrapper; + return this.dom; }, doc: function() { @@ -248,7 +262,7 @@ $.extend(Canvas.prototype, Backbone.Events, { toggleElementHighlight: function(node, toggle) { var element = utils.getElementForNode(node); - element.toggleHighlight(toggle); + element.updateState({exposed: toggle}); }, getCursor: function() { @@ -257,14 +271,11 @@ $.extend(Canvas.prototype, Backbone.Events, { getCurrentNodeElement: function() { - var htmlElement = this.wrapper.find('.current-node-element').parent()[0]; - if(htmlElement) { - return this.getDocumentElement(htmlElement); - } + return this.currentNodeElement; }, getCurrentTextElement: function() { - var htmlElement = this.wrapper.find('.current-text-element')[0]; + var htmlElement = this.rootWrapper.find('.current-text-element')[0]; if(htmlElement) { return this.getDocumentElement(htmlElement); } @@ -285,7 +296,7 @@ $.extend(Canvas.prototype, Backbone.Events, { }, contains: function(element) { - return element.dom.parents().index(this.wrapper) !== -1; + return element && element.dom && element.dom.parents().index(this.rootWrapper) !== -1; }, triggerSelectionChanged: function() { @@ -293,15 +304,10 @@ $.extend(Canvas.prototype, Backbone.Events, { var s = this.getSelection(), f = s.toDocumentFragment(); if(f && f instanceof f.RangeFragment) { - var $current = this.wrapper.find('.current-node-element'); - var current = $current && this.getDocumentElement($current.parent()[0]); - - if($current) { - $current.removeClass('current-node-element'); - } - if(current) { - current.markAsCurrent(false); - } + if(this.currentNodeElement) { + this.currentNodeElement.updateState({active: false}); + this.currentNodeElement = null; + } } }, @@ -347,18 +353,14 @@ $.extend(Canvas.prototype, Backbone.Events, { }.bind(this); var _markAsCurrent = function(element) { if(element instanceof documentElement.DocumentTextElement) { - this.wrapper.find('.current-text-element').removeClass('current-text-element'); + this.rootWrapper.find('.current-text-element').removeClass('current-text-element'); element.dom.addClass('current-text-element'); } else { - var $current = this.wrapper.find('.current-node-element'); - var current = this.getDocumentElement($current.parent()[0]); - $current.removeClass('current-node-element'); - - if(current) { - current.markAsCurrent(false); + if(this.currentNodeElement) { + this.currentNodeElement.updateState({active: false}); } - element._container().addClass('current-node-element'); - element.markAsCurrent(true); + element.updateState({active: true}); + this.currentNodeElement = element; } }.bind(this); @@ -370,7 +372,7 @@ $.extend(Canvas.prototype, Backbone.Events, { currentNodeElement = this.getCurrentNodeElement(); if(currentTextElement && !(currentTextElement.sameNode(textElementToLand))) { - this.wrapper.find('.current-text-element').removeClass('current-text-element'); + this.rootWrapper.find('.current-text-element').removeClass('current-text-element'); } if(textElementToLand) { @@ -409,7 +411,7 @@ $.extend(Canvas.prototype, Backbone.Events, { selection.removeAllRanges(); selection.addRange(range); - this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't. + this.rootWrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't. }, setCursorPosition: function(position) { @@ -419,11 +421,11 @@ $.extend(Canvas.prototype, Backbone.Events, { }, toggleGrid: function() { - this.wrapper.toggleClass('grid-on'); + this.rootWrapper.toggleClass('grid-on'); this.trigger('changed'); }, isGridToggled: function() { - return this.wrapper.hasClass('grid-on'); + return this.rootWrapper.hasClass('grid-on'); } }); @@ -615,8 +617,8 @@ $.extend(Cursor.prototype, { }); return { - fromXMLDocument: function(wlxmlDocument, elements) { - return new Canvas(wlxmlDocument, elements); + fromXMLDocument: function(wlxmlDocument, elements, metadata) { + return new Canvas(wlxmlDocument, elements, metadata); } }; diff --git a/src/editor/modules/documentCanvas/canvas/canvas.less b/src/editor/modules/documentCanvas/canvas/canvas.less new file mode 100644 index 0000000..2c6edc2 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/canvas.less @@ -0,0 +1,9 @@ +.view-table { + display: table; + width: calc(~'100% - 100px'); + margin: 10px 0 20px 100px; + + .view-row { + display: table-row; + } +} diff --git a/src/editor/modules/documentCanvas/canvas/canvas.test.js b/src/editor/modules/documentCanvas/canvas/canvas.test.js index 0654170..be4a8b9 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.test.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.test.js @@ -293,7 +293,11 @@ describe('Custom elements based on wlxml class attribute', function() { onNodeAdded: function(event) { void(event); this.refresh2(); - } + }, + onNodeTextChange: function(event) { + this.header.text(event.meta.node.getText()); + }, + children: function() { return []; } }); var c = getCanvasFromXML('
', [ @@ -309,6 +313,14 @@ describe('Custom elements based on wlxml class attribute', function() { node.append({tagName: 'div'}); expect(header.text()).to.equal('2', 'added div'); + + var textNode = node.append({text: 'test'}); + + expect(header.text()).to.equal('3', 'added text node'); + + textNode.setText('test2'); + + expect(header.text()).to.equal('test2', 'text node change handled'); }); describe('Handling unknown class', function() { diff --git a/src/editor/modules/documentCanvas/canvas/comments/comment.html b/src/editor/modules/documentCanvas/canvas/comments/comment.html new file mode 100644 index 0000000..3e9fb28 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/comments/comment.html @@ -0,0 +1,29 @@ +
+
+ <%= author %> + <%= date %> +
+
+
+ <%= content %> +
+
+ +
+ + +
+
+
+ +
+
<%= gettext('Delete this comment?') %>
+
+ + +
+
+
diff --git a/src/editor/modules/documentCanvas/canvas/comments/comments.html b/src/editor/modules/documentCanvas/canvas/comments/comments.html new file mode 100644 index 0000000..7ca0760 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/comments/comments.html @@ -0,0 +1,9 @@ +
+
+
+ + + +
+
+
\ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/comments/comments.js b/src/editor/modules/documentCanvas/canvas/comments/comments.js new file mode 100644 index 0000000..3076f52 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/comments/comments.js @@ -0,0 +1,219 @@ +define(function(require) { + +'use strict'; +/* globals gettext */ + + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + datetime = require('fnpjs/datetime'), + commentsTemplate = require('libs/text!./comments.html'), + commentTemplate = require('libs/text!./comment.html'); + + +var makeAutoResizable = function(textarea) { + textarea.on('input', function() { + resize(textarea); + }); +}; + +var resize = function(textarea) { + if(textarea.prop('scrollHeight') > textarea.prop('clientHeight')) { + textarea.height(textarea.prop('scrollHeight')); + } +}; + + +var CommentsView = function(node, user) { + this.node = node; + this.user = user; + this.dom = $(_.template(commentsTemplate)()); + this.list = this.dom.find('.list'); + this.textarea = this.dom.find('textarea'); + this.addButton = this.dom.find('button.btnAdd'); + this.cancelButton = this.dom.find('button.btnCancel'); + + this.textarea.on('input', function() { + this.addButton.attr('disabled', this.textarea.val() === ''); + }.bind(this)); + makeAutoResizable(this.textarea); + + this.addButton.hide(); + this.cancelButton.hide(); + this.textarea.on('focus', function() { + this.addButton.show(); + this.cancelButton.show(); + }.bind(this)); + + this.addButton.on('click', function() { + if(!this.node) { + return; + } + + this.node.document.transaction(function() { + var commentNode = this.node.document.createDocumentNode({tagName: 'aside', attrs: {'class': 'comment'}}), + metadata = commentNode.getMetadata(), + creator; + + if(this.user) { + creator = this.user.name; + if(this.user.email) { + creator += ' (' + this.user.email + ')'; + } + } else { + creator = 'anonymous'; + } + + metadata.add({key: 'creator', value: creator}); + metadata.add({key: 'date', value: datetime.currentStrfmt()}); + commentNode.append({text: this.textarea.val()}); + + this.node.append(commentNode); + }.bind(this), { + metadata: { + description: gettext('Add comment') + }, + success: function() { + this.textarea.val(''); + }.bind(this) + }); + + }.bind(this)); + + this.cancelButton.on('click', function() { + this.addButton.hide(); + this.cancelButton.hide(); + this.textarea.val(''); + }.bind(this)); + + this.render(); + this.onDeactivated(); + +}; + +_.extend(CommentsView.prototype, { + render: function() { + this.list.empty(); + this.textarea.attr('placeholder', gettext('Comment')); + + this.node.contents() + .filter(function(child) { + return child.is({tag: 'aside', klass: 'comment'}); + }) + .forEach(function(commentNode) { + var commentView = new CommentView(commentNode); + this.list.append(commentView.dom); + this.textarea.attr('placeholder', gettext('Respond') + '...'); + }.bind(this)); + }, + onActivated: function() { + this.dom.find('.newComment').toggle(true); + }, + onDeactivated: function() { + this.dom.find('.newComment').toggle(false); + this.addButton.hide(); + this.cancelButton.hide(); + }, +}); + + +var CommentView = function(commentNode) { + this.node = commentNode; + + var metaData = this.node.getMetadata(), + author, date; + + metaData.some(function(row) { + author = row.getValue(); + if(author) { + author = author.split(' ')[0]; + } + return true; + }, 'creator'); + + metaData.some(function(row) { + date = row.getValue(); + if(/[0-9][0-9]:[0-9][0-9]:[0-9][0-9]$/g.test(date)) { + date = date.split(':'); + date.pop(); + date = date.join(':'); + } + return true; + }, 'date'); + + this.dom = $(_.template(commentTemplate)({ + author: author ||'?', + date: date || '?', + content: this.node.object.getText() || '?' + })); + + this.contentElement = this.dom.find('.content'); + this.editElement = this.dom.find('.edit'); + this.deleteDialogElement = this.dom.find('.deleteDialog'); + + this.dom.find('.remove-btn').on('click', function() { + this.deleteDialogElement.show(); + }.bind(this)); + + this.dom.find('.deleteDialog-confirm').on('click', function() { + this.node.document.transaction(function() { + this.node.detach(); + }.bind(this), { + metadata: { + description: gettext('Remove comment') + } + }); + }.bind(this)); + + this.dom.find('.deleteDialog-cancel').on('click', function() { + this.deleteDialogElement.hide(); + }.bind(this)); + + this.dom.find('.edit-start-btn').on('click', function() { + this.startEditing(); + }.bind(this)); + + this.dom.find('.edit-save-btn').on('click', function() { + this.saveEditing(); + }.bind(this)); + + this.dom.find('.edit-cancel-btn').on('click', function() { + this.cancelEditing(); + }.bind(this)); + + this.textarea = this.editElement.find('textarea'); + this.textarea.on('input', function() { + this.dom.find('.edit-save-btn').attr('disabled', this.textarea.val() === ''); + }.bind(this)); + makeAutoResizable(this.textarea); +}; + +$.extend(CommentView.prototype, { + startEditing: function() { + this.contentElement.hide(); + this.editElement.show(); + this.textarea.val(this.node.object.getText()); + resize(this.textarea); + this.textarea.focus(); + }, + saveEditing: function() { + var newContent = this.editElement.find('textarea').val(); + this.node.document.transaction(function() { + this.node.object.setText(newContent); + }.bind(this), { + metadata: { + description: gettext('Edit comment') + } + }); + }, + cancelEditing: function() { + this.contentElement.show(); + this.editElement.find('textarea').val(''); + this.editElement.hide(); + }, +}); + + +return CommentsView; + +}); \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/comments/comments.less b/src/editor/modules/documentCanvas/canvas/comments/comments.less new file mode 100644 index 0000000..688d3f8 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/comments/comments.less @@ -0,0 +1,69 @@ +.comments { + @borderColor: darken(#FFFCB7, 45%); + + font-size: 12px; + + .comment { + position: relative; + + border-color: @borderColor; + border-style: solid; + border-width: 1px 0; + margin-top: -1px; + padding-bottom: 10px; + + &:first-child { + margin-top:0; + } + + .header { + padding: 0 3px; + font-size: 10px; + + .author { + + } + .date { + float: right; + } + } + + .content { + padding: 5px; + } + + .edit { + display: none; + } + + .deleteDialog { + display: none; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + padding-top: 15px; + color: white; + background-color: rgba(0,0,0,0.7); + text-align: center; + } + } + + .newComment { + margin-top: 10px; + } + + textarea { + display: block; + width: calc(~'100% - 4px'); + padding: 2px 2px; + margin: 0 0 10px 0; + font-size: 12px; + resize: vertical; + outline: none !important; + box-shadow: none; + + } + +} \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/documentElement.js b/src/editor/modules/documentCanvas/canvas/documentElement.js index 45592ef..d40ca9b 100644 --- a/src/editor/modules/documentCanvas/canvas/documentElement.js +++ b/src/editor/modules/documentCanvas/canvas/documentElement.js @@ -11,6 +11,10 @@ define([ var DocumentElement = function(wlxmlNode, canvas) { this.wlxmlNode = wlxmlNode; this.canvas = canvas; + this.state = { + exposed: false, + active: false + }; this.dom = this.createDOM(); this.dom.data('canvas-element', this); @@ -26,6 +30,32 @@ $.extend(DocumentElement.prototype, { refresh: function() { // noop }, + updateState: function(toUpdate) { + var changes = {}; + _.keys(toUpdate) + .filter(function(key) { + return this.state.hasOwnProperty(key); + }.bind(this)) + .forEach(function(key) { + if(this.state !== toUpdate[key]) { + this.state[key] = changes[key] = toUpdate[key]; + } + }.bind(this)); + if(_.isFunction(this.onStateChange)) { + this.onStateChange(changes); + if(_.isBoolean(changes.active)) { + if(changes.active) { + var ptr = this; + while(ptr && ptr.wlxmlNode.getTagName() === 'span') { + ptr = ptr.parent(); + } + if(ptr && ptr.gutterGroup) { + ptr.gutterGroup.show(); + } + } + } + } + }, parent: function() { var parents = this.dom.parents('[document-node-element]'); if(parents.length) { @@ -71,8 +101,10 @@ var manipulate = function(e, params, action) { } else { element = e.canvas.createElement(params); } - e.dom[action](element.dom); - e.refreshPath(); + if(element.dom) { + e.dom[action](element.dom); + e.refreshPath(); + } return element; }; @@ -88,6 +120,16 @@ $.extend(DocumentNodeElement.prototype, { clearWidgets: function() { this.dom.children('.canvas-widgets').empty(); }, + addToGutter: function(view) { + if(!this.gutterGroup) { + this.gutterGroup = this.canvas.gutter.createViewGroup({ + offsetHint: function() { + return this.canvas.getElementOffset(this); + }.bind(this) + }, this); + } + this.gutterGroup.addView(view); + }, handle: function(event) { var method = 'on' + event.type[0].toUpperCase() + event.type.substr(1); if(this[method]) { @@ -109,13 +151,26 @@ $.extend(DocumentNodeElement.prototype, { _container: function() { return this.dom.children('[document-element-content]'); }, - detach: function() { - var parents = this.parents(); - this.dom.detach(); - if(parents[0]) { - parents[0].refreshPath(); + detach: function(isChild) { + var parents; + + if(this.gutterGroup) { + this.gutterGroup.remove(); } - return this; + if(_.isFunction(this.children)) { + this.children().forEach(function(child) { + child.detach(true); + }); + } + + if(!isChild) { + parents = this.parents(); + this.dom.detach(); + if(parents[0]) { + parents[0].refreshPath(); + } + } + return this; }, before: function(params) { return manipulate(this, params, 'before'); @@ -125,19 +180,6 @@ $.extend(DocumentNodeElement.prototype, { return manipulate(this, params, 'after'); }, - toggleLabel: function(toggle) { - var displayCss = toggle ? 'inline-block' : 'none'; - var label = this.dom.children('.canvas-widgets').find('.canvas-widget-label'); - label.css('display', displayCss); - this.toggleHighlight(toggle); - }, - - markAsCurrent: function() {}, - - toggleHighlight: function(toggle) { - this._container().toggleClass('highlighted-element', toggle); - }, - isBlock: function() { return this.dom.css('display') === 'block'; }, @@ -185,8 +227,10 @@ $.extend(DocumentTextElement.prototype, { .text(this.wlxmlNode.getText() || utils.unicode.ZWS); return dom; }, - detach: function() { - this.dom.detach(); + detach: function(isChild) { + if(!isChild) { + this.dom.detach(); + } return this; }, setText: function(text) { @@ -197,6 +241,9 @@ $.extend(DocumentTextElement.prototype, { this.dom.contents()[0].data = text; } }, + handle: function(event) { + this.setText(event.meta.node.getText()); + }, getText: function(options) { options = _.extend({raw: false}, options || {}); var toret = this.dom.text(); @@ -219,10 +266,12 @@ $.extend(DocumentTextElement.prototype, { } else { element = this.canvas.createElement(params); } - this.dom.wrap('
'); - this.dom.parent().after(element.dom); - this.dom.unwrap(); - this.refreshPath(); + if(element.dom) { + this.dom.wrap('
'); + this.dom.parent().after(element.dom); + this.dom.unwrap(); + this.refreshPath(); + } return element; }, before: function(params) { @@ -235,16 +284,15 @@ $.extend(DocumentTextElement.prototype, { } else { element = this.canvas.createElement(params); } - this.dom.wrap('
'); - this.dom.parent().before(element.dom); - this.dom.unwrap(); - this.refreshPath(); + if(element.dom) { + this.dom.wrap('
'); + this.dom.parent().before(element.dom); + this.dom.unwrap(); + this.refreshPath(); + } return element; }, - toggleHighlight: function() { - // do nothing for now - }, children: function() { return []; } diff --git a/src/editor/modules/documentCanvas/canvas/elementsRegister.js b/src/editor/modules/documentCanvas/canvas/elementsRegister.js index 24a700a..9a44514 100644 --- a/src/editor/modules/documentCanvas/canvas/elementsRegister.js +++ b/src/editor/modules/documentCanvas/canvas/elementsRegister.js @@ -5,9 +5,10 @@ var _ = require('libs/underscore'), wlxml = require('wlxml/wlxml'); -var ElementsRegister = function(BaseType) { +var ElementsRegister = function(BaseType, NullType) { this._register = {}; this.BaseType = BaseType; + this.NullType = NullType; }; _.extend(ElementsRegister.prototype, { @@ -21,7 +22,7 @@ _.extend(ElementsRegister.prototype, { }, register: function(params) { params.klass = params.klass || ''; - params.prototype = params.prototype || Object.create({}); + params.prototype = params.prototype || this.NullType; this._register[params.tag] = this._register[params.tag] || {}; this._register[params.tag][params.klass] = this.createCanvasElementType(params.prototype); diff --git a/src/editor/modules/documentCanvas/canvas/genericElement.js b/src/editor/modules/documentCanvas/canvas/genericElement.js index 44615f5..878e140 100644 --- a/src/editor/modules/documentCanvas/canvas/genericElement.js +++ b/src/editor/modules/documentCanvas/canvas/genericElement.js @@ -3,9 +3,11 @@ define(function(require) { 'use strict'; var $ = require('libs/jquery'), + _ = require('libs/underscore'), documentElement = require('./documentElement'), utils = require('./utils'), - wlxmlUtils = require('utils/wlxml'); + wlxmlUtils = require('utils/wlxml'), + CommentsView = require('./comments/comments'); var labelWidget = function(tag, klass) { return $('') @@ -25,8 +27,21 @@ $.extend(generic, { .attr('wlxml-tag', this.wlxmlNode.getTagName()); this.setWlxmlClass(this.wlxmlNode.getClass()); this.wlxmlNode.contents().forEach(function(node) { - this._container().append(this.canvas.createElement(node).dom); + var el = this.canvas.createElement(node); + if(el.dom) { + this._container().append(el.dom); + } }.bind(this)); + + this.commentsView = new CommentsView(this.wlxmlNode, this.canvas.metadata.user); + this.addToGutter(this.commentsView); + this.commentTip = $('
'); + this.addWidget(this.commentTip); + + if(!this.wlxmlNode.hasChild({klass: 'comment'})) { + this.commentTip.hide(); + } + this.refresh(); }, @@ -92,31 +107,37 @@ $.extend(generic, { return; } - var nodeIndex = event.meta.node.getIndex(), + var ptr = event.meta.node.prev(), referenceElement, referenceAction, actionArg; + + while(ptr && !(referenceElement = utils.getElementForElementRootNode(ptr))) { + ptr = ptr.prev(); + } - if(nodeIndex === 0) { + if(referenceElement) { + referenceAction = 'after'; + } else { referenceElement = this; referenceAction = 'prepend'; - } else { - referenceElement = this.children()[nodeIndex-1]; - referenceAction = 'after'; } if(event.meta.move) { /* Let's check if this node had its own canvas element and it's accessible. */ actionArg = utils.getElementForElementRootNode(event.meta.node); - if(actionArg && actionArg.sameNode(referenceElement)) { - referenceElement = this.children()[nodeIndex]; - } } if(!actionArg) { actionArg = event.meta.node; } referenceElement[referenceAction](actionArg); + + if(event.meta.node.is('comment')) { + this.commentTip.show(); + this.commentsView.render(); + } }, onNodeDetached: function(event) { + var isComment = event.meta.node.is('comment'); if(event.meta.node.sameNode(this)) { this.detach(); } else { @@ -126,12 +147,19 @@ $.extend(generic, { return true; } }); + if(isComment && !this.wlxmlNode.hasChild({klass: 'comment'})) { + this.commentTip.hide(); + } + this.commentsView.render(); } }, onNodeTextChange: function(event) { - var toSet = event.meta.node.getText(); - this.children().some(function(child) { - if(child.wlxmlNode.sameNode(event.meta.node)) { + var node = event.meta.node, + toSet = node.getText(), + handled; + + handled = this.children().some(function(child) { + if(child.wlxmlNode.sameNode(node)) { if(toSet === '') { toSet = utils.unicode.ZWS; } @@ -141,10 +169,26 @@ $.extend(generic, { return true; } }); + + if(!handled && node.parent() && node.parent().is('comment') && this.wlxmlNode.sameNode(node.parent().parent())) { + this.commentsView.render(); + } }, + onStateChange: function(changes) { + if(_.isBoolean(changes.exposed) && !this.isSpan()) { + this._container().toggleClass('highlighted-element', changes.exposed); + } + if(_.isBoolean(changes.active) && !this.isSpan()) { + this._container().toggleClass('current-node-element', changes.active); + } + }, /// + + isSpan: function() { + return this.wlxmlNode.getTagName() === 'span'; + }, containsBlock: function() { return this.children() @@ -167,8 +211,10 @@ $.extend(generic, { } else { element = this.canvas.createElement(param); } - this._container().prepend(element.dom); - this.refreshPath(); + if(element.dom) { + this._container().prepend(element.dom); + this.refreshPath(); + } return element; }, diff --git a/src/editor/modules/documentCanvas/canvas/genericElement.less b/src/editor/modules/documentCanvas/canvas/genericElement.less index c935a79..dcc23f4 100644 --- a/src/editor/modules/documentCanvas/canvas/genericElement.less +++ b/src/editor/modules/documentCanvas/canvas/genericElement.less @@ -13,4 +13,15 @@ font-family: monospace; z-index:9999; white-space: nowrap; +} + +.comment-tip { + position:absolute; + left:-15px; + top:3px; + opacity: 0.25; +} + +.current-node-element > .canvas-widgets > .comment-tip { + opacity: 1; } \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/gutter.js b/src/editor/modules/documentCanvas/canvas/gutter.js new file mode 100644 index 0000000..472998a --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/gutter.js @@ -0,0 +1,117 @@ +define(function(require) { + +'use strict'; + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + Backbone = require('libs/backbone'), + gutterBoxTemplate = require('libs/text!./gutterBox.html'); + + +var GutterView = function(gutter) { + gutter.on('show', function(group) { + if(this.groupView) { + this.groupView.remove(); + } + this.groupView = new GutterGroupView(this, group); + this.dom.append(this.groupView.dom); + this.groupView.dom.css({top: group.getOffsetHint()}); + this.groupView.show(); + }, this); + this.dom = $('
'); +}; + + +var GutterGroupView = function(gutterView, group) { + this.gutterView = gutterView; + this.group = group; + this.views = []; + + this.dom = $(gutterBoxTemplate); + + this.dom.on('click', function() { + if(!this.dom.hasClass('focused')) { + var canvas = this.group.meta.canvas; + canvas.setCurrentElement(this.group.meta); + } + }.bind(this)); + + this.group.views.forEach(function(view) { + this.onViewAdded(view); + }.bind(this)); + + this.group.on('viewAdded', this.onViewAdded, this); + this.group.on('focusToggled', this.onFocusToggled, this); + this.group.on('removed', this.remove, this); +}; +$.extend(GutterGroupView.prototype, { + remove: function() { + this.group.off('viewAdded', this.onViewAdded); + this.group.off('focusToggled', this.onFocusToggled); + this.group.off('removed', this.removed); + this.dom.detach(); + }, + onViewAdded: function(view) { + this.views.push(view); + this.dom.append(view.dom); + }, + show: function() { + this.dom.addClass('focused'); + this.views.forEach(function(view) { + if(view.onActivated) { + view.onActivated(); + } + }); + } +}); + + + +/// model + +var ViewGroup = function(params, gutter, meta) { + this.gutter = gutter; + this.params = params; + this.meta = meta; + this.view = $(gutterBoxTemplate); + this.views = []; +}; +$.extend(ViewGroup.prototype, Backbone.Events, { + getOffsetHint: function() { + return _.isFunction(this.params.offsetHint) ? this.params.offsetHint() : this.params.offsetHint; + }, + addView: function(view) { + this.views.push(view); + this.trigger('viewAdded', view); + }, + show: function() { + this.gutter.show(this); + }, + remove: function() { + this.trigger('removed'); + } +}); + + +var Gutter = function() { +}; + +_.extend(Gutter.prototype, Backbone.Events, { + createViewGroup: function(params, meta) { + return new ViewGroup(params, this, meta); + }, + show: function(group) { + this.trigger('show', group); + }, +}); + + +return { + create: function() { + return new Gutter(); + }, + GutterView: GutterView, + GutterGroupView: GutterGroupView +}; + +}); \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/gutter.less b/src/editor/modules/documentCanvas/canvas/gutter.less new file mode 100644 index 0000000..17178c7 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/gutter.less @@ -0,0 +1,14 @@ +.gutter { + display: table-cell; + width: calc(~'100% - 800px'); + min-width: 250px; + padding: 0 25px; +} + +.gutter-box { + border: 1px solid darken(#ddd, 10%); + padding: 5px 10px; + position: relative; + background-color: darken(#FFFCB7, 15%); +} + diff --git a/src/editor/modules/documentCanvas/canvas/gutterBox.html b/src/editor/modules/documentCanvas/canvas/gutterBox.html new file mode 100644 index 0000000..7f53e72 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/gutterBox.html @@ -0,0 +1 @@ +
diff --git a/src/editor/modules/documentCanvas/canvas/nullElement.js b/src/editor/modules/documentCanvas/canvas/nullElement.js new file mode 100644 index 0000000..b57a62a --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/nullElement.js @@ -0,0 +1,16 @@ +define(function(require) { + +'use strict'; +var documentElement = require('./documentElement'); + + +var NullElement = Object.create(documentElement.DocumentNodeElement.prototype); + +NullElement.init = function() { + this.dom = null; + this.wlxmlNode.setData('canvasElement', undefined); +}; + +return NullElement; + +}); \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/utils.js b/src/editor/modules/documentCanvas/canvas/utils.js index 1df3f82..4e262a0 100644 --- a/src/editor/modules/documentCanvas/canvas/utils.js +++ b/src/editor/modules/documentCanvas/canvas/utils.js @@ -58,7 +58,7 @@ var _getElementForTextNode = function(textNode, withParent) { return true; } }); - return toret; + return toret || parentElement; }; var getElementForDetachedNode = function(node, originalParent) { diff --git a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js index 02dc657..d107612 100644 --- a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js +++ b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js @@ -75,7 +75,7 @@ var handlers = { }, nodeTextChange: function(event) { var element = utils.getElementForNode(event.meta.node); - element.setText(event.meta.node.getText()); + element.handle(event); }, metadataChanged: _metadataEventHandler, diff --git a/src/editor/modules/documentCanvas/documentCanvas.js b/src/editor/modules/documentCanvas/documentCanvas.js index 8742aef..7e70f7f 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.js +++ b/src/editor/modules/documentCanvas/documentCanvas.js @@ -20,7 +20,9 @@ return function(sandbox) { canvasElements = canvasElements.concat(plugin.canvasElements || []); }); - var canvas = canvas3.fromXMLDocument(null, canvasElements); + var canvas = canvas3.fromXMLDocument(null, canvasElements, { + user: sandbox.getConfig().user + }); var canvasWrapper = $(template); var shownAlready = false; var scrollbarPosition = 0, @@ -70,7 +72,7 @@ return function(sandbox) { }, setDocument: function(wlxmlDocument) { canvas.loadWlxmlDocument(wlxmlDocument); - canvasWrapper.find('#rng-module-documentCanvas-content').empty().append(canvas.view()); + canvasWrapper.find('#rng-module-documentCanvas-contentWrapper').empty().append(canvas.view()); }, highlightElement: function(node) { canvas.toggleElementHighlight(node, true); diff --git a/src/editor/modules/documentCanvas/documentCanvas.less b/src/editor/modules/documentCanvas/documentCanvas.less index bd39b6a..879d78d 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.less +++ b/src/editor/modules/documentCanvas/documentCanvas.less @@ -1,26 +1,45 @@ @import 'nodes.less'; +@import 'canvas/canvas.less'; @import 'canvas/documentElement.less'; @import 'canvas/genericElement.less'; +@import 'canvas/gutter.less'; +@import 'canvas/comments/comments.less'; #rng-module-documentCanvas { height: 100%; } -#rng-module-documentCanvas-mainArea { - height: 100%; - margin-bottom: 20px; -} - #rng-module-documentCanvas-contentWrapper { - border-color: #ddd; - border-style: solid; - border-width: 1px; + background-color: #ddd; float:left; width: 100%; height: 100%; overflow-y: scroll; - padding: 0 10px; + &:before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -15px; + right: -15px; + box-shadow: inset 0 20px 12px -20px rgba(0,0,0,0.6); + pointer-events:none; + z-index: 1; + } + + &:after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -15px; + right: -15px; + box-shadow: inset 0px -20px 12px -20px rgba(0,0,0,0.6); + pointer-events:none; + z-index: 1; + } + &::-webkit-scrollbar { .rng-mixin-scrollbar; } @@ -34,8 +53,14 @@ .rng-mixin-scrollbar-thumb-window-inactive; } - .canvas-wrapper { + .root-wrapper { + display: table-cell; + vertical-align: top; + width: 600px; outline: 0px solid transparent; + padding: 0 100px; + background-color: white; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.2), inset 0 0 10px rgba(0, 0, 0, 0.1); } .current-text-element { diff --git a/src/editor/modules/documentCanvas/template.html b/src/editor/modules/documentCanvas/template.html index 63dc331..7a32c16 100644 --- a/src/editor/modules/documentCanvas/template.html +++ b/src/editor/modules/documentCanvas/template.html @@ -1,7 +1,4 @@
-
-
-
-
+
\ No newline at end of file diff --git a/src/editor/modules/mainBar/mainBar.js b/src/editor/modules/mainBar/mainBar.js index 256111f..e5cf522 100644 --- a/src/editor/modules/mainBar/mainBar.js +++ b/src/editor/modules/mainBar/mainBar.js @@ -40,6 +40,9 @@ return function(sandbox) { trigger.data('originalContent', trigger.html()); trigger.text(disabledText); } + }, + setSummaryView: function(summaryView) { + view.find('.bottom').prepend(summaryView); } }; diff --git a/src/editor/modules/mainBar/mainBar.less b/src/editor/modules/mainBar/mainBar.less index 8fb99a5..7a2e49b 100644 --- a/src/editor/modules/mainBar/mainBar.less +++ b/src/editor/modules/mainBar/mainBar.less @@ -13,14 +13,20 @@ border-style: solid; margin: 0 5px 0 0; padding: 0 5px 0 0; - &:last-child { - margin-right: 0; - padding-right: 0; - } + } ul { list-style-type: none; + display: inline-block; + color: #8F8A8A; + margin: 0; + &:last-child { + li:last-child { + margin-right: 0; + padding-right: 0; + } + } } .top { diff --git a/src/editor/modules/mainBar/template.html b/src/editor/modules/mainBar/template.html index 5320525..59f0708 100644 --- a/src/editor/modules/mainBar/template.html +++ b/src/editor/modules/mainBar/template.html @@ -3,8 +3,10 @@ <%= userName %> (<%= gettext('Exit') %>)
- +
+ +
\ No newline at end of file diff --git a/src/editor/modules/rng/documentSummary.html b/src/editor/modules/rng/documentSummary.html index 95c7552..8d5fcbd 100644 --- a/src/editor/modules/rng/documentSummary.html +++ b/src/editor/modules/rng/documentSummary.html @@ -1,14 +1,6 @@ -
-

<%= title %>

- - <% properties.forEach(function(propertyDesc) { %> - - - - <% }); %> - - - - -
<%= propertyDesc.label %><%= propertyValues[propertyDesc.name] %>
<%= gettext('Draft Saved') %>
-
\ No newline at end of file +
  • <%= gettext('Draft Saved') %>:
  • +<% properties.forEach(function(propertyDesc) { %> +
  • + <%= propertyDesc.label %>: <%= propertyValues[propertyDesc.name] %> +
  • +<% }); %> diff --git a/src/editor/modules/rng/documentSummary.js b/src/editor/modules/rng/documentSummary.js index fe593c3..8e6927d 100644 --- a/src/editor/modules/rng/documentSummary.js +++ b/src/editor/modules/rng/documentSummary.js @@ -8,7 +8,7 @@ var $ = require('libs/jquery'), var view = { - dom: $('
    '), + dom: $('
      '), init: function(config, doc) { this.config = config; this.doc = doc; diff --git a/src/editor/modules/rng/editingLayout.html b/src/editor/modules/rng/editingLayout.html index 54a29aa..9fb37e6 100644 --- a/src/editor/modules/rng/editingLayout.html +++ b/src/editor/modules/rng/editingLayout.html @@ -1,7 +1,4 @@
      -
      -
      -
      \ No newline at end of file diff --git a/src/editor/modules/rng/editingLayout.less b/src/editor/modules/rng/editingLayout.less index 0f5254a..fe46e6d 100644 --- a/src/editor/modules/rng/editingLayout.less +++ b/src/editor/modules/rng/editingLayout.less @@ -3,54 +3,6 @@ width: 600px;*/ } -.rng-module-rng2-right { - /*float: right; - position: relative; - width: 258px; - margin-left: 50px;*/ - - border-width: 1px 1px 1px 1px; - border-style: solid; - border-color: #ddd; - padding: 5px 15px; - - p, td, label, input, select { - font-size: 11px; - line-height:13px; - } - - select { - -webkit-appearance: button; - -moz-appearance: button; - appearance: button; - height: auto; - line-height: 14px; - } - - legend { - font-size:11px; - height:30px; - } - - - .rng-view-tabs-tabBar { - position:absolute; - top:-1px; - right:-50px; - border-width: 1px 1px 1px 0px; - border-style: solid; - border-color: #ddd; - padding: 5px; - background: #ededed; - } - - label + select { - position:relative; - top: 5px; - } - -} - .rng-module-rng2-statusBar { margin: 10px 5px; font-size:0.9em; @@ -58,6 +10,8 @@ .fnp-module-rng-editingLayout { + margin-left: 60px; + [fnpjs-place="statusBar"] { position: absolute; bottom: 0; @@ -67,22 +21,11 @@ } - [fnpjs-place="leftColumn"], [fnpjs-place="rightColumn"] { + [fnpjs-place="leftColumn"] { position: absolute; top: 30px; // - bottom: 50px; // - } - - [fnpjs-place="leftColumn"] { + bottom: 0px; // left:0; - right: 360px; - } - - [fnpjs-place="rightColumn"] { - right: 0px; - width: 290px; - + right: 0; } - - } \ No newline at end of file diff --git a/src/editor/modules/rng/mainLayout.less b/src/editor/modules/rng/mainLayout.less index c1a7e8e..859b581 100644 --- a/src/editor/modules/rng/mainLayout.less +++ b/src/editor/modules/rng/mainLayout.less @@ -2,8 +2,8 @@ position: fixed; top: 5px; bottom: 5px; - left: 80px; - right: 80px; + left: 0px; + right: 0px; [fnpjs-place="messages"] { position: absolute; @@ -16,6 +16,7 @@ float: right; position: relative; z-index: 2; + margin-right: 60px; } [fnpjs-place="mainView"] { @@ -29,6 +30,7 @@ > .rng-view-tabs { position: relative; height: 100%; + padding-left: 60px; > .rng-view-tabs-content { position: absolute; diff --git a/src/editor/modules/rng/rng.js b/src/editor/modules/rng/rng.js index 3274067..032fea4 100644 --- a/src/editor/modules/rng/rng.js +++ b/src/editor/modules/rng/rng.js @@ -2,13 +2,12 @@ define([ './documentSummary', 'libs/underscore', 'fnpjs/layout', -'fnpjs/vbox', 'fnpjs/logging/logging', 'views/tabs/tabs', 'libs/text!./mainLayout.html', 'libs/text!./editingLayout.html', 'libs/text!./diffLayout.html', -], function(documentSummary, _, layout, vbox, logging, tabs, mainLayoutTemplate, visualEditingLayoutTemplate, diffLayoutTemplate) { +], function(documentSummary, _, layout, logging, tabs, mainLayoutTemplate, visualEditingLayoutTemplate, diffLayoutTemplate) { 'use strict'; @@ -35,10 +34,8 @@ return function(sandbox) { if(fragment && fragment.node) { elementParent = fragment.node.getNearestElementNode(); sandbox.getModule('nodeBreadCrumbs').setNodeElement(elementParent); - sandbox.getModule('metadataEditor').setNodeElement(elementParent); } else { sandbox.getModule('nodeBreadCrumbs').setNodeElement(null); - sandbox.getModule('metadataEditor').setNodeElement(null); } }, }; @@ -48,20 +45,15 @@ return function(sandbox) { mainLayout: new layout.Layout(mainLayoutTemplate), mainTabs: (new tabs.View()).render(), visualEditing: new layout.Layout(visualEditingLayoutTemplate), - visualEditingSidebar: (new tabs.View({stacked: true})).render(), - currentNodePaneLayout: new vbox.VBox(), diffLayout: new layout.Layout(diffLayoutTemplate) }; - views.visualEditing.setView('rightColumn', views.visualEditingSidebar.getAsView()); addMainTab(gettext('Editor'), 'editor', views.visualEditing.getAsView()); addMainTab(gettext('Source'), 'sourceEditor', ''); addMainTab(gettext('History'), 'history', views.diffLayout.getAsView()); sandbox.getDOM().append(views.mainLayout.getAsView()); - views.visualEditingSidebar.addTab({icon: 'pencil'}, 'edit', views.currentNodePaneLayout.getAsView()); - var wlxmlDocument, documentIsDirty; /* Events handling */ @@ -84,12 +76,12 @@ return function(sandbox) { documentSummary.init(sandbox.getConfig().documentSummaryView, wlxmlDocument); documentSummary.render(); documentSummary.setDraftField(usingDraft ? (draftTimestamp || '???') : '-'); - views.currentNodePaneLayout.appendView(documentSummary.dom); + sandbox.getModule('mainBar').setSummaryView(documentSummary.dom); sandbox.getModule('mainBar').setCommandEnabled('drop-draft', usingDraft); sandbox.getModule('mainBar').setCommandEnabled('save', usingDraft); - _.each(['sourceEditor', 'documentCanvas', 'documentToolbar', 'metadataEditor', 'nodeBreadCrumbs', 'mainBar', 'indicator', 'documentHistory', 'diffViewer', 'statusBar'], function(moduleName) { + _.each(['sourceEditor', 'documentCanvas', 'documentToolbar', 'mainBar', 'indicator', 'documentHistory', 'diffViewer', 'statusBar'], function(moduleName) { sandbox.getModule(moduleName).start(); }); @@ -195,13 +187,6 @@ return function(sandbox) { } }; - eventHandlers.metadataEditor = { - ready: function() { - sandbox.getModule('metadataEditor').setDocument(sandbox.getModule('data').getDocument()); - views.visualEditingSidebar.addTab({icon: 'info-sign'}, 'metadataEditor', sandbox.getModule('metadataEditor').getView()); - } - }; - eventHandlers.documentToolbar = { ready: function() { views.visualEditing.setView('toolbar', sandbox.getModule('documentToolbar').getView()); diff --git a/src/editor/plugins/core/canvasElements.js b/src/editor/plugins/core/canvasElements.js index 229cd47..0d7cffa 100644 --- a/src/editor/plugins/core/canvasElements.js +++ b/src/editor/plugins/core/canvasElements.js @@ -111,7 +111,7 @@ $.extend(footnote, { return [ - {tag: 'aside', klass: 'comment', prototype: comment}, + {tag: 'aside', klass: 'comment', prototype: null}, {tag: 'aside', klass: 'footnote', prototype: footnote}, {tag: 'span', klass: 'link', prototype: linkElement} ]; diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index bbc27ac..c12405b 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -26,6 +26,14 @@ plugin.documentExtension.textNode.transformations = { return true; // break } }); + newNodes.second.contents() + .filter(function(child) { + return child.object.describesParent; + }) + .forEach(function(child) { + //child.detach(); + newNodes.first.append(child); + }); return _.extend(newNodes, {emptyText: emptyText}); }, getChangeRoot: function() { diff --git a/src/editor/plugins/core/links/linkElement.js b/src/editor/plugins/core/links/linkElement.js index ceefee1..61d2abc 100644 --- a/src/editor/plugins/core/links/linkElement.js +++ b/src/editor/plugins/core/links/linkElement.js @@ -26,8 +26,11 @@ _.extend(linkElement, { this.box.hide(); this.addWidget(this.box); }, - markAsCurrent: function(toggle) { - this.box.toggle(toggle); + onStateChange: function(changes) { + genericElement.onStateChange.call(this, changes); + if(_.isBoolean(changes.active)) { + this.box.toggle(changes.active); + } }, onNodeAttrChange: function(event) { if(event.meta.attr === 'href') { diff --git a/src/editor/styles/mixins.less b/src/editor/styles/mixins.less index 3a14f31..28effc8 100644 --- a/src/editor/styles/mixins.less +++ b/src/editor/styles/mixins.less @@ -1,5 +1,5 @@ .rng-mixin-scrollbar { - width: 9px; + width: 12px; } .rng-mixin-scrollbar-track { diff --git a/src/smartxml/core.js b/src/smartxml/core.js index 007468f..fdce751 100644 --- a/src/smartxml/core.js +++ b/src/smartxml/core.js @@ -187,15 +187,21 @@ var elementNodeTransformations = { return; } + this.contents() + .filter(function(child) { + return child.getProperty('describesParent'); + }.bind(this)) + .forEach(function(child) { + child.detach(); + }); + var myContents = this.contents(), myIdx = parent.indexOf(this); - if(myContents.length === 0) { return this.detach(); } - var childrenLength = this.contents().length, first = true, shiftRange = false; @@ -381,7 +387,9 @@ var documentTransformations = { } for(var i = idx1; i <= idx2; i++) { - wrapper.append(parentContents[i].detach()); + if(!parentContents[i].getProperty('describesParent')) { + wrapper.append(parentContents[i].detach()); + } } insertingTarget[insertingMethod](wrapper); @@ -426,7 +434,9 @@ var documentTransformations = { wrapperElement.append({text: prefixInside}); } for(var i = idx1 + 1; i < idx2; i++) { - wrapperElement.append(contentsInside[i]); + if(!contentsInside[i].getProperty('describesParent')) { + wrapperElement.append(contentsInside[i]); + } } if(suffixInside.length > 0) { wrapperElement.append({text: suffixInside}); diff --git a/src/smartxml/smartxml.js b/src/smartxml/smartxml.js index cfae7c7..8039916 100644 --- a/src/smartxml/smartxml.js +++ b/src/smartxml/smartxml.js @@ -17,12 +17,21 @@ var DocumentNode = function(nativeNode, document) { throw new Error('undefined document for a node'); } this.document = document; + this.object = {}; this._setNativeNode(nativeNode); }; $.extend(DocumentNode.prototype, { + getProperty: function(propName) { + var toret = this.object[propName]; + if(toret && _.isFunction(toret)) { + toret = toret.call(this); + } + return toret; + }, + transform: function(Transformation, args) { var transformation = new Transformation(this.document, this, args); return this.document.transform(transformation); diff --git a/src/smartxml/smartxml.test.js b/src/smartxml/smartxml.test.js index 5430278..b7676e2 100644 --- a/src/smartxml/smartxml.test.js +++ b/src/smartxml/smartxml.test.js @@ -578,6 +578,24 @@ describe('smartxml', function() { expect(node.contents()[2].getText()).to.equal(' a cat!'); }); + it('removes parent-describing sibling nodes of unwrapped node', function() { + var doc = getDocumentFromXML('
      '), + div = doc.root.contents()[0], + x = div.contents()[1]; + + doc.registerExtension({documentNode: {methods: { + object: { + describesParent: function() { + return this.getTagName() === 'x'; + } + } + }}}); + + div.unwrapContent(); + expect(doc.root.contents().length).to.equal(2); + expect(x.isInDocument()).to.be.false; + }); + it('unwrap single element node from its parent', function() { var doc = getDocumentFromXML('
      '), div = doc.root, @@ -630,6 +648,28 @@ describe('smartxml', function() { expect(wrapperContents[1].contents().length).to.equal(1); expect(wrapperContents[1].contents()[0].getText()).to.equal('small'); }); + + it('keeps parent-describing nodes in place', function() { + var doc = getDocumentFromXML('Alice has a cat'), + root = doc.root, + x = root.contents()[1]; + + doc.registerExtension({documentNode: {methods: { + object: { + describesParent: function() { + return this.getTagName() === 'x'; + } + } + }}}); + + root.wrapText({ + _with: {tagName: 'span', attrs: {'attr1': 'value1'}}, + offsetStart: 1, + offsetEnd: 4, + textNodeIdx: [0,2] + }); + expect(x.parent().sameNode(root)).to.be.true; + }); }); describe('Wrapping Nodes', function() { @@ -678,6 +718,29 @@ describe('smartxml', function() { expect(headerChildren[0].sameNode(div2)).to.equal(true, 'first node wrapped'); expect(headerChildren[1].sameNode(div3)).to.equal(true, 'second node wrapped'); }); + + it('keeps parent-describing nodes in place', function() { + var section = elementNodeFromXML('
      Alice
      a cat
      '), + aliceText = section.contents()[0], + x = section.contents()[1], + lastDiv = section.contents()[2]; + + section.document.registerExtension({documentNode: {methods: { + object: { + describesParent: function() { + return this.getTagName() === 'x'; + } + } + }}}); + + section.document.wrapNodes({ + node1: aliceText, + node2: lastDiv, + _with: {tagName: 'header'} + }); + + expect(x.parent().sameNode(section)).to.be.true; + }); }); }); diff --git a/src/wlxml/extensions/comments/comments.js b/src/wlxml/extensions/comments/comments.js new file mode 100644 index 0000000..521bad8 --- /dev/null +++ b/src/wlxml/extensions/comments/comments.js @@ -0,0 +1,36 @@ +define(function() { + +'use strict'; + +var extension = {wlxmlClass: {comment: { + methods: { + describesParent: true, + getText: function() { + var text = ''; + this.contents() + .filter(function(node) { + /* globals Node */ + return node && node.nodeType === Node.TEXT_NODE; + }) + .forEach(function(node) { + text = text + node.getText(); + }); + return text; + }, + setText: function(text) { + var contents = this.contents(); + if(contents.length === 1 && contents[0].nodeType === Node.TEXT_NODE) { + contents[0].setText(text); + } else { + contents.forEach(function(node) { + node.detach(); + }); + this.append({text: text}); + } + } + } +}}}; + +return extension; + +}); \ No newline at end of file diff --git a/src/wlxml/wlxml.js b/src/wlxml/wlxml.js index 0505116..f370223 100644 --- a/src/wlxml/wlxml.js +++ b/src/wlxml/wlxml.js @@ -3,8 +3,9 @@ define([ 'libs/underscore', 'smartxml/smartxml', 'smartxml/transformations', - 'wlxml/extensions/metadata/metadata' -], function($, _, smartxml, transformations, metadataExtension) { + 'wlxml/extensions/metadata/metadata', + 'wlxml/extensions/comments/comments' +], function($, _, smartxml, transformations, metadataExtension, commentExtension) { 'use strict'; @@ -58,7 +59,9 @@ var installObject = function(instance, klass) { }); instance.object = Object.create(_.extend({}, methods, transformations)); _.keys(methods).concat(_.keys(transformations)).forEach(function(key) { - instance.object[key] = _.bind(instance.object[key], instance); + if(_.isFunction(instance.object[key])) { + instance.object[key] = _.bind(instance.object[key], instance); + } }); }; @@ -88,6 +91,11 @@ $.extend(WLXMLElementNode.prototype, WLXMLDocumentNodeMethods, smartxml.ElementN return (_.isUndefined(query.klass) || this.getClass().substr(0, query.klass.length) === query.klass) && (_.isUndefined(query.tagName) || this.getTagName() === query.tagName); }, + hasChild: function(query) { + return this.contents().some(function(child) { + return child.is(query); + }.bind(this)); + }, getMetaAttributes: function() { var toret = new AttributesList(), classParts = [''].concat(this.getClass().split('.')), @@ -206,12 +214,14 @@ var WLXMLTextNode = function() { smartxml.TextNode.apply(this, arguments); }; WLXMLTextNode.prototype = Object.create(smartxml.TextNode.prototype); -$.extend(WLXMLTextNode.prototype, WLXMLDocumentNodeMethods); +$.extend(WLXMLTextNode.prototype, WLXMLDocumentNodeMethods, { + is: function() { return false; } +}); var WLXMLDocument = function(xml, options) { this.classMethods = {}; this.classTransformations = {}; - smartxml.Document.call(this, xml, [metadataExtension]); + smartxml.Document.call(this, xml, [metadataExtension, commentExtension]); this.options = options; }; diff --git a/src/wlxml/wlxml.test.js b/src/wlxml/wlxml.test.js index 9904154..af0e23b 100644 --- a/src/wlxml/wlxml.test.js +++ b/src/wlxml/wlxml.test.js @@ -286,6 +286,15 @@ describe('WLXMLDocument', function() { expect(testClassNode.object.testMethod().sameNode(testClassNode)).to.equal(true, '1'); }); + it('allows adding non-function properties to an ElementNode of specific class', function() { + extension = {wlxmlClass: {test_class: {methods: { + testProp: 123 + }}}}; + doc.registerExtension(extension); + testClassNode = doc.root.contents()[1]; + expect(testClassNode.object.testProp).to.equal(123); + }); + it('allows adding transformation to an ElementNode of specific class', function() { extension = {wlxmlClass: {test_class: {transformations: { testTransformation: function() { return this; },