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