Fix: missing canvas reference
[fnpeditor.git] / modules / documentCanvas / canvas / documentElement.js
1 define([
2 'libs/jquery-1.9.1.min',
3 'libs/underscore-min',
4 'modules/documentCanvas/classAttributes',
5 'modules/documentCanvas/canvas/utils',
6 'modules/documentCanvas/canvas/widgets',
7 'modules/documentCanvas/canvas/wlxmlManagers'
8 ], function($, _, classAttributes, utils, widgets, wlxmlManagers) {
9     
10 'use strict';
11
12
13 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
14 var DocumentElement = function(htmlElement, canvas) {
15     if(arguments.length === 0)
16         return;
17     this.canvas = canvas;
18     this._setupDOMHandler(htmlElement);
19 }
20
21 var elementTypeFromParams = function(params) {
22     return params.text !== undefined ? DocumentTextElement : DocumentNodeElement;
23
24 };
25
26 $.extend(DocumentElement, {
27     create: function(params, canvas) {
28         return elementTypeFromParams(params).create(params);
29     },
30
31     createDOM: function(params) {
32         return elementTypeFromParams(params).createDOM(params);
33     },
34
35     fromHTMLElement: function(htmlElement, canvas) {
36         var $element = $(htmlElement);
37         if(htmlElement.nodeType === Node.ELEMENT_NODE && $element.attr('document-node-element') !== undefined)
38             return DocumentNodeElement.fromHTMLElement(htmlElement, canvas);
39         if($element.attr('document-text-element') !== undefined || (htmlElement.nodeType === Node.TEXT_NODE && $element.parent().attr('document-text-element') !== undefined))
40             return DocumentTextElement.fromHTMLElement(htmlElement, canvas);
41         return undefined;
42     }
43 });
44
45 $.extend(DocumentElement.prototype, {
46     _setupDOMHandler: function(htmlElement) {
47         this.$element = $(htmlElement);
48     },
49     bound: function() {
50         return this.canvas !== undefined;
51     },
52     dom: function() {
53         return this.$element;
54     },
55     parent: function() {
56         var parents = this.$element.parents('[document-node-element]');
57         if(parents.length)
58             return DocumentElement.fromHTMLElement(parents[0], this.canvas);
59         return null;
60     },
61
62     parents: function() {
63         var parents = [],
64             parent = this.parent();
65         while(parent) {
66             parents.push(parent);
67             parent = parent.parent();
68         }
69         return parents;
70     },
71
72     sameNode: function(other) {
73         return other && (typeof other === typeof this) && other.dom()[0] === this.dom()[0];
74     },
75
76     wrapWithNodeElement: function(wlxmlNode) {
77         var wrapper = DocumentNodeElement.create({tag: wlxmlNode.tag, klass: wlxmlNode.klass}, this);
78         this.dom().replaceWith(wrapper.dom());
79         wrapper.append(this);
80         return wrapper;
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             if(child instanceof DocumentTextElement) {
93                 toret = child;
94                 return true; // break
95             } else {
96                 toret = child.getVerticallyFirstTextElement();
97                 if(toret)
98                     return true; // break
99             }
100         });
101         return toret;
102     },
103
104     isVisible: function() {
105         return this instanceof DocumentTextElement || this.getWlxmlTag() !== 'metadata';
106     },
107
108     isInsideList: function() {
109         return this.parents().some(function(parent) {
110             return parent.is('list');
111         });
112     }
113 });
114
115
116 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
117 var DocumentNodeElement = function(htmlElement, canvas) {
118     DocumentElement.call(this, htmlElement, canvas);
119 };
120
121 $.extend(DocumentNodeElement, {
122     createDOM: function(params, canvas) {
123         var dom = $('<div>')
124                 .attr('document-node-element', ''),
125             widgetsContainer = $('<div>')
126                 .addClass('canvas-widgets')
127                 .attr('contenteditable', false),
128             container = $('<div>')
129                 .attr('document-element-content', '');
130         
131         dom.append(widgetsContainer, container);
132         // Make sure widgets aren't navigable with arrow keys
133         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
134
135         var element = this.fromHTMLElement(dom[0], canvas);
136
137         element.setWlxml({tag: params.tag, klass: params.klass});
138         if(params.meta) {
139             _.keys(params.meta).forEach(function(key) {
140                 element.setWlxmlMetaAttr(key, params.meta[key]);
141             });
142         }
143         element.data('other-attrs', params.others);
144
145         if(params.rawChildren) {
146             container.append(params.rawChildren);
147         }
148         return dom;
149     },
150
151     create: function(params, canvas) {
152         return this.fromHTMLElement(this.createDOM(params, canvas)[0], canvas);
153     },
154
155     fromHTMLElement: function(htmlElement, canvas) {
156         return new this(htmlElement, canvas);
157     }
158 });
159
160 var manipulate = function(e, params, action) {
161     var element;
162     if(params instanceof DocumentElement) {
163         element = params;
164     } else {
165         element = DocumentElement.create(params);
166     }
167     var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom();
168     target[action](element.dom());
169     return element;
170 };
171
172 DocumentNodeElement.prototype = new DocumentElement();
173
174
175 $.extend(DocumentNodeElement.prototype, {
176     _container: function() {
177         return this.dom().children('[document-element-content]');
178     },
179     detach: function() {
180         var parent = this.parent();
181         if(!parent)
182             return;
183
184         var parentChildren = parent.children(),
185             myIdx = parent.childIndex(this);
186
187         if(myIdx > 0 && myIdx < parentChildren.length) {
188             if((parentChildren[myIdx-1] instanceof DocumentTextElement) && (parentChildren[myIdx+1] instanceof DocumentTextElement)) {
189                 parentChildren[myIdx-1].appendText(parentChildren[myIdx+1].getText());
190                 parentChildren[myIdx+1].detach();
191             }
192         }
193         this.dom().detach();
194         this.canvas = null;
195         return this;
196     },
197     data: function() {
198         var dom = this.dom(),
199             args = Array.prototype.slice.call(arguments, 0);
200         if(args.length === 2 && args[1] === undefined)
201             return dom.removeData(args[0]);
202         return dom.data.apply(dom, arguments);
203     },
204     toXML: function(level) {
205         var node = $('<' + this.getWlxmlTag() + '>');
206
207         if(this.getWlxmlClass())
208             node.attr('class', this.getWlxmlClass());
209         var meta = this.getWlxmlMetaAttrs();
210         meta.forEach(function(attr) {
211             if(attr.value)
212                 node.attr('meta-' + attr.name, attr.value);
213         });
214         _.keys(this.data('other-attrs') || {}).forEach(function(key) {
215             node.attr(key, this.data('other-attrs')[key]);
216         }, this);
217
218         var addFormatting = function() {
219             var toret = $('<div>');
220             var formattings = {};
221
222             if(this.data('orig-before') !== undefined) {
223                 if(this.data('orig-before')) {
224                     toret.prepend(document.createTextNode(this.data('orig-before')));
225                 }
226             } else if(level && this.getWlxmlTag() !== 'span') {
227                 toret.append('\n' + (new Array(level * 2 + 1)).join(' '));
228             }
229
230             toret.append(node);
231
232             if(this.data('orig-after')) {
233                 toret.append(document.createTextNode(this.data('orig-after')));
234             }
235
236             /* Inside node */
237             if(this.data('orig-begin')) {
238                 node.prepend(this.data('orig-begin'));
239                 formattings.begin = true;
240             }
241
242             if(this.data('orig-end') !== undefined) {
243                 if(this.data('orig-end')) {
244                     node.append(this.data('orig-end'));
245                 }
246             } else if(this.getWlxmlTag() !== 'span' && children.length){
247                 node.append('\n' + (new Array(level * 2 + 1)).join(' '));
248             }
249            
250             return {parts: toret.contents(), formattings: formattings};
251         }.bind(this);
252
253         
254         
255         var children = this.children(),
256             childParts;
257
258         var formatting = addFormatting(node);
259
260         for(var i = children.length - 1; i >= 0; i--) {
261             childParts = children[i].toXML(level + 1);
262             if(typeof childParts === 'string')
263                 childParts = [document.createTextNode(childParts)];
264
265             if(formatting.formattings.begin) {
266                 $(node.contents()[0]).after(childParts);
267             } else
268                 node.prepend(childParts);
269         }
270         return formatting.parts;
271     },
272     append: function(params) {
273         if(params.tag !== 'span')
274             this.data('orig-end', undefined);
275         return manipulate(this, params, 'append');
276     },
277     prepend: function(params) {
278         return manipulate(this, params, 'prepend');
279     },
280     before: function(params) {
281         return manipulate(this, params, 'before');
282
283     },
284     after: function(params) {
285         return manipulate(this, params, 'after');
286     },
287     children: function() {
288         var toret = [];
289         if(this instanceof DocumentTextElement)
290             return toret;
291
292
293         var elementContent = this._container().contents();
294         var element = this;
295         elementContent.each(function(idx) {
296             var childElement = DocumentElement.fromHTMLElement(this, element.canvas);
297             if(childElement === undefined)
298                 return true;
299             if(idx === 0 && elementContent.length > 1 && elementContent[1].nodeType === Node.ELEMENT_NODE && (childElement instanceof DocumentTextElement) && $.trim($(this).text()) === '')
300                 return true;
301             if(idx > 0 && childElement instanceof DocumentTextElement) {
302                 if(toret[toret.length-1] instanceof DocumentNodeElement && $.trim($(this).text()) === '')
303                     return true;
304             }
305             toret.push(childElement);
306         });
307         return toret;
308     },
309     childIndex: function(child) {
310         var children = this.children(),
311             toret = null;
312         children.forEach(function(c, idx) {
313             if(c.sameNode(child)) {
314                 toret = idx;
315                 return false;
316             }
317         });
318         return toret;
319     },
320     getWlxmlTag: function() {
321         return this._container().attr('wlxml-tag');
322     },
323     setWlxmlTag: function(tag) {
324         if(tag === this.getWlxmlTag())
325             return;
326
327         this._container().attr('wlxml-tag', tag);
328         if(!this.__updatingWlxml)
329             this._updateWlxmlManager();
330     },
331     getWlxmlClass: function() {
332         var klass = this._container().attr('wlxml-class');
333         if(klass)
334             return klass.replace(/-/g, '.');
335         return undefined;
336     },
337     setWlxmlClass: function(klass) {
338         if(klass === this.getWlxmlClass())
339             return;
340
341         this.getWlxmlMetaAttrs().forEach(function(attr) {
342             if(!classAttributes.hasMetaAttr(klass, attr.name))
343                 this.dom().removeAttr('wlxml-meta-' + attr.name);
344         }, this);
345
346         if(klass)
347             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
348         else
349             this._container().removeAttr('wlxml-class');
350         if(!this.__updatingWlxml)
351             this._updateWlxmlManager();
352     },
353     setWlxml: function(params) {
354         this.__updatingWlxml = true;
355         if(params.tag !== undefined)
356             this.setWlxmlTag(params.tag);
357         if(params.klass !== undefined)
358             this.setWlxmlClass(params.klass);
359         this._updateWlxmlManager();
360         this.__updatingWlxml = false;
361     },
362     _updateWlxmlManager: function() {
363         var manager = wlxmlManagers.getFor(this);
364         this.data('_wlxmlManager', manager);
365         manager.setup();
366     },
367     is: function(what) {
368         if(what === 'list' && _.contains(['list.items', 'list.items.enum'], this.getWlxmlClass()))
369             return true;
370         return false;
371     },
372
373     getWlxmlMetaAttr: function(attr) {
374         return this.dom().attr('wlxml-meta-'+attr);
375     },
376
377     getWlxmlMetaAttrs: function() {
378         var toret = [];
379         var attrList = classAttributes.getMetaAttrsList(this.getWlxmlClass());
380         attrList.all.forEach(function(attr) {
381             toret.push({name: attr.name, value: this.getWlxmlMetaAttr(attr.name) || ''});
382         }, this);
383         return toret;
384     },
385
386     setWlxmlMetaAttr: function(attr, value) {
387         this.dom().attr('wlxml-meta-'+attr, value);
388     },
389     
390     toggleLabel: function(toggle) {
391         var displayCss = toggle ? 'inline-block' : 'none';
392         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
393         label.css('display', displayCss);
394         this.toggleHighlight(toggle);
395     },
396
397     toggleHighlight: function(toggle) {
398         this._container().toggleClass('highlighted-element', toggle);
399     },
400
401     toggle: function(toggle) {
402         var mng = this.data('_wlxmlManager');
403         if(mng) {
404             mng.toggle(toggle);
405         }
406     }
407 });
408
409
410 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
411 var DocumentTextElement = function(htmlElement, canvas) {
412     DocumentElement.call(this, htmlElement, canvas);
413 };
414
415 $.extend(DocumentTextElement, {
416     createDOM: function(params) {
417         return $('<div>')
418             .attr('document-text-element', '')
419             .text(params.text || utils.unicode.ZWS);
420     },
421
422     create: function(params, canvas) {
423         return this.fromHTMLElement(this.createDOM(params)[0]);
424     },
425
426     fromHTMLElement: function(htmlElement, canvas) {
427         return new this(htmlElement, canvas);
428     },
429     isContentContainer: function(htmlElement) {
430         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
431     }
432 });
433
434 DocumentTextElement.prototype = new DocumentElement();
435
436 $.extend(DocumentTextElement.prototype, {
437     toXML: function(parent) {
438         return this.getText();
439     },
440     _setupDOMHandler: function(htmlElement) {
441         var $element = $(htmlElement);
442         if(htmlElement.nodeType === Node.TEXT_NODE)
443             this.$element = $element.parent();
444         else
445             this.$element = $element;
446     },
447     detach: function() {
448         this.dom().detach();
449         this.canvas = null;
450         return this;
451     },
452     setText: function(text) {
453         this.dom().contents()[0].data = text;
454     },
455     appendText: function(text) {
456         this.dom().contents()[0].data += text;
457     },
458     getText: function() {
459         return this.dom().text().replace(utils.unicode.ZWS, '');
460     },
461     isEmpty: function() {
462         // Having at least Zero Width Space is guaranteed be Content Observer
463         return this.dom().contents()[0].data === utils.unicode.ZWS;
464     },
465     after: function(params) {
466         if(params instanceof DocumentTextElement || params.text)
467             return false;
468         var element;
469         if(params instanceof DocumentNodeElement) {
470             element = params;
471         } else {
472             element = DocumentNodeElement.create(params, this.canvas);
473         }
474         this.dom().wrap('<div>');
475         this.dom().parent().after(element.dom());
476         this.dom().unwrap();
477         return element;
478     },
479     before: function(params) {
480         if(params instanceof DocumentTextElement || params.text)
481             return false;
482         var element;
483         if(params instanceof DocumentNodeElement) {
484             element = params;
485         } else {
486             element = DocumentNodeElement.create(params, this.canvas);
487         }
488         this.dom().wrap('<div>');
489         this.dom().parent().before(element.dom());
490         this.dom().unwrap();
491         return element;
492     },
493     wrapWithNodeElement: function(wlxmlNode) {
494         if(typeof wlxmlNode.start === 'number' && typeof wlxmlNode.end === 'number') {
495             return this.canvas.wrapText({
496                 inside: this.parent(),
497                 textNodeIdx: this.parent().childIndex(this),
498                 offsetStart: Math.min(wlxmlNode.start, wlxmlNode.end),
499                 offsetEnd: Math.max(wlxmlNode.start, wlxmlNode.end),
500                 _with: {tag: wlxmlNode.tag, klass: wlxmlNode.klass}
501             });
502         } else {
503             return DocumentElement.prototype.wrapWithNodeElement.call(this, wlxmlNode);
504         }
505     },
506     unwrap: function() {
507         var parent = this.parent(),
508             toret;
509         if(parent.children().length === 1) {
510             toret = parent.parent();
511             var grandParent = parent.parent();
512             if(grandParent) {
513                 var grandParentChildren = grandParent.children(),
514                     idx = grandParent.childIndex(parent),
515                     prev = idx - 1 > -1 ? grandParentChildren[idx-1] : null,
516                     next = idx + 1 < grandParentChildren.length ? grandParentChildren[idx+1] : null;
517                 
518                 prev = (prev instanceof DocumentTextElement) ? prev : null;
519                 next = (next instanceof DocumentTextElement) ? next : null;
520
521                 if(prev && next) {
522                     prev.setText(prev.getText() + this.getText() + next.getText());
523                     next.detach();
524                 } else if (prev || next) {
525                     var target = prev ? prev : next;
526                     target.setText(target.getText() + this.getText());
527                 } else {
528                     parent.after(this);
529                 }
530             } else {
531                 parent.after(this);
532             }
533             parent.detach();
534             return toret;
535         }
536     },
537     split: function(params) {
538         var parentElement = this.parent(),
539             myIdx = parentElement.childIndex(this),
540             myCanvas = this.canvas,
541             passed = false,
542             succeedingChildren = [],
543             thisElement = this,
544             prefix = this.getText().substr(0, params.offset),
545             suffix = this.getText().substr(params.offset);
546
547         parentElement.children().forEach(function(child) {
548             if(passed)
549                 succeedingChildren.push(child);
550             if(child.sameNode(thisElement))
551                 passed = true;
552         });
553
554         if(prefix.length > 0)
555             this.setText(prefix);
556         else
557             this.detach();
558         
559         var newElement = DocumentNodeElement.create({tag: parentElement.getWlxmlTag(), klass: parentElement.getWlxmlClass()}, myCanvas);
560         parentElement.after(newElement);
561
562         if(suffix.length > 0)
563             newElement.append({text: suffix});
564         succeedingChildren.forEach(function(child) {
565             newElement.append(child);
566         });
567
568         return {first: parentElement, second: newElement};
569     },
570     divide: function(params) {
571         var myText = this.getText();
572
573         if(params.offset === myText.length)
574             return this.after(params);
575         if(params.offset === 0)
576             return this.before(params);
577
578         var lhsText = myText.substr(0, params.offset),
579             rhsText = myText.substr(params.offset),
580             newElement = DocumentNodeElement.create({tag: params.tag, klass: params.klass}, this.canvas),
581             rhsTextElement = DocumentTextElement.create({text: rhsText});
582
583         this.setText(lhsText);
584         this.after(newElement);
585         newElement.after(rhsTextElement);
586         return newElement;
587     },
588
589     toggleHighlight: function() {
590         // do nothing for now
591     }
592 });
593
594 return {
595     DocumentElement: DocumentElement,
596     DocumentNodeElement: DocumentNodeElement,
597     DocumentTextElement: DocumentTextElement
598 };
599
600 });