wip: extracting core transformations
[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.transform('smartxml.setText', {text: 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     list: {},
181
182     
183     getCurrentNodeElement: function() {
184         return this.getDocumentElement(this.wrapper.find('.current-node-element').parent()[0]);
185     },
186
187     getCurrentTextElement: function() {
188         return this.getDocumentElement(this.wrapper.find('.current-text-element')[0]);
189     },
190
191
192
193     setCurrentElement: function(element, params) {
194         if(!(element instanceof documentElement.DocumentElement)) {
195             element = utils.findCanvasElement(element);
196         }
197
198         params = _.extend({caretTo: 'end'}, params);
199         var findFirstDirectTextChild = function(e, nodeToLand) {
200             var byBrowser = this.getCursor().getPosition().element;
201             if(byBrowser && byBrowser.parent().sameNode(nodeToLand))
202                 return byBrowser;
203             var children = e.children();
204             for(var i = 0; i < children.length; i++) {
205                 if(children[i] instanceof documentElement.DocumentTextElement)
206                     return children[i];
207             }
208             return null;
209         }.bind(this);
210         var _markAsCurrent = function(element) {
211             if(element instanceof documentElement.DocumentTextElement) {
212                 this.wrapper.find('.current-text-element').removeClass('current-text-element');
213                 element.dom().addClass('current-text-element');
214             } else {
215                 this.wrapper.find('.current-node-element').removeClass('current-node-element')
216                 element._container().addClass('current-node-element');
217                 this.publisher('currentElementChanged', element);
218             }
219         }.bind(this);
220
221
222         var isTextElement = element instanceof documentElement.DocumentTextElement,
223             nodeElementToLand = isTextElement ? element.parent() : element,
224             textElementToLand = isTextElement ? element : findFirstDirectTextChild(element, nodeElementToLand),
225             currentTextElement = this.getCurrentTextElement(),
226             currentNodeElement = this.getCurrentNodeElement();
227
228         if(currentTextElement && !(currentTextElement.sameNode(textElementToLand)))
229             this.wrapper.find('.current-text-element').removeClass('current-text-element');
230
231         if(textElementToLand) {
232             _markAsCurrent(textElementToLand);
233             if(params.caretTo || !textElementToLand.sameNode(this.getCursor().getPosition().element))
234                 this._moveCaretToTextElement(textElementToLand, params.caretTo); // as method on element?
235             if(!(textElementToLand.sameNode(currentTextElement)))
236                 this.publisher('currentTextElementSet', textElementToLand.data('wlxmlNode'));
237         } else {
238             document.getSelection().removeAllRanges();
239         }
240
241         if(!(currentNodeElement && currentNodeElement.sameNode(nodeElementToLand))) {
242             _markAsCurrent(nodeElementToLand);
243
244             this.publisher('currentNodeElementSet', nodeElementToLand.data('wlxmlNode'));
245         }
246     },
247
248     _moveCaretToTextElement: function(element, where) {
249         var range = document.createRange(),
250             node = element.dom().contents()[0];
251
252         if(typeof where !== 'number') {
253             range.selectNodeContents(node);
254         } else {
255             range.setStart(node, where);
256         }
257         
258         var collapseArg = true;
259         if(where === 'end')
260             collapseArg = false;
261         range.collapse(collapseArg);
262         
263         var selection = document.getSelection();
264
265         selection.removeAllRanges();
266         selection.addRange(range);
267         this.wrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't.
268     },
269
270     setCursorPosition: function(position) {
271         if(position.element)
272             this._moveCaretToTextElement(position.element, position.offset);
273     }
274 });
275
276 $.extend(Canvas.prototype.list, {
277     create: function(params) {
278         if(!(params.element1.parent().sameNode(params.element2.parent())))
279             return false;
280             
281         var parent = params.element1.parent(),
282             canvas = params.element1.canvas;
283         
284         if(parent.childIndex(params.element1) > parent.childIndex(params.element2)) {
285             var tmp = params.element1;
286             params.element1 = params.element2;
287             params.element2 = tmp;
288         }
289         
290         var elementsToWrap = [];
291         
292         var place = 'before';
293         parent.children().some(function(element) {
294             var _e = element;
295             if(element.sameNode(params.element1))
296                 place = 'inside';
297             if(place === 'inside') {
298                 if(element instanceof documentElement.DocumentTextElement) {
299                     element = element.wrapWithNodeElement({tag: 'div', klass: 'list.item'});
300                     if(element.children()[0].sameNode(params.element1))
301                         params.element1 = element;
302                 }
303                 element.setWlxmlClass('item');
304                 elementsToWrap.push(element);
305             }
306             if(_e.sameNode(params.element2))
307                 return true;
308         });
309         
310         var listElement = canvas.createNodeElement({tag: 'div', klass: 'list-items' + (params.type === 'enum' ? '-enum' : '')});
311         var toret;
312         if(parent.is('list')) {
313             var item = listElement.wrapWithNodeElement({tag: 'div', klass: 'item'});
314             item.exec('toggleBullet', false);
315             toret = listElement.parent();
316         } else {
317             toret = listElement;
318         }  
319         
320         params.element1.before(toret);
321         
322         elementsToWrap.forEach(function(element) {
323             element.detach();
324             listElement.append(element);
325         });
326     },
327     extractItems: function(params) {
328         params = _.extend({merge: true}, params);
329         var list = params.element1.parent();
330         if(!list.is('list') || !(list.sameNode(params.element2.parent())))
331             return false;
332
333         var idx1 = list.childIndex(params.element1),
334             idx2 = list.childIndex(params.element2),
335             precedingItems = [],
336             extractedItems = [],
337             succeedingItems = [],
338             items = list.children(),
339             listIsNested = list.parent().getWlxmlClass() === 'item',
340             canvas = params.element1.canvas,
341             i;
342
343         if(idx1 > idx2) {
344             var tmp = idx1; idx1 = idx2; idx2 = tmp;
345         }
346
347         items.forEach(function(item, idx) {
348             if(idx < idx1)
349                 precedingItems.push(item);
350             else if(idx >= idx1 && idx <= idx2) {
351                 extractedItems.push(item);
352             }
353             else {
354                 succeedingItems.push(item);
355             }
356         });
357
358         var reference = listIsNested ? list.parent() : list;
359         if(succeedingItems.length === 0) {
360             var reference_orig = reference;
361             extractedItems.forEach(function(item) {
362                 reference.after(item);
363                 reference = item;
364                 if(!listIsNested)
365                     item.setWlxmlClass(null);
366             });
367             if(precedingItems.length === 0)
368                 reference_orig.detach();
369         } else if(precedingItems.length === 0) {
370             extractedItems.forEach(function(item) {
371                 reference.before(item);
372                 if(!listIsNested)
373                     item.setWlxmlClass(null);
374             });
375         } else {
376             extractedItems.forEach(function(item) {
377                 reference.after(item);
378                 if(!listIsNested)
379                     item.setWlxmlClass(null);
380                 reference = item;
381             });
382             var secondList = canvas.createNodeElement({tag: 'div', klass:'list-items'}),
383                 toAdd = secondList;
384             
385             if(listIsNested) {
386                 toAdd = secondList.wrapWithNodeElement({tag: 'div', klass:'item'});
387             }
388             succeedingItems.forEach(function(item) {
389                 secondList.append(item);
390             });
391
392             reference.after(toAdd);
393         }
394         if(!params.merge && listIsNested) {
395             return this.extractItems({element1: extractedItems[0], element2: extractedItems[extractedItems.length-1]});
396         }
397         return true;
398     },
399     areItemsOfTheSameList: function(params) {
400         var e1 = params.element1,
401             e2 = params.element2;
402         return e1.parent().sameNode(e2.parent())
403             && e1.parent().is('list');
404     }
405 });
406
407
408 var Cursor = function(canvas) {
409     this.canvas = canvas;
410 };
411
412 $.extend(Cursor.prototype, {
413     isSelecting: function() {
414         var selection = window.getSelection();
415         return !selection.isCollapsed;
416     },
417     isSelectingWithinElement: function() {
418         return this.isSelecting() && this.getSelectionStart().element.sameNode(this.getSelectionEnd().element);
419     },
420     isSelectingSiblings: function() {
421         return this.isSelecting() && this.getSelectionStart().element.parent().sameNode(this.getSelectionEnd().element.parent());
422     },
423     getPosition: function() {
424         return this.getSelectionAnchor();
425     },
426     getSelectionStart: function() {
427         return this.getSelectionBoundry('start');
428     },
429     getSelectionEnd: function() {
430         return this.getSelectionBoundry('end');
431     },
432     getSelectionAnchor: function() {
433         return this.getSelectionBoundry('anchor');
434     },
435     getSelectionFocus: function() {
436         return this.getSelectionBoundry('focus');
437     },
438     getSelectionBoundry: function(which) {
439         var selection = window.getSelection(),
440             anchorElement = this.canvas.getDocumentElement(selection.anchorNode),
441             focusElement = this.canvas.getDocumentElement(selection.focusNode);
442         
443         if((!anchorElement) || (anchorElement instanceof documentElement.DocumentNodeElement) || (!focusElement) || focusElement instanceof documentElement.DocumentNodeElement)
444             return {};
445
446         if(which === 'anchor') {
447             return {
448                 element: anchorElement,
449                 offset: selection.anchorOffset,
450                 offsetAtBeginning: selection.anchorOffset === 0,
451                 offsetAtEnd: selection.anchorNode.data.length === selection.anchorOffset
452             };
453         }
454         if(which === 'focus') {
455             return {
456                 element: focusElement,
457                 offset: selection.focusOffset,
458                 offsetAtBeginning: selection.focusOffset === 0,
459                 offsetAtEnd: selection.focusNode.data.length === selection.focusOffset
460             };
461         }
462         
463         var element,
464             offset;
465
466         if(anchorElement.parent().sameNode(focusElement.parent())) {
467             var parent = anchorElement.parent(),
468                 anchorFirst = parent.childIndex(anchorElement) < parent.childIndex(focusElement);
469             if(anchorFirst) {
470                 if(which === 'start') {
471                     element = anchorElement;
472                     offset = selection.anchorOffset
473                 }
474                 else if(which === 'end') {
475                     element = focusElement,
476                     offset = selection.focusOffset
477                 }
478             } else {
479                 if(which === 'start') {
480                     element = focusElement,
481                     offset = selection.focusOffset
482                 }
483                 else if(which === 'end') {
484                     element = anchorElement;
485                     offset = selection.anchorOffset
486                 }
487             }
488         } else {
489             // TODO: Handle order via https://developer.mozilla.org/en-US/docs/Web/API/Node.compareDocumentPosition
490             if(which === 'start') {
491                 element = anchorElement;
492                 offset = selection.anchorOffset
493             } else {
494                 element = focusElement;
495                 offset = selection.focusOffset
496             }
497         }
498
499         var nodeLen = (element.sameNode(focusElement) ? selection.focusNode : selection.anchorNode).length;
500         return {
501             element: element,
502             offset: offset,
503             offsetAtBeginning: offset === 0,
504             offsetAtEnd: nodeLen === offset
505         }
506     }
507 })
508
509 return {
510     fromXMLDocument: function(wlxmlDocument, publisher) {
511         return new Canvas(wlxmlDocument, publisher);
512     }
513 };
514
515 });