96e3bea727c1136ac5827181db3fea1053655395
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / canvas.js
1 define([
2 'libs/jquery',
3 'libs/underscore',
4 'libs/backbone',
5 'fnpjs/logging/logging',
6 'modules/documentCanvas/canvas/documentElement',
7 'modules/documentCanvas/canvas/keyboard',
8 'modules/documentCanvas/canvas/utils',
9 'modules/documentCanvas/canvas/wlxmlListener'
10 ], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener) {
11     
12 'use strict';
13 /* global document:false, window:false, Node:false */
14
15 var logger = logging.getLogger('canvas');
16
17 var TextHandler = function(canvas) {this.canvas = canvas; this.buffer = null;};
18 $.extend(TextHandler.prototype, {
19     handle: function(node, text) {
20         this.setText(text, node);
21         // return;
22         // if(!this.node) {
23         //     this.node = node;
24         // }
25         // if(this.node.sameNode(node)) {
26         //     this._ping(text);
27         // } else {
28         //     this.flush();
29         //     this.node = node;
30         //     this._ping(text);
31         // }
32     },
33     _ping: _.throttle(function(text) {
34         this.buffer = text;
35         this.flush();
36     }, 1000),
37     flush: function() {
38         if(this.buffer !== null) {
39             this.setText(this.buffer, this.node);
40             this.buffer = null;
41         }
42     },
43     setText: function(text, node) {
44         //this.canvas.wlxmlDocument.transform('setText', {node:node, text: text});
45         node.setText(text);
46
47     }
48
49 });
50
51
52 var Canvas = function(wlxmlDocument) {
53     this.eventBus = _.extend({}, Backbone.Events);
54     this.wrapper = $('<div>').addClass('canvas-wrapper').attr('contenteditable', true);
55     this.wlxmlListener = wlxmlListener.create(this);
56     this.loadWlxmlDocument(wlxmlDocument);
57     this.setupEventHandling();
58     this.textHandler = new TextHandler(this);
59 };
60
61 $.extend(Canvas.prototype, Backbone.Events, {
62
63     loadWlxmlDocument: function(wlxmlDocument) {
64         if(!wlxmlDocument) {
65             return false;
66         }
67
68         this.wlxmlListener.listenTo(wlxmlDocument);
69         this.wlxmlDocument = wlxmlDocument;
70         this.reloadRoot();
71     },
72
73     createElement: function(wlxmlNode) {
74         var Factory = wlxmlNode.nodeType === Node.TEXT_NODE ? documentElement.DocumentTextElement : documentElement.DocumentNodeElement;
75         return new Factory(wlxmlNode, this);
76     },
77
78     getDocumentElement: function(htmlElement) {
79         /* globals HTMLElement, Text */
80         if(!htmlElement || !(htmlElement instanceof HTMLElement || htmlElement instanceof Text)) {
81             return null;
82         }
83         var $element = $(htmlElement);
84         if(htmlElement.nodeType === Node.ELEMENT_NODE && $element.attr('document-node-element') !== undefined) {
85             return $element.data('canvas-element');
86         }
87
88         if(htmlElement.nodeType === Node.TEXT_NODE && $element.parent().attr('document-text-element') !== undefined) {
89             $element = $element.parent();
90         }
91
92         if($element.attr('document-text-element') !== undefined || (htmlElement.nodeType === Node.TEXT_NODE && $element.parent().attr('document-text-element') !== undefined)) {
93             //return DocumentTextElement.fromHTMLElement(htmlElement, canvas);
94             return $element.data('canvas-element');
95         }
96     },
97
98     reloadRoot: function() {
99         this.rootElement = this.createElement(this.wlxmlDocument.root);
100         this.wrapper.empty();
101         this.wrapper.append(this.rootElement.dom());
102     },
103
104     setupEventHandling: function() {
105         var canvas = this;
106
107         this.wrapper.on('keyup keydown keypress', function(e) {
108             keyboard.handleKey(e, canvas);
109         });
110
111         this.wrapper.on('mouseup', function() {
112             canvas.triggerSelectionChanged();
113         });
114
115         var mouseDown;
116         this.wrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) {
117             mouseDown = e.target;
118         });
119
120         this.wrapper.on('click', '[document-node-element], [document-text-element]', function(e) {
121             e.stopPropagation();
122             if(e.originalEvent.detail === 3) {
123                 e.preventDefault();
124                 canvas._moveCaretToTextElement(canvas.getDocumentElement(e.currentTarget), 'whole');
125             } else {
126                 if(mouseDown === e.target) {
127                     canvas.setCurrentElement(canvas.getDocumentElement(e.currentTarget), {caretTo: false});
128                 }
129             }
130         });
131
132         this.wrapper.on('paste', function(e) {
133             e.preventDefault();
134
135             var clipboardData = e.originalEvent.clipboardData;
136             if(!clipboardData || !clipboardData.getData) {
137                 return; // TODO: alert
138             }
139
140             var text = clipboardData.getData('text/plain').replace(/\r?\n|\r/g, ' '),
141                 cursor = canvas.getCursor(),
142                 element = cursor.getPosition().element,
143                 lhs, rhs;
144             
145             if(element && cursor.isWithinElement()) {
146                 lhs = element.getText().substr(0, cursor.getSelectionStart().offset);
147                 rhs = element.getText().substr(cursor.getSelectionEnd().offset);
148                 element.setText(lhs+text+rhs);
149                 canvas.setCurrentElement(element, {caretTo: lhs.length + text.length});
150             } else {
151                 /* jshint noempty:false */
152                 // TODO: alert
153             }
154         });
155
156         /* globals MutationObserver */
157         var observer = new MutationObserver(function(mutations) {
158             mutations.forEach(function(mutation) {
159                 if(documentElement.DocumentTextElement.isContentContainer(mutation.target)) {
160                     observer.disconnect();
161                     if(mutation.target.data === '') {
162                         mutation.target.data = utils.unicode.ZWS;
163                     }
164                     else if(mutation.oldValue === utils.unicode.ZWS) {
165                         mutation.target.data = mutation.target.data.replace(utils.unicode.ZWS, '');
166                         canvas._moveCaretToTextElement(canvas.getDocumentElement(mutation.target), 'end');
167                     }
168                     observer.observe(canvas.wrapper[0], config);
169
170                     var textElement = canvas.getDocumentElement(mutation.target),
171                         toSet = mutation.target.data !== utils.unicode.ZWS ? mutation.target.data : '';
172
173                     //textElement.data('wlxmlNode').setText(toSet);
174                     //textElement.data('wlxmlNode').document.transform('setText', {node: textElement.data('wlxmlNode'), text: toSet});
175                     if(textElement.wlxmlNode.getText() !== toSet) {
176                         canvas.textHandler.handle(textElement.wlxmlNode, toSet);
177                     }
178                 }
179             });
180         });
181         var config = { attributes: false, childList: false, characterData: true, subtree: true, characterDataOldValue: true};
182         observer.observe(this.wrapper[0], config);
183
184
185         this.wrapper.on('mouseover', '[document-node-element], [document-text-element]', function(e) {
186             var el = canvas.getDocumentElement(e.currentTarget);
187             if(!el) {
188                 return;
189             }
190             e.stopPropagation();
191             if(el instanceof documentElement.DocumentTextElement) {
192                 el = el.parent();
193             }
194             el.toggleLabel(true);
195         });
196         this.wrapper.on('mouseout', '[document-node-element], [document-text-element]', function(e) {
197             var el = canvas.getDocumentElement(e.currentTarget);
198             if(!el) {
199                 return;
200             }
201             e.stopPropagation();
202             if(el instanceof documentElement.DocumentTextElement) {
203                 el = el.parent();
204             }
205             el.toggleLabel(false);
206         });
207
208         this.eventBus.on('elementToggled', function(toggle, element) {
209             if(!toggle) {
210                 canvas.setCurrentElement(element.getPreviousTextElement());
211             }
212         });
213     },
214
215     view: function() {
216         return this.wrapper;
217     },
218
219     doc: function() {
220         return this.rootElement;
221     },
222
223     toggleElementHighlight: function(node, toggle) {
224         var element = utils.findCanvasElement(node);
225         element.toggleHighlight(toggle);
226     },
227
228     getCursor: function() {
229         return new Cursor(this);
230     },
231
232     
233     getCurrentNodeElement: function() {
234         var htmlElement = this.wrapper.find('.current-node-element').parent()[0];
235         if(htmlElement) {
236             return this.getDocumentElement(htmlElement);
237         }
238     },
239
240     getCurrentTextElement: function() {
241         var htmlElement = this.wrapper.find('.current-text-element')[0];
242         if(htmlElement) {
243             return this.getDocumentElement(htmlElement);
244         }
245     },
246
247     contains: function(element) {
248         return element.dom().parents().index(this.wrapper) !== -1;
249     },
250
251     triggerSelectionChanged: function() {
252         this.trigger('selectionChanged', this.getSelection());
253     },
254
255     getSelection: function() {
256         return new Selection(this);
257     },
258
259     setCurrentElement: function(element, params) {
260         if(!element) {
261             logger.debug('Invalid element passed to setCurrentElement: ' + element);
262             return;
263         }
264
265         if(!(element instanceof documentElement.DocumentElement)) {
266             element = utils.findCanvasElement(element);
267         }
268
269         if(!element || !this.contains(element)) {
270             logger.warning('Cannot set current element: element doesn\'t exist on canvas');
271             return;
272         }
273
274         params = _.extend({caretTo: 'end'}, params);
275         var findFirstDirectTextChild = function(e, nodeToLand) {
276             var byBrowser = this.getCursor().getPosition().element;
277             if(byBrowser && byBrowser.parent().sameNode(nodeToLand)) {
278                 return byBrowser;
279             }
280             var children = e.children();
281             for(var i = 0; i < children.length; i++) {
282                 if(children[i] instanceof documentElement.DocumentTextElement) {
283                     return children[i];
284                 }
285             }
286             return null;
287         }.bind(this);
288         var _markAsCurrent = function(element) {
289             if(element instanceof documentElement.DocumentTextElement) {
290                 this.wrapper.find('.current-text-element').removeClass('current-text-element');
291                 element.dom().addClass('current-text-element');
292             } else {
293                 this.wrapper.find('.current-node-element').removeClass('current-node-element');
294                 element._container().addClass('current-node-element');
295             }
296         }.bind(this);
297
298
299         var isTextElement = element instanceof documentElement.DocumentTextElement,
300             nodeElementToLand = isTextElement ? element.parent() : element,
301             textElementToLand = isTextElement ? element : findFirstDirectTextChild(element, nodeElementToLand),
302             currentTextElement = this.getCurrentTextElement(),
303             currentNodeElement = this.getCurrentNodeElement();
304
305         if(currentTextElement && !(currentTextElement.sameNode(textElementToLand))) {
306             this.wrapper.find('.current-text-element').removeClass('current-text-element');
307         }
308
309         if(textElementToLand) {
310             _markAsCurrent(textElementToLand);
311             if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element)) {
312                 this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
313             }
314         } else {
315             document.getSelection().removeAllRanges();
316         }
317
318         if(!(currentNodeElement && currentNodeElement.sameNode(nodeElementToLand))) {
319             _markAsCurrent(nodeElementToLand);
320         }
321         this.triggerSelectionChanged();
322     },
323
324     _moveCaretToTextElement: function(element, where) {
325         var range = document.createRange(),
326             node = element.dom().contents()[0];
327
328         if(typeof where !== 'number') {
329             range.selectNodeContents(node);
330         } else {
331             range.setStart(node, Math.min(node.data.length, where));
332         }
333         
334         if(where !== 'whole') {
335             var collapseArg = true;
336             if(where === 'end') {
337                 collapseArg = false;
338             }
339             range.collapse(collapseArg);
340         }
341         var selection = document.getSelection();
342
343         selection.removeAllRanges();
344         selection.addRange(range);
345         this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
346     },
347
348     setCursorPosition: function(position) {
349         if(position.element) {
350             this._moveCaretToTextElement(position.element, position.offset);
351         }
352     },
353
354     findCanvasElement: function(node) {
355         return utils.findCanvasElement(node);
356     },
357
358     toggleGrid: function() {
359         this.wrapper.toggleClass('grid-on');
360         this.trigger('changed');
361     },
362     isGridToggled: function() {
363         return this.wrapper.hasClass('grid-on');
364     }
365 });
366
367
368 var isText = function(node) {
369     return node && node.nodeType === Node.TEXT_NODE && $(node.parentNode).is('[document-text-element]');
370 };
371
372 var Selection = function(canvas) {
373     this.canvas = canvas;
374     var nativeSelection = this.nativeSelection = window.getSelection();
375     Object.defineProperty(this, 'type', {
376         get: function() {
377             if(nativeSelection.focusNode) {
378                 if(nativeSelection.isCollapsed && isText(nativeSelection.focusNode)) {
379                     return 'caret';
380                 }
381                 if(isText(nativeSelection.focusNode) && isText(nativeSelection.anchorNode)) {
382                     return 'textSelection';
383                 }
384             }
385             if(canvas.getCurrentNodeElement()) {
386                 return 'node';
387             }
388         }
389     });
390 };
391
392 $.extend(Selection.prototype, {
393     toDocumentFragment: function() {
394         var doc = this.canvas.wlxmlDocument,
395             anchorElement = this.canvas.getDocumentElement(this.nativeSelection.anchorNode),
396             focusElement = this.canvas.getDocumentElement(this.nativeSelection.focusNode),
397             anchorNode = anchorElement ? anchorElement.wlxmlNode : null,
398             focusNode = focusElement ? focusElement.wlxmlNode : null;
399         if(this.type === 'caret') {
400             return doc.createFragment(doc.CaretFragment, {node: anchorNode, offset: this.nativeSelection.anchorOffset});
401         }
402         if(this.type === 'textSelection') {
403             if(anchorNode.isSiblingOf(focusNode)) {
404                 return doc.createFragment(doc.TextRangeFragment, {
405                     node1: anchorNode,
406                     offset1: this.nativeSelection.anchorOffset,
407                     node2: focusNode,
408                     offset2: this.nativeSelection.focusOffset,
409                 });
410             }
411             else {
412                 var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode});
413                 return doc.createFragment(doc.RangeFragment, {
414                     node1: siblingParents.node1,
415                     node2: siblingParents.node2
416                 });
417             }
418         }
419         if(this.type === 'node') {
420             return doc.createFragment(doc.NodeFragment, {node: this.canvas.getCurrentNodeElement().wlxmlNode});
421         }
422     },
423     sameAs: function(other) {
424         void(other);
425     }
426 });
427
428 var Cursor = function(canvas) {
429     this.canvas = canvas;
430     this.selection = window.getSelection();
431 };
432
433 $.extend(Cursor.prototype, {
434     sameAs: function(other) {
435         var same = true;
436         if(!other) {
437             return false;
438         }
439
440         ['focusNode', 'focusOffset', 'anchorNode', 'anchorOffset'].some(function(prop) {
441             same = same && this.selection[prop] === other.selection[prop];
442             if(!same) {
443                 return true; // break
444             }
445         }.bind(this));
446
447         return same;
448     },
449     isSelecting: function() {
450         var selection = window.getSelection();
451         return !selection.isCollapsed;
452     },
453     isSelectingWithinElement: function() {
454         return this.isSelecting() && this.getSelectionStart().element.sameNode(this.getSelectionEnd().element);
455     },
456     isWithinElement: function() {
457         return !this.isSelecting() || this.isSelectingWithinElement();
458     },
459     isSelectingSiblings: function() {
460         return this.isSelecting() && this.getSelectionStart().element.parent().sameNode(this.getSelectionEnd().element.parent());
461     },
462     getPosition: function() {
463         return this.getSelectionAnchor();
464     },
465     getSelectionStart: function() {
466         return this.getSelectionBoundry('start');
467     },
468     getSelectionEnd: function() {
469         return this.getSelectionBoundry('end');
470     },
471     getSelectionAnchor: function() {
472         return this.getSelectionBoundry('anchor');
473     },
474     getSelectionFocus: function() {
475         return this.getSelectionBoundry('focus');
476     },
477     getSelectionBoundry: function(which) {
478         /* globals window */
479         var selection = window.getSelection(),
480             anchorElement = this.canvas.getDocumentElement(selection.anchorNode),
481             focusElement = this.canvas.getDocumentElement(selection.focusNode);
482         
483         if((!anchorElement) || (anchorElement instanceof documentElement.DocumentNodeElement) || (!focusElement) || focusElement instanceof documentElement.DocumentNodeElement) {
484             return {};
485         }
486
487         if(which === 'anchor') {
488             return {
489                 element: anchorElement,
490                 offset: selection.anchorOffset,
491                 offsetAtBeginning: selection.anchorOffset === 0 || anchorElement.getText() === '',
492                 offsetAtEnd: selection.anchorNode.data.length === selection.anchorOffset || anchorElement.getText() === ''
493             };
494         }
495         if(which === 'focus') {
496             return {
497                 element: focusElement,
498                 offset: selection.focusOffset,
499                 offsetAtBeginning: selection.focusOffset === 0 || focusElement.getText() === '',
500                 offsetAtEnd: selection.focusNode.data.length === selection.focusOffset || focusElement.getText() === '',
501             };
502         }
503         
504         var getPlaceData = function(anchorFirst) {
505             var element, offset;
506             if(anchorFirst) {
507                 if(which === 'start') {
508                     element = anchorElement;
509                     offset = selection.anchorOffset;
510                 }
511                 else if(which === 'end') {
512                     element = focusElement;
513                     offset = selection.focusOffset;
514                 }
515             } else {
516                 if(which === 'start') {
517                     element = focusElement;
518                     offset = selection.focusOffset;
519                 }
520                 else if(which === 'end') {
521                     element = anchorElement;
522                     offset = selection.anchorOffset;
523                 }
524             }
525             return {element: element, offset: offset};
526         };
527
528         var anchorFirst, placeData, parent;
529
530         if(anchorElement.parent().sameNode(focusElement.parent())) {
531             parent = anchorElement.parent();
532             if(selection.anchorNode === selection.focusNode) {
533                 anchorFirst = selection.anchorOffset <= selection.focusOffset;
534             } else {
535                 anchorFirst = parent.childIndex(anchorElement) < parent.childIndex(focusElement);
536             }
537             placeData = getPlaceData(anchorFirst);
538         } else {
539             /*jshint bitwise: false*/
540             anchorFirst = selection.anchorNode.compareDocumentPosition(selection.focusNode) & Node.DOCUMENT_POSITION_FOLLOWING;
541             placeData = getPlaceData(anchorFirst);
542         }
543
544         var nodeLen = (placeData.element.sameNode(focusElement) ? selection.focusNode : selection.anchorNode).length;
545         return {
546             element: placeData.element,
547             offset: placeData.offset,
548             offsetAtBeginning: placeData.offset === 0 || focusElement.getText() === '',
549             offsetAtEnd: nodeLen === placeData.offset || focusElement.getText() === ''
550         };
551     }
552 });
553
554 return {
555     fromXMLDocument: function(wlxmlDocument) {
556         return new Canvas(wlxmlDocument);
557     }
558 };
559
560 });