editor: canvas - context menu support
[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.contextMenuActions = [];
99     this.init(this.dom);
100 };
101
102
103 var manipulate = function(e, params, action) {
104     var element;
105     if(params instanceof DocumentElement) {
106         element = params;
107     } else {
108         element = e.canvas.createElement(params);
109     }
110     if(element.dom) {
111         e.dom[action](element.dom);
112         e.refreshPath();
113     }
114     return element;
115 };
116
117 DocumentNodeElement.prototype = Object.create(DocumentElement.prototype);
118
119
120 $.extend(DocumentNodeElement.prototype, {
121     defaultDisplayStyle: 'block',
122     init: function() {},
123     addWidget: function(widget) {
124         this.dom.children('.canvas-widgets').append(widget.DOM ? widget.DOM : widget);
125     },
126     clearWidgets: function() {
127         this.dom.children('.canvas-widgets').empty();
128     },
129     addToGutter: function(view) {
130         if(!this.gutterGroup) {
131             this.gutterGroup = this.canvas.gutter.createViewGroup({
132                 offsetHint: function() {
133                     return this.canvas.getElementOffset(this);
134                 }.bind(this)
135             }, this);
136         }
137         this.gutterGroup.addView(view);
138     },
139     createContainer: function(nodes, params) {
140         var toret = container.create(nodes, params, this);
141         this.containers.push(toret);
142         return toret;
143     },
144     removeContainer: function(container) {
145         var idx;
146         if((idx = this.containers.indexOf(container)) !== -1) {
147             this.containers.splice(idx, 1);
148         }
149     },
150     addToContextMenu: function(actionFqName) {
151         this.contextMenuActions.push(this.canvas.createAction(actionFqName));
152     },
153     handle: function(event) {
154         var method = 'on' + event.type[0].toUpperCase() + event.type.substr(1),
155             target;
156         if(event.type === 'nodeAdded' || event.type === 'nodeDetached') {
157             this.containers.some(function(container) {
158                 if(container.manages(event.meta.node, event.meta.parent)) {
159                     target = container;
160                     return true;
161                 }
162             });
163         }
164         
165         if(!target && this[method]) {
166             target = this;
167         }
168         
169         if(target) {
170             target[method](event);
171         }
172     },
173     createDOM: function() {
174         var wrapper = $('<div>').attr('document-node-element', ''),
175             widgetsContainer = $('<div>')
176                 .addClass('canvas-widgets'),
177             contentContainer = $('<div>')
178                 .attr('document-element-content', '');
179         
180         wrapper.append(contentContainer, widgetsContainer);
181         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
182         return wrapper;
183     },
184     _container: function() {
185         return this.dom.children('[document-element-content]');
186     },
187     detach: function(isChild) {
188         var parents;
189
190         if(this.gutterGroup) {
191             this.gutterGroup.remove();
192         }
193         if(_.isFunction(this.children)) {
194             this.children().forEach(function(child) {
195                 child.detach(true);
196             });
197         }
198
199         if(!isChild) {
200             parents = this.parents();
201             this.dom.detach();
202             if(parents[0]) {
203                 parents[0].refreshPath();
204             }
205         }
206         return this;
207     },
208     before: function(params) {
209         return manipulate(this, params, 'before');
210
211     },
212     after: function(params) {
213         return manipulate(this, params, 'after');
214     },
215
216     isBlock: function() {
217         return this.dom.css('display') === 'block';
218     },
219
220     displayAsBlock: function() {
221         this.dom.css('display', 'block');
222         this._container().css('display', 'block');
223     },
224     displayInline: function() {
225         this.dom.css('display', 'inline');
226         this._container().css('display', 'inline');
227     },
228     displayAs: function(what) {
229         // [this.dom(), this._container()].forEach(e) {
230         //     var isBlock = window.getComputedStyle(e).display === 'block';
231         //     if(!isBlock && what === 'block') {
232         //         e.css('display', what);
233         //     } else if(isBlock && what === 'inline') {
234         //         e.css('display')
235         //     }
236         // })
237         this.dom.css('display', what);
238         this._container().css('display', what);
239     }
240 });
241
242
243 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
244 var DocumentTextElement = function(wlxmlTextNode, canvas) {
245     DocumentElement.call(this, wlxmlTextNode, canvas);
246 };
247
248 $.extend(DocumentTextElement, {
249     isContentContainer: function(htmlElement) {
250         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
251     }
252 });
253
254 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
255
256 $.extend(DocumentTextElement.prototype, {
257     createDOM: function() {
258         var dom = $('<div>')
259             .attr('document-text-element', '')
260             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
261         return dom;
262     },
263     detach: function(isChild) {
264         if(!isChild) {
265             this.dom.detach();
266         }
267         return this;
268     },
269     setText: function(text) {
270         if(text === '') {
271             text = utils.unicode.ZWS;
272         }
273         if(text !== this.getText()) {
274             this.dom.contents()[0].data = text;
275         }
276     },
277     handle: function(event) {
278         this.setText(event.meta.node.getText());
279     },
280     getText: function(options) {
281         options = _.extend({raw: false}, options || {});
282         var toret = this.dom.text();
283         if(!options.raw) {
284             toret = toret.replace(utils.unicode.ZWS, '');
285         }
286         return toret;
287     },
288     isEmpty: function() {
289         // Having at least Zero Width Space is guaranteed be Content Observer
290         return this.dom.contents()[0].data === utils.unicode.ZWS;
291     },
292     after: function(params) {
293         if(params instanceof DocumentTextElement || params.text) {
294             return false;
295         }
296         var element;
297         if(params instanceof DocumentNodeElement) {
298             element = params;
299         } else {
300             element = this.canvas.createElement(params);
301         }
302         if(element.dom) {
303             this.dom.wrap('<div>');
304             this.dom.parent().after(element.dom);
305             this.dom.unwrap();
306             this.refreshPath();
307         }
308         return element;
309     },
310     before: function(params) {
311         if(params instanceof DocumentTextElement || params.text) {
312             return false;
313         }
314         var element;
315         if(params instanceof DocumentNodeElement) {
316             element = params;
317         } else {
318             element = this.canvas.createElement(params);
319         }
320         if(element.dom) {
321             this.dom.wrap('<div>');
322             this.dom.parent().before(element.dom);
323             this.dom.unwrap();
324             this.refreshPath();
325         }
326         return element;
327     },
328
329     children: function() {
330         return [];
331     }
332
333 });
334
335
336 return {
337     DocumentElement: DocumentElement,
338     DocumentNodeElement: DocumentNodeElement,
339     DocumentTextElement: DocumentTextElement
340 };
341
342 });