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