d62411245398f1e30cdbd686c064081ad776d480
[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.data('wlxmlNode', wlxmlNode);
173         wlxmlNode.setData('canvasElement', element);
174
175         element.setWlxml({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()});
176
177         _.keys(wlxmlNode.getMetaAttributes()).forEach(function(key) {
178             element.setWlxmlMetaAttr(key, params.meta[key]);
179         });
180
181         wlxmlNode.contents().forEach((function(node) {
182             container.append(DocumentElement.create(node).dom());
183         }).bind(this));
184
185         return dom;
186     }
187
188 });
189
190 var manipulate = function(e, params, action) {
191     var element;
192     if(params instanceof DocumentElement) {
193         element = params;
194     } else {
195         element = DocumentElement.create(params);
196     }
197     var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom();
198     target[action](element.dom());
199     return element;
200 };
201
202 DocumentNodeElement.prototype = new DocumentElement();
203
204
205 $.extend(DocumentNodeElement.prototype, {
206     _container: function() {
207         return this.dom().children('[document-element-content]');
208     },
209     detach: function() {
210         var parent = this.parent();
211         if(!parent)
212             return;
213
214         var parentChildren = parent.children(),
215             myIdx = parent.childIndex(this);
216
217         if(myIdx > 0 && myIdx < parentChildren.length) {
218             if((parentChildren[myIdx-1] instanceof DocumentTextElement) && (parentChildren[myIdx+1] instanceof DocumentTextElement)) {
219                 parentChildren[myIdx-1].appendText(parentChildren[myIdx+1].getText());
220                 parentChildren[myIdx+1].detach();
221             }
222         }
223         this.dom().detach();
224         this.canvas = null;
225         return this;
226     },
227     unwrapContents: function() {
228         var parent = this.parent();
229         if(!parent)
230             return;
231
232         var parentChildren = parent.children(),
233             myChildren = this.children(),
234             myIdx = parent.childIndex(this);
235
236         if(myChildren.length === 0)
237             return this.detach();
238
239         var moveLeftRange, moveRightRange, leftMerged;
240
241         if(myIdx > 0 && (parentChildren[myIdx-1] instanceof DocumentTextElement) && (myChildren[0] instanceof DocumentTextElement)) {
242             parentChildren[myIdx-1].appendText(myChildren[0].getText());
243             myChildren[0].detach();
244             moveLeftRange = true;
245             leftMerged = true;
246         } else {
247             leftMerged = false;
248         }
249
250         if(!(leftMerged && myChildren.length === 1)) {
251             if(myIdx < parentChildren.length - 1 && (parentChildren[myIdx+1] instanceof DocumentTextElement) && (myChildren[myChildren.length-1] instanceof DocumentTextElement)) {
252                 parentChildren[myIdx+1].prependText(myChildren[myChildren.length-1].getText());
253                 myChildren[myChildren.length-1].detach();
254                 moveRightRange = true;
255             }
256         }
257
258         var childrenLength = this.children().length;
259         this.children().forEach(function(child) {
260             this.before(child);
261         }.bind(this));
262
263         this.detach();
264
265         return {
266             element1: parent.children()[myIdx + (moveLeftRange ? -1 : 0)],
267             element2: parent.children()[myIdx + childrenLength-1 + (moveRightRange ? 1 : 0)]
268         };
269     },
270     append: function(params) {
271         if(params.tag !== 'span')
272             this.data('orig-end', undefined);
273         return manipulate(this, params, 'append');
274     },
275     prepend: function(params) {
276         return manipulate(this, params, 'prepend');
277     },
278     before: function(params) {
279         return manipulate(this, params, 'before');
280
281     },
282     after: function(params) {
283         return manipulate(this, params, 'after');
284     },
285     children: function() {
286         var toret = [];
287         if(this instanceof DocumentTextElement)
288             return toret;
289
290
291         var elementContent = this._container().contents();
292         var element = this;
293         elementContent.each(function(idx) {
294             var childElement = DocumentElement.fromHTMLElement(this, element.canvas);
295             if(childElement === undefined)
296                 return true;
297             if(idx === 0 && elementContent.length > 1 && elementContent[1].nodeType === Node.ELEMENT_NODE && (childElement instanceof DocumentTextElement) && $.trim($(this).text()) === '')
298                 return true;
299             if(idx > 0 && childElement instanceof DocumentTextElement) {
300                 if(toret[toret.length-1] instanceof DocumentNodeElement && $.trim($(this).text()) === '')
301                     return true;
302             }
303             toret.push(childElement);
304         });
305         return toret;
306     },
307     childIndex: function(child) {
308         var children = this.children(),
309             toret = null;
310         children.forEach(function(c, idx) {
311             if(c.sameNode(child)) {
312                 toret = idx;
313                 return false;
314             }
315         });
316         return toret;
317     },
318     getWlxmlTag: function() {
319         return this._container().attr('wlxml-tag');
320     },
321     setWlxmlTag: function(tag) {
322         if(tag === this.getWlxmlTag())
323             return;
324
325         this._container().attr('wlxml-tag', tag);
326         if(!this.__updatingWlxml)
327             this._updateWlxmlManager();
328     },
329     getWlxmlClass: function() {
330         var klass = this._container().attr('wlxml-class');
331         if(klass)
332             return klass.replace(/-/g, '.');
333         return undefined;
334     },
335     setWlxmlClass: function(klass) {
336         if(klass === this.getWlxmlClass())
337             return;
338
339         this.getWlxmlMetaAttrs().forEach(function(attr) {
340             if(!classAttributes.hasMetaAttr(klass, attr.name))
341                 this.dom().removeAttr('wlxml-meta-' + attr.name);
342         }, this);
343
344         if(klass)
345             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
346         else
347             this._container().removeAttr('wlxml-class');
348         if(!this.__updatingWlxml)
349             this._updateWlxmlManager();
350     },
351     setWlxml: function(params) {
352         this.__updatingWlxml = true;
353         if(params.tag !== undefined)
354             this.setWlxmlTag(params.tag);
355         if(params.klass !== undefined)
356             this.setWlxmlClass(params.klass);
357         this._updateWlxmlManager();
358         this.__updatingWlxml = false;
359     },
360     _updateWlxmlManager: function() {
361         var manager = wlxmlManagers.getFor(this);
362         this.data('_wlxmlManager', manager);
363         manager.setup();
364     },
365     is: function(what) {
366         if(what === 'list' && _.contains(['list.items', 'list.items.enum'], this.getWlxmlClass()))
367             return true;
368         return false;
369     },
370
371     getWlxmlMetaAttr: function(attr) {
372         return this.dom().attr('wlxml-meta-'+attr);
373     },
374
375     getWlxmlMetaAttrs: function() {
376         var toret = [];
377         var attrList = classAttributes.getMetaAttrsList(this.getWlxmlClass());
378         attrList.all.forEach(function(attr) {
379             toret.push({name: attr.name, value: this.getWlxmlMetaAttr(attr.name) || ''});
380         }, this);
381         return toret;
382     },
383
384     setWlxmlMetaAttr: function(attr, value) {
385         this.dom().attr('wlxml-meta-'+attr, value);
386     },
387     
388     toggleLabel: function(toggle) {
389         var displayCss = toggle ? 'inline-block' : 'none';
390         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
391         label.css('display', displayCss);
392         this.toggleHighlight(toggle);
393     },
394
395     toggleHighlight: function(toggle) {
396         this._container().toggleClass('highlighted-element', toggle);
397     },
398
399     toggle: function(toggle) {
400         var mng = this.data('_wlxmlManager');
401         if(mng) {
402             mng.toggle(toggle);
403         }
404     }
405 });
406
407
408 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
409 var DocumentTextElement = function(htmlElement, canvas) {
410     DocumentElement.call(this, htmlElement, canvas);
411 };
412
413 $.extend(DocumentTextElement, {
414     createDOM: function(wlxmlTextNode) {
415         var dom = $('<div>')
416             .attr('document-text-element', '')
417             .text(wlxmlTextNode.getText() || utils.unicode.ZWS),
418         element = this.fromHTMLElement(dom[0], this);
419         element.data('wlxmlNode', wlxmlTextNode);
420         return dom;
421     },
422
423     create: function(wlxmlTextNode, canvas) {
424         return this.fromHTMLElement(this.createDOM(wlxmlTextNode)[0]);
425     },
426
427     fromHTMLElement: function(htmlElement, canvas) {
428         return new this(htmlElement, canvas);
429     },
430     isContentContainer: function(htmlElement) {
431         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
432     }
433 });
434
435 DocumentTextElement.prototype = new DocumentElement();
436
437 $.extend(DocumentTextElement.prototype, {
438     _setupDOMHandler: function(htmlElement) {
439         var $element = $(htmlElement);
440         if(htmlElement.nodeType === Node.TEXT_NODE)
441             this.$element = $element.parent();
442         else
443             this.$element = $element;
444     },
445     detach: function() {
446         this.dom().detach();
447         this.canvas = null;
448         return this;
449     },
450     setText: function(text) {
451         this.dom().contents()[0].data = text;
452     },
453     appendText: function(text) {
454         this.dom().contents()[0].data += text;
455     },
456     prependText: function(text) {
457         this.dom().contents()[0].data = text + this.dom().contents()[0].data;
458     },
459     getText: function(options) {
460         options = _.extend({raw: false}, options || {});
461         var toret = this.dom().text();
462         if(!options.raw) {
463             toret = toret.replace(utils.unicode.ZWS, '');
464         }
465         return toret;
466     },
467     isEmpty: function() {
468         // Having at least Zero Width Space is guaranteed be Content Observer
469         return this.dom().contents()[0].data === utils.unicode.ZWS;
470     },
471     after: function(params) {
472         if(params instanceof DocumentTextElement || params.text)
473             return false;
474         var element;
475         if(params instanceof DocumentNodeElement) {
476             element = params;
477         } else {
478             element = DocumentNodeElement.create(params, this.canvas);
479         }
480         this.dom().wrap('<div>');
481         this.dom().parent().after(element.dom());
482         this.dom().unwrap();
483         return element;
484     },
485     before: function(params) {
486         if(params instanceof DocumentTextElement || params.text)
487             return false;
488         var element;
489         if(params instanceof DocumentNodeElement) {
490             element = params;
491         } else {
492             element = DocumentNodeElement.create(params, this.canvas);
493         }
494         this.dom().wrap('<div>');
495         this.dom().parent().before(element.dom());
496         this.dom().unwrap();
497         return element;
498     },
499     wrapWithNodeElement: function(wlxmlNode) {
500         if(typeof wlxmlNode.start === 'number' && typeof wlxmlNode.end === 'number') {
501             return this.canvas.wrapText({
502                 inside: this.parent(),
503                 textNodeIdx: this.parent().childIndex(this),
504                 offsetStart: Math.min(wlxmlNode.start, wlxmlNode.end),
505                 offsetEnd: Math.max(wlxmlNode.start, wlxmlNode.end),
506                 _with: {tag: wlxmlNode.tag, klass: wlxmlNode.klass}
507             });
508         } else {
509             return DocumentElement.prototype.wrapWithNodeElement.call(this, wlxmlNode);
510         }
511     },
512     unwrap: function() {
513         var parent = this.parent(),
514             toret;
515         if(parent.children().length === 1) {
516             toret = parent.parent();
517             var grandParent = parent.parent();
518             if(grandParent) {
519                 var grandParentChildren = grandParent.children(),
520                     idx = grandParent.childIndex(parent),
521                     prev = idx - 1 > -1 ? grandParentChildren[idx-1] : null,
522                     next = idx + 1 < grandParentChildren.length ? grandParentChildren[idx+1] : null;
523                 
524                 prev = (prev instanceof DocumentTextElement) ? prev : null;
525                 next = (next instanceof DocumentTextElement) ? next : null;
526
527                 if(prev && next) {
528                     prev.setText(prev.getText() + this.getText() + next.getText());
529                     next.detach();
530                 } else if (prev || next) {
531                     var target = prev ? prev : next,
532                         newText = prev ? target.getText() + this.getText() : this.getText() + target.getText();
533                     target.setText(newText);
534                 } else {
535                     parent.after(this);
536                 }
537             } else {
538                 parent.after(this);
539             }
540             parent.detach();
541             return toret;
542         }
543     },
544     split: function(params) {
545         var parentElement = this.parent(),
546             myIdx = parentElement.childIndex(this),
547             myCanvas = this.canvas,
548             passed = false,
549             succeedingChildren = [],
550             thisElement = this,
551             prefix = this.getText().substr(0, params.offset),
552             suffix = this.getText().substr(params.offset);
553
554         parentElement.children().forEach(function(child) {
555             if(passed)
556                 succeedingChildren.push(child);
557             if(child.sameNode(thisElement))
558                 passed = true;
559         });
560
561         if(prefix.length > 0)
562             this.setText(prefix);
563         else
564             this.detach();
565         
566         var newElement = DocumentNodeElement.create({tag: parentElement.getWlxmlTag(), klass: parentElement.getWlxmlClass()}, myCanvas);
567         parentElement.after(newElement);
568
569         if(suffix.length > 0)
570             newElement.append({text: suffix});
571         succeedingChildren.forEach(function(child) {
572             newElement.append(child);
573         });
574
575         return {first: parentElement, second: newElement};
576     },
577     divide: function(params) {
578         var myText = this.getText();
579
580         if(params.offset === myText.length)
581             return this.after(params);
582         if(params.offset === 0)
583             return this.before(params);
584
585         var lhsText = myText.substr(0, params.offset),
586             rhsText = myText.substr(params.offset),
587             newElement = DocumentNodeElement.create({tag: params.tag, klass: params.klass}, this.canvas),
588             rhsTextElement = DocumentTextElement.create({text: rhsText});
589
590         this.setText(lhsText);
591         this.after(newElement);
592         newElement.after(rhsTextElement);
593         return newElement;
594     },
595
596     toggleHighlight: function() {
597         // do nothing for now
598     }
599 });
600
601 return {
602     DocumentElement: DocumentElement,
603     DocumentNodeElement: DocumentNodeElement,
604     DocumentTextElement: DocumentTextElement
605 };
606
607 });