editor: fixing handling nodeTextChange on text nodes belonging to custom-rendered...
[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() {
155         var parents = this.parents();
156         this.dom.detach();
157         if(parents[0]) {
158             parents[0].refreshPath();
159         }
160          return this;
161     },
162     before: function(params) {
163         return manipulate(this, params, 'before');
164
165     },
166     after: function(params) {
167         return manipulate(this, params, 'after');
168     },
169
170     isBlock: function() {
171         return this.dom.css('display') === 'block';
172     },
173
174     displayAsBlock: function() {
175         this.dom.css('display', 'block');
176         this._container().css('display', 'block');
177     },
178     displayInline: function() {
179         this.dom.css('display', 'inline');
180         this._container().css('display', 'inline');
181     },
182     displayAs: function(what) {
183         // [this.dom(), this._container()].forEach(e) {
184         //     var isBlock = window.getComputedStyle(e).display === 'block';
185         //     if(!isBlock && what === 'block') {
186         //         e.css('display', what);
187         //     } else if(isBlock && what === 'inline') {
188         //         e.css('display')
189         //     }
190         // })
191         this.dom.css('display', what);
192         this._container().css('display', what);
193     }
194 });
195
196
197 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
198 var DocumentTextElement = function(wlxmlTextNode, canvas) {
199     DocumentElement.call(this, wlxmlTextNode, canvas);
200 };
201
202 $.extend(DocumentTextElement, {
203     isContentContainer: function(htmlElement) {
204         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
205     }
206 });
207
208 DocumentTextElement.prototype = Object.create(DocumentElement.prototype);
209
210 $.extend(DocumentTextElement.prototype, {
211     createDOM: function() {
212         var dom = $('<div>')
213             .attr('document-text-element', '')
214             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
215         return dom;
216     },
217     detach: function() {
218         this.dom.detach();
219         return this;
220     },
221     setText: function(text) {
222         if(text === '') {
223             text = utils.unicode.ZWS;
224         }
225         if(text !== this.getText()) {
226             this.dom.contents()[0].data = text;
227         }
228     },
229     handle: function(event) {
230         this.setText(event.meta.node.getText());
231     },
232     getText: function(options) {
233         options = _.extend({raw: false}, options || {});
234         var toret = this.dom.text();
235         if(!options.raw) {
236             toret = toret.replace(utils.unicode.ZWS, '');
237         }
238         return toret;
239     },
240     isEmpty: function() {
241         // Having at least Zero Width Space is guaranteed be Content Observer
242         return this.dom.contents()[0].data === utils.unicode.ZWS;
243     },
244     after: function(params) {
245         if(params instanceof DocumentTextElement || params.text) {
246             return false;
247         }
248         var element;
249         if(params instanceof DocumentNodeElement) {
250             element = params;
251         } else {
252             element = this.canvas.createElement(params);
253         }
254         if(element.dom) {
255             this.dom.wrap('<div>');
256             this.dom.parent().after(element.dom);
257             this.dom.unwrap();
258             this.refreshPath();
259         }
260         return element;
261     },
262     before: function(params) {
263         if(params instanceof DocumentTextElement || params.text) {
264             return false;
265         }
266         var element;
267         if(params instanceof DocumentNodeElement) {
268             element = params;
269         } else {
270             element = this.canvas.createElement(params);
271         }
272         if(element.dom) {
273             this.dom.wrap('<div>');
274             this.dom.parent().before(element.dom);
275             this.dom.unwrap();
276             this.refreshPath();
277         }
278         return element;
279     },
280
281     children: function() {
282         return [];
283     }
284
285 });
286
287
288 return {
289     DocumentElement: DocumentElement,
290     DocumentNodeElement: DocumentNodeElement,
291     DocumentTextElement: DocumentTextElement
292 };
293
294 });