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