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