refactoring: fromHTMLElement @ canvas, no need to pass canvas instance
[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, publisher) {
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.publisher = publisher ? publisher : function() {};
59     this.textHandler = new TextHandler(this);
60 };
61
62 $.extend(Canvas.prototype, {
63
64     loadWlxmlDocument: function(wlxmlDocument) {
65         if(!wlxmlDocument) {
66             return false;
67         }
68
69         this.wlxmlListener.listenTo(wlxmlDocument);
70         this.wlxmlDocument = wlxmlDocument;
71         this.reloadRoot();
72     },
73
74     createElement: function(wlxmlNode) {
75         var Factory = wlxmlNode.nodeType === Node.TEXT_NODE ? documentElement.DocumentTextElement : documentElement.DocumentNodeElement;
76         return Factory.create(wlxmlNode, this);
77     },
78
79     getDocumentElement: function(htmlElement) {
80         /* globals HTMLElement, Text */
81         if(!htmlElement || !(htmlElement instanceof HTMLElement || htmlElement instanceof Text)) {
82             return null;
83         }
84         var $element = $(htmlElement);
85         if(htmlElement.nodeType === Node.ELEMENT_NODE && $element.attr('document-node-element') !== undefined) {
86             return new documentElement.DocumentNodeElement(htmlElement, this);
87         }
88         if($element.attr('document-text-element') !== undefined || (htmlElement.nodeType === Node.TEXT_NODE && $element.parent().attr('document-text-element') !== undefined)) {
89             //return DocumentTextElement.fromHTMLElement(htmlElement, canvas);
90             return new documentElement.DocumentTextElement(htmlElement, this);
91         }
92     },
93
94     reloadRoot: function() {
95         var canvasDOM = this.generateCanvasDOM(this.wlxmlDocument.root);
96         //var canvasDOM = this.wlxmlDocument.root.getData('canvasElement') ? this.wlxmlDocument.root.getData('canvasElement').dom() : this.generateCanvasDOM(this.wlxmlDocument.root);
97
98         this.wrapper.empty();
99         this.wrapper.append(canvasDOM);
100         this.d = this.wrapper.children(0);
101     },
102
103     generateCanvasDOM: function(wlxmlNode) {
104         var element = documentElement.DocumentNodeElement.create(wlxmlNode, this);
105         return element.dom();
106     },
107
108     setupEventHandling: function() {
109         var canvas = this;
110         this.wrapper.on('keyup keydown keypress', function(e) {
111             keyboard.handleKey(e, this);
112         }.bind(this));
113
114         var mouseDown;
115         this.wrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) {
116             mouseDown = e.target;
117         });
118
119         this.wrapper.on('click', '[document-node-element], [document-text-element]', function(e) {
120             e.stopPropagation();
121             if(e.originalEvent.detail === 3) {
122                 e.preventDefault();
123                 canvas._moveCaretToTextElement(canvas.getDocumentElement(e.currentTarget), 'whole');
124             } else {
125                 if(mouseDown === e.target) {
126                     canvas.setCurrentElement(canvas.getDocumentElement(e.currentTarget), {caretTo: false});
127                 }
128             }
129         });
130
131         this.wrapper.on('paste', function(e) {
132             e.preventDefault();
133
134             var clipboardData = e.originalEvent.clipboardData;
135             if(!clipboardData || !clipboardData.getData) {
136                 return; // TODO: alert
137             }
138
139             var text = clipboardData.getData('text/plain').replace(/\r?\n|\r/g, ' '),
140                 cursor = canvas.getCursor(),
141                 element = cursor.getPosition().element,
142                 lhs, rhs;
143             
144             if(element && cursor.isWithinElement()) {
145                 lhs = element.getText().substr(0, cursor.getSelectionStart().offset);
146                 rhs = element.getText().substr(cursor.getSelectionEnd().offset);
147                 element.setText(lhs+text+rhs);
148                 canvas.setCurrentElement(element, {caretTo: lhs.length + text.length});
149             } else {
150                 /* jshint noempty:false */
151                 // TODO: alert
152             }
153         });
154
155         /* globals MutationObserver */
156         var observer = new MutationObserver(function(mutations) {
157             mutations.forEach(function(mutation) {
158                 if(documentElement.DocumentTextElement.isContentContainer(mutation.target)) {
159                     observer.disconnect();
160                     if(mutation.target.data === '') {
161                         mutation.target.data = utils.unicode.ZWS;
162                     }
163                     else if(mutation.oldValue === utils.unicode.ZWS) {
164                         mutation.target.data = mutation.target.data.replace(utils.unicode.ZWS, '');
165                         canvas._moveCaretToTextElement(canvas.getDocumentElement(mutation.target), 'end');
166                     }
167                     observer.observe(canvas.wrapper[0], config);
168
169                     var textElement = canvas.getDocumentElement(mutation.target),
170                         toSet = mutation.target.data !== utils.unicode.ZWS ? mutation.target.data : '';
171
172                     //textElement.data('wlxmlNode').setText(toSet);
173                     //textElement.data('wlxmlNode').document.transform('setText', {node: textElement.data('wlxmlNode'), text: toSet});
174                     if(textElement.data('wlxmlNode').getText() !== toSet) {
175                         canvas.textHandler.handle(textElement.data('wlxmlNode'), toSet);
176                     }
177                 }
178             });
179         });
180         var config = { attributes: false, childList: false, characterData: true, subtree: true, characterDataOldValue: true};
181         observer.observe(this.wrapper[0], config);
182
183
184         this.wrapper.on('mouseover', '[document-node-element], [document-text-element]', function(e) {
185             var el = canvas.getDocumentElement(e.currentTarget);
186             if(!el) {
187                 return;
188             }
189             e.stopPropagation();
190             if(el instanceof documentElement.DocumentTextElement) {
191                 el = el.parent();
192             }
193             el.toggleLabel(true);
194         });
195         this.wrapper.on('mouseout', '[document-node-element], [document-text-element]', function(e) {
196             var el = canvas.getDocumentElement(e.currentTarget);
197             if(!el) {
198                 return;
199             }
200             e.stopPropagation();
201             if(el instanceof documentElement.DocumentTextElement) {
202                 el = el.parent();
203             }
204             el.toggleLabel(false);
205         });
206
207         this.eventBus.on('elementToggled', function(toggle, element) {
208             if(!toggle) {
209                 canvas.setCurrentElement(element.getPreviousTextElement());
210             }
211         });
212     },
213
214     view: function() {
215         return this.wrapper;
216     },
217
218     doc: function() {
219         if(this.d === null) {
220             return null;
221         }
222         return this.getDocumentElement(this.d[0]);
223     },
224
225     toggleElementHighlight: function(node, toggle) {
226         var element = utils.findCanvasElement(node);
227         element.toggleHighlight(toggle);
228     },
229
230     getCursor: function() {
231         return new Cursor(this);
232     },
233
234     
235     getCurrentNodeElement: function() {
236         var htmlElement = this.wrapper.find('.current-node-element').parent()[0];
237         if(htmlElement) {
238             return this.getDocumentElement(htmlElement);
239         }
240     },
241
242     getCurrentTextElement: function() {
243         var htmlElement = this.wrapper.find('.current-text-element')[0];
244         if(htmlElement) {
245             return this.getDocumentElement(htmlElement);
246         }
247     },
248
249     contains: function(element) {
250         return element.dom().parents().index(this.wrapper) !== -1;
251     },
252
253     setCurrentElement: function(element, params) {
254         if(!(element instanceof documentElement.DocumentElement)) {
255             element = utils.findCanvasElement(element);
256         }
257
258         if(!element || !this.contains(element)) {
259             logger.warning('Cannot set current element: element doesn\'t exist on canvas');
260             return;
261         }
262
263         params = _.extend({caretTo: 'end'}, params);
264         var findFirstDirectTextChild = function(e, nodeToLand) {
265             var byBrowser = this.getCursor().getPosition().element;
266             if(byBrowser && byBrowser.parent().sameNode(nodeToLand)) {
267                 return byBrowser;
268             }
269             var children = e.children();
270             for(var i = 0; i < children.length; i++) {
271                 if(children[i] instanceof documentElement.DocumentTextElement) {
272                     return children[i];
273                 }
274             }
275             return null;
276         }.bind(this);
277         var _markAsCurrent = function(element) {
278             if(element instanceof documentElement.DocumentTextElement) {
279                 this.wrapper.find('.current-text-element').removeClass('current-text-element');
280                 element.dom().addClass('current-text-element');
281             } else {
282                 this.wrapper.find('.current-node-element').removeClass('current-node-element');
283                 element._container().addClass('current-node-element');
284             }
285         }.bind(this);
286
287
288         var isTextElement = element instanceof documentElement.DocumentTextElement,
289             nodeElementToLand = isTextElement ? element.parent() : element,
290             textElementToLand = isTextElement ? element : findFirstDirectTextChild(element, nodeElementToLand),
291             currentTextElement = this.getCurrentTextElement(),
292             currentNodeElement = this.getCurrentNodeElement();
293
294         if(currentTextElement && !(currentTextElement.sameNode(textElementToLand))) {
295             this.wrapper.find('.current-text-element').removeClass('current-text-element');
296         }
297
298         if(textElementToLand) {
299             _markAsCurrent(textElementToLand);
300             if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element)) {
301                 this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
302             }
303             if(!(textElementToLand.sameNode(currentTextElement))) {
304                 this.publisher('currentTextElementSet', textElementToLand.data('wlxmlNode'));
305             }
306         } else {
307             document.getSelection().removeAllRanges();
308         }
309
310         if(!(currentNodeElement && currentNodeElement.sameNode(nodeElementToLand))) {
311             _markAsCurrent(nodeElementToLand);
312
313             this.publisher('currentNodeElementSet', nodeElementToLand.data('wlxmlNode'));
314         }
315     },
316
317     _moveCaretToTextElement: function(element, where) {
318         var range = document.createRange(),
319             node = element.dom().contents()[0];
320
321         if(typeof where !== 'number') {
322             range.selectNodeContents(node);
323         } else {
324             range.setStart(node, where);
325         }
326         
327         if(where !== 'whole') {
328             var collapseArg = true;
329             if(where === 'end') {
330                 collapseArg = false;
331             }
332             range.collapse(collapseArg);
333         }
334         var selection = document.getSelection();
335
336         selection.removeAllRanges();
337         selection.addRange(range);
338         this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
339     },
340
341     setCursorPosition: function(position) {
342         if(position.element) {
343             this._moveCaretToTextElement(position.element, position.offset);
344         }
345     }
346 });
347
348
349 var Cursor = function(canvas) {
350     this.canvas = canvas;
351 };
352
353 $.extend(Cursor.prototype, {
354     isSelecting: function() {
355         var selection = window.getSelection();
356         return !selection.isCollapsed;
357     },
358     isSelectingWithinElement: function() {
359         return this.isSelecting() && this.getSelectionStart().element.sameNode(this.getSelectionEnd().element);
360     },
361     isWithinElement: function() {
362         return !this.isSelecting() || this.isSelectingWithinElement();
363     },
364     isSelectingSiblings: function() {
365         return this.isSelecting() && this.getSelectionStart().element.parent().sameNode(this.getSelectionEnd().element.parent());
366     },
367     getPosition: function() {
368         return this.getSelectionAnchor();
369     },
370     getSelectionStart: function() {
371         return this.getSelectionBoundry('start');
372     },
373     getSelectionEnd: function() {
374         return this.getSelectionBoundry('end');
375     },
376     getSelectionAnchor: function() {
377         return this.getSelectionBoundry('anchor');
378     },
379     getSelectionFocus: function() {
380         return this.getSelectionBoundry('focus');
381     },
382     getSelectionBoundry: function(which) {
383         /* globals window */
384         var selection = window.getSelection(),
385             anchorElement = this.canvas.getDocumentElement(selection.anchorNode),
386             focusElement = this.canvas.getDocumentElement(selection.focusNode);
387         
388         if((!anchorElement) || (anchorElement instanceof documentElement.DocumentNodeElement) || (!focusElement) || focusElement instanceof documentElement.DocumentNodeElement) {
389             return {};
390         }
391
392         if(which === 'anchor') {
393             return {
394                 element: anchorElement,
395                 offset: selection.anchorOffset,
396                 offsetAtBeginning: selection.anchorOffset === 0 || anchorElement.getText() === '',
397                 offsetAtEnd: selection.anchorNode.data.length === selection.anchorOffset || anchorElement.getText() === ''
398             };
399         }
400         if(which === 'focus') {
401             return {
402                 element: focusElement,
403                 offset: selection.focusOffset,
404                 offsetAtBeginning: selection.focusOffset === 0 || focusElement.getText() === '',
405                 offsetAtEnd: selection.focusNode.data.length === selection.focusOffset || focusElement.getText() === '',
406             };
407         }
408         
409         var getPlaceData = function(anchorFirst) {
410             var element, offset;
411             if(anchorFirst) {
412                 if(which === 'start') {
413                     element = anchorElement;
414                     offset = selection.anchorOffset;
415                 }
416                 else if(which === 'end') {
417                     element = focusElement;
418                     offset = selection.focusOffset;
419                 }
420             } else {
421                 if(which === 'start') {
422                     element = focusElement;
423                     offset = selection.focusOffset;
424                 }
425                 else if(which === 'end') {
426                     element = anchorElement;
427                     offset = selection.anchorOffset;
428                 }
429             }
430             return {element: element, offset: offset};
431         };
432
433         var anchorFirst, placeData, parent;
434
435         if(anchorElement.parent().sameNode(focusElement.parent())) {
436             parent = anchorElement.parent();
437             if(selection.anchorNode === selection.focusNode) {
438                 anchorFirst = selection.anchorOffset <= selection.focusOffset;
439             } else {
440                 anchorFirst = parent.childIndex(anchorElement) < parent.childIndex(focusElement);
441             }
442             placeData = getPlaceData(anchorFirst);
443         } else {
444             /*jshint bitwise: false*/
445             anchorFirst = selection.anchorNode.compareDocumentPosition(selection.focusNode) & Node.DOCUMENT_POSITION_FOLLOWING;
446             placeData = getPlaceData(anchorFirst);
447         }
448
449         var nodeLen = (placeData.element.sameNode(focusElement) ? selection.focusNode : selection.anchorNode).length;
450         return {
451             element: placeData.element,
452             offset: placeData.offset,
453             offsetAtBeginning: placeData.offset === 0 || focusElement.getText() === '',
454             offsetAtEnd: nodeLen === placeData.offset || focusElement.getText() === ''
455         };
456     }
457 });
458
459 return {
460     fromXMLDocument: function(wlxmlDocument, publisher) {
461         return new Canvas(wlxmlDocument, publisher);
462     }
463 };
464
465 });