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