Merge branch 'objects' implementing canvas objects extending ability
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Tue, 6 May 2014 10:05:47 +0000 (12:05 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Tue, 6 May 2014 10:05:47 +0000 (12:05 +0200)
24 files changed:
src/editor/modules/documentCanvas/canvas/canvas.js
src/editor/modules/documentCanvas/canvas/canvas.test.js
src/editor/modules/documentCanvas/canvas/documentElement.js
src/editor/modules/documentCanvas/canvas/documentElement.less [new file with mode: 0644]
src/editor/modules/documentCanvas/canvas/elementsRegister.js [new file with mode: 0644]
src/editor/modules/documentCanvas/canvas/elementsRegister.test.js [new file with mode: 0644]
src/editor/modules/documentCanvas/canvas/genericElement.js [new file with mode: 0644]
src/editor/modules/documentCanvas/canvas/genericElement.less [new file with mode: 0644]
src/editor/modules/documentCanvas/canvas/keyboard.js
src/editor/modules/documentCanvas/canvas/utils.js
src/editor/modules/documentCanvas/canvas/widgets.js [deleted file]
src/editor/modules/documentCanvas/canvas/widgets.less [deleted file]
src/editor/modules/documentCanvas/canvas/wlxmlListener.js
src/editor/modules/documentCanvas/canvas/wlxmlManagers.js [deleted file]
src/editor/modules/documentCanvas/documentCanvas.js
src/editor/modules/documentCanvas/documentCanvas.less
src/editor/plugins/core/canvasElements.js [new file with mode: 0644]
src/editor/plugins/core/canvasElements.less [new file with mode: 0644]
src/editor/plugins/core/core.js
src/editor/plugins/core/core.less [new file with mode: 0644]
src/editor/plugins/plugins.less [new file with mode: 0644]
src/editor/styles/main.less
src/wlxml/wlxml.js
src/wlxml/wlxml.test.js

index d58ea7b..ff2b31e 100644 (file)
@@ -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 = $('<div>').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);
     }
 };
 
