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