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