5 'modules/documentCanvas/canvas/documentElement',
6 'modules/documentCanvas/canvas/keyboard',
7 'modules/documentCanvas/canvas/utils'
8 ], function($, _, Backbone, documentElement, keyboard, utils) {
12 var Canvas = function(wlxmlDocument, publisher) {
13 this.eventBus = _.extend({}, Backbone.Events);
14 this.wrapper = $('<div>').addClass('canvas-wrapper').attr('contenteditable', true);
15 this.loadWlxmlDocument(wlxmlDocument);
16 this.publisher = publisher ? publisher : function() {};
19 $.extend(Canvas.prototype, {
21 loadWlxmlDocument: function(wlxmlDocument) {
22 wlxmlDocument = wlxmlDocument || this.wlxmlDocument;
26 var canvasDOM = this.generateCanvasDOM(wlxmlDocument.root);
29 this.wrapper.append(canvasDOM);
30 this.d = this.wrapper.children(0);
31 this.setupEventHandling();
35 var findCanvasElement = function(node) {
36 if(node.nodeType === Node.ELEMENT_NODE) {
37 return node.getData('canvasElement');
39 if(node.nodeType === Node.TEXT_NODE) {
40 var parent = node.parent(),
43 parent.children().forEach(function(child) {
44 if(child.data('wlxmlNode').sameNode(node))
52 if(this.wlxmlDocument !== wlxmlDocument) {
53 wlxmlDocument.on('change', function(event) {
54 var canvasNode = findCanvasElement(event.meta.node);
55 if(event.type === 'nodeAttrChange' && event.meta.attr === 'class') {
56 canvasNode.setWlxmlClass(event.meta.newVal);
61 this.wlxmlDocument = wlxmlDocument;
64 generateCanvasDOM: function(wlxmlNode) {
65 var element = documentElement.DocumentNodeElement.create(wlxmlNode, this);
69 setupEventHandling: function() {
71 this.wrapper.on('keyup keydown keypress', function(e) {
72 keyboard.handleKey(e, this);
75 this.wrapper.on('click', '[document-node-element], [document-text-element]', function(e) {
77 canvas.setCurrentElement(canvas.getDocumentElement(e.currentTarget), {caretTo: false});
80 var observer = new MutationObserver(function(mutations) {
81 mutations.forEach(function(mutation) {
82 if(documentElement.DocumentTextElement.isContentContainer(mutation.target)) {
83 observer.disconnect();
84 if(mutation.target.data === '')
85 mutation.target.data = utils.unicode.ZWS;
86 else if(mutation.oldValue === utils.unicode.ZWS) {
87 mutation.target.data = mutation.target.data.replace(utils.unicode.ZWS, '');
88 canvas._moveCaretToTextElement(canvas.getDocumentElement(mutation.target), 'end');
90 observer.observe(canvas.d[0], config);
91 canvas.publisher('contentChanged');
95 var config = { attributes: false, childList: false, characterData: true, subtree: true, characterDataOldValue: true};
96 observer.observe(this.d[0], config);
99 this.wrapper.on('mouseover', '[document-node-element], [document-text-element]', function(e) {
100 var el = canvas.getDocumentElement(e.currentTarget);
104 if(el instanceof documentElement.DocumentTextElement)
106 el.toggleLabel(true);
108 this.wrapper.on('mouseout', '[document-node-element], [document-text-element]', function(e) {
109 var el = canvas.getDocumentElement(e.currentTarget);
113 if(el instanceof documentElement.DocumentTextElement)
115 el.toggleLabel(false);
118 this.eventBus.on('elementToggled', function(toggle, element) {
120 canvas.setCurrentElement(element.getPreviousTextElement());
132 return documentElement.DocumentNodeElement.fromHTMLElement(this.d.get(0), this); //{wlxmlTag: this.d.prop('tagName')};
135 createNodeElement: function(params) {
136 return documentElement.DocumentNodeElement.create(params, this);
139 wrapText: function(params) {
140 params = _.extend({textNodeIdx: 0}, params);
141 if(typeof params.textNodeIdx === 'number')
142 params.textNodeIdx = [params.textNodeIdx];
144 var childrenInside = params.inside.children(),
145 idx1 = Math.min.apply(Math, params.textNodeIdx),
146 idx2 = Math.max.apply(Math, params.textNodeIdx),
147 textNode1 = childrenInside[idx1],
148 textNode2 = childrenInside[idx2],
149 sameNode = textNode1.sameNode(textNode2),
150 prefixOutside = textNode1.getText().substr(0, params.offsetStart),
151 prefixInside = textNode1.getText().substr(params.offsetStart),
152 suffixInside = textNode2.getText().substr(0, params.offsetEnd),
153 suffixOutside = textNode2.getText().substr(params.offsetEnd)
156 var wrapperElement = this.createNodeElement({tag: params._with.tag, klass: params._with.klass});
157 textNode1.after(wrapperElement);
160 if(prefixOutside.length > 0)
161 wrapperElement.before({text:prefixOutside});
163 var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
164 wrapperElement.append({text: core});
167 if(prefixInside.length > 0)
168 wrapperElement.append({text: prefixInside});
169 for(var i = idx1 + 1; i < idx2; i++) {
170 wrapperElement.append(childrenInside[i]);
172 if(suffixInside.length > 0)
173 wrapperElement.append({text: suffixInside});
175 if(suffixOutside.length > 0)
176 wrapperElement.after({text: suffixOutside});
177 return wrapperElement;
180 wrapElements: function(params) {
181 if(!(params.element1.parent().sameNode(params.element2.parent())))
184 var parent = params.element1.parent(),
185 parentChildren = parent.children(),
186 wrapper = this.createNodeElement({
187 tag: params._with.tag,
188 klass: params._with.klass}),
189 idx1 = parent.childIndex(params.element1),
190 idx2 = parent.childIndex(params.element2);
198 var insertingMethod, insertingTarget;
200 insertingMethod = 'prepend';
201 insertingTarget = parent;
203 insertingMethod = 'after';
204 insertingTarget = parentChildren[idx1-1];
207 for(var i = idx1; i <= idx2; i++) {
208 wrapper.append(parentChildren[i].detach());
211 insertingTarget[insertingMethod](wrapper);
215 getSiblingParents: function(params) {
216 var parents1 = [params.element1].concat(params.element1.parents()).reverse(),
217 parents2 = [params.element2].concat(params.element2.parents()).reverse(),
218 noSiblingParents = null;
220 if(parents1.length === 0 || parents2.length === 0 || !(parents1[0].sameNode(parents2[0])))
221 return noSiblingParents;
224 for(i = 0; i < Math.min(parents1.length, parents2.length); i++) {
225 if(parents1[i].sameNode(parents2[i]))
229 return {element1: parents1[i], element2: parents2[i]};
232 getDocumentElement: function(from) {
233 if(from instanceof HTMLElement || from instanceof Text) {
234 return documentElement.DocumentElement.fromHTMLElement(from, this);
237 getCursor: function() {
238 return new Cursor(this);
244 getCurrentNodeElement: function() {
245 return this.getDocumentElement(this.wrapper.find('.current-node-element').parent()[0]);
248 getCurrentTextElement: function() {
249 return this.getDocumentElement(this.wrapper.find('.current-text-element')[0]);
254 setCurrentElement: function(element, params) {
255 params = _.extend({caretTo: 'end'}, params);
256 var findFirstDirectTextChild = function(e, nodeToLand) {
257 var byBrowser = this.getCursor().getPosition().element;
258 if(byBrowser && byBrowser.parent().sameNode(nodeToLand))
260 var children = e.children();
261 for(var i = 0; i < children.length; i++) {
262 if(children[i] instanceof documentElement.DocumentTextElement)
267 var _markAsCurrent = function(element) {
268 if(element instanceof documentElement.DocumentTextElement) {
269 this.wrapper.find('.current-text-element').removeClass('current-text-element');
270 element.dom().addClass('current-text-element');
272 this.wrapper.find('.current-node-element').removeClass('current-node-element')
273 element._container().addClass('current-node-element');
274 this.publisher('currentElementChanged', element);
279 var isTextElement = element instanceof documentElement.DocumentTextElement,
280 nodeElementToLand = isTextElement ? element.parent() : element,
281 textElementToLand = isTextElement ? element : findFirstDirectTextChild(element, nodeElementToLand),
282 currentTextElement = this.getCurrentTextElement(),
283 currentNodeElement = this.getCurrentNodeElement();
285 if(currentTextElement && !(currentTextElement.sameNode(textElementToLand)))
286 this.wrapper.find('.current-text-element').removeClass('current-text-element');
288 if(textElementToLand) {
289 _markAsCurrent(textElementToLand);
290 if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element))
291 this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
292 if(!(textElementToLand.sameNode(currentTextElement)))
293 this.publisher('currentTextElementSet', textElementToLand);
295 document.getSelection().removeAllRanges();
298 if(!(currentNodeElement && currentNodeElement.sameNode(nodeElementToLand))) {
299 _markAsCurrent(nodeElementToLand);
301 this.publisher('currentNodeElementSet', nodeElementToLand);
305 _moveCaretToTextElement: function(element, where) {
306 var range = document.createRange(),
307 node = element.dom().contents()[0];
309 if(typeof where !== 'number') {
310 range.selectNodeContents(node);
312 range.setStart(node, where);
315 var collapseArg = true;
318 range.collapse(collapseArg);
320 var selection = document.getSelection();
322 selection.removeAllRanges();
323 selection.addRange(range);
324 this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
327 setCursorPosition: function(position) {
329 this._moveCaretToTextElement(position.element, position.offset);
333 var parent = $('<div>'),
334 parts = this.doc().toXML(0);
335 parent.append(parts);
336 return parent.html();
340 $.extend(Canvas.prototype.list, {
341 create: function(params) {
342 if(!(params.element1.parent().sameNode(params.element2.parent())))
345 var parent = params.element1.parent(),
346 canvas = params.element1.canvas;
348 if(parent.childIndex(params.element1) > parent.childIndex(params.element2)) {
349 var tmp = params.element1;
350 params.element1 = params.element2;
351 params.element2 = tmp;
354 var elementsToWrap = [];
356 var place = 'before';
357 parent.children().some(function(element) {
359 if(element.sameNode(params.element1))
361 if(place === 'inside') {
362 if(element instanceof documentElement.DocumentTextElement) {
363 element = element.wrapWithNodeElement({tag: 'div', klass: 'list.item'});
364 if(element.children()[0].sameNode(params.element1))
365 params.element1 = element;
367 element.setWlxmlClass('item');
368 elementsToWrap.push(element);
370 if(_e.sameNode(params.element2))
374 var listElement = canvas.createNodeElement({tag: 'div', klass: 'list-items' + (params.type === 'enum' ? '-enum' : '')});
376 if(parent.is('list')) {
377 var item = listElement.wrapWithNodeElement({tag: 'div', klass: 'item'});
378 item.exec('toggleBullet', false);
379 toret = listElement.parent();
384 params.element1.before(toret);
386 elementsToWrap.forEach(function(element) {
388 listElement.append(element);
391 extractItems: function(params) {
392 params = _.extend({merge: true}, params);
393 var list = params.element1.parent();
394 if(!list.is('list') || !(list.sameNode(params.element2.parent())))
397 var idx1 = list.childIndex(params.element1),
398 idx2 = list.childIndex(params.element2),
401 succeedingItems = [],
402 items = list.children(),
403 listIsNested = list.parent().getWlxmlClass() === 'item',
404 canvas = params.element1.canvas,
408 var tmp = idx1; idx1 = idx2; idx2 = tmp;
411 items.forEach(function(item, idx) {
413 precedingItems.push(item);
414 else if(idx >= idx1 && idx <= idx2) {
415 extractedItems.push(item);
418 succeedingItems.push(item);
422 var reference = listIsNested ? list.parent() : list;
423 if(succeedingItems.length === 0) {
424 var reference_orig = reference;
425 extractedItems.forEach(function(item) {
426 reference.after(item);
429 item.setWlxmlClass(null);
431 if(precedingItems.length === 0)
432 reference_orig.detach();
433 } else if(precedingItems.length === 0) {
434 extractedItems.forEach(function(item) {
435 reference.before(item);
437 item.setWlxmlClass(null);
440 extractedItems.forEach(function(item) {
441 reference.after(item);
443 item.setWlxmlClass(null);
446 var secondList = canvas.createNodeElement({tag: 'div', klass:'list-items'}),
450 toAdd = secondList.wrapWithNodeElement({tag: 'div', klass:'item'});
452 succeedingItems.forEach(function(item) {
453 secondList.append(item);
456 reference.after(toAdd);
458 if(!params.merge && listIsNested) {
459 return this.extractItems({element1: extractedItems[0], element2: extractedItems[extractedItems.length-1]});
463 areItemsOfTheSameList: function(params) {
464 var e1 = params.element1,
465 e2 = params.element2;
466 return e1.parent().sameNode(e2.parent())
467 && e1.parent().is('list');
472 var Cursor = function(canvas) {
473 this.canvas = canvas;
476 $.extend(Cursor.prototype, {
477 isSelecting: function() {
478 var selection = window.getSelection();
479 return !selection.isCollapsed;
481 isSelectingWithinElement: function() {
482 return this.isSelecting() && this.getSelectionStart().element.sameNode(this.getSelectionEnd().element);
484 isSelectingSiblings: function() {
485 return this.isSelecting() && this.getSelectionStart().element.parent().sameNode(this.getSelectionEnd().element.parent());
487 getPosition: function() {
488 return this.getSelectionAnchor();
490 getSelectionStart: function() {
491 return this.getSelectionBoundry('start');
493 getSelectionEnd: function() {
494 return this.getSelectionBoundry('end');
496 getSelectionAnchor: function() {
497 return this.getSelectionBoundry('anchor');
499 getSelectionFocus: function() {
500 return this.getSelectionBoundry('focus');
502 getSelectionBoundry: function(which) {
503 var selection = window.getSelection(),
504 anchorElement = this.canvas.getDocumentElement(selection.anchorNode),
505 focusElement = this.canvas.getDocumentElement(selection.focusNode);
507 if((!anchorElement) || (anchorElement instanceof documentElement.DocumentNodeElement) || (!focusElement) || focusElement instanceof documentElement.DocumentNodeElement)
510 if(which === 'anchor') {
512 element: anchorElement,
513 offset: selection.anchorOffset,
514 offsetAtBeginning: selection.anchorOffset === 0,
515 offsetAtEnd: selection.anchorNode.data.length === selection.anchorOffset
518 if(which === 'focus') {
520 element: focusElement,
521 offset: selection.focusOffset,
522 offsetAtBeginning: selection.focusOffset === 0,
523 offsetAtEnd: selection.focusNode.data.length === selection.focusOffset
530 if(anchorElement.parent().sameNode(focusElement.parent())) {
531 var parent = anchorElement.parent(),
532 anchorFirst = parent.childIndex(anchorElement) < parent.childIndex(focusElement);
534 if(which === 'start') {
535 element = anchorElement;
536 offset = selection.anchorOffset
538 else if(which === 'end') {
539 element = focusElement,
540 offset = selection.focusOffset
543 if(which === 'start') {
544 element = focusElement,
545 offset = selection.focusOffset
547 else if(which === 'end') {
548 element = anchorElement;
549 offset = selection.anchorOffset
553 // TODO: Handle order via https://developer.mozilla.org/en-US/docs/Web/API/Node.compareDocumentPosition
554 if(which === 'start') {
555 element = anchorElement;
556 offset = selection.anchorOffset
558 element = focusElement;
559 offset = selection.focusOffset
563 var nodeLen = (element.sameNode(focusElement) ? selection.focusNode : selection.anchorNode).length;
567 offsetAtBeginning: offset === 0,
568 offsetAtEnd: nodeLen === offset
574 fromXMLDocument: function(wlxmlDocument, publisher) {
575 return new Canvas(wlxmlDocument, publisher);