editor: canvas - let every child of a detached element clean up after itself
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / documentElement.js
1 define([
2 'libs/jquery',
3 'libs/underscore',
4 'modules/documentCanvas/canvas/utils'
5 ], function($, _, utils) {
6     
7 'use strict';
8 /* global Node:false */
9
10 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
11 var DocumentElement = function(wlxmlNode, canvas) {
12     this.wlxmlNode = wlxmlNode;
13     this.canvas = canvas;
14     this.state = {
15         exposed: false,
16         active: false
17     };
18
19     this.dom = this.createDOM();
20     this.dom.data('canvas-element', this);
21 };
22
23 $.extend(DocumentElement.prototype, {
24     refreshPath: function() {
25         this.parents().forEach(function(parent) {
26             parent.refresh();
27         });
28         this.refresh();
29     },
30     refresh: function() {
31         // noop
32     },
33     updateState: function(toUpdate) {
34         var changes = {};
35         _.keys(toUpdate)
36             .filter(function(key) {
37                 return this.state.hasOwnProperty(key);
38             }.bind(this))
39             .forEach(function(key) {
40                 if(this.state !== toUpdate[key]) {
41                     this.state[key] = changes[key] = toUpdate[key];
42                 }
43             }.bind(this));
44         if(_.isFunction(this.onStateChange)) {
45             this.onStateChange(changes);
46             if(_.isBoolean(changes.active)) {
47                 if(changes.active) {
48                     var ptr = this;
49                     while(ptr && ptr.wlxmlNode.getTagName() === 'span') {
50                         ptr = ptr.parent();
51                     }
52                     if(ptr && ptr.gutterGroup) {
53                         ptr.gutterGroup.show();
54                     }
55                 }
56             }
57         }
58     },
59     parent: function() {
60         var parents = this.dom.parents('[document-node-element]');
61         if(parents.length) {
62             return this.canvas.getDocumentElement(parents[0]);
63         }
64         return null;
65     },
66
67     parents: function() {
68         var parents = [],
69             parent = this.parent();
70         while(parent) {
71             parents.push(parent);
72             parent = parent.parent();
73         }
74         return parents;
75     },
76
77     sameNode: function(other) {
78         return other && (typeof other === typeof this) && other.dom[0] === this.dom[0];
79     },
80
81     trigger: function() {
82         this.canvas.eventBus.trigger.apply(this.canvas.eventBus, Array.prototype.slice.call(arguments, 0));
83     }
84
85
86 });
87
88
89 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
90 var DocumentNodeElement = function(wlxmlNode, canvas) {
91     DocumentElement.call(this, wlxmlNode, canvas);
92     wlxmlNode.setData('canvasElement', this);
93     this.init(this.dom);
94 };
95
96
97 var manipulate = function(e, params, action) {
98     var element;
99     if(params instanceof DocumentElement) {
100         element = params;
101     } else {
102         element = e.canvas.createElement(params);
103     }
104     if(element.dom) {
105         e.dom[action](element.dom);
106         e.refreshPath();
107     }
108     return element;
109 };
110
111 DocumentNodeElement.prototype = Object.create(DocumentElement.prototype);
112
113
114 $.extend(DocumentNodeElement.prototype, {
115     defaultDisplayStyle: 'block',
116     init: function() {},
117     addWidget: function(widget) {
118         this.dom.children('.canvas-widgets').append(widget.DOM ? widget.DOM : widget);
119     },
120     clearWidgets: function() {
121         this.dom.children('.canvas-widgets').empty();
122     },
123     addToGutter: function(view) {
124         if(!this.gutterGroup) {
125             this.gutterGroup = this.canvas.gutter.createViewGroup({
126                 offsetHint: function() {
127                     return this.canvas.getElementOffset(this);
128                 }.bind(this)
129             }, this);
130         }
131         this.gutterGroup.addView(view);
132     },
133     handle: function(event) {
134         var method = 'on' + event.type[0].toUpperCase() + event.type.substr(1);
135         if(this[method]) {
136             this[method](event);
137         }
138     },
139     createDOM: function() {
140         var wrapper = $('<div>').attr('document-node-element', ''),
141             widgetsContainer = $('<div>')
142                 .addClass('canvas-widgets')
143                 .attr('contenteditable', false),
144             contentContainer = $('<div>')
145                 .attr('document-element-content', '');
146         
147         wrapper.append(contentContainer, widgetsContainer);
148         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
149         return wrapper;
150     },
151     _container: function() {
152         return this.dom.children('[document-element-content]');
153     },
154     detach: function(isChild) {
155         var parents;
156
157         if(_.isFunction(this.children)) {
158             this.children().forEach(function(child) {
159                 child.detach(true);
160             });
161         }
162
163         if(!isChild) {
164             parents = this.parents();
165             this.dom.detach();
166             if(parents[0]) {
167                 parents[0].refreshPath();
168             }
169         }
170         return this;
171     },
172     before: function(params) {
173         return manipulate(this, params, 'before');
174
175     },
176     after: function(params) {
177         return manipulate(this, params, 'after');
178     },
179
180     isBlock: function() {
181         return this.dom.css('display') === 'block';
182     },
183
184     displayAsBlock: function() {
185         this.dom.css('display', 'block');
186         this._container().css('display', 'block');
187     },
188     displayInline: function() {
189         this.dom.css('display', 'inline');
190         this._container().css('display', 'inline');
191     },
192     displayAs: function(what) {
193         // [this.dom(), this._container()].forEach(e) {
194         //     var isBlock = window.getComputedStyle(e).display === 'block';
195         //     if(!isBlock && what === 'block') {
196         //         e.css('display', what);
197         //     } else if(isBlock && what === 'inline') {
198         //         e.css('display')
199         //     }
200         // })
201         this.dom.css('display', what);
202         this._container().css('display', what);
203     }
204 });
205
206
207 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
208 var DocumentTextElement = function(wlxmlTextNode, canvas) {
209     DocumentElement.call(this, wlxmlTextNode, canvas);
210 };
211
212 $.extend(DocumentTextElement, {
213     isContentContainer: function(htmlElement) {
214         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
215     }
216 });
217
218 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
219
220 $.extend(DocumentTextElement.prototype, {
221     createDOM: function() {
222         var dom = $('<div>')
223             .attr('document-text-element', '')
224             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
225         return dom;
226     },
227     detach: function(isChild) {
228         if(!isChild) {
229             this.dom.detach();
230         }
231         return this;
232     },
233     setText: function(text) {
234         if(text === '') {
235             text = utils.unicode.ZWS;
236         }
237         if(text !== this.getText()) {
238             this.dom.contents()[0].data = text;
239         }
240     },
241     handle: function(event) {
242         this.setText(event.meta.node.getText());
243     },
244     getText: function(options) {
245         options = _.extend({raw: false}, options || {});
246         var toret = this.dom.text();
247         if(!options.raw) {
248             toret = toret.replace(utils.unicode.ZWS, '');
249         }
250         return toret;
251     },
252     isEmpty: function() {
253         // Having at least Zero Width Space is guaranteed be Content Observer
254         return this.dom.contents()[0].data === utils.unicode.ZWS;
255     },
256     after: function(params) {
257         if(params instanceof DocumentTextElement || params.text) {
258             return false;
259         }
260         var element;
261         if(params instanceof DocumentNodeElement) {
262             element = params;
263         } else {
264             element = this.canvas.createElement(params);
265         }
266         if(element.dom) {
267             this.dom.wrap('<div>');
268             this.dom.parent().after(element.dom);
269             this.dom.unwrap();
270             this.refreshPath();
271         }
272         return element;
273     },
274     before: function(params) {
275         if(params instanceof DocumentTextElement || params.text) {
276             return false;
277         }
278         var element;
279         if(params instanceof DocumentNodeElement) {
280             element = params;
281         } else {
282             element = this.canvas.createElement(params);
283         }
284         if(element.dom) {
285             this.dom.wrap('<div>');
286             this.dom.parent().before(element.dom);
287             this.dom.unwrap();
288             this.refreshPath();
289         }
290         return element;
291     },
292
293     children: function() {
294         return [];
295     }
296
297 });
298
299
300 return {
301     DocumentElement: DocumentElement,
302     DocumentNodeElement: DocumentNodeElement,
303     DocumentTextElement: DocumentTextElement
304 };
305
306 });