index 346a82b..f64754f 100644 (file)
@@ -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('<section>Alice</section>'),
+            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('<section>Alice <span>has</span> a cat!</div>'),
@@ -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('<section><span>Alice</span></section>'),
+            spanElement = c.doc().children()[0];
+        expect(spanElement.isBlock()).to.equal(false);
+    });
+    it('renders non-span element as a block', function() {
+        var c = getCanvasFromXML('<section><span></span></section>'),
+            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('<section><span>Alice <div>has</div> a cat!</span></section>'),
+            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('<section><div><div></div></div></section>'),
+            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('<section></section>');
+        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('<section></section>');
+        c.wlxmlDocument.root.setAttr('class', 'test');
+        expect(c.doc().wlxmlNode.getClass()).to.equal('test');
+    });
+
+    it('handles detached node', function() {
+        var c = getCanvasFromXML('<section><div></div></section>');
+        c.wlxmlDocument.root.contents()[0].detach();
+        expect(c.doc().children().length).to.equal(0);
+    });
+
+    it('handles moved node', function() {
+        var doc = getDocumentFromXML('<section><a></a><b></b></section>'),
+            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('<section>Alice<div><span>has</span></div>a cat.</section>'),
+            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('<section>Alice</section>');
+        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('<test></test>');
+                }
+            }),
+            c = getCanvasFromXML('<section><div class="testClass"></div></section>', [
+            {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 = $('<h1>');
+                    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('<section><div class="testClass"><a></a></div></section>', [
+            {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 <a>');
+
+        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('<section><div class="unknown">Hi!</div></section>');
+            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('<section>Alice has a cat</section>'),
-            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('<section><span>Alice</span><span>has a cat</span><div>abc<span>...</span>cde</div></section>'),
-            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('<section>Alice <span>has</span> a <span>big</span> cat</section>'),
-            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('<section>Alice <span>has</span> a <span>big</span> cat</section>'),
-            dom = c.doc().dom(),
+            dom = c.doc().dom,
             text = {
                 alice: findTextNode(dom, 'Alice '),
                 has: findTextNode(dom, 'has'),
index 27160e7..3073e72 100644 (file)
@@ -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 = $('<div>')
-                .attr('document-node-element', ''),
+        var wrapper = $('<div>').attr('document-node-element', ''),
             widgetsContainer = $('<div>')
                 .addClass('canvas-widgets')
                 .attr('contenteditable', false),
-            container = $('<div>')
+            contentContainer = $('<div>')
                 .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 $('<div>')
+        var dom = $('<div>')
             .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('<div>');
-        this.dom().parent().after(element.dom());
-        this.dom().unwrap();
+        this.dom.wrap('<div>');
+        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('<div>');
-        this.dom().parent().before(element.dom());
-        this.dom().unwrap();
+        this.dom.wrap('<div>');
+        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 (file)
index 0000000..43a32f5
--- /dev/null
@@ -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 (file)
index 0000000..24a700a
--- /dev/null
@@ -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 (file)
index 0000000..542e199
--- /dev/null
@@ -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 (file)
index 0000000..4bdc524
--- /dev/null
@@ -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 $('<span>')
+        .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 (file)
index 0000000..c935a79
--- /dev/null
@@ -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
index 4cf3d77..18fcddb 100644 (file)
@@ -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();
index 56093f6..130d51c 100644 (file)
@@ -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 (file)
index aecb199..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-define([
-'libs/jquery',
-'utils/wlxml'
-], function($, wlxmlUtils) {
-    
-'use strict';
-
-return {
-    labelWidget: function(tag, klass) {
-        return $('<span>')
-            .addClass('canvas-widget canvas-widget-label')
-            .text(wlxmlUtils.getTagLabel(tag) + (klass ? ' / ' + wlxmlUtils.getClassLabel(klass) : ''));
-    },
-
-    footnoteHandler: function(clickHandler) {
-        var mydom = $('<span>')
-            .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 = $('<span>x</span>')
-            .addClass('canvas-widget canvas-widget-hide-button');
-        mydom.click(function(e) {
-            e.stopPropagation();
-            clickHandler();
-        });
-        return mydom;
-    },
-
-    commentAdnotation: function(node) {
-        var widget = {
-            DOM: $('<div>').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 (file)
index 442af99..0000000
+++ /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
index 7d19e94..760208f 100644 (file)
@@ -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 (file)
index 6b8b5ff..0000000
+++ /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
index 9a88b75..2cb3fa1 100644 (file)
@@ -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,
index 9421482..bd39b6a 100644 (file)
@@ -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 (file)
index 0000000..addaeae
--- /dev/null
@@ -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 = $('<span>')
+            .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: $('<div>').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 = $('<span>x</span>')
+            .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 (file)
index 0000000..6003476
--- /dev/null
@@ -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
index 38675fb..150464e 100644 (file)
@@ -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 (file)
index 0000000..0754e03
--- /dev/null
@@ -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 (file)
index 0000000..a6ba84b
--- /dev/null
@@ -0,0 +1 @@
+@import 'core/core.less';
\ No newline at end of file
index 411a2aa..f9ae798 100644 (file)
@@ -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';
index 5194c19..0505116 100644 (file)
@@ -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
index c37b21f..9904154 100644 (file)
@@ -27,6 +27,11 @@ describe('WLXMLDocument', function() {
             expect(node.getClass()).to.equal('class.subclass');
         });
 
+        it('returns its class hierarchy', function() {
+            var node = nodeFromXML('<div class="a.b.c"></div>');
+            expect(node.getClassHierarchy()).to.eql(['', 'a', 'a.b', 'a.b.c']);
+        });
+
         it('returns unregistered attributes', function() {
             var testClasses = {
                     'testClass': {