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