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