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