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