editor: start using data api on text nodes for keeping references to canvas elements
[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     this.wlxmlNode.setData('canvasElement', this);
22 };
23
24 $.extend(DocumentElement.prototype, {
25     refreshPath: function() {
26         this.parents().forEach(function(parent) {
27             parent.refresh();
28         });
29         this.refresh();
30     },
31     refresh: function() {
32         // noop
33     },
34     updateState: function(toUpdate) {
35         var changes = {};
36         _.keys(toUpdate)
37             .filter(function(key) {
38                 return this.state.hasOwnProperty(key);
39             }.bind(this))
40             .forEach(function(key) {
41                 if(this.state !== toUpdate[key]) {
42                     this.state[key] = changes[key] = toUpdate[key];
43                 }
44             }.bind(this));
45         if(_.isFunction(this.onStateChange)) {
46             this.onStateChange(changes);
47             if(_.isBoolean(changes.active)) {
48                 if(changes.active) {
49                     var ptr = this;
50                     while(ptr && ptr.wlxmlNode.getTagName() === 'span') {
51                         ptr = ptr.parent();
52                     }
53                     if(ptr && ptr.gutterGroup) {
54                         ptr.gutterGroup.show();
55                     }
56                 }
57             }
58         }
59     },
60     parent: function() {
61         var parents = this.dom.parents('[document-node-element]');
62         if(parents.length) {
63             return this.canvas.getDocumentElement(parents[0]);
64         }
65         return null;
66     },
67
68     parents: function() {
69         var parents = [],
70             parent = this.parent();
71         while(parent) {
72             parents.push(parent);
73             parent = parent.parent();
74         }
75         return parents;
76     },
77
78     sameNode: function(other) {
79         return other && (typeof other === typeof this) && other.dom[0] === this.dom[0];
80     },
81
82     trigger: function() {
83         this.canvas.eventBus.trigger.apply(this.canvas.eventBus, Array.prototype.slice.call(arguments, 0));
84     }
85
86
87 });
88
89
90 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
91 var DocumentNodeElement = function(wlxmlNode, canvas) {
92     DocumentElement.call(this, wlxmlNode, canvas);
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(this.gutterGroup) {
158             this.gutterGroup.remove();
159         }
160         if(_.isFunction(this.children)) {
161             this.children().forEach(function(child) {
162                 child.detach(true);
163             });
164         }
165
166         if(!isChild) {
167             parents = this.parents();
168             this.dom.detach();
169             if(parents[0]) {
170                 parents[0].refreshPath();
171             }
172         }
173         return this;
174     },
175     before: function(params) {
176         return manipulate(this, params, 'before');
177
178     },
179     after: function(params) {
180         return manipulate(this, params, 'after');
181     },
182
183     isBlock: function() {
184         return this.dom.css('display') === 'block';
185     },
186
187     displayAsBlock: function() {
188         this.dom.css('display', 'block');
189         this._container().css('display', 'block');
190     },
191     displayInline: function() {
192         this.dom.css('display', 'inline');
193         this._container().css('display', 'inline');
194     },
195     displayAs: function(what) {
196         // [this.dom(), this._container()].forEach(e) {
197         //     var isBlock = window.getComputedStyle(e).display === 'block';
198         //     if(!isBlock && what === 'block') {
199         //         e.css('display', what);
200         //     } else if(isBlock && what === 'inline') {
201         //         e.css('display')
202         //     }
203         // })
204         this.dom.css('display', what);
205         this._container().css('display', what);
206     }
207 });
208
209
210 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
211 var DocumentTextElement = function(wlxmlTextNode, canvas) {
212     DocumentElement.call(this, wlxmlTextNode, canvas);
213 };
214
215 $.extend(DocumentTextElement, {
216     isContentContainer: function(htmlElement) {
217         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
218     }
219 });
220
221 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
222
223 $.extend(DocumentTextElement.prototype, {
224     createDOM: function() {
225         var dom = $('<div>')
226             .attr('document-text-element', '')
227             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
228         return dom;
229     },
230     detach: function(isChild) {
231         if(!isChild) {
232             this.dom.detach();
233         }
234         return this;
235     },
236     setText: function(text) {
237         if(text === '') {
238             text = utils.unicode.ZWS;
239         }
240         if(text !== this.getText()) {
241             this.dom.contents()[0].data = text;
242         }
243     },
244     handle: function(event) {
245         this.setText(event.meta.node.getText());
246     },
247     getText: function(options) {
248         options = _.extend({raw: false}, options || {});
249         var toret = this.dom.text();
250         if(!options.raw) {
251             toret = toret.replace(utils.unicode.ZWS, '');
252         }
253         return toret;
254     },
255     isEmpty: function() {
256         // Having at least Zero Width Space is guaranteed be Content Observer
257         return this.dom.contents()[0].data === utils.unicode.ZWS;
258     },
259     after: function(params) {
260         if(params instanceof DocumentTextElement || params.text) {
261             return false;
262         }
263         var element;
264         if(params instanceof DocumentNodeElement) {
265             element = params;
266         } else {
267             element = this.canvas.createElement(params);
268         }
269         if(element.dom) {
270             this.dom.wrap('<div>');
271             this.dom.parent().after(element.dom);
272             this.dom.unwrap();
273             this.refreshPath();
274         }
275         return element;
276     },
277     before: function(params) {
278         if(params instanceof DocumentTextElement || params.text) {
279             return false;
280         }
281         var element;
282         if(params instanceof DocumentNodeElement) {
283             element = params;
284         } else {
285             element = this.canvas.createElement(params);
286         }
287         if(element.dom) {
288             this.dom.wrap('<div>');
289             this.dom.parent().before(element.dom);
290             this.dom.unwrap();
291             this.refreshPath();
292         }
293         return element;
294     },
295
296     children: function() {
297         return [];
298     }
299
300 });
301
302
303 return {
304     DocumentElement: DocumentElement,
305     DocumentNodeElement: DocumentNodeElement,
306     DocumentTextElement: DocumentTextElement
307 };
308
309 });