refactoring: fromHTMLElement @ canvas, no need to pass canvas 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(htmlElement, canvas) {
14     if(arguments.length === 0) {
15         return;
16     }
17     this.canvas = canvas;
18     this._setupDOMHandler(htmlElement);
19 };
20
21 $.extend(DocumentElement.prototype, {
22     _setupDOMHandler: function(htmlElement) {
23         this.$element = $(htmlElement);
24     },
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(htmlElement, canvas) {
119     DocumentElement.call(this, htmlElement, canvas);
120 };
121
122 $.extend(DocumentNodeElement, {
123     create: function(wlxmlNode, canvas) {
124         var dom = $('<div>')
125                 .attr('document-node-element', ''),
126             widgetsContainer = $('<div>')
127                 .addClass('canvas-widgets')
128                 .attr('contenteditable', false),
129             container = $('<div>')
130                 .attr('document-element-content', '');
131         
132         dom.append(widgetsContainer, container);
133         // Make sure widgets aren't navigable with arrow keys
134         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
135
136         var element = canvas.getDocumentElement(dom[0]);
137
138         element.data('wlxmlNode', wlxmlNode);
139         wlxmlNode.setData('canvasElement', element);
140
141         element.setWlxml({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()});
142
143         wlxmlNode.contents().forEach(function(node) {
144             container.append(canvas.createElement(node).dom());
145         }.bind(this));
146
147         return element;
148     }
149 });
150
151 var manipulate = function(e, params, action) {
152     var element;
153     if(params instanceof DocumentElement) {
154         element = params;
155     } else {
156         element = e.canvas.createElement(params);
157     }
158     var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom();
159     target[action](element.dom());
160     return element;
161 };
162
163 DocumentNodeElement.prototype = new DocumentElement();
164
165
166 $.extend(DocumentNodeElement.prototype, {
167     _container: function() {
168         return this.dom().children('[document-element-content]');
169     },
170     detach: function() {
171         this.dom().detach();
172         this.canvas = null;
173         return this;
174     },
175     append: function(params) {
176         return manipulate(this, params, 'append');
177     },
178     prepend: function(params) {
179         return manipulate(this, params, 'prepend');
180     },
181     before: function(params) {
182         return manipulate(this, params, 'before');
183
184     },
185     after: function(params) {
186         return manipulate(this, params, 'after');
187     },
188     children: function() {
189         var toret = [];
190         if(this instanceof DocumentTextElement) {
191             return toret;
192         }
193
194
195         var elementContent = this._container().contents();
196         var element = this;
197         elementContent.each(function() {
198             var childElement = element.canvas.getDocumentElement(this);
199             if(childElement === undefined) {
200                 return true;
201             }
202             toret.push(childElement);
203         });
204         return toret;
205     },
206     childIndex: function(child) {
207         var children = this.children(),
208             toret = null;
209         children.forEach(function(c, idx) {
210             if(c.sameNode(child)) {
211                 toret = idx;
212                 return false;
213             }
214         });
215         return toret;
216     },
217     getWlxmlTag: function() {
218         return this._container().attr('wlxml-tag');
219     },
220     setWlxmlTag: function(tag) {
221         if(tag === this.getWlxmlTag()) {
222             return;
223         }
224
225         this._container().attr('wlxml-tag', tag);
226         if(!this.__updatingWlxml) {
227             this._updateWlxmlManager();
228         }
229     },
230     getWlxmlClass: function() {
231         var klass = this._container().attr('wlxml-class');
232         if(klass) {
233             return klass.replace(/-/g, '.');
234         }
235         return undefined;
236     },
237     setWlxmlClass: function(klass) {
238         if(klass === this.getWlxmlClass()) {
239             return;
240         }
241         if(klass) {
242             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
243         }
244         else {
245             this._container().removeAttr('wlxml-class');
246         }
247         if(!this.__updatingWlxml) {
248             this._updateWlxmlManager();
249         }
250     },
251     setWlxml: function(params) {
252         this.__updatingWlxml = true;
253         if(params.tag !== undefined) {
254             this.setWlxmlTag(params.tag);
255         }
256         if(params.klass !== undefined) {
257             this.setWlxmlClass(params.klass);
258         }
259         this._updateWlxmlManager();
260         this.__updatingWlxml = false;
261     },
262     _updateWlxmlManager: function() {
263         var manager = wlxmlManagers.getFor(this);
264         this.data('_wlxmlManager', manager);
265         manager.setup();
266     },
267     is: function(what) {
268         if(what === 'list' && _.contains(['list.items', 'list.items.enum'], this.getWlxmlClass())) {
269             return true;
270         }
271         return false;
272     },
273     toggleLabel: function(toggle) {
274         var displayCss = toggle ? 'inline-block' : 'none';
275         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
276         label.css('display', displayCss);
277         this.toggleHighlight(toggle);
278     },
279
280     toggleHighlight: function(toggle) {
281         this._container().toggleClass('highlighted-element', toggle);
282     },
283
284     toggle: function(toggle) {
285         var mng = this.data('_wlxmlManager');
286         if(mng) {
287             mng.toggle(toggle);
288         }
289     }
290 });
291
292
293 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
294 var DocumentTextElement = function(htmlElement, canvas) {
295     DocumentElement.call(this, htmlElement, canvas);
296 };
297
298 $.extend(DocumentTextElement, {
299     create: function(wlxmlTextNode, canvas) {
300         var dom = $('<div>')
301             .attr('document-text-element', '')
302             .text(wlxmlTextNode.getText() || utils.unicode.ZWS),
303         element = canvas.getDocumentElement(dom[0]);
304         element.data('wlxmlNode', wlxmlTextNode);
305         return element;
306     },
307
308     isContentContainer: function(htmlElement) {
309         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
310     }
311 });
312
313 DocumentTextElement.prototype = new DocumentElement();
314
315 $.extend(DocumentTextElement.prototype, {
316     _setupDOMHandler: function(htmlElement) {
317         var $element = $(htmlElement);
318         if(htmlElement.nodeType === Node.TEXT_NODE) {
319             this.$element = $element.parent();
320         }
321         else {
322             this.$element = $element;
323         }
324     },
325     detach: function() {
326         this.dom().detach();
327         this.canvas = null;
328         return this;
329     },
330     setText: function(text) {
331         this.dom().contents()[0].data = text;
332     },
333     getText: function(options) {
334         options = _.extend({raw: false}, options || {});
335         var toret = this.dom().text();
336         if(!options.raw) {
337             toret = toret.replace(utils.unicode.ZWS, '');
338         }
339         return toret;
340     },
341     isEmpty: function() {
342         // Having at least Zero Width Space is guaranteed be Content Observer
343         return this.dom().contents()[0].data === utils.unicode.ZWS;
344     },
345     after: function(params) {
346         if(params instanceof DocumentTextElement || params.text) {
347             return false;
348         }
349         var element;
350         if(params instanceof DocumentNodeElement) {
351             element = params;
352         } else {
353             element = this.canvas.createElement(params);
354         }
355         this.dom().wrap('<div>');
356         this.dom().parent().after(element.dom());
357         this.dom().unwrap();
358         return element;
359     },
360     before: function(params) {
361         if(params instanceof DocumentTextElement || params.text) {
362             return false;
363         }
364         var element;
365         if(params instanceof DocumentNodeElement) {
366             element = params;
367         } else {
368             element = this.canvas.createElement(params);
369         }
370         this.dom().wrap('<div>');
371         this.dom().parent().before(element.dom());
372         this.dom().unwrap();
373         return element;
374     },
375
376     toggleHighlight: function() {
377         // do nothing for now
378     }
379 });
380
381 return {
382     DocumentElement: DocumentElement,
383     DocumentNodeElement: DocumentNodeElement,
384     DocumentTextElement: DocumentTextElement
385 };
386
387 });