prevent empty span in a new node
[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     children: function() {
246         return [];
247     }
248 });
249
250
251 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
252 var DocumentTextElement = function(wlxmlTextNode, canvas) {
253     DocumentElement.call(this, wlxmlTextNode, canvas);
254 };
255
256 $.extend(DocumentTextElement, {
257     isContentContainer: function(htmlElement) {
258         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
259     }
260 });
261
262 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
263
264 $.extend(DocumentTextElement.prototype, {
265     createDOM: function() {
266         var dom = $('<div>')
267             .attr('document-text-element', '')
268             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
269         return dom;
270     },
271     detach: function(isChild) {
272         if(!isChild) {
273             this.dom.detach();
274         }
275         return this;
276     },
277     setText: function(text) {
278         if(text === '') {
279             text = utils.unicode.ZWS;
280         }
281         if(text !== this.getText()) {
282             this.dom.contents()[0].data = text;
283         }
284     },
285     handle: function(event) {
286         this.setText(event.meta.node.getText());
287     },
288     getText: function(options) {
289         options = _.extend({raw: false}, options || {});
290         var toret = this.dom.text();
291         if(!options.raw) {
292             toret = toret.replace(utils.unicode.ZWS, '');
293         }
294         return toret;
295     },
296     isEmpty: function() {
297         // Having at least Zero Width Space is guaranteed be Content Observer
298         return this.dom.contents()[0].data === utils.unicode.ZWS;
299     },
300     after: function(params) {
301         if(params instanceof DocumentTextElement || params.text) {
302             return false;
303         }
304         var element;
305         if(params instanceof DocumentNodeElement) {
306             element = params;
307         } else {
308             element = this.parent().createElement(params);
309         }
310         if(element.dom) {
311             this.dom.wrap('<div>');
312             this.dom.parent().after(element.dom);
313             this.dom.unwrap();
314             this.refreshPath();
315         }
316         return element;
317     },
318     before: function(params) {
319         if(params instanceof DocumentTextElement || params.text) {
320             return false;
321         }
322         var element;
323         if(params instanceof DocumentNodeElement) {
324             element = params;
325         } else {
326             element = this.createElement(params);
327         }
328         if(element.dom) {
329             this.dom.wrap('<div>');
330             this.dom.parent().before(element.dom);
331             this.dom.unwrap();
332             this.refreshPath();
333         }
334         return element;
335     },
336
337     children: function() {
338         return [];
339     }
340
341 });
342
343
344 return {
345     DocumentElement: DocumentElement,
346     DocumentNodeElement: DocumentNodeElement,
347     DocumentTextElement: DocumentTextElement
348 };
349
350 });