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