editor: fix selecting text spanning multiple nodes in Chrome
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / documentElement.js
1 define([
2 'libs/jquery',
3 'libs/underscore',
4 'modules/documentCanvas/canvas/utils',
5 'modules/documentCanvas/canvas/wlxmlManagers'
6 ], function($, _, utils, wlxmlManagers) {
7     
8 'use strict';
9 /* global Node:false, document:false */
10
11
12 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
13 var DocumentElement = function(htmlElement, canvas) {
14     if(arguments.length === 0) {
15         return;
16     }
17     this.canvas = canvas;
18     this._setupDOMHandler(htmlElement);
19 };
20
21
22 var elementTypeFromWlxmlNode = function(wlxmlNode) {
23     return wlxmlNode.nodeType === Node.TEXT_NODE ? DocumentTextElement : DocumentNodeElement;
24 };
25
26 $.extend(DocumentElement, {
27     create: function(node, canvas) {
28         return elementTypeFromWlxmlNode(node).create(node, canvas);
29     },
30
31     fromHTMLElement: function(htmlElement, canvas) {
32         var $element = $(htmlElement);
33         if(htmlElement.nodeType === Node.ELEMENT_NODE && $element.attr('document-node-element') !== undefined) {
34             return DocumentNodeElement.fromHTMLElement(htmlElement, canvas);
35         }
36         if($element.attr('document-text-element') !== undefined || (htmlElement.nodeType === Node.TEXT_NODE && $element.parent().attr('document-text-element') !== undefined)) {
37             return DocumentTextElement.fromHTMLElement(htmlElement, canvas);
38         }
39         return undefined;
40     }
41 });
42
43 $.extend(DocumentElement.prototype, {
44     _setupDOMHandler: function(htmlElement) {
45         this.$element = $(htmlElement);
46     },
47     bound: function() {
48         return $.contains(document.documentElement, this.dom()[0]);
49     },
50     dom: function() {
51         return this.$element;
52     },
53     data: function() {
54         var dom = this.dom(),
55             args = Array.prototype.slice.call(arguments, 0);
56         if(args.length === 2 && args[1] === undefined) {
57             return dom.removeData(args[0]);
58         }
59         return dom.data.apply(dom, arguments);
60     },
61     parent: function() {
62         var parents = this.$element.parents('[document-node-element]');
63         if(parents.length) {
64             return DocumentElement.fromHTMLElement(parents[0], this.canvas);
65         }
66         return null;
67     },
68
69     parents: function() {
70         var parents = [],
71             parent = this.parent();
72         while(parent) {
73             parents.push(parent);
74             parent = parent.parent();
75         }
76         return parents;
77     },
78
79     sameNode: function(other) {
80         return other && (typeof other === typeof this) && other.dom()[0] === this.dom()[0];
81     },
82
83     markAsCurrent: function() {
84         this.canvas.markAsCurrent(this);
85     },
86
87     getVerticallyFirstTextElement: function() {
88         var toret;
89         this.children().some(function(child) {
90             if(!child.isVisible()) {
91                 return false; // continue
92             }
93             if(child instanceof DocumentTextElement) {
94                 toret = child;
95                 return true; // break
96             } else {
97                 toret = child.getVerticallyFirstTextElement();
98                 if(toret) {
99                     return true; // break
100                 }
101             }
102         });
103         return toret;
104     },
105
106     getPreviousTextElement: function(includeInvisible) {
107         return this.getNearestTextElement('above', includeInvisible);
108     },
109
110     getNextTextElement: function(includeInvisible) {
111         return this.getNearestTextElement('below', includeInvisible);
112     },
113
114     getNearestTextElement: function(direction, includeInvisible) {
115         includeInvisible = includeInvisible !== undefined ? includeInvisible : false;
116         var selector = '[document-text-element]' + (includeInvisible ? '' : ':visible');
117         return this.canvas.getDocumentElement(utils.nearestInDocumentOrder(selector, direction, this.dom()[0]));
118     },
119
120     isVisible: function() {
121         return this instanceof DocumentTextElement || this.getWlxmlTag() !== 'metadata';
122     },
123
124     isInsideList: function() {
125         return this.parents().some(function(parent) {
126             return parent.is('list');
127         });
128     },
129
130     exec: function(method) {
131         var manager = this.data('_wlxmlManager');
132         if(manager[method]) {
133             return manager[method].apply(manager, Array.prototype.slice.call(arguments, 1));
134         }
135     }
136 });
137
138
139 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
140 var DocumentNodeElement = function(htmlElement, canvas) {
141     DocumentElement.call(this, htmlElement, canvas);
142 };
143
144 $.extend(DocumentNodeElement, {
145     create: function(wlxmlNode, canvas) {
146         return this.fromHTMLElement(this.createDOM(wlxmlNode, canvas)[0], canvas);
147     },
148
149     fromHTMLElement: function(htmlElement, canvas) {
150         return new this(htmlElement, canvas);
151     },
152
153     createDOM: function(wlxmlNode, canvas) {
154         var dom = $('<div>')
155                 .attr('document-node-element', ''),
156             widgetsContainer = $('<div>')
157                 .addClass('canvas-widgets')
158                 .attr('contenteditable', false),
159             container = $('<div>')
160                 .attr('document-element-content', '');
161         
162         dom.append(widgetsContainer, container);
163         // Make sure widgets aren't navigable with arrow keys
164         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
165
166         var element = this.fromHTMLElement(dom[0], canvas);
167
168         element.data('wlxmlNode', wlxmlNode);
169         wlxmlNode.setData('canvasElement', element);
170
171         element.setWlxml({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()});
172
173         wlxmlNode.contents().forEach(function(node) {
174             container.append(DocumentElement.create(node, canvas).dom());
175         }.bind(this));
176
177         return dom;
178     }
179
180 });
181
182 var manipulate = function(e, params, action) {
183     var element;
184     if(params instanceof DocumentElement) {
185         element = params;
186     } else {
187         element = DocumentElement.create(params);
188     }
189     var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom();
190     target[action](element.dom());
191     return element;
192 };
193
194 DocumentNodeElement.prototype = new DocumentElement();
195
196
197 $.extend(DocumentNodeElement.prototype, {
198     _container: function() {
199         return this.dom().children('[document-element-content]');
200     },
201     detach: function() {
202         this.dom().detach();
203         this.canvas = null;
204         return this;
205     },
206     append: function(params) {
207         return manipulate(this, params, 'append');
208     },
209     prepend: function(params) {
210         return manipulate(this, params, 'prepend');
211     },
212     before: function(params) {
213         return manipulate(this, params, 'before');
214
215     },
216     after: function(params) {
217         return manipulate(this, params, 'after');
218     },
219     children: function() {
220         var toret = [];
221         if(this instanceof DocumentTextElement) {
222             return toret;
223         }
224
225
226         var elementContent = this._container().contents();
227         var element = this;
228         elementContent.each(function() {
229             var childElement = DocumentElement.fromHTMLElement(this, element.canvas);
230             if(childElement === undefined) {
231                 return true;
232             }
233             toret.push(childElement);
234         });
235         return toret;
236     },
237     childIndex: function(child) {
238         var children = this.children(),
239             toret = null;
240         children.forEach(function(c, idx) {
241             if(c.sameNode(child)) {
242                 toret = idx;
243                 return false;
244             }
245         });
246         return toret;
247     },
248     getWlxmlTag: function() {
249         return this._container().attr('wlxml-tag');
250     },
251     setWlxmlTag: function(tag) {
252         if(tag === this.getWlxmlTag()) {
253             return;
254         }
255
256         this._container().attr('wlxml-tag', tag);
257         if(!this.__updatingWlxml) {
258             this._updateWlxmlManager();
259         }
260     },
261     getWlxmlClass: function() {
262         var klass = this._container().attr('wlxml-class');
263         if(klass) {
264             return klass.replace(/-/g, '.');
265         }
266         return undefined;
267     },
268     setWlxmlClass: function(klass) {
269         if(klass === this.getWlxmlClass()) {
270             return;
271         }
272         if(klass) {
273             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
274         }
275         else {
276             this._container().removeAttr('wlxml-class');
277         }
278         if(!this.__updatingWlxml) {
279             this._updateWlxmlManager();
280         }
281     },
282     setWlxml: function(params) {
283         this.__updatingWlxml = true;
284         if(params.tag !== undefined) {
285             this.setWlxmlTag(params.tag);
286         }
287         if(params.klass !== undefined) {
288             this.setWlxmlClass(params.klass);
289         }
290         this._updateWlxmlManager();
291         this.__updatingWlxml = false;
292     },
293     _updateWlxmlManager: function() {
294         var manager = wlxmlManagers.getFor(this);
295         this.data('_wlxmlManager', manager);
296         manager.setup();
297     },
298     is: function(what) {
299         if(what === 'list' && _.contains(['list.items', 'list.items.enum'], this.getWlxmlClass())) {
300             return true;
301         }
302         return false;
303     },
304     toggleLabel: function(toggle) {
305         var displayCss = toggle ? 'inline-block' : 'none';
306         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
307         label.css('display', displayCss);
308         this.toggleHighlight(toggle);
309     },
310
311     toggleHighlight: function(toggle) {
312         this._container().toggleClass('highlighted-element', toggle);
313     },
314
315     toggle: function(toggle) {
316         var mng = this.data('_wlxmlManager');
317         if(mng) {
318             mng.toggle(toggle);
319         }
320     }
321 });
322
323
324 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
325 var DocumentTextElement = function(htmlElement, canvas) {
326     DocumentElement.call(this, htmlElement, canvas);
327 };
328
329 $.extend(DocumentTextElement, {
330     createDOM: function(wlxmlTextNode, canvas) {
331         var dom = $('<div>')
332             .attr('document-text-element', '')
333             .text(wlxmlTextNode.getText() || utils.unicode.ZWS),
334         element = this.fromHTMLElement(dom[0], canvas);
335         element.data('wlxmlNode', wlxmlTextNode);
336         return dom;
337     },
338
339     create: function(wlxmlTextNode, canvas) {
340         return this.fromHTMLElement(this.createDOM(wlxmlTextNode, canvas)[0], canvas);
341     },
342
343     fromHTMLElement: function(htmlElement, canvas) {
344         return new this(htmlElement, canvas);
345     },
346     isContentContainer: function(htmlElement) {
347         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
348     }
349 });
350
351 DocumentTextElement.prototype = new DocumentElement();
352
353 $.extend(DocumentTextElement.prototype, {
354     _setupDOMHandler: function(htmlElement) {
355         var $element = $(htmlElement);
356         if(htmlElement.nodeType === Node.TEXT_NODE) {
357             this.$element = $element.parent();
358         }
359         else {
360             this.$element = $element;
361         }
362     },
363     detach: function() {
364         this.dom().detach();
365         this.canvas = null;
366         return this;
367     },
368     setText: function(text) {
369         this.dom().contents()[0].data = text;
370     },
371     getText: function(options) {
372         options = _.extend({raw: false}, options || {});
373         var toret = this.dom().text();
374         if(!options.raw) {
375             toret = toret.replace(utils.unicode.ZWS, '');
376         }
377         return toret;
378     },
379     isEmpty: function() {
380         // Having at least Zero Width Space is guaranteed be Content Observer
381         return this.dom().contents()[0].data === utils.unicode.ZWS;
382     },
383     after: function(params) {
384         if(params instanceof DocumentTextElement || params.text) {
385             return false;
386         }
387         var element;
388         if(params instanceof DocumentNodeElement) {
389             element = params;
390         } else {
391             element = DocumentElement.create(params, this.canvas);
392         }
393         this.dom().wrap('<div>');
394         this.dom().parent().after(element.dom());
395         this.dom().unwrap();
396         return element;
397     },
398     before: function(params) {
399         if(params instanceof DocumentTextElement || params.text) {
400             return false;
401         }
402         var element;
403         if(params instanceof DocumentNodeElement) {
404             element = params;
405         } else {
406             element = DocumentNodeElement.create(params, this.canvas);
407         }
408         this.dom().wrap('<div>');
409         this.dom().parent().before(element.dom());
410         this.dom().unwrap();
411         return element;
412     },
413
414     divide: function(params) {
415         var myText = this.getText();
416
417         if(params.offset === myText.length) {
418             return this.after(params);
419         }
420         if(params.offset === 0) {
421             return this.before(params);
422         }
423
424         var lhsText = myText.substr(0, params.offset),
425             rhsText = myText.substr(params.offset),
426             newElement = DocumentNodeElement.create({tag: params.tag, klass: params.klass}, this.canvas),
427             rhsTextElement = DocumentTextElement.create({text: rhsText});
428
429         this.setText(lhsText);
430         this.after(newElement);
431         newElement.after(rhsTextElement);
432         return newElement;
433     },
434
435     toggleHighlight: function() {
436         // do nothing for now
437     }
438 });
439
440 return {
441     DocumentElement: DocumentElement,
442     DocumentNodeElement: DocumentNodeElement,
443     DocumentTextElement: DocumentTextElement
444 };
445
446 });