From: Aleksander Ɓukasz Date: Tue, 6 May 2014 10:05:47 +0000 (+0200) Subject: Merge branch 'objects' implementing canvas objects extending ability X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/e1e728df4cd9e96c5865fcec0762eaaf5b37a3f7?hp=7868dda583569339a88ef875d50fd88f85241547 Merge branch 'objects' implementing canvas objects extending ability --- diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index d58ea7b..ff2b31e 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -6,8 +6,10 @@ define([ 'modules/documentCanvas/canvas/documentElement', 'modules/documentCanvas/canvas/keyboard', 'modules/documentCanvas/canvas/utils', -'modules/documentCanvas/canvas/wlxmlListener' -], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener) { +'modules/documentCanvas/canvas/wlxmlListener', +'modules/documentCanvas/canvas/elementsRegister', +'modules/documentCanvas/canvas/genericElement', +], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement) { 'use strict'; /* global document:false, window:false, Node:false, gettext */ @@ -55,7 +57,20 @@ $.extend(TextHandler.prototype, { }); -var Canvas = function(wlxmlDocument) { +var Canvas = function(wlxmlDocument, elements) { + this.elementsRegister = new ElementsRegister(documentElement.DocumentNodeElement); + + elements = [ + {tag: 'section', klass: null, prototype: genericElement}, + {tag: 'div', klass: null, prototype: genericElement}, + {tag: 'header', klass: null, prototype: genericElement}, + {tag: 'span', klass: null, prototype: genericElement}, + {tag: 'aside', klass: null, prototype: genericElement} + ].concat(elements || []); + + (elements).forEach(function(elementDesc) { + this.elementsRegister.register(elementDesc); + }.bind(this)); this.eventBus = _.extend({}, Backbone.Events); this.wrapper = $('
').addClass('canvas-wrapper').attr('contenteditable', true); this.wlxmlListener = wlxmlListener.create(this); @@ -77,7 +92,12 @@ $.extend(Canvas.prototype, Backbone.Events, { }, createElement: function(wlxmlNode) { - var Factory = wlxmlNode.nodeType === Node.TEXT_NODE ? documentElement.DocumentTextElement : documentElement.DocumentNodeElement; + var Factory; + if(wlxmlNode.nodeType === Node.TEXT_NODE) { + Factory = documentElement.DocumentTextElement; + } else { + Factory = this.elementsRegister.getElement({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()}); + } return new Factory(wlxmlNode, this); }, @@ -104,7 +124,7 @@ $.extend(Canvas.prototype, Backbone.Events, { reloadRoot: function() { this.rootElement = this.createElement(this.wlxmlDocument.root); this.wrapper.empty(); - this.wrapper.append(this.rootElement.dom()); + this.wrapper.append(this.rootElement.dom); }, setupEventHandling: function() { @@ -213,7 +233,7 @@ $.extend(Canvas.prototype, Backbone.Events, { this.eventBus.on('elementToggled', function(toggle, element) { if(!toggle) { - canvas.setCurrentElement(element.getPreviousTextElement()); + canvas.setCurrentElement(canvas.getPreviousTextElement(element)); } }); }, @@ -227,7 +247,7 @@ $.extend(Canvas.prototype, Backbone.Events, { }, toggleElementHighlight: function(node, toggle) { - var element = utils.findCanvasElement(node); + var element = utils.getElementForNode(node); element.toggleHighlight(toggle); }, @@ -250,8 +270,22 @@ $.extend(Canvas.prototype, Backbone.Events, { } }, + getPreviousTextElement: function(relativeToElement, includeInvisible) { + return this.getNearestTextElement('above', relativeToElement, includeInvisible); + }, + + getNextTextElement: function(relativeToElement, includeInvisible) { + return this.getNearestTextElement('below', relativeToElement, includeInvisible); + }, + + getNearestTextElement: function(direction, relativeToElement, includeInvisible) { + includeInvisible = includeInvisible !== undefined ? includeInvisible : false; + var selector = '[document-text-element]' + (includeInvisible ? '' : ':visible'); + return this.getDocumentElement(utils.nearestInDocumentOrder(selector, direction, relativeToElement.dom[0])); + }, + contains: function(element) { - return element.dom().parents().index(this.wrapper) !== -1; + return element.dom.parents().index(this.wrapper) !== -1; }, triggerSelectionChanged: function() { @@ -269,7 +303,7 @@ $.extend(Canvas.prototype, Backbone.Events, { } if(!(element instanceof documentElement.DocumentElement)) { - element = utils.findCanvasElement(element); + element = utils.getElementForNode(element); } if(!element || !this.contains(element)) { @@ -283,18 +317,12 @@ $.extend(Canvas.prototype, Backbone.Events, { if(byBrowser && byBrowser.parent().sameNode(nodeToLand)) { return byBrowser; } - var children = e.children(); - for(var i = 0; i < children.length; i++) { - if(children[i] instanceof documentElement.DocumentTextElement) { - return children[i]; - } - } - return null; + return e.getVerticallyFirstTextElement(); }.bind(this); var _markAsCurrent = function(element) { if(element instanceof documentElement.DocumentTextElement) { this.wrapper.find('.current-text-element').removeClass('current-text-element'); - element.dom().addClass('current-text-element'); + element.dom.addClass('current-text-element'); } else { this.wrapper.find('.current-node-element').removeClass('current-node-element'); element._container().addClass('current-node-element'); @@ -329,7 +357,7 @@ $.extend(Canvas.prototype, Backbone.Events, { _moveCaretToTextElement: function(element, where) { var range = document.createRange(), - node = element.dom().contents()[0]; + node = element.dom.contents()[0]; if(typeof where !== 'number') { range.selectNodeContents(node); @@ -357,10 +385,6 @@ $.extend(Canvas.prototype, Backbone.Events, { } }, - findCanvasElement: function(node) { - return utils.findCanvasElement(node); - }, - toggleGrid: function() { this.wrapper.toggleClass('grid-on'); this.trigger('changed'); @@ -538,7 +562,7 @@ $.extend(Cursor.prototype, { if(selection.anchorNode === selection.focusNode) { anchorFirst = selection.anchorOffset <= selection.focusOffset; } else { - anchorFirst = parent.childIndex(anchorElement) < parent.childIndex(focusElement); + anchorFirst = (parent.getFirst(anchorElement, focusElement) === anchorElement); } placeData = getPlaceData(anchorFirst); } else { @@ -558,8 +582,8 @@ $.extend(Cursor.prototype, { }); return { - fromXMLDocument: function(wlxmlDocument) { - return new Canvas(wlxmlDocument); + fromXMLDocument: function(wlxmlDocument, elements) { + return new Canvas(wlxmlDocument, elements); } }; diff --git a/src/editor/modules/documentCanvas/canvas/canvas.test.js b/src/editor/modules/documentCanvas/canvas/canvas.test.js index 346a82b..f64754f 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.test.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.test.js @@ -4,16 +4,17 @@ define([ 'libs/sinon', 'modules/documentCanvas/canvas/canvas', 'modules/documentCanvas/canvas/utils', -'wlxml/wlxml' -], function($, chai, sinon, canvas, utils, wlxml) { +'modules/documentCanvas/canvas/documentElement', +'wlxml/wlxml', +], function($, chai, sinon, canvas, utils, documentElement, wlxml) { 'use strict'; /* global describe, it, beforeEach, afterEach */ var expect = chai.expect; -var getCanvasFromXML = function(xml) { - return canvas.fromXMLDocument(getDocumentFromXML(xml)); +var getCanvasFromXML = function(xml, elements) { + return canvas.fromXMLDocument(getDocumentFromXML(xml), elements); }; var getDocumentFromXML = function(xml) { @@ -26,6 +27,17 @@ var wait = function(callback, timeout) { }; +describe('wtf', function() { + it('wtf!', function() { + var c = getCanvasFromXML('
Alice
'), + doc = c.wlxmlDocument; + + var txtNode = doc.root.contents()[0]; + txtNode.wrapWith({tagName: 'header', start: 1, end: 2}); + expect(c.doc().children().length).to.equal(3); + }); +}); + describe('new Canvas', function() { it('abc', function() { var doc = wlxml.WLXMLDocumentFromXML('
Alice has a cat!
'), @@ -73,8 +85,8 @@ describe('Listening to document changes', function() { a.before(b); var sectionChildren = c.doc().children(); expect(sectionChildren.length).to.equal(2); - expect(sectionChildren[0].getWlxmlTag()).to.equal('b'); - expect(sectionChildren[1].getWlxmlTag()).to.equal('a'); + expect(sectionChildren[0].wlxmlNode.getTagName()).to.equal('b'); + expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('a'); }); it('Handling text node moved', function() { @@ -87,7 +99,7 @@ describe('Listening to document changes', function() { var sectionChildren = c.doc().children(); expect(sectionChildren.length).to.equal(2); expect(sectionChildren[0].getText()).to.equal('Alice'); - expect(sectionChildren[1].getWlxmlTag()).to.equal('a'); + expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('a'); }); it('Handles nodeTagChange event', function() { @@ -100,7 +112,7 @@ describe('Listening to document changes', function() { var headerNode = doc.root.contents()[0], headerElement = c.doc().children()[0]; - expect(headerElement.getWlxmlTag()).to.equal('header', 'element ok'); + expect(headerElement.wlxmlNode.getTagName()).to.equal('header', 'element ok'); /* Make sure we handle invalidation of reference to wlxmlNode after changing its tag */ expect(headerNode.getData('canvasElement').sameNode(headerElement)).to.equal(true, 'node->element'); @@ -113,7 +125,7 @@ describe('Listening to document changes', function() { aTextElement; canvas.fromXMLDocument(doc); - aTextElement = utils.findCanvasElementInParent(aTextNode, aTextNode.parent()); // TODO: This really should be easier... + aTextElement = utils.getElementForNode(aTextNode); aTextElement.setText(''); @@ -122,12 +134,170 @@ describe('Listening to document changes', function() { expect(aTextElement.getText({raw:true})).to.equal(utils.unicode.ZWS, 'canvas represents this as empty node'); aTextElement.wlxmlNode.detach(); expect(parent.children().length).to.equal(1); - expect(parent.children()[0].getWlxmlTag()).to.equal('span'); + expect(parent.children()[0].wlxmlNode.getTagName()).to.equal('span'); done(); }); }); }); +describe('Displaying span nodes', function() { + it('inlines a span element with a text', function() { + var c = getCanvasFromXML('
Alice
'), + spanElement = c.doc().children()[0]; + expect(spanElement.isBlock()).to.equal(false); + }); + it('renders non-span element as a block', function() { + var c = getCanvasFromXML('
'), + element = c.doc().children()[0], + node = element.wlxmlNode; + + expect(element.isBlock()).to.equal(false, 'initially inline'); + node = node.setTag('div'); + expect(node.getData('canvasElement').isBlock()).to.equal(true, 'block'); + }); + + it('inlines a span element if its block content gets removed', function() { + var c = getCanvasFromXML('
Alice
has
a cat!
'), + spanElement = c.doc().children()[0], + divNode = spanElement.wlxmlNode.contents()[1]; + + expect(spanElement.isBlock()).to.equal(true, 'initially a block'); + divNode.detach(); + expect(spanElement.isBlock()).to.equal(false, 'inlined after removing inner block'); + + spanElement.wlxmlNode.append({tagName: 'div'}); + + expect(spanElement.isBlock()).to.equal(true, 'block again after bringing back inner block'); + }); + + it('keeps showing element as a block after changing its node tag to span if it contains elements of non-span nodes', function() { + var c = getCanvasFromXML('
'), + outerDivElement = c.doc().children()[0], + outerDivNode = outerDivElement.wlxmlNode; + outerDivNode = outerDivNode.setTag('span'); + expect(c.doc().children()[0].isBlock()).to.equal(true); + }); +}); + + +describe('Default document changes handling', function() { + it('handles added node', function() { + var c = getCanvasFromXML('
'); + c.wlxmlDocument.root.append({tagName:'div'}); + expect(c.doc().children().length).to.equal(1); + c.wlxmlDocument.root.prepend({tagName:'div'}); + expect(c.doc().children().length).to.equal(2); + + var node = c.wlxmlDocument.root.contents()[1]; + node.before({tagName: 'div'}); + expect(c.doc().children().length).to.equal(3); + node.after({tagName: 'div'}); + expect(c.doc().children().length).to.equal(4); + }); + + it('handles attribute value change for a class attribute', function() { + var c = getCanvasFromXML('
'); + c.wlxmlDocument.root.setAttr('class', 'test'); + expect(c.doc().wlxmlNode.getClass()).to.equal('test'); + }); + + it('handles detached node', function() { + var c = getCanvasFromXML('
'); + c.wlxmlDocument.root.contents()[0].detach(); + expect(c.doc().children().length).to.equal(0); + }); + + it('handles moved node', function() { + var doc = getDocumentFromXML('
'), + a = doc.root.contents()[0], + b = doc.root.contents()[1], + c = canvas.fromXMLDocument(doc); + + a.before(b); + var sectionChildren = c.doc().children(); + expect(sectionChildren.length).to.equal(2); + expect(sectionChildren[0].wlxmlNode.getTagName()).to.equal('b'); + expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('a'); + }); + + it('handles moving text node to another parent', function() { + var c = getCanvasFromXML('
Alice
has
a cat.
'), + doc = c.wlxmlDocument, + text = doc.root.contents()[0], + div = doc.root.contents()[1]; + + div.append(text); + + var sectionChildren = c.doc().children(); + expect(sectionChildren.length).to.equal(2); + expect(sectionChildren[0].wlxmlNode.sameNode(div)).to.equal(true); + expect(sectionChildren[1].getText()).to.equal('a cat.'); + + expect(div.contents().length).to.equal(2); + expect(div.contents()[0].getTagName()).to.equal('span'); + expect(div.contents()[1].getText()).to.equal('Alice'); + }); + + it('handles change in a text node', function() { + var c = getCanvasFromXML('
Alice
'); + c.wlxmlDocument.root.contents()[0].setText('cat'); + expect(c.doc().children()[0].getText()).to.equal('cat'); + }); +}); + +describe('Custom elements based on wlxml class attribute', function() { + it('allows custom rendering', function() { + var prototype = $.extend({}, documentElement.DocumentNodeElement.prototype, { + init: function() { + this._container().append(''); + } + }), + c = getCanvasFromXML('
', [ + {tag: 'div', klass: 'testClass', prototype: prototype} + ]); + + expect(c.doc().children()[0]._container().children('test').length).to.equal(1); // @! + }); + + it('allows handling changes to internal structure of rendered node', function() { + var prototype = $.extend({}, documentElement.DocumentNodeElement.prototype, { + init: function() { + this.header = $('

'); + this._container().append(this.header); + this.refresh2(); + }, + refresh2: function() { + this.header.text(this.wlxmlNode.contents().length); + }, + onNodeAdded: function(event) { + void(event); + this.refresh2(); + } + }); + + var c = getCanvasFromXML('
', [ + {tag: 'div', klass: 'testClass', prototype: prototype} + ]); + + var node = c.wlxmlDocument.root.contents()[0], + element = node.getData('canvasElement'); + + var header = element.dom.find('h1'); + expect(header.text()).to.equal('1', 'just '); + + node.append({tagName: 'div'}); + + expect(header.text()).to.equal('2', 'added div'); + }); + + describe('Handling unknown class', function() { + it('Inherits default behavior', function() { + var c = getCanvasFromXML('
Hi!
'); + expect(c.doc().children()[0].children()[0].getText()).to.equal('Hi!'); + }); + }); +}); + describe('Cursor', function() { /* globals Node */ var getSelection; @@ -153,7 +323,7 @@ describe('Cursor', function() { it('returns position when browser selection collapsed', function() { var c = getCanvasFromXML('
Alice has a cat
'), - dom = c.doc().dom(), + dom = c.doc().dom, text = findTextNode(dom, 'Alice has a cat'); expect(text.nodeType).to.equal(Node.TEXT_NODE, 'correct node selected'); @@ -187,7 +357,7 @@ describe('Cursor', function() { it('recognizes selection start and end on document order', function() { var c = getCanvasFromXML('
Alicehas a cat
abc...cde
'), - dom = c.doc().dom(), + dom = c.doc().dom, textFirst = findTextNode(dom, 'Alice'), textSecond = findTextNode(dom, 'has a cat'), textAbc = findTextNode(dom, 'abc'), @@ -288,7 +458,7 @@ describe('Cursor', function() { it('returns boundries of selection when browser selection not collapsed', function() { var c = getCanvasFromXML('
Alice has a big cat
'), - dom = c.doc().dom(), + dom = c.doc().dom, text = { alice: findTextNode(dom, 'Alice '), has: findTextNode(dom, 'has'), @@ -320,7 +490,7 @@ describe('Cursor', function() { it('recognizes when browser selection boundries lies in sibling DocumentTextElements', function() { var c = getCanvasFromXML('
Alice has a big cat
'), - dom = c.doc().dom(), + dom = c.doc().dom, text = { alice: findTextNode(dom, 'Alice '), has: findTextNode(dom, 'has'), diff --git a/src/editor/modules/documentCanvas/canvas/documentElement.js b/src/editor/modules/documentCanvas/canvas/documentElement.js index 27160e7..3073e72 100644 --- a/src/editor/modules/documentCanvas/canvas/documentElement.js +++ b/src/editor/modules/documentCanvas/canvas/documentElement.js @@ -1,32 +1,33 @@ define([ 'libs/jquery', 'libs/underscore', -'modules/documentCanvas/canvas/utils', -'modules/documentCanvas/canvas/wlxmlManagers' -], function($, _, utils, wlxmlManagers) { +'modules/documentCanvas/canvas/utils' +], function($, _, utils) { 'use strict'; -/* global Node:false, document:false */ - +/* global Node:false */ // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas var DocumentElement = function(wlxmlNode, canvas) { this.wlxmlNode = wlxmlNode; this.canvas = canvas; - this.$element = this.createDOM(); - this.$element.data('canvas-element', this); + this.dom = this.createDOM(); + this.dom.data('canvas-element', this); }; $.extend(DocumentElement.prototype, { - bound: function() { - return $.contains(document.documentElement, this.dom()[0]); + refreshPath: function() { + this.parents().forEach(function(parent) { + parent.refresh(); + }); + this.refresh(); }, - dom: function() { - return this.$element; + refresh: function() { + // noop }, parent: function() { - var parents = this.$element.parents('[document-node-element]'); + var parents = this.dom.parents('[document-node-element]'); if(parents.length) { return this.canvas.getDocumentElement(parents[0]); } @@ -44,44 +45,14 @@ $.extend(DocumentElement.prototype, { }, sameNode: function(other) { - return other && (typeof other === typeof this) && other.dom()[0] === this.dom()[0]; - }, - - getVerticallyFirstTextElement: function() { - var toret; - this.children().some(function(child) { - if(child instanceof DocumentTextElement) { - toret = child; - return true; // break - } else { - toret = child.getVerticallyFirstTextElement(); - if(toret) { - return true; // break - } - } - }); - return toret; - }, - - getPreviousTextElement: function(includeInvisible) { - return this.getNearestTextElement('above', includeInvisible); + return other && (typeof other === typeof this) && other.dom[0] === this.dom[0]; }, - getNextTextElement: function(includeInvisible) { - return this.getNearestTextElement('below', includeInvisible); - }, + trigger: function() { + this.canvas.eventBus.trigger.apply(this.canvas.eventBus, Array.prototype.slice.call(arguments, 0)); + } - getNearestTextElement: function(direction, includeInvisible) { - includeInvisible = includeInvisible !== undefined ? includeInvisible : false; - var selector = '[document-text-element]' + (includeInvisible ? '' : ':visible'); - return this.canvas.getDocumentElement(utils.nearestInDocumentOrder(selector, direction, this.dom()[0])); - }, - exec: function(method) { - if(this.manager && this.manager[method]) { - return this.manager[method].apply(this.manager, Array.prototype.slice.call(arguments, 1)); - } - } }); @@ -89,6 +60,7 @@ $.extend(DocumentElement.prototype, { var DocumentNodeElement = function(wlxmlNode, canvas) { DocumentElement.call(this, wlxmlNode, canvas); wlxmlNode.setData('canvasElement', this); + this.init(this.dom); }; @@ -99,8 +71,8 @@ var manipulate = function(e, params, action) { } else { element = e.canvas.createElement(params); } - var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom(); - target[action](element.dom()); + e.dom[action](element.dom); + e.refreshPath(); return element; }; @@ -108,41 +80,43 @@ DocumentNodeElement.prototype = Object.create(DocumentElement.prototype); $.extend(DocumentNodeElement.prototype, { + defaultDisplayStyle: 'block', + init: function() {}, + addWidget: function(widget) { + this.dom.children('.canvas-widgets').append(widget.DOM ? widget.DOM : widget); + }, + clearWidgets: function() { + this.dom.children('.canvas-widgets').empty(); + }, + handle: function(event) { + var method = 'on' + event.type[0].toUpperCase() + event.type.substr(1); + if(this[method]) { + this[method](event); + } + }, createDOM: function() { - var dom = $('
') - .attr('document-node-element', ''), + var wrapper = $('
').attr('document-node-element', ''), widgetsContainer = $('
') .addClass('canvas-widgets') .attr('contenteditable', false), - container = $('
') + contentContainer = $('
') .attr('document-element-content', ''); - dom.append(widgetsContainer, container); - // Make sure widgets aren't navigable with arrow keys + wrapper.append(widgetsContainer, contentContainer); widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1); - this.$element = dom; //@!!! - - this.setWlxmlTag(this.wlxmlNode.getTagName()); - this.setWlxmlClass(this.wlxmlNode.getClass()); - - this.wlxmlNode.contents().forEach(function(node) { - container.append(this.canvas.createElement(node).dom()); - }.bind(this)); - return dom; + return wrapper; }, _container: function() { - return this.dom().children('[document-element-content]'); + return this.dom.children('[document-element-content]'); }, detach: function() { - this.dom().detach(); + var parents = this.parents(); + this.dom.detach(); this.canvas = null; - return this; - }, - append: function(params) { - return manipulate(this, params, 'append'); - }, - prepend: function(params) { - return manipulate(this, params, 'prepend'); + if(parents[0]) { + parents[0].refreshPath(); + } + return this; }, before: function(params) { return manipulate(this, params, 'before'); @@ -151,64 +125,10 @@ $.extend(DocumentNodeElement.prototype, { after: function(params) { return manipulate(this, params, 'after'); }, - children: function() { - var toret = []; - if(this instanceof DocumentTextElement) { - return toret; - } - - var elementContent = this._container().contents(); - var element = this; - elementContent.each(function() { - var childElement = element.canvas.getDocumentElement(this); - if(childElement === undefined) { - return true; - } - toret.push(childElement); - }); - return toret; - }, - childIndex: function(child) { - var children = this.children(), - toret = null; - children.forEach(function(c, idx) { - if(c.sameNode(child)) { - toret = idx; - return false; - } - }); - return toret; - }, - getWlxmlTag: function() { - return this._container().attr('wlxml-tag'); - }, - setWlxmlTag: function(tag) { - this._container().attr('wlxml-tag', tag); - }, - getWlxmlClass: function() { - var klass = this._container().attr('wlxml-class'); - if(klass) { - return klass.replace(/-/g, '.'); - } - return undefined; - }, - setWlxmlClass: function(klass) { - if(klass === this.getWlxmlClass()) { - return; - } - if(klass) { - this._container().attr('wlxml-class', klass.replace(/\./g, '-')); - } - else { - this._container().removeAttr('wlxml-class'); - } - this.manager = wlxmlManagers.getFor(this); - this.manager.setup(); - }, toggleLabel: function(toggle) { var displayCss = toggle ? 'inline-block' : 'none'; - var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label'); + var label = this.dom.children('.canvas-widgets').find('.canvas-widget-label'); label.css('display', displayCss); this.toggleHighlight(toggle); }, @@ -217,10 +137,29 @@ $.extend(DocumentNodeElement.prototype, { this._container().toggleClass('highlighted-element', toggle); }, - toggle: function(toggle) { - if(this.manager) { - this.manager.toggle(toggle); - } + isBlock: function() { + return this.dom.css('display') === 'block'; + }, + + displayAsBlock: function() { + this.dom.css('display', 'block'); + this._container().css('display', 'block'); + }, + displayInline: function() { + this.dom.css('display', 'inline'); + this._container().css('display', 'inline'); + }, + displayAs: function(what) { + // [this.dom(), this._container()].forEach(e) { + // var isBlock = window.getComputedStyle(e).display === 'block'; + // if(!isBlock && what === 'block') { + // e.css('display', what); + // } else if(isBlock && what === 'inline') { + // e.css('display') + // } + // }) + this.dom.css('display', what); + this._container().css('display', what); } }); @@ -240,21 +179,27 @@ DocumentTextElement.prototype = Object.create(DocumentElement.prototype); $.extend(DocumentTextElement.prototype, { createDOM: function() { - return $('
') + var dom = $('
') .attr('document-text-element', '') .text(this.wlxmlNode.getText() || utils.unicode.ZWS); + return dom; }, detach: function() { - this.dom().detach(); + this.dom.detach(); this.canvas = null; return this; }, setText: function(text) { - this.dom().contents()[0].data = text; + if(text === '') { + text = utils.unicode.ZWS; + } + if(text !== this.getText()) { + this.dom.contents()[0].data = text; + } }, getText: function(options) { options = _.extend({raw: false}, options || {}); - var toret = this.dom().text(); + var toret = this.dom.text(); if(!options.raw) { toret = toret.replace(utils.unicode.ZWS, ''); } @@ -262,7 +207,7 @@ $.extend(DocumentTextElement.prototype, { }, isEmpty: function() { // Having at least Zero Width Space is guaranteed be Content Observer - return this.dom().contents()[0].data === utils.unicode.ZWS; + return this.dom.contents()[0].data === utils.unicode.ZWS; }, after: function(params) { if(params instanceof DocumentTextElement || params.text) { @@ -274,9 +219,10 @@ $.extend(DocumentTextElement.prototype, { } else { element = this.canvas.createElement(params); } - this.dom().wrap('
'); - this.dom().parent().after(element.dom()); - this.dom().unwrap(); + this.dom.wrap('
'); + this.dom.parent().after(element.dom); + this.dom.unwrap(); + this.refreshPath(); return element; }, before: function(params) { @@ -289,17 +235,23 @@ $.extend(DocumentTextElement.prototype, { } else { element = this.canvas.createElement(params); } - this.dom().wrap('
'); - this.dom().parent().before(element.dom()); - this.dom().unwrap(); + 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 []; } + }); + return { DocumentElement: DocumentElement, DocumentNodeElement: DocumentNodeElement, diff --git a/src/editor/modules/documentCanvas/canvas/documentElement.less b/src/editor/modules/documentCanvas/canvas/documentElement.less new file mode 100644 index 0000000..43a32f5 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/documentElement.less @@ -0,0 +1,12 @@ +[document-node-element] { + .canvas-widgets { + display: inline; + text-indent: 0; + } + + .canvas-widget { + display: none; + } + + +} \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/elementsRegister.js b/src/editor/modules/documentCanvas/canvas/elementsRegister.js new file mode 100644 index 0000000..24a700a --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/elementsRegister.js @@ -0,0 +1,50 @@ +define(function(require) { + +'use strict'; +var _ = require('libs/underscore'), + wlxml = require('wlxml/wlxml'); + + +var ElementsRegister = function(BaseType) { + this._register = {}; + this.BaseType = BaseType; +}; + +_.extend(ElementsRegister.prototype, { + createCanvasElementType: function(elementPrototype) { + var register = this; + var Constructor = function() { + register.BaseType.apply(this, Array.prototype.slice.call(arguments, 0)); + }; + Constructor.prototype = elementPrototype; + return Constructor; + }, + register: function(params) { + params.klass = params.klass || ''; + params.prototype = params.prototype || Object.create({}); + + this._register[params.tag] = this._register[params.tag] || {}; + this._register[params.tag][params.klass] = this.createCanvasElementType(params.prototype); + }, + getElement: function(params) { + params.klass = params.klass || ''; + var Factory; + if(this._register[params.tag]) { + wlxml.getClassHierarchy(params.klass).reverse().some(function(klass) { + Factory = this._register[params.tag][klass]; + if(Factory) { + return true; + } + }.bind(this)); + } + if(!Factory) { + Factory = this.BaseType; + } + return Factory; + } +}); + + +return ElementsRegister; + +}); diff --git a/src/editor/modules/documentCanvas/canvas/elementsRegister.test.js b/src/editor/modules/documentCanvas/canvas/elementsRegister.test.js new file mode 100644 index 0000000..542e199 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/elementsRegister.test.js @@ -0,0 +1,40 @@ +define(function(require) { + +'use strict'; +/* globals describe, it */ + + +var ElementsRegister = require('./elementsRegister.js'), + documentElement = require('./documentElement.js'), + chai = require('libs/chai'); + +var expect = chai.expect; + +describe('Elements register', function() { + it('registers element for a tag', function() { + var register = new ElementsRegister(documentElement.DocumentNodeElement), + prototype = Object.create({}); + + register.register({ + tag: 'div', + prototype: prototype, + }); + var Element = register.getElement({tag: 'div'}); + expect(Element.prototype).to.equal(prototype); + }); + it('registers element for a class', function() { + var register = new ElementsRegister(documentElement.DocumentNodeElement), + prototype = Object.create({}); + + register.register({ + tag: 'div', + klass: 'a.b', + prototype: prototype, + }); + var Element = register.getElement({tag: 'div', klass: 'a.b.c'}); + expect(Element.prototype).to.equal(prototype); + }); +}); + + +}); diff --git a/src/editor/modules/documentCanvas/canvas/genericElement.js b/src/editor/modules/documentCanvas/canvas/genericElement.js new file mode 100644 index 0000000..4bdc524 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/genericElement.js @@ -0,0 +1,211 @@ +define(function(require) { + +'use strict'; + +var $ = require('libs/jquery'), + documentElement = require('./documentElement'), + utils = require('./utils'), + wlxmlUtils = require('utils/wlxml'); + +var labelWidget = function(tag, klass) { + return $('') + .addClass('canvas-widget canvas-widget-label') + .text(wlxmlUtils.getTagLabel(tag) + (klass ? ' / ' + wlxmlUtils.getClassLabel(klass) : '')); +}; +void(labelWidget); // for linters; labelWidget is unused on purpose for now + +var DocumentNodeElement = documentElement.DocumentNodeElement; + +var generic = Object.create(DocumentNodeElement.prototype); + +$.extend(generic, { + init: function() { + DocumentNodeElement.prototype.init.call(this); + this._container() + .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); + }.bind(this)); + this.refresh(); + }, + + refresh: function() { + if(this.wlxmlNode.getTagName() === 'span') { + if(this.containsBlock()) { + this.displayAsBlock(); + } else { + this.displayInline(); + } + } else { + this.displayAsBlock(); + } + }, + + getFirst: function(e1, e2) { + var idx1 = this.childIndex(e1), + idx2 = this.childIndex(e2); + if(e1 === null || e2 === null) { + return undefined; + } + return idx1 <= idx2 ? e1: e2; + }, + + children: function() { + var element = this, + toret = []; + this._container().contents().each(function() { + var childElement = element.canvas.getDocumentElement(this); + if(childElement === undefined) { + return true; + } + + toret.push(childElement); + }); + return toret; + }, + + getVerticallyFirstTextElement: function() { + var toret; + this.children().some(function(child) { + if(child instanceof documentElement.DocumentTextElement) { + toret = child; + return true; // break + } else { + toret = child.getVerticallyFirstTextElement(); + if(toret) { + return true; // break + } + } + }); + return toret; + }, + + onNodeAttrChange: function(event) { + if(event.meta.attr === 'class') { + this.setWlxmlClass(event.meta.newVal); // + } + }, + onNodeAdded: function(event) { + if(event.meta.node.isRoot()) { + this.canvas.reloadRoot(); + return; + } + + var nodeIndex = event.meta.node.getIndex(), + referenceElement, referenceAction, actionArg; + + if(nodeIndex === 0) { + referenceElement = this; + referenceAction = 'prepend'; + } else { + referenceElement = this.children()[nodeIndex-1]; + referenceAction = 'after'; + } + + if(event.type === 'nodeMoved') { + actionArg = utils.getElementForNode(event.meta.node, event.meta.parent); + } else { + actionArg = event.meta.node; + } + + referenceElement[referenceAction](actionArg); + }, + onNodeMoved: function(event) { + return this.onNodeAdded.call(this, event); + }, + onNodeDetached: function(event) { + if(event.meta.node.sameNode(this)) { + this.detach(); + } else { + this.children().some(function(child) { + if(child.wlxmlNode.sameNode(event.meta.node)) { + child.detach(); + return true; + } + }); + } + }, + onNodeTextChange: function(event) { + var toSet = event.meta.node.getText(); + this.children().some(function(child) { + if(child.wlxmlNode.sameNode(event.meta.node)) { + if(toSet === '') { + toSet = utils.unicode.ZWS; + } + if(toSet !== child.getText()) { + child.setText(toSet); + } + return true; + } + }); + }, + + + /// + + containsBlock: function() { + return this.children() + .filter(function(child) { + return child instanceof documentElement.DocumentNodeElement; + }) + .some(function(child) { + if(child.isBlock()) { + return true; + } else { + return child.containsBlock(); + } + }); + }, + + prepend: function(param) { + var element; + if(param instanceof documentElement.DocumentElement) { + element = param; + } else { + element = this.canvas.createElement(param); + } + this._container().prepend(element.dom); + this.refreshPath(); + return element; + }, + + childIndex: function(child) { + var children = this.children(), + toret = null; + children.forEach(function(c, idx) { + if(c.sameNode(child)) { + toret = idx; + return false; + } + }); + return toret; + }, + + getWlxmlClass: function() { + var klass = this._container().attr('wlxml-class'); + if(klass) { + return klass.replace(/-/g, '.'); + } + return undefined; + }, + setWlxmlClass: function(klass) { + if(klass === this.getWlxmlClass()) { + return; + } + if(klass) { + this._container().attr('wlxml-class', klass.replace(/\./g, '-')); + } + else { + this._container().removeAttr('wlxml-class'); + } + this.refreshPath(); + } +}); + + +return generic; + + + +}); \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/genericElement.less b/src/editor/modules/documentCanvas/canvas/genericElement.less new file mode 100644 index 0000000..c935a79 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/genericElement.less @@ -0,0 +1,16 @@ +.canvas-widget-label { + position: absolute; + display: none; + top: -20px; + left:0; + background-color: red; + color: white; + font-size:12px; + font-weight: bold; + padding: 1px 3px; + //width:300px; + opacity: 0.65; + font-family: monospace; + z-index:9999; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/keyboard.js b/src/editor/modules/documentCanvas/canvas/keyboard.js index 4cf3d77..18fcddb 100644 --- a/src/editor/modules/documentCanvas/canvas/keyboard.js +++ b/src/editor/modules/documentCanvas/canvas/keyboard.js @@ -61,7 +61,7 @@ handlers.push({key: KEYS.ENTER, description: gettext('Splitting text') }, success: function(ret) { - canvas.setCurrentElement(utils.findCanvasElement(ret), {caretTo: 'start'}); + canvas.setCurrentElement(utils.getElementForNode(ret), {caretTo: 'start'}); } }); @@ -86,7 +86,7 @@ handlers.push({key: KEYS.ENTER, description: gettext('Splitting text') }, success: function(ret) { - canvas.setCurrentElement(utils.findCanvasElement(ret), {caretTo: 'start'}); + canvas.setCurrentElement(utils.getElementForNode(ret), {caretTo: 'start'}); } }); @@ -133,7 +133,7 @@ handlers.push({key: KEYS.ENTER, gotoOptions = {caretTo: 'start'}; } - canvas.setCurrentElement(utils.findCanvasElement(goto), gotoOptions); + canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions); } } } @@ -152,7 +152,7 @@ handlers.push({keys: [KEYS.ARROW_UP, KEYS.ARROW_DOWN, KEYS.ARROW_LEFT, KEYS.ARRO direction = 'below'; caretTo = 'start'; } - var el = canvas.getDocumentElement(utils.nearestInDocumentOrder('[document-text-element]', direction, element.dom()[0])); + var el = canvas.getDocumentElement(utils.nearestInDocumentOrder('[document-text-element]', direction, element.dom[0])); if(el) { canvas.setCurrentElement(el, {caretTo: caretTo}); } @@ -226,7 +226,7 @@ handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE], if(direction === 'above') { if(start.offsetAtBeginning) { - goto = start.element.getNearestTextElement('above'); + goto = canvas.getNearestTextElement('above', start.element); caretTo = 'end'; } else { goto = start.element; @@ -234,7 +234,7 @@ handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE], } } else { if(end.offsetAtEnd) { - goto = start.element.getNearestTextElement('below'); + goto = canvas.getNearestTextElement('below', start.element); caretTo = 'start'; } else { goto = end.element; @@ -283,18 +283,18 @@ handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE], if(grandParent && grandParent.children().length === 1) { goto = grandParent.wlxmlNode.append({text: ''}); } else { - goto = element.getNearestTextElement(direction); + goto = canvas.getNearestTextElement(direction, element); } parent.wlxmlNode.detach(); } else { - goto = element.getNearestTextElement(direction); + goto = canvas.getNearestTextElement(direction, element); element.wlxmlNode.detach(); } canvas.setCurrentElement(goto, {caretTo: caretTo}); } else if(cursorAtOperationEdge) { if(direction === 'below') { - element = element.getNearestTextElement(direction); + element = canvas.getNearestTextElement(direction, element); } if(element) { goto = element.wlxmlNode.mergeContentUp(); diff --git a/src/editor/modules/documentCanvas/canvas/utils.js b/src/editor/modules/documentCanvas/canvas/utils.js index 56093f6..130d51c 100644 --- a/src/editor/modules/documentCanvas/canvas/utils.js +++ b/src/editor/modules/documentCanvas/canvas/utils.js @@ -20,46 +20,49 @@ var nearestInDocumentOrder = function(selector, direction, element) { return null; }; -var findCanvasElement = function(node, childOf) { - if(node.nodeType === Node.ELEMENT_NODE) { - return node.getData('canvasElement'); - } + +var getElementForNode = function(node, withParent) { if(node.nodeType === Node.TEXT_NODE) { - return findCanvasElementInParent(node, childOf || node.parent()); + return _getElementForTextNode(node, withParent); + } + while(!node.getData('canvasElement')) { + node = node.parent(); } + return node.getData('canvasElement'); }; -/** - * We take child and its parent as arguments separatly to - * handle situation where child was removed from WLXMLDocument - * and it lost reference to its parent (but we may still have it on canvas). -*/ -var findCanvasElementInParent = function(wlxmlChildNode, wlxmlParentNode) { - var parentElement, toret; - - if(wlxmlParentNode === null) { - toret = wlxmlChildNode.getData('canvasElement'); - if(toret.parent()) { - throw new Error('This should never happen: root canvas element doesn\'t render root document node!'); +var _getElementForTextNode = function(textNode, withParent) { + var parentElement = getElementForNode(withParent || textNode.parent()), + toret; + parentElement.children().some(function(child) { + if(child.wlxmlNode.sameNode(textNode)) { + toret = child; + return true; } - } else { - parentElement = findCanvasElement(wlxmlParentNode); - parentElement.children().forEach(function(child) { - if(child.wlxmlNode.sameNode(wlxmlChildNode)) { // czemu tu, przy drugim undo child nie mial data? - toret = child; - } - }); - } + }); return toret; }; +var getElementForDetachedNode = function(node, originalParent) { + var ptr = originalParent; + if(ptr === null) { + return node.getData('canvasElement'); + } + while(!ptr.getData('canvasElement')) { + ptr = ptr.parent(); + } + return ptr.getData('canvasElement'); +}; + + + return { nearestInDocumentOrder: nearestInDocumentOrder, - findCanvasElement: findCanvasElement, - findCanvasElementInParent: findCanvasElementInParent, unicode: { ZWS: '\u200B' - } + }, + getElementForNode: getElementForNode, + getElementForDetachedNode: getElementForDetachedNode }; }); diff --git a/src/editor/modules/documentCanvas/canvas/widgets.js b/src/editor/modules/documentCanvas/canvas/widgets.js deleted file mode 100644 index aecb199..0000000 --- a/src/editor/modules/documentCanvas/canvas/widgets.js +++ /dev/null @@ -1,65 +0,0 @@ -define([ -'libs/jquery', -'utils/wlxml' -], function($, wlxmlUtils) { - -'use strict'; - -return { - labelWidget: function(tag, klass) { - return $('') - .addClass('canvas-widget canvas-widget-label') - .text(wlxmlUtils.getTagLabel(tag) + (klass ? ' / ' + wlxmlUtils.getClassLabel(klass) : '')); - }, - - footnoteHandler: function(clickHandler) { - var mydom = $('') - .addClass('canvas-widget canvas-widget-footnote-handle') - .css('display', 'inline') - .show(); - - mydom.click(function(e) { - e.stopPropagation(); - clickHandler(); - }); - - return mydom; - }, - - hideButton: function(clickHandler) { - var mydom = $('x') - .addClass('canvas-widget canvas-widget-hide-button'); - mydom.click(function(e) { - e.stopPropagation(); - clickHandler(); - }); - return mydom; - }, - - commentAdnotation: function(node) { - var widget = { - DOM: $('
').addClass('canvas-widget canvas-widget-comment-adnotation'), - update: function(node) { - var parts = [], - metadata = node.getMetadata(), - dt; - metadata.forEach(function(row) { - parts.push(row.getValue()); - }, 'creator'); - metadata.some(function(row) { - dt = row.getValue(); - return true; // break - }, 'date'); - if(dt) { - parts.push(dt); - } - this.DOM.text(parts.join(', ')); - } - }; - widget.update(node); - return widget; - } - -}; - -}); \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/widgets.less b/src/editor/modules/documentCanvas/canvas/widgets.less deleted file mode 100644 index 442af99..0000000 --- a/src/editor/modules/documentCanvas/canvas/widgets.less +++ /dev/null @@ -1,69 +0,0 @@ -[document-node-element] { - .canvas-widgets { - display: inline; - text-indent: 0; - } - - .canvas-widget { - display: none; - } - - .canvas-widget-label { - position: absolute; - display: none; - top: -20px; - left:0; - background-color: red; - color: white; - font-size:12px; - font-weight: bold; - padding: 1px 3px; - //width:300px; - opacity: 0.65; - font-family: monospace; - z-index:9999; - white-space: nowrap; - } - - - - .canvas-widget-footnote-handle { - display: inline; - outline: 0px solid transparent; - cursor: pointer; - counter-increment: footnote; - vertical-align: super; - color: blue; - font-size: .8em; - - &::before, { - content: "[" counter(footnote) "]"; - } - } - - .canvas-widget-hide-button { - @line_height: 12px; - @padding: 2px; - @temporary_footnote_hack: 10px; - - position: absolute; - top: -(@line_height + 2 * @padding) + @temporary_footnote_hack; - right: 0; - line-height: @line_height; - padding: @padding; - font-weight: bold; - color: white; - background-image: linear-gradient(to bottom,#ee5f5b,#bd362f); - border-radius: 1px; - cursor: pointer; - } - - .canvas-widget-comment-adnotation { - position:absolute; - top: 10px; - right:10px; - font-size: 10px; - color: lighten(@black, 10%); - z-index:1000; - } -} \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js index 7d19e94..760208f 100644 --- a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js +++ b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js @@ -35,54 +35,51 @@ $.extend(Listener.prototype, { var _metadataEventHandler = function(event) { - var canvasNode = utils.findCanvasElement(event.meta.node); - canvasNode.exec('updateMetadata'); + var element = utils.getElementForNode(event.meta.node); + element.handle(event); }; + var handlers = { nodeAttrChange: function(event) { + var element = utils.getElementForNode(event.meta.node), + newElement; if(event.meta.attr === 'class') { - var canvasNode = utils.findCanvasElement(event.meta.node); - canvasNode.setWlxmlClass(event.meta.newVal); + if(element.wlxmlNode.getClass() !== event.meta.attr) { + if(event.meta.node.isRoot()) { + this.canvas.reloadRoot(); + } else { + newElement = this.canvas.createElement(event.meta.node); + element.dom.replaceWith(newElement.dom); + } + } + + } else { + element.handle(event); } }, - nodeAdded: function(event, checkForExistence) { + nodeAdded: function(event) { if(event.meta.node.isRoot()) { this.canvas.reloadRoot(); return; } - var parentElement = utils.findCanvasElement(event.meta.node.parent()), - nodeIndex = event.meta.node.getIndex(), - referenceElement, referenceAction, actionArg; - if(nodeIndex === 0) { - referenceElement = parentElement; - referenceAction = 'prepend'; - } else { - referenceElement = parentElement.children()[nodeIndex-1]; - referenceAction = 'after'; - } + var containingNode = event.meta.node.parent(), + containingElement = utils.getElementForNode(containingNode); - actionArg = (checkForExistence && utils.findCanvasElement(event.meta.node, event.meta.parent)) || event.meta.node; - referenceElement[referenceAction](actionArg); + containingElement.handle(event); }, nodeMoved: function(event) { - return handlers.nodeAdded.call(this, event, true); + return handlers.nodeAdded.call(this, event); // + // }, nodeDetached: function(event) { - var canvasNode = utils.findCanvasElementInParent(event.meta.node, event.meta.parent); - canvasNode.detach(); + var element = utils.getElementForDetachedNode(event.meta.node, event.meta.parent); + element.handle(event); }, nodeTextChange: function(event) { - //console.log('wlxmlListener: ' + event.meta.node.getText()); - var canvasElement = utils.findCanvasElement(event.meta.node), - toSet = event.meta.node.getText(); - if(toSet === '') { - toSet = utils.unicode.ZWS; - } - if(toSet !== canvasElement.getText()) { - canvasElement.setText(toSet); - } + var element = utils.getElementForNode(event.meta.node); + element.setText(event.meta.node.getText()); }, metadataChanged: _metadataEventHandler, diff --git a/src/editor/modules/documentCanvas/canvas/wlxmlManagers.js b/src/editor/modules/documentCanvas/canvas/wlxmlManagers.js deleted file mode 100644 index 6b8b5ff..0000000 --- a/src/editor/modules/documentCanvas/canvas/wlxmlManagers.js +++ /dev/null @@ -1,177 +0,0 @@ -define([ -'libs/jquery', -'modules/documentCanvas/canvas/widgets' -], function($, widgets) { - -'use strict'; - - -var DocumentElementWrapper = function(documentElement) { - - this.documentElement = documentElement; - - this.addWidget = function(widget) { - documentElement.dom().children('.canvas-widgets').append(widget.DOM ? widget.DOM : widget); - }; - - this.clearWidgets = function() { - documentElement.dom().children('.canvas-widgets').empty(); - }; - - this.setDisplayStyle = function(displayStyle) { - documentElement.dom().css('display', displayStyle || ''); - documentElement._container().css('display', displayStyle || ''); - }; - - this.tag = function() { - return documentElement.getWlxmlTag(); - }; - - this.klass = function() { - return documentElement.getWlxmlClass(); - }; - - this.toggle = function(toggle) { - documentElement._container().toggle(toggle); - }; - - var eventBus = documentElement.canvas ? documentElement.canvas.eventBus : - {trigger: function() {}}; - this.trigger = function() { - eventBus.trigger.apply(eventBus, arguments); - }; - - this.node = documentElement.wlxmlNode; -}; - -var getDisplayStyle = function(tag, klass) { - if(tag === 'metadata') { - return 'none'; - } - if(tag === 'span') { - return 'inline'; - } - if(klass && klass.substr(0, 4) === 'item') { - return null; - } - if(klass === 'gap') { - return 'inline'; - } - return 'block'; -}; - -var GenericManager = function(wlxmlElement) { - this.el = wlxmlElement; -}; - -$.extend(GenericManager.prototype, { - setup: function() { - this.el.setDisplayStyle(getDisplayStyle(this.el.tag(), this.el.klass())); - - this.el.clearWidgets(); - }, - toggle: function(toggle) { - this.el.toggle(toggle); - } - -}); - -var managers = { - _m: {}, - set: function(tag, klass, manager) { - if(!this._m[tag]) { - this._m[tag] = {}; - } - this._m[tag][klass] = manager; - }, - get: function(tag,klass) { - if(this._m[tag] && this._m[tag][klass]) { - return this._m[tag][klass]; - } - return GenericManager; - } -}; - -var FootnoteManager = function(wlxmlElement) { - this.el = wlxmlElement; -}; -$.extend(FootnoteManager.prototype, { - setup: function() { - this.el.clearWidgets(); - - var clickHandler = function() { - this.toggle(true); - }.bind(this); - this.footnoteHandler = widgets.footnoteHandler(clickHandler); - this.el.addWidget(this.footnoteHandler); - - var closeHandler = function() { - this.toggle(false); - - }.bind(this); - this.hideButton = widgets.hideButton(closeHandler); - this.el.addWidget(this.hideButton); - - this.toggle(false, {silent: true}); - }, - toggle: function(toggle, options) { - options = options || {}; - this.hideButton.toggle(toggle); - this.footnoteHandler.toggle(!toggle); - - this.el.setDisplayStyle(toggle ? 'block' : 'inline'); - this.el.toggle(toggle); - if(!options.silent) { - this.el.trigger('elementToggled', toggle, this.el.documentElement); - } - } -}); -managers.set('aside', 'footnote', FootnoteManager); - - -var ListItemManager = function(wlxmlElement) { - this.el = wlxmlElement; -}; -$.extend(ListItemManager.prototype, { - setup: function() { - this.el.clearWidgets(); - }, - toggleBullet: function(toggle) { - this.el.documentElement._container().css({display : toggle ? 'list-item' : 'block'}); - } -}); -managers.set('div', 'item', ListItemManager); - - -var CommentManager = function(wlxmlElement) { - this.el = wlxmlElement; -}; - -$.extend(CommentManager.prototype, { - setup: function() { - this.el.clearWidgets(); - - this.widget = widgets.commentAdnotation(this.el.node); - this.el.addWidget(this.widget); - this.widget.DOM.show(); - }, - updateMetadata: function() { - // var parts = []; - // this.el.node.getMetadata().forEach(function(row) { - // parts.push(row.getValue()); - // }, 'creator'); - // this.widget.text(parts.join(', ')); - this.widget.update(this.el.node); - } -}); -managers.set('aside', 'comment', CommentManager); - -return { - getFor: function(documentElement) { - var wlxmlElement = new DocumentElementWrapper(documentElement); - return new (managers.get(wlxmlElement.tag(), wlxmlElement.klass()))(wlxmlElement); - - } -}; - -}); \ No newline at end of file diff --git a/src/editor/modules/documentCanvas/documentCanvas.js b/src/editor/modules/documentCanvas/documentCanvas.js index 9a88b75..2cb3fa1 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.js +++ b/src/editor/modules/documentCanvas/documentCanvas.js @@ -14,7 +14,13 @@ var logger = logging.getLogger('documentCanvas'); return function(sandbox) { - var canvas = canvas3.fromXMLDocument(null); + var canvasElements = []; + + sandbox.getPlugins().forEach(function(plugin) { + canvasElements = canvasElements.concat(plugin.canvasElements || []); + }); + + var canvas = canvas3.fromXMLDocument(null, canvasElements); var canvasWrapper = $(template); var shownAlready = false; var scrollbarPosition = 0, diff --git a/src/editor/modules/documentCanvas/documentCanvas.less b/src/editor/modules/documentCanvas/documentCanvas.less index 9421482..bd39b6a 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.less +++ b/src/editor/modules/documentCanvas/documentCanvas.less @@ -1,5 +1,6 @@ @import 'nodes.less'; -@import 'canvas/widgets.less'; +@import 'canvas/documentElement.less'; +@import 'canvas/genericElement.less'; #rng-module-documentCanvas { height: 100%; diff --git a/src/editor/plugins/core/canvasElements.js b/src/editor/plugins/core/canvasElements.js new file mode 100644 index 0000000..addaeae --- /dev/null +++ b/src/editor/plugins/core/canvasElements.js @@ -0,0 +1,118 @@ +define(function(require) { + +'use strict'; +var $ = require('libs/jquery'), + genericElement = require('modules/documentCanvas/canvas/genericElement'); // TODO: This should be accessible via plugin infrastructure + + +var widgets = { + footnoteHandler: function(clickHandler) { + var mydom = $('') + .addClass('canvas-widget canvas-widget-footnote-handle') + .css('display', 'inline') + .show(); + + mydom.click(function(e) { + e.stopPropagation(); + clickHandler(); + }); + + return mydom; + }, + commentAdnotation: function(node) { + var widget = { + DOM: $('
').addClass('canvas-widget canvas-widget-comment-adnotation'), + update: function(node) { + var parts = [], + metadata = node.getMetadata(), + dt; + metadata.forEach(function(row) { + parts.push(row.getValue()); + }, 'creator'); + metadata.some(function(row) { + dt = row.getValue(); + return true; // break + }, 'date'); + if(dt) { + parts.push(dt); + } + this.DOM.text(parts.join(', ')); + } + }; + widget.update(node); + return widget; + }, + hideButton: function(clickHandler) { + var mydom = $('x') + .addClass('canvas-widget canvas-widget-hide-button'); + mydom.click(function(e) { + e.stopPropagation(); + clickHandler(); + }); + return mydom; + } +}; + + +var comment = Object.create(genericElement); +$.extend(comment, { + init: function() { + genericElement.init.call(this); + this.commentAdnotation = widgets.commentAdnotation(this.wlxmlNode); + this.addWidget(this.commentAdnotation, 'show'); + this.commentAdnotation.DOM.show(); + }, + + onMetadataChanged: function(event) { + this.commentAdnotation.update(event.meta.node); + }, + onMetadataAdded: function(event) { + return this.onMetadataChanged(event); + }, + onMetadataRemoved: function(event) { + return this.onMetadataChanged(event); + } +}); + +var footnote = Object.create(genericElement); +$.extend(footnote, { + init: function() { + genericElement.init.call(this); + var clickHandler = function() { + this.toggle(true); + }.bind(this); + this.footnoteHandler = widgets.footnoteHandler(clickHandler); + this.addWidget(this.footnoteHandler); + + var closeHandler = function() { + this.toggle(false); + }.bind(this); + this.hideButton = widgets.hideButton(closeHandler); + this.addWidget(this.hideButton); + this.toggle(false, {silent: true}); + }, + toggle: function(toggle, options) { + options = options || {}; + this.hideButton.toggle(toggle); + this.footnoteHandler.toggle(!toggle); + + if(toggle) { + this.displayAsBlock(); + } else { + this.displayInline(); + } + this._container().toggle(toggle); + if(!options.silent) { + this.trigger('elementToggled', toggle, this); + } + } +}); + + +return [ + {tag: 'aside', klass: 'comment', prototype: comment}, + {tag: 'aside', klass: 'footnote', prototype: footnote} +]; + + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/canvasElements.less b/src/editor/plugins/core/canvasElements.less new file mode 100644 index 0000000..6003476 --- /dev/null +++ b/src/editor/plugins/core/canvasElements.less @@ -0,0 +1,39 @@ +.canvas-widget-footnote-handle { + display: inline; + outline: 0px solid transparent; + cursor: pointer; + counter-increment: footnote; + vertical-align: super; + color: blue; + font-size: .8em; + + &::before, { + content: "[" counter(footnote) "]"; + } +} + +.canvas-widget-hide-button { + @line_height: 12px; + @padding: 2px; + @temporary_footnote_hack: 10px; + + position: absolute; + top: -(@line_height + 2 * @padding) + @temporary_footnote_hack; + right: 0; + line-height: @line_height; + padding: @padding; + font-weight: bold; + color: white; + background-image: linear-gradient(to bottom,#ee5f5b,#bd362f); + border-radius: 1px; + cursor: pointer; +} + +.canvas-widget-comment-adnotation { + position:absolute; + top: 10px; + right:10px; + font-size: 10px; + color: lighten(@black, 10%); + z-index:1000; +} \ No newline at end of file diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index 38675fb..150464e 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -9,7 +9,9 @@ var _ = require('libs/underscore'), switchTo = require('plugins/core/switch'), lists = require('plugins/core/lists'), plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}}}, - Dialog = require('views/dialog/dialog'); + Dialog = require('views/dialog/dialog'), + canvasElements = require('plugins/core/canvasElements'); + plugin.documentExtension.textNode.transformations = { @@ -374,6 +376,8 @@ plugin.config = function(config) { templates.actions[0].params.template.options = config.templates; }; +plugin.canvasElements = canvasElements; + return plugin; }); \ No newline at end of file diff --git a/src/editor/plugins/core/core.less b/src/editor/plugins/core/core.less new file mode 100644 index 0000000..0754e03 --- /dev/null +++ b/src/editor/plugins/core/core.less @@ -0,0 +1 @@ +@import 'canvasElements.less'; \ No newline at end of file diff --git a/src/editor/plugins/plugins.less b/src/editor/plugins/plugins.less new file mode 100644 index 0000000..a6ba84b --- /dev/null +++ b/src/editor/plugins/plugins.less @@ -0,0 +1 @@ +@import 'core/core.less'; \ No newline at end of file diff --git a/src/editor/styles/main.less b/src/editor/styles/main.less index 411a2aa..f9ae798 100644 --- a/src/editor/styles/main.less +++ b/src/editor/styles/main.less @@ -2,6 +2,7 @@ @import 'mixins.less'; @import 'common.less'; +@import '../plugins/plugins.less'; @import '../views/openSelect/openSelect.less'; @import '../views/dialog/dialog.less'; @import '../modules/rng/rng.less'; diff --git a/src/wlxml/wlxml.js b/src/wlxml/wlxml.js index 5194c19..0505116 100644 --- a/src/wlxml/wlxml.js +++ b/src/wlxml/wlxml.js @@ -72,6 +72,9 @@ $.extend(WLXMLElementNode.prototype, WLXMLDocumentNodeMethods, smartxml.ElementN getClass: function() { return this.getAttr('class') || ''; }, + getClassHierarchy: function() { + return getClassLists(this.getClass()); + }, setClass: function(klass) { if(klass !== this.klass) { installObject(this, klass); @@ -384,7 +387,8 @@ return { return this.WLXMLDocumentFromXML(xml).root; }, - WLXMLDocument: WLXMLDocument + WLXMLDocument: WLXMLDocument, + getClassHierarchy: getClassLists }; }); \ No newline at end of file diff --git a/src/wlxml/wlxml.test.js b/src/wlxml/wlxml.test.js index c37b21f..9904154 100644 --- a/src/wlxml/wlxml.test.js +++ b/src/wlxml/wlxml.test.js @@ -27,6 +27,11 @@ describe('WLXMLDocument', function() { expect(node.getClass()).to.equal('class.subclass'); }); + it('returns its class hierarchy', function() { + var node = nodeFromXML('
'); + expect(node.getClassHierarchy()).to.eql(['', 'a', 'a.b', 'a.b.c']); + }); + it('returns unregistered attributes', function() { var testClasses = { 'testClass': {