752b947a223c7c7b6b061d14e1f959450ddc06ea
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / canvas.js
1 define([
2 'libs/jquery',
3 'libs/underscore',
4 'libs/backbone',
5 'modules/documentCanvas/canvas/documentElement',
6 'modules/documentCanvas/canvas/keyboard',
7 'modules/documentCanvas/canvas/utils',
8 'modules/documentCanvas/canvas/wlxmlListener'
9 ], function($, _, Backbone, documentElement, keyboard, utils, wlxmlListener) {
10     
11 'use strict';
12
13 var TextHandler = function(canvas) {this.canvas = canvas; this.buffer = null};
14 $.extend(TextHandler.prototype, {
15     handle: function(node, text) {
16         //console.log('canvas text handler: ' + text);
17         this.setText(text, node);
18         return;
19         if(!this.node) {
20             this.node = node;
21         }
22         if(this.node.sameNode(node)) {
23             this._ping(text);
24         } else {
25             this.flush();
26             this.node = node;
27             this._ping(text);
28         }
29     },
30     _ping: _.throttle(function(text) {
31         this.buffer = text;
32         this.flush();
33     }, 1000),
34     flush: function() {
35         if(this.buffer != null) {
36             this.setText(this.buffer, this.node);
37             this.buffer = null;
38         }
39     },
40     setText: function(text, node) {
41         //this.canvas.wlxmlDocument.transform('setText', {node:node, text: text});
42         node.setText(text);
43
44     }
45
46 });
47
48
49 var Canvas = function(wlxmlDocument, publisher) {
50     this.eventBus = _.extend({}, Backbone.Events);
51     this.wrapper = $('<div>').addClass('canvas-wrapper').attr('contenteditable', true);
52     this.wlxmlListener = wlxmlListener.create(this);
53     this.loadWlxmlDocument(wlxmlDocument);
54     this.publisher = publisher ? publisher : function() {};
55     this.textHandler = new TextHandler(this);
56 };
57
58 $.extend(Canvas.prototype, {
59
60     loadWlxmlDocument: function(wlxmlDocument) {
61         if(!wlxmlDocument) {
62             return false;
63         }
64
65         this.wlxmlListener.listenTo(wlxmlDocument);
66         this.wlxmlDocument = wlxmlDocument;
67         this.reloadRoot();
68         this.setupEventHandling();
69     },
70
71     reloadRoot: function() {
72         var canvasDOM = this.generateCanvasDOM(this.wlxmlDocument.root);
73         //var canvasDOM = this.wlxmlDocument.root.getData('canvasElement') ? this.wlxmlDocument.root.getData('canvasElement').dom() : this.generateCanvasDOM(this.wlxmlDocument.root);
74
75         this.wrapper.empty();
76         this.wrapper.append(canvasDOM);
77         this.d = this.wrapper.children(0);
78     },
79
80     generateCanvasDOM: function(wlxmlNode) {
81         var element = documentElement.DocumentNodeElement.create(wlxmlNode, this);
82         return element.dom();
83     },
84
85     setupEventHandling: function() {
86         var canvas = this;
87         this.wrapper.on('keyup keydown keypress', function(e) {
88             keyboard.handleKey(e, this);
89         }.bind(this));
90
91         this.wrapper.on('click', '[document-node-element], [document-text-element]', function(e) {
92             e.stopPropagation();
93             canvas.setCurrentElement(canvas.getDocumentElement(e.currentTarget), {caretTo: false});
94         });
95
96
97
98         var observer = new MutationObserver(function(mutations) {
99             mutations.forEach(function(mutation) {
100                 if(documentElement.DocumentTextElement.isContentContainer(mutation.target)) {
101                     observer.disconnect();
102                     if(mutation.target.data === '')
103                         mutation.target.data = utils.unicode.ZWS;
104                     else if(mutation.oldValue === utils.unicode.ZWS) {
105                         mutation.target.data = mutation.target.data.replace(utils.unicode.ZWS, '');
106                         canvas._moveCaretToTextElement(canvas.getDocumentElement(mutation.target), 'end');
107                     }
108                     observer.observe(canvas.wrapper[0], config);
109                     canvas.publisher('contentChanged');
110
111                     var textElement = canvas.getDocumentElement(mutation.target),
112                         toSet = mutation.target.data !== utils.unicode.ZWS ? mutation.target.data : '';
113
114                     //textElement.data('wlxmlNode').setText(toSet);
115                     //textElement.data('wlxmlNode').document.transform('setText', {node: textElement.data('wlxmlNode'), text: toSet});
116                     if(textElement.data('wlxmlNode').getText() != toSet) {
117                         canvas.textHandler.handle(textElement.data('wlxmlNode'), toSet);
118                     }
119                 }
120             });
121         });
122         var config = { attributes: false, childList: false, characterData: true, subtree: true, characterDataOldValue: true};
123         observer.observe(this.wrapper[0], config);
124
125
126         this.wrapper.on('mouseover', '[document-node-element], [document-text-element]', function(e) {
127             var el = canvas.getDocumentElement(e.currentTarget);
128             if(!el)
129                 return;
130             e.stopPropagation();
131             if(el instanceof documentElement.DocumentTextElement)
132                 el = el.parent();
133             el.toggleLabel(true);
134         });
135         this.wrapper.on('mouseout', '[document-node-element], [document-text-element]', function(e) {
136             var el = canvas.getDocumentElement(e.currentTarget);
137             if(!el)
138                 return;
139             e.stopPropagation();
140             if(el instanceof documentElement.DocumentTextElement)
141                 el = el.parent();
142             el.toggleLabel(false);
143         });
144
145         this.eventBus.on('elementToggled', function(toggle, element) {
146             if(!toggle) {
147                 canvas.setCurrentElement(element.getPreviousTextElement());
148             }
149         });
150     },
151
152     view: function() {
153         return this.wrapper;
154     },
155
156     doc: function() {
157         if(this.d === null)
158             return null;
159         return documentElement.DocumentNodeElement.fromHTMLElement(this.d.get(0), this); //{wlxmlTag: this.d.prop('tagName')};
160     },
161
162     toggleElementHighlight: function(node, toggle) {
163         var element = utils.findCanvasElement(node);
164         element.toggleHighlight(toggle);
165     },
166
167     createNodeElement: function(params) {
168         return documentElement.DocumentNodeElement.create(params, this);
169     },
170
171     getDocumentElement: function(from) {
172         if(from instanceof HTMLElement || from instanceof Text) {
173            return documentElement.DocumentElement.fromHTMLElement(from, this);
174         }
175     },
176     getCursor: function() {
177         return new Cursor(this);
178     },
179
180     
181     getCurrentNodeElement: function() {
182         return this.getDocumentElement(this.wrapper.find('.current-node-element').parent()[0]);
183     },
184
185     getCurrentTextElement: function() {
186         return this.getDocumentElement(this.wrapper.find('.current-text-element')[0]);
187     },
188
189
190
191     setCurrentElement: function(element, params) {
192         if(!(element instanceof documentElement.DocumentElement)) {
193             element = utils.findCanvasElement(element);
194         }
195
196         params = _.extend({caretTo: 'end'}, params);
197         var findFirstDirectTextChild = function(e, nodeToLand) {
198             var byBrowser = this.getCursor().getPosition().element;
199             if(byBrowser && byBrowser.parent().sameNode(nodeToLand))
200                 return byBrowser;
201             var children = e.children();
202             for(var i = 0; i < children.length; i++) {
203                 if(children[i] instanceof documentElement.DocumentTextElement)
204                     return children[i];
205             }
206             return null;
207         }.bind(this);
208         var _markAsCurrent = function(element) {
209             if(element instanceof documentElement.DocumentTextElement) {
210                 this.wrapper.find('.current-text-element').removeClass('current-text-element');
211                 element.dom().addClass('current-text-element');
212             } else {
213                 this.wrapper.find('.current-node-element').removeClass('current-node-element')
214                 element._container().addClass('current-node-element');
215                 this.publisher('currentElementChanged', element);
216             }
217         }.bind(this);
218
219
220         var isTextElement = element instanceof documentElement.DocumentTextElement,
221             nodeElementToLand = isTextElement ? element.parent() : element,
222             textElementToLand = isTextElement ? element : findFirstDirectTextChild(element, nodeElementToLand),
223             currentTextElement = this.getCurrentTextElement(),
224             currentNodeElement = this.getCurrentNodeElement();
225
226         if(currentTextElement && !(currentTextElement.sameNode(textElementToLand)))
227             this.wrapper.find('.current-text-element').removeClass('current-text-element');
228
229         if(textElementToLand) {
230             _markAsCurrent(textElementToLand);
231             if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element))
232                 this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
233             if(!(textElementToLand.sameNode(currentTextElement)))
234                 this.publisher('currentTextElementSet', textElementToLand.data('wlxmlNode'));
235         } else {
236             document.getSelection().removeAllRanges();
237         }
238
239         if(!(currentNodeElement && currentNodeElement.sameNode(nodeElementToLand))) {
240             _markAsCurrent(nodeElementToLand);
241
242             this.publisher('currentNodeElementSet', nodeElementToLand.data('wlxmlNode'));
243         }
244     },
245
246     _moveCaretToTextElement: function(element, where) {
247         var range = document.createRange(),
248             node = element.dom().contents()[0];
249
250         if(typeof where !== 'number') {
251             range.selectNodeContents(node);
252         } else {
253             range.setStart(node, where);
254         }
255         
256         var collapseArg = true;
257         if(where === 'end')
258             collapseArg = false;
259         range.collapse(collapseArg);
260         
261         var selection = document.getSelection();
262
263         selection.removeAllRanges();
264         selection.addRange(range);
265         this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
266     },
267
268     setCursorPosition: function(position) {
269         if(position.element)
270             this._moveCaretToTextElement(position.element, position.offset);
271     }
272 });
273
274
275 var Cursor = function(canvas) {
276     this.canvas = canvas;
277 };
278
279 $.extend(Cursor.prototype, {
280     isSelecting: function() {
281         var selection = window.getSelection();
282         return !selection.isCollapsed;
283     },
284     isSelectingWithinElement: function() {
285         return this.isSelecting() && this.getSelectionStart().element.sameNode(this.getSelectionEnd().element);
286     },
287     isSelectingSiblings: function() {
288         return this.isSelecting() && this.getSelectionStart().element.parent().sameNode(this.getSelectionEnd().element.parent());
289     },
290     getPosition: function() {
291         return this.getSelectionAnchor();
292     },
293     getSelectionStart: function() {
294         return this.getSelectionBoundry('start');
295     },
296     getSelectionEnd: function() {
297         return this.getSelectionBoundry('end');
298     },
299     getSelectionAnchor: function() {
300         return this.getSelectionBoundry('anchor');
301     },
302     getSelectionFocus: function() {
303         return this.getSelectionBoundry('focus');
304     },
305     getSelectionBoundry: function(which) {
306         var selection = window.getSelection(),
307             anchorElement = this.canvas.getDocumentElement(selection.anchorNode),
308             focusElement = this.canvas.getDocumentElement(selection.focusNode);
309         
310         if((!anchorElement) || (anchorElement instanceof documentElement.DocumentNodeElement) || (!focusElement) || focusElement instanceof documentElement.DocumentNodeElement)
311             return {};
312
313         if(which === 'anchor') {
314             return {
315                 element: anchorElement,
316                 offset: selection.anchorOffset,
317                 offsetAtBeginning: selection.anchorOffset === 0,
318                 offsetAtEnd: selection.anchorNode.data.length === selection.anchorOffset
319             };
320         }
321         if(which === 'focus') {
322             return {
323                 element: focusElement,
324                 offset: selection.focusOffset,
325                 offsetAtBeginning: selection.focusOffset === 0,
326                 offsetAtEnd: selection.focusNode.data.length === selection.focusOffset
327             };
328         }
329         
330         var element,
331             offset;
332
333         if(anchorElement.parent().sameNode(focusElement.parent())) {
334             var parent = anchorElement.parent(),
335                 anchorFirst = parent.childIndex(anchorElement) < parent.childIndex(focusElement);
336             if(anchorFirst) {
337                 if(which === 'start') {
338                     element = anchorElement;
339                     offset = selection.anchorOffset
340                 }
341                 else if(which === 'end') {
342                     element = focusElement,
343                     offset = selection.focusOffset
344                 }
345             } else {
346                 if(which === 'start') {
347                     element = focusElement,
348                     offset = selection.focusOffset
349                 }
350                 else if(which === 'end') {
351                     element = anchorElement;
352                     offset = selection.anchorOffset
353                 }
354             }
355         } else {
356             // TODO: Handle order via https://developer.mozilla.org/en-US/docs/Web/API/Node.compareDocumentPosition
357             if(which === 'start') {
358                 element = anchorElement;
359                 offset = selection.anchorOffset
360             } else {
361                 element = focusElement;
362                 offset = selection.focusOffset
363             }
364         }
365
366         var nodeLen = (element.sameNode(focusElement) ? selection.focusNode : selection.anchorNode).length;
367         return {
368             element: element,
369             offset: offset,
370             offsetAtBeginning: offset === 0,
371             offsetAtEnd: nodeLen === offset
372         }
373     }
374 })
375
376 return {
377     fromXMLDocument: function(wlxmlDocument, publisher) {
378         return new Canvas(wlxmlDocument, publisher);
379     }
380 };
381
382 });