editor: canvas - first take on hierarchical canvas element registers
[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/container'
6 ], function($, _, utils, container) {
7     
8 'use strict';
9 /* global Node:false */
10
11 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
12 var DocumentElement = function(wlxmlNode, canvas) {
13     this.wlxmlNode = wlxmlNode;
14     this.canvas = canvas;
15     this.state = {
16         exposed: false,
17         active: false
18     };
19
20     this.dom = this.createDOM();
21     this.dom.data('canvas-element', this);
22     this.wlxmlNode.setData('canvasElement', this);
23 };
24
25 $.extend(DocumentElement.prototype, {
26     refreshPath: function() {
27         this.parents().forEach(function(parent) {
28             parent.refresh();
29         });
30         this.refresh();
31     },
32     refresh: function() {
33         // noop
34     },
35     updateState: function(toUpdate) {
36         var changes = {};
37         _.keys(toUpdate)
38             .filter(function(key) {
39                 return this.state.hasOwnProperty(key);
40             }.bind(this))
41             .forEach(function(key) {
42                 if(this.state !== toUpdate[key]) {
43                     this.state[key] = changes[key] = toUpdate[key];
44                 }
45             }.bind(this));
46         if(_.isFunction(this.onStateChange)) {
47             this.onStateChange(changes);
48             if(_.isBoolean(changes.active)) {
49                 if(changes.active) {
50                     var ptr = this;
51                     while(ptr && ptr.wlxmlNode.getTagName() === 'span') {
52                         ptr = ptr.parent();
53                     }
54                     if(ptr && ptr.gutterGroup) {
55                         ptr.gutterGroup.show();
56                     }
57                 }
58             }
59         }
60     },
61     parent: function() {
62         var parents = this.dom.parents('[document-node-element]');
63         if(parents.length) {
64             return this.canvas.getDocumentElement(parents[0]);
65         }
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     isRootElement: function() {
83         return this.sameNode(this.canvas.rootElement);
84     },
85
86     trigger: function() {
87         this.canvas.eventBus.trigger.apply(this.canvas.eventBus, Array.prototype.slice.call(arguments, 0));
88     }
89
90
91 });
92
93
94 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
95 var DocumentNodeElement = function(wlxmlNode, canvas) {
96     DocumentElement.call(this, wlxmlNode, canvas);
97     this.containers = [];
98     this.elementsRegister = canvas.createElementsRegister();
99     this.contextMenuActions = [];
100     this.init(this.dom);
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.createElement(params);
110     }
111     if(element.dom) {
112         e.dom[action](element.dom);
113         e.refreshPath();
114     }
115     return element;
116 };
117
118 DocumentNodeElement.prototype = Object.create(DocumentElement.prototype);
119
120
121 $.extend(DocumentNodeElement.prototype, {
122     defaultDisplayStyle: 'block',
123     init: function() {},
124     addWidget: function(widget) {
125         this.dom.children('.canvas-widgets').append(widget.DOM ? widget.DOM : widget);
126     },
127     clearWidgets: function() {
128         this.dom.children('.canvas-widgets').empty();
129     },
130     addToGutter: function(view) {
131         if(!this.gutterGroup) {
132             this.gutterGroup = this.canvas.gutter.createViewGroup({
133                 offsetHint: function() {
134                     return this.canvas.getElementOffset(this);
135                 }.bind(this)
136             }, this);
137         }
138         this.gutterGroup.addView(view);
139     },
140     createContainer: function(nodes, params) {
141         var toret = container.create(nodes, params, this);
142         this.containers.push(toret);
143         return toret;
144     },
145     removeContainer: function(container) {
146         var idx;
147         if((idx = this.containers.indexOf(container)) !== -1) {
148             this.containers.splice(idx, 1);
149         }
150     },
151     createElement: function(wlxmlNode) {
152         var parent = this.wlxmlNode.parent() ? utils.getElementForNode(this.wlxmlNode.parent()) : null;
153         return this.canvas.createElement(wlxmlNode, this.elementsRegister, !parent) || parent.createElement(wlxmlNode);
154     },
155     addToContextMenu: function(actionFqName) {
156         this.contextMenuActions.push(this.canvas.createAction(actionFqName));
157     },
158     handle: function(event) {
159         var method = 'on' + event.type[0].toUpperCase() + event.type.substr(1),
160             target;
161         if(event.type === 'nodeAdded' || event.type === 'nodeDetached') {
162             this.containers.some(function(container) {
163                 if(container.manages(event.meta.node, event.meta.parent)) {
164                     target = container;
165                     return true;
166                 }
167             });
168         }
169         
170         if(!target && this[method]) {
171             target = this;
172         }
173         
174         if(target) {
175             target[method](event);
176         }
177     },
178     createDOM: function() {
179         var wrapper = $('<div>').attr('document-node-element', ''),
180             widgetsContainer = $('<div>')
181                 .addClass('canvas-widgets'),
182             contentContainer = $('<div>')
183                 .attr('document-element-content', '');
184         
185         wrapper.append(contentContainer, widgetsContainer);
186         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
187         return wrapper;
188     },
189     _container: function() {
190         return this.dom.children('[document-element-content]');
191     },
192     detach: function(isChild) {
193         var parents;
194
195         if(this.gutterGroup) {
196             this.gutterGroup.remove();
197         }
198         if(_.isFunction(this.children)) {
199             this.children().forEach(function(child) {
200                 child.detach(true);
201             });
202         }
203
204         if(!isChild) {
205             parents = this.parents();
206             this.dom.detach();
207             if(parents[0]) {
208                 parents[0].refreshPath();
209             }
210         }
211         return this;
212     },
213     before: function(params) {
214         return manipulate(this, params, 'before');
215
216     },
217     after: function(params) {
218         return manipulate(this, params, 'after');
219     },
220
221     isBlock: function() {
222         return this.dom.css('display') === 'block';
223     },
224
225     displayAsBlock: function() {
226         this.dom.css('display', 'block');
227         this._container().css('display', 'block');
228     },
229     displayInline: function() {
230         this.dom.css('display', 'inline');
231         this._container().css('display', 'inline');
232     },
233     displayAs: function(what) {
234         // [this.dom(), this._container()].forEach(e) {
235         //     var isBlock = window.getComputedStyle(e).display === 'block';
236         //     if(!isBlock && what === 'block') {
237         //         e.css('display', what);
238         //     } else if(isBlock && what === 'inline') {
239         //         e.css('display')
240         //     }
241         // })
242         this.dom.css('display', what);
243         this._container().css('display', what);
244     }
245 });
246
247
248 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
249 var DocumentTextElement = function(wlxmlTextNode, canvas) {
250     DocumentElement.call(this, wlxmlTextNode, canvas);
251 };
252
253 $.extend(DocumentTextElement, {
254     isContentContainer: function(htmlElement) {
255         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
256     }
257 });
258
259 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
260
261 $.extend(DocumentTextElement.prototype, {
262     createDOM: function() {
263         var dom = $('<div>')
264             .attr('document-text-element', '')
265             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
266         return dom;
267     },
268     detach: function(isChild) {
269         if(!isChild) {
270             this.dom.detach();
271         }
272         return this;
273     },
274     setText: function(text) {
275         if(text === '') {
276             text = utils.unicode.ZWS;
277         }
278         if(text !== this.getText()) {
279             this.dom.contents()[0].data = text;
280         }
281     },
282     handle: function(event) {
283         this.setText(event.meta.node.getText());
284     },
285     getText: function(options) {
286         options = _.extend({raw: false}, options || {});
287         var toret = this.dom.text();
288         if(!options.raw) {
289             toret = toret.replace(utils.unicode.ZWS, '');
290         }
291         return toret;
292     },
293     isEmpty: function() {
294         // Having at least Zero Width Space is guaranteed be Content Observer
295         return this.dom.contents()[0].data === utils.unicode.ZWS;
296     },
297     after: function(params) {
298         if(params instanceof DocumentTextElement || params.text) {
299             return false;
300         }
301         var element;
302         if(params instanceof DocumentNodeElement) {
303             element = params;
304         } else {
305             element = this.parent().createElement(params);
306         }
307         if(element.dom) {
308             this.dom.wrap('<div>');
309             this.dom.parent().after(element.dom);
310             this.dom.unwrap();
311             this.refreshPath();
312         }
313         return element;
314     },
315     before: function(params) {
316         if(params instanceof DocumentTextElement || params.text) {
317             return false;
318         }
319         var element;
320         if(params instanceof DocumentNodeElement) {
321             element = params;
322         } else {
323             element = this.createElement(params);
324         }
325         if(element.dom) {
326             this.dom.wrap('<div>');
327             this.dom.parent().before(element.dom);
328             this.dom.unwrap();
329             this.refreshPath();
330         }
331         return element;
332     },
333
334     children: function() {
335         return [];
336     }
337
338 });
339
340
341 return {
342     DocumentElement: DocumentElement,
343     DocumentNodeElement: DocumentNodeElement,
344     DocumentTextElement: DocumentTextElement
345 };
346
347 });