editor: canvas displays span node as block if it contains non span nodes
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / documentElement.js
1 define([
2 'libs/jquery',
3 'libs/underscore',
4 'modules/documentCanvas/canvas/utils',
5 'modules/documentCanvas/canvas/wlxmlManagers'
6 ], function($, _, utils, wlxmlManagers) {
7     
8 'use strict';
9 /* global Node:false, document:false */
10
11
12 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
13 var DocumentElement = function(wlxmlNode, canvas) {
14     this.wlxmlNode = wlxmlNode;
15     this.canvas = canvas;
16
17     this.$element = this.createDOM();
18     this.$element.data('canvas-element', this);
19 };
20
21 $.extend(DocumentElement.prototype, {
22     refreshPath: function() {
23         this.parents().forEach(function(parent) {
24             parent.refresh();
25         });
26         this.refresh();
27     },
28     refresh: function() {
29         // noop
30     },
31     bound: function() {
32         return $.contains(document.documentElement, this.dom()[0]);
33     },
34     dom: function() {
35         return this.$element;
36     },
37     parent: function() {
38         var parents = this.$element.parents('[document-node-element]');
39         if(parents.length) {
40             return this.canvas.getDocumentElement(parents[0]);
41         }
42         return null;
43     },
44
45     parents: function() {
46         var parents = [],
47             parent = this.parent();
48         while(parent) {
49             parents.push(parent);
50             parent = parent.parent();
51         }
52         return parents;
53     },
54
55     sameNode: function(other) {
56         return other && (typeof other === typeof this) && other.dom()[0] === this.dom()[0];
57     },
58
59     getVerticallyFirstTextElement: function() {
60         var toret;
61         this.children().some(function(child) {
62             if(child instanceof DocumentTextElement) {
63                 toret = child;
64                 return true; // break
65             } else {
66                 toret = child.getVerticallyFirstTextElement();
67                 if(toret) {
68                     return true; // break
69                 }
70             }
71         });
72         return toret;
73     },
74
75     getPreviousTextElement: function(includeInvisible) {
76         return this.getNearestTextElement('above', includeInvisible);
77     },
78
79     getNextTextElement: function(includeInvisible) {
80         return this.getNearestTextElement('below', includeInvisible);
81     },
82
83     getNearestTextElement: function(direction, includeInvisible) {
84         includeInvisible = includeInvisible !== undefined ? includeInvisible : false;
85         var selector = '[document-text-element]' + (includeInvisible ? '' : ':visible');
86         return this.canvas.getDocumentElement(utils.nearestInDocumentOrder(selector, direction, this.dom()[0]));
87     },
88
89     exec: function(method) {
90         if(this.manager && this.manager[method]) {
91             return this.manager[method].apply(this.manager, Array.prototype.slice.call(arguments, 1));
92         }
93     }
94 });
95
96
97 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
98 var DocumentNodeElement = function(wlxmlNode, canvas) {
99     DocumentElement.call(this, wlxmlNode, canvas);
100     wlxmlNode.setData('canvasElement', this);
101 };
102
103
104 var manipulate = function(e, params, action) {
105     var element;
106     if(params instanceof DocumentElement) {
107         element = params;
108     } else {
109         element = e.canvas.createElement(params);
110     }
111     var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom();
112     target[action](element.dom());
113     e.refreshPath();
114     return element;
115 };
116
117 DocumentNodeElement.prototype = Object.create(DocumentElement.prototype);
118
119
120 $.extend(DocumentNodeElement.prototype, {
121     init: function() {
122         // noo[]
123     },
124     createDOM: function() {
125         var dom = $('<div>')
126                 .attr('document-node-element', ''),
127             widgetsContainer = $('<div>')
128                 .addClass('canvas-widgets')
129                 .attr('contenteditable', false),
130             container = $('<div>')
131                 .attr('document-element-content', '');
132         
133         dom.append(widgetsContainer, container);
134         // Make sure widgets aren't navigable with arrow keys
135         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
136         this.$element = dom; //@!!!
137
138         this.setWlxmlTag(this.wlxmlNode.getTagName());
139         this.setWlxmlClass(this.wlxmlNode.getClass());
140
141         this.wlxmlNode.contents().forEach(function(node) {
142             container.append(this.canvas.createElement(node).dom());
143         }.bind(this));
144
145         this.init();
146
147         return dom;
148     },
149     _container: function() {
150         return this.dom().children('[document-element-content]');
151     },
152     detach: function() {
153         var parents = this.parents();
154         this.dom().detach();
155         this.canvas = null;
156         if(parents[0]) {
157             parents[0].refreshPath();
158         }
159          return this;
160     },
161     append: function(params) {
162         return manipulate(this, params, 'append');
163     },
164     prepend: function(params) {
165         return manipulate(this, params, 'prepend');
166     },
167     before: function(params) {
168         return manipulate(this, params, 'before');
169
170     },
171     after: function(params) {
172         return manipulate(this, params, 'after');
173     },
174     children: function() {
175         var toret = [];
176         if(this instanceof DocumentTextElement) {
177             return toret;
178         }
179
180
181         var elementContent = this._container().contents();
182         var element = this;
183         elementContent.each(function() {
184             var childElement = element.canvas.getDocumentElement(this);
185             if(childElement === undefined) {
186                 return true;
187             }
188             toret.push(childElement);
189         });
190         return toret;
191     },
192     childIndex: function(child) {
193         var children = this.children(),
194             toret = null;
195         children.forEach(function(c, idx) {
196             if(c.sameNode(child)) {
197                 toret = idx;
198                 return false;
199             }
200         });
201         return toret;
202     },
203     getWlxmlTag: function() {
204         return this._container().attr('wlxml-tag');
205     },
206     setWlxmlTag: function(tag) {
207         this._container().attr('wlxml-tag', tag);
208     },
209     getWlxmlClass: function() {
210         var klass = this._container().attr('wlxml-class');
211         if(klass) {
212             return klass.replace(/-/g, '.');
213         }
214         return undefined;
215     },
216     setWlxmlClass: function(klass) {
217         if(klass === this.getWlxmlClass()) {
218             return;
219         }
220         if(klass) {
221             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
222         }
223         else {
224             this._container().removeAttr('wlxml-class');
225         }
226         this.manager = wlxmlManagers.getFor(this);
227         this.manager.setup();
228
229         this.refreshPath();
230     },
231     toggleLabel: function(toggle) {
232         var displayCss = toggle ? 'inline-block' : 'none';
233         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
234         label.css('display', displayCss);
235         this.toggleHighlight(toggle);
236     },
237
238     toggleHighlight: function(toggle) {
239         this._container().toggleClass('highlighted-element', toggle);
240     },
241
242     toggle: function(toggle) {
243         if(this.manager) {
244             this.manager.toggle(toggle);
245         }
246     },
247
248     isBlock: function() {
249         return this.dom().css('display') === 'block';
250     },
251
252     containsBlock: function() {
253         return this.children()
254             .filter(function(child) {
255                 return child instanceof DocumentNodeElement;
256             })
257             .some(function(child) {
258                 if(child.isBlock()) {
259                     return true;
260                 } else {
261                     return child.containsBlock();
262                 }
263             });
264     },
265
266     displayAsBlock: function() {
267         this.dom().css('display', 'block');
268         this._container().css('display', 'block');
269     },
270     displayInline: function() {
271         this.dom().css('display', 'inline');
272         this._container().css('display', 'inline');
273     }
274 });
275
276
277 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
278 var DocumentTextElement = function(wlxmlTextNode, canvas) {
279     DocumentElement.call(this, wlxmlTextNode, canvas);
280 };
281
282 $.extend(DocumentTextElement, {
283     isContentContainer: function(htmlElement) {
284         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
285     }
286 });
287
288 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
289
290 $.extend(DocumentTextElement.prototype, {
291     createDOM: function() {
292         return $('<div>')
293             .attr('document-text-element', '')
294             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
295     },
296     detach: function() {
297         this.dom().detach();
298         this.canvas = null;
299         return this;
300     },
301     setText: function(text) {
302         this.dom().contents()[0].data = text;
303     },
304     getText: function(options) {
305         options = _.extend({raw: false}, options || {});
306         var toret = this.dom().text();
307         if(!options.raw) {
308             toret = toret.replace(utils.unicode.ZWS, '');
309         }
310         return toret;
311     },
312     isEmpty: function() {
313         // Having at least Zero Width Space is guaranteed be Content Observer
314         return this.dom().contents()[0].data === utils.unicode.ZWS;
315     },
316     after: function(params) {
317         if(params instanceof DocumentTextElement || params.text) {
318             return false;
319         }
320         var element;
321         if(params instanceof DocumentNodeElement) {
322             element = params;
323         } else {
324             element = this.canvas.createElement(params);
325         }
326         this.dom().wrap('<div>');
327         this.dom().parent().after(element.dom());
328         this.dom().unwrap();
329         this.refreshPath();
330         return element;
331     },
332     before: function(params) {
333         if(params instanceof DocumentTextElement || params.text) {
334             return false;
335         }
336         var element;
337         if(params instanceof DocumentNodeElement) {
338             element = params;
339         } else {
340             element = this.canvas.createElement(params);
341         }
342         this.dom().wrap('<div>');
343         this.dom().parent().before(element.dom());
344         this.dom().unwrap();
345         this.refreshPath();
346         return element;
347     },
348
349     toggleHighlight: function() {
350         // do nothing for now
351     }
352
353 });
354
355 var SpanElement = function() {
356     DocumentNodeElement.apply(this, Array.prototype.slice.call(arguments, 0));
357 };
358 SpanElement.prototype = $.extend(Object.create(DocumentNodeElement.prototype), {
359     init: function() {
360         if(this.containsBlock()) {
361             this.displayAsBlock();
362         } else {
363             this.displayInline();
364         }
365     },
366     refresh: function() {
367         this.init();
368     }
369 });
370
371 var elements = {
372     span: SpanElement
373 };
374
375
376 return {
377     DocumentElement: DocumentElement,
378     DocumentNodeElement: DocumentNodeElement,
379     DocumentTextElement: DocumentTextElement, //,
380     factoryForTag: function(tagName) {
381         return elements[tagName] || DocumentNodeElement;
382     }
383 };
384
385 });