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