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