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