wip: moving non wlxml specific extension support from wlxml.js to smartxml.js
[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         var parent = this.parent();
199         if(!parent)
200             return;
201
202         var parentChildren = parent.children(),
203             myIdx = parent.childIndex(this);
204
205         if(myIdx > 0 && myIdx < parentChildren.length) {
206             if((parentChildren[myIdx-1] instanceof DocumentTextElement) && (parentChildren[myIdx+1] instanceof DocumentTextElement)) {
207                 parentChildren[myIdx-1].appendText(parentChildren[myIdx+1].getText());
208                 parentChildren[myIdx+1].detach();
209             }
210         }
211         this.dom().detach();
212         this.canvas = null;
213         return this;
214     },
215     append: function(params) {
216         if(params.tag !== 'span')
217             this.data('orig-end', undefined);
218         return manipulate(this, params, 'append');
219     },
220     prepend: function(params) {
221         return manipulate(this, params, 'prepend');
222     },
223     before: function(params) {
224         return manipulate(this, params, 'before');
225
226     },
227     after: function(params) {
228         return manipulate(this, params, 'after');
229     },
230     children: function() {
231         var toret = [];
232         if(this instanceof DocumentTextElement)
233             return toret;
234
235
236         var elementContent = this._container().contents();
237         var element = this;
238         elementContent.each(function(idx) {
239             var childElement = DocumentElement.fromHTMLElement(this, element.canvas);
240             if(childElement === undefined)
241                 return true;
242             if(idx === 0 && elementContent.length > 1 && elementContent[1].nodeType === Node.ELEMENT_NODE && (childElement instanceof DocumentTextElement) && $.trim($(this).text()) === '')
243                 return true;
244             if(idx > 0 && childElement instanceof DocumentTextElement) {
245                 if(toret[toret.length-1] instanceof DocumentNodeElement && $.trim($(this).text()) === '')
246                     return true;
247             }
248             toret.push(childElement);
249         });
250         return toret;
251     },
252     childIndex: function(child) {
253         var children = this.children(),
254             toret = null;
255         children.forEach(function(c, idx) {
256             if(c.sameNode(child)) {
257                 toret = idx;
258                 return false;
259             }
260         });
261         return toret;
262     },
263     getWlxmlTag: function() {
264         return this._container().attr('wlxml-tag');
265     },
266     setWlxmlTag: function(tag) {
267         if(tag === this.getWlxmlTag())
268             return;
269
270         this._container().attr('wlxml-tag', tag);
271         if(!this.__updatingWlxml)
272             this._updateWlxmlManager();
273     },
274     getWlxmlClass: function() {
275         var klass = this._container().attr('wlxml-class');
276         if(klass)
277             return klass.replace(/-/g, '.');
278         return undefined;
279     },
280     setWlxmlClass: function(klass) {
281         if(klass === this.getWlxmlClass())
282             return;
283         if(klass)
284             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
285         else
286             this._container().removeAttr('wlxml-class');
287         if(!this.__updatingWlxml)
288             this._updateWlxmlManager();
289     },
290     setWlxml: function(params) {
291         this.__updatingWlxml = true;
292         if(params.tag !== undefined)
293             this.setWlxmlTag(params.tag);
294         if(params.klass !== undefined)
295             this.setWlxmlClass(params.klass);
296         this._updateWlxmlManager();
297         this.__updatingWlxml = false;
298     },
299     _updateWlxmlManager: function() {
300         var manager = wlxmlManagers.getFor(this);
301         this.data('_wlxmlManager', manager);
302         manager.setup();
303     },
304     is: function(what) {
305         if(what === 'list' && _.contains(['list.items', 'list.items.enum'], this.getWlxmlClass()))
306             return true;
307         return false;
308     },
309     toggleLabel: function(toggle) {
310         var displayCss = toggle ? 'inline-block' : 'none';
311         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
312         label.css('display', displayCss);
313         this.toggleHighlight(toggle);
314     },
315
316     toggleHighlight: function(toggle) {
317         this._container().toggleClass('highlighted-element', toggle);
318     },
319
320     toggle: function(toggle) {
321         var mng = this.data('_wlxmlManager');
322         if(mng) {
323             mng.toggle(toggle);
324         }
325     }
326 });
327
328
329 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
330 var DocumentTextElement = function(htmlElement, canvas) {
331     DocumentElement.call(this, htmlElement, canvas);
332 };
333
334 $.extend(DocumentTextElement, {
335     createDOM: function(wlxmlTextNode) {
336         var dom = $('<div>')
337             .attr('document-text-element', '')
338             .text(wlxmlTextNode.getText() || utils.unicode.ZWS),
339         element = this.fromHTMLElement(dom[0], this);
340         element.data('wlxmlNode', wlxmlTextNode);
341         return dom;
342     },
343
344     create: function(wlxmlTextNode, canvas) {
345         return this.fromHTMLElement(this.createDOM(wlxmlTextNode)[0]);
346     },
347
348     fromHTMLElement: function(htmlElement, canvas) {
349         return new this(htmlElement, canvas);
350     },
351     isContentContainer: function(htmlElement) {
352         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
353     }
354 });
355
356 DocumentTextElement.prototype = new DocumentElement();
357
358 $.extend(DocumentTextElement.prototype, {
359     _setupDOMHandler: function(htmlElement) {
360         var $element = $(htmlElement);
361         if(htmlElement.nodeType === Node.TEXT_NODE)
362             this.$element = $element.parent();
363         else
364             this.$element = $element;
365     },
366     detach: function() {
367         this.dom().detach();
368         this.canvas = null;
369         return this;
370     },
371     setText: function(text) {
372         this.dom().contents()[0].data = text;
373     },
374     appendText: function(text) {
375         this.dom().contents()[0].data += text;
376     },
377     prependText: function(text) {
378         this.dom().contents()[0].data = text + this.dom().contents()[0].data;
379     },
380     getText: function(options) {
381         options = _.extend({raw: false}, options || {});
382         var toret = this.dom().text();
383         if(!options.raw) {
384             toret = toret.replace(utils.unicode.ZWS, '');
385         }
386         return toret;
387     },
388     isEmpty: function() {
389         // Having at least Zero Width Space is guaranteed be Content Observer
390         return this.dom().contents()[0].data === utils.unicode.ZWS;
391     },
392     after: function(params) {
393         if(params instanceof DocumentTextElement || params.text)
394             return false;
395         var element;
396         if(params instanceof DocumentNodeElement) {
397             element = params;
398         } else {
399             element = DocumentElement.create(params, this.canvas);
400         }
401         this.dom().wrap('<div>');
402         this.dom().parent().after(element.dom());
403         this.dom().unwrap();
404         return element;
405     },
406     before: function(params) {
407         if(params instanceof DocumentTextElement || params.text)
408             return false;
409         var element;
410         if(params instanceof DocumentNodeElement) {
411             element = params;
412         } else {
413             element = DocumentNodeElement.create(params, this.canvas);
414         }
415         this.dom().wrap('<div>');
416         this.dom().parent().before(element.dom());
417         this.dom().unwrap();
418         return element;
419     },
420
421     divide: function(params) {
422         var myText = this.getText();
423
424         if(params.offset === myText.length)
425             return this.after(params);
426         if(params.offset === 0)
427             return this.before(params);
428
429         var lhsText = myText.substr(0, params.offset),
430             rhsText = myText.substr(params.offset),
431             newElement = DocumentNodeElement.create({tag: params.tag, klass: params.klass}, this.canvas),
432             rhsTextElement = DocumentTextElement.create({text: rhsText});
433
434         this.setText(lhsText);
435         this.after(newElement);
436         newElement.after(rhsTextElement);
437         return newElement;
438     },
439
440     toggleHighlight: function() {
441         // do nothing for now
442     }
443 });
444
445 return {
446     DocumentElement: DocumentElement,
447     DocumentNodeElement: DocumentNodeElement,
448     DocumentTextElement: DocumentTextElement
449 };
450
451 });