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