'libs/underscore',
'libs/backbone',
'fnpjs/logging/logging',
+'views/menu/menu',
'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',
+'modules/documentCanvas/canvas/nullElement',
+'modules/documentCanvas/canvas/gutter',
+'modules/documentCanvas/canvas/selection',
+'modules/documentCanvas/canvas/keyEvent',
+'libs/text!./canvas.html'
+], function($, _, Backbone, logging, Menu, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement, nullElement, gutter, selection, keyEvent, canvasTemplate) {
+
'use strict';
-/* global document:false, window:false, Node:false */
+/* global document:false, window:false, Node:false, gettext */
var logger = logging.getLogger('canvas');
},
setText: function(text, node) {
//this.canvas.wlxmlDocument.transform('setText', {node:node, text: text});
- node.setText(text);
+ node.document.transaction(function() {
+ node.setText(text);
+ }, {
+ metadata:{
+ description: gettext('Changing text')
+ }
+ });
}
});
-var Canvas = function(wlxmlDocument) {
+var Canvas = function(wlxmlDocument, elements, metadata, sandbox) {
+ this.metadata = metadata || {};
+ this.sandbox = sandbox;
+ this.elementsRegister = this.createElementsRegister();
+
+ 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.dom = $(canvasTemplate);
+ this.rootWrapper = this.dom.find('.root-wrapper');
+
+
+ this.gutter = gutter.create();
+ this.gutterView = new gutter.GutterView(this.gutter);
+ this.dom.find('.view-row').append(this.gutterView.dom);
+
this.wlxmlListener = wlxmlListener.create(this);
this.loadWlxmlDocument(wlxmlDocument);
this.setupEventHandling();
$.extend(Canvas.prototype, Backbone.Events, {
+ createElementType: function(elementPrototype) {
+ /* TODO: reconcile this with ElementsRegister behavior */
+ var Constructor = function() {
+ documentElement.DocumentNodeElement.apply(this, Array.prototype.slice.call(arguments, 0));
+ };
+ Constructor.prototype = elementPrototype;
+ return Constructor;
+ },
+
+ getElementOffset: function(element) {
+ return element.dom.offset().top - this.dom.offset().top;
+ },
+
loadWlxmlDocument: function(wlxmlDocument) {
if(!wlxmlDocument) {
return false;
this.reloadRoot();
},
- createElement: function(wlxmlNode) {
- var Factory = wlxmlNode.nodeType === Node.TEXT_NODE ? documentElement.DocumentTextElement : documentElement.DocumentNodeElement;
- return new Factory(wlxmlNode, this);
+ createElement: function(wlxmlNode, register, useRoot) {
+ var Factory;
+ register = register || this.elementsRegister;
+ if(wlxmlNode.nodeType === Node.TEXT_NODE) {
+ Factory = documentElement.DocumentTextElement;
+ } else {
+ Factory = register.getElement({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()});
+ }
+ if(!Factory && useRoot) {
+ Factory = this.elementsRegister.getElement({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()});
+ if(!Factory) {
+ Factory = documentElement.DocumentNodeElement;
+ }
+ }
+
+ if(Factory) {
+ return new Factory(wlxmlNode, this);
+ }
+ },
+
+ createElementsRegister: function() {
+ return new ElementsRegister(documentElement.DocumentNodeElement, nullElement);
},
getDocumentElement: function(htmlElement) {
//return DocumentTextElement.fromHTMLElement(htmlElement, canvas);
return $element.data('canvas-element');
}
+
+ /* experimental */
+ if($element.parent().data('canvas-element')) {
+ return $element.parent().data('canvas-element');
+ }
},
reloadRoot: function() {
+ if(this.rootElement) {
+ this.rootElement.detach();
+ }
this.rootElement = this.createElement(this.wlxmlDocument.root);
- this.wrapper.empty();
- this.wrapper.append(this.rootElement.dom());
+ this.rootWrapper.append(this.rootElement.dom);
+ },
+
+ triggerKeyEvent: function(keyEvent, selection) {
+ selection = selection || this.getSelection();
+ if(selection && (
+ (selection.type === 'caret' || selection.type === 'textSelection') && selection.toDocumentFragment().isValid()
+ || selection.type == 'nodeSelection')) {
+ keyboard.handleKeyEvent(keyEvent, selection);
+ }
+ },
+
+ createAction: function(fqName, config) {
+ return this.sandbox.createAction(fqName, config);
},
setupEventHandling: function() {
var canvas = this;
- this.wrapper.on('keyup keydown keypress', function(e) {
- keyboard.handleKey(e, canvas);
+ /* globals document */
+ $(document.body).on('keydown', function(e) {
+ canvas.triggerKeyEvent(keyEvent.fromNativeEvent(e));
});
- this.wrapper.on('mouseup', function() {
+ this.rootWrapper.on('mouseup', function() {
canvas.triggerSelectionChanged();
});
var mouseDown;
- this.wrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) {
+ this.rootWrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) {
mouseDown = e.target;
+ canvas.rootWrapper.find('[contenteditable]').attr('contenteditable', null);
});
- this.wrapper.on('click', '[document-node-element], [document-text-element]', function(e) {
+ this.rootWrapper.on('click', '[document-node-element], [document-text-element]', function(e) {
+ var position, element;
e.stopPropagation();
if(e.originalEvent.detail === 3) {
e.preventDefault();
canvas._moveCaretToTextElement(canvas.getDocumentElement(e.currentTarget), 'whole');
} else {
if(mouseDown === e.target) {
- canvas.setCurrentElement(canvas.getDocumentElement(e.currentTarget), {caretTo: false});
+ element = canvas.getDocumentElement(e.target);
+ if(element && element.wlxmlNode.nodeType === Node.ELEMENT_NODE) {
+ if(element.getVerticallyFirstTextElement && !element.getVerticallyFirstTextElement({considerChildren: false})) {
+ canvas.setCurrentElement(element);
+ return;
+ }
+ }
+ if(window.getSelection().isCollapsed) {
+ position = utils.caretPositionFromPoint(e.clientX, e.clientY);
+ canvas.setCurrentElement(canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
+ }
}
}
});
- this.wrapper.on('paste', function(e) {
+ this.rootWrapper.on('contextmenu', function(e) {
+ var el = canvas.getDocumentElement(e.target);
+
+ if(!el) {
+ return;
+ }
+
+ e.preventDefault();
+ this.showContextMenu(el, {x: e.clientX, y: e.clientY});
+ }.bind(this));
+
+ this.rootWrapper.on('paste', function(e) {
e.preventDefault();
var clipboardData = e.originalEvent.clipboardData;
/* globals MutationObserver */
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
- if(documentElement.DocumentTextElement.isContentContainer(mutation.target)) {
+ if(canvas.dom[0].contains(mutation.target) && documentElement.DocumentTextElement.isContentContainer(mutation.target)) {
observer.disconnect();
if(mutation.target.data === '') {
mutation.target.data = utils.unicode.ZWS;
mutation.target.data = mutation.target.data.replace(utils.unicode.ZWS, '');
canvas._moveCaretToTextElement(canvas.getDocumentElement(mutation.target), 'end');
}
- observer.observe(canvas.wrapper[0], config);
+ observer.observe(canvas.dom[0], config);
var textElement = canvas.getDocumentElement(mutation.target),
toSet = mutation.target.data !== utils.unicode.ZWS ? mutation.target.data : '';
});
});
var config = { attributes: false, childList: false, characterData: true, subtree: true, characterDataOldValue: true};
- observer.observe(this.wrapper[0], config);
+ observer.observe(this.rootWrapper[0], config);
- this.wrapper.on('mouseover', '[document-node-element], [document-text-element]', function(e) {
- var el = canvas.getDocumentElement(e.currentTarget);
- if(!el) {
- return;
- }
- e.stopPropagation();
- if(el instanceof documentElement.DocumentTextElement) {
- el = el.parent();
- }
- el.toggleLabel(true);
- });
- this.wrapper.on('mouseout', '[document-node-element], [document-text-element]', function(e) {
- var el = canvas.getDocumentElement(e.currentTarget);
+ var hoverHandler = function(e) {
+ var el = canvas.getDocumentElement(e.currentTarget),
+ expose = {
+ mouseover: true,
+ mouseout: false
+ };
if(!el) {
return;
}
if(el instanceof documentElement.DocumentTextElement) {
el = el.parent();
}
- el.toggleLabel(false);
- });
+ el.updateState({exposed:expose[e.type]});
+ };
+
+ this.rootWrapper.on('mouseover', '[document-node-element], [document-text-element]', hoverHandler);
+ this.rootWrapper.on('mouseout', '[document-node-element], [document-text-element]', hoverHandler);
this.eventBus.on('elementToggled', function(toggle, element) {
if(!toggle) {
- canvas.setCurrentElement(element.getPreviousTextElement());
+ canvas.setCurrentElement(canvas.getPreviousTextElement(element));
}
});
},
view: function() {
- return this.wrapper;
+ return this.dom;
},
doc: function() {
},
toggleElementHighlight: function(node, toggle) {
- var element = utils.findCanvasElement(node);
- element.toggleHighlight(toggle);
+ var element = utils.getElementForNode(node);
+ element.updateState({exposed: toggle});
},
getCursor: function() {
getCurrentNodeElement: function() {
- var htmlElement = this.wrapper.find('.current-node-element').parent()[0];
- if(htmlElement) {
- return this.getDocumentElement(htmlElement);
- }
+ return this.currentNodeElement;
},
getCurrentTextElement: function() {
- var htmlElement = this.wrapper.find('.current-text-element')[0];
+ var htmlElement = this.rootWrapper.find('.current-text-element')[0];
if(htmlElement) {
return this.getDocumentElement(htmlElement);
}
},
+ 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 && element.dom && element.dom.parents().index(this.rootWrapper) !== -1;
},
triggerSelectionChanged: function() {
- this.trigger('selectionChanged', this.getSelection());
+ var s = this.getSelection(),
+ f;
+ if(!s) {
+ return;
+ }
+ this.trigger('selectionChanged', s);
+ f = s.toDocumentFragment();
+
+ if(f && f instanceof f.RangeFragment) {
+ if(this.currentNodeElement) {
+ this.currentNodeElement.updateState({active: false});
+ this.currentNodeElement = null;
+ }
+ }
},
getSelection: function() {
- return new Selection(this);
+ return selection.fromNativeSelection(this);
+ },
+
+ select: function(fragment) {
+ if(fragment instanceof this.wlxmlDocument.RangeFragment) {
+ this.setCurrentElement(fragment.endNode, {caretTo: fragment.endOffset});
+ } else if(fragment instanceof this.wlxmlDocument.NodeFragment) {
+ var params = {
+ caretTo: fragment instanceof this.wlxmlDocument.CaretFragment ? fragment.offset : 'start'
+ };
+ this.setCurrentElement(fragment.node, params);
+ } else {
+ logger.debug('Fragment not supported');
+ }
+ },
+
+ setSelection: function(selection) {
+ this.select(this, selection.toDocumentFragment());
},
+ createSelection: function(params) {
+ return selection.fromParams(this, params);
+ },
setCurrentElement: function(element, params) {
if(!element) {
logger.debug('Invalid element passed to setCurrentElement: ' + element);
}
if(!(element instanceof documentElement.DocumentElement)) {
- element = utils.findCanvasElement(element);
+ element = utils.getElementForNode(element);
}
if(!element || !this.contains(element)) {
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 _.isFunction(e.getVerticallyFirstTextElement) ? e.getVerticallyFirstTextElement({considerChildren: false}) : null;
}.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');
+ this.rootWrapper.find('.current-text-element').removeClass('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');
+ if(this.currentNodeElement) {
+ this.currentNodeElement.updateState({active: false});
+ }
+ element.updateState({active: true});
+ this.currentNodeElement = element;
}
}.bind(this);
currentNodeElement = this.getCurrentNodeElement();
if(currentTextElement && !(currentTextElement.sameNode(textElementToLand))) {
- this.wrapper.find('.current-text-element').removeClass('current-text-element');
+ this.rootWrapper.find('.current-text-element').removeClass('current-text-element');
}
if(textElementToLand) {
_markAsCurrent(textElementToLand);
- if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element)) {
+ if((params.caretTo || params.caretTo === 0) || !textElementToLand.sameNode(this.getCursor().getPosition().element)) {
this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
}
} else {
_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);
}
var selection = document.getSelection();
+ $(node).parent().attr('contenteditable', true);
selection.removeAllRanges();
selection.addRange(range);
- this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
+ $(node).parent().focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
},
setCursorPosition: function(position) {
this._moveCaretToTextElement(position.element, position.offset);
}
},
+ showContextMenu: function(element, coors) {
+ var menu = new Menu();
- findCanvasElement: function(node) {
- return utils.findCanvasElement(node);
- },
-
- toggleGrid: function() {
- this.wrapper.toggleClass('grid-on');
- this.trigger('changed');
- },
- isGridToggled: function() {
- return this.wrapper.hasClass('grid-on');
- }
-});
-
-
-var isText = function(node) {
- return node && node.nodeType === Node.TEXT_NODE && $(node.parentNode).is('[document-text-element]');
-};
-
-var Selection = function(canvas) {
- this.canvas = canvas;
- var nativeSelection = this.nativeSelection = window.getSelection();
- Object.defineProperty(this, 'type', {
- get: function() {
- if(nativeSelection.focusNode) {
- if(nativeSelection.isCollapsed && isText(nativeSelection.focusNode)) {
- return 'caret';
- }
- if(isText(nativeSelection.focusNode) && isText(nativeSelection.anchorNode)) {
- return 'textSelection';
- }
- }
- if(canvas.getCurrentNodeElement()) {
- return 'node';
- }
- }
- });
-};
-
-$.extend(Selection.prototype, {
- toDocumentFragment: function() {
- var doc = this.canvas.wlxmlDocument,
- anchorElement = this.canvas.getDocumentElement(this.nativeSelection.anchorNode),
- focusElement = this.canvas.getDocumentElement(this.nativeSelection.focusNode),
- anchorNode = anchorElement ? anchorElement.wlxmlNode : null,
- focusNode = focusElement ? focusElement.wlxmlNode : null;
- if(this.type === 'caret') {
- return doc.createFragment(doc.CaretFragment, {node: anchorNode, offset: this.nativeSelection.anchorOffset});
- }
- if(this.type === 'textSelection') {
- if(anchorNode.isSiblingOf(focusNode)) {
- return doc.createFragment(doc.TextRangeFragment, {
- node1: anchorNode,
- offset1: this.nativeSelection.anchorOffset,
- node2: focusNode,
- offset2: this.nativeSelection.focusOffset,
- });
- }
- else {
- var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode});
- return doc.createFragment(doc.RangeFragment, {
- node1: siblingParents.node1,
- node2: siblingParents.node2
- });
- }
+ while(element) {
+ (element.contextMenuActions || []).forEach(menu.addAction.bind(menu));
+ element = element.parent();
}
- if(this.type === 'node') {
- return doc.createFragment(doc.NodeFragment, {node: this.canvas.getCurrentNodeElement().wlxmlNode});
+ if(menu.actions.length) {
+ menu.updateContextParam('fragment', this.getSelection().toDocumentFragment());
+ this.sandbox.showContextMenu(menu, {x: coors.x, y: coors.y});
}
- },
- sameAs: function(other) {
- void(other);
}
});
+
var Cursor = function(canvas) {
this.canvas = canvas;
this.selection = window.getSelection();
return this.getSelectionAnchor();
},
getSelectionStart: function() {
- return this.getSelectionBoundry('start');
+ return this.getSelectionBoundary('start');
},
getSelectionEnd: function() {
- return this.getSelectionBoundry('end');
+ return this.getSelectionBoundary('end');
},
getSelectionAnchor: function() {
- return this.getSelectionBoundry('anchor');
+ return this.getSelectionBoundary('anchor');
},
getSelectionFocus: function() {
- return this.getSelectionBoundry('focus');
+ return this.getSelectionBoundary('focus');
},
- getSelectionBoundry: function(which) {
+ getSelectionBoundary: function(which) {
/* globals window */
var selection = window.getSelection(),
anchorElement = this.canvas.getDocumentElement(selection.anchorNode),
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 {
});
return {
- fromXMLDocument: function(wlxmlDocument) {
- return new Canvas(wlxmlDocument);
+ fromXMLDocument: function(wlxmlDocument, elements, metadata, sandbox) {
+ return new Canvas(wlxmlDocument, elements, metadata, sandbox);
}
};