editor: inform user about errors that occured during document transaction
[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/wlxmlManagers'
6 ], function($, _, utils, wlxmlManagers) {
7     
8 'use strict';
9 /* global Node:false, document:false */
10
11
12 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
13 var DocumentElement = function(wlxmlNode, canvas) {
14     if(arguments.length === 0) {
15         return;
16     }
17     this.wlxmlNode = wlxmlNode;
18     this.canvas = canvas;
19
20     this.$element = this.createDOM();
21     this.$element.data('canvas-element', this);
22 };
23
24 $.extend(DocumentElement.prototype, {
25     bound: function() {
26         return $.contains(document.documentElement, this.dom()[0]);
27     },
28     dom: function() {
29         return this.$element;
30     },
31     parent: function() {
32         var parents = this.$element.parents('[document-node-element]');
33         if(parents.length) {
34             return this.canvas.getDocumentElement(parents[0]);
35         }
36         return null;
37     },
38
39     parents: function() {
40         var parents = [],
41             parent = this.parent();
42         while(parent) {
43             parents.push(parent);
44             parent = parent.parent();
45         }
46         return parents;
47     },
48
49     sameNode: function(other) {
50         return other && (typeof other === typeof this) && other.dom()[0] === this.dom()[0];
51     },
52
53     markAsCurrent: function() {
54         this.canvas.markAsCurrent(this);
55     },
56
57     getVerticallyFirstTextElement: function() {
58         var toret;
59         this.children().some(function(child) {
60             if(!child.isVisible()) {
61                 return false; // continue
62             }
63             if(child instanceof DocumentTextElement) {
64                 toret = child;
65                 return true; // break
66             } else {
67                 toret = child.getVerticallyFirstTextElement();
68                 if(toret) {
69                     return true; // break
70                 }
71             }
72         });
73         return toret;
74     },
75
76     getPreviousTextElement: function(includeInvisible) {
77         return this.getNearestTextElement('above', includeInvisible);
78     },
79
80     getNextTextElement: function(includeInvisible) {
81         return this.getNearestTextElement('below', includeInvisible);
82     },
83
84     getNearestTextElement: function(direction, includeInvisible) {
85         includeInvisible = includeInvisible !== undefined ? includeInvisible : false;
86         var selector = '[document-text-element]' + (includeInvisible ? '' : ':visible');
87         return this.canvas.getDocumentElement(utils.nearestInDocumentOrder(selector, direction, this.dom()[0]));
88     },
89
90     isVisible: function() {
91         return this instanceof DocumentTextElement || this.getWlxmlTag() !== 'metadata';
92     },
93
94     isInsideList: function() {
95         return this.parents().some(function(parent) {
96             return parent.is('list');
97         });
98     },
99
100     exec: function(method) {
101         if(this.manager && this.manager[method]) {
102             return this.manager[method].apply(this.manager, Array.prototype.slice.call(arguments, 1));
103         }
104     }
105 });
106
107
108 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
109 var DocumentNodeElement = function(wlxmlNode, canvas) {
110     DocumentElement.call(this, wlxmlNode, canvas);
111     wlxmlNode.setData('canvasElement', this);
112 };
113
114
115 var manipulate = function(e, params, action) {
116     var element;
117     if(params instanceof DocumentElement) {
118         element = params;
119     } else {
120         element = e.canvas.createElement(params);
121     }
122     var target = (action === 'append' || action === 'prepend') ? e._container() : e.dom();
123     target[action](element.dom());
124     return element;
125 };
126
127 DocumentNodeElement.prototype = new DocumentElement();
128
129
130 $.extend(DocumentNodeElement.prototype, {
131     createDOM: function() {
132         var dom = $('<div>')
133                 .attr('document-node-element', ''),
134             widgetsContainer = $('<div>')
135                 .addClass('canvas-widgets')
136                 .attr('contenteditable', false),
137             container = $('<div>')
138                 .attr('document-element-content', '');
139         
140         dom.append(widgetsContainer, container);
141         // Make sure widgets aren't navigable with arrow keys
142         widgetsContainer.find('*').add(widgetsContainer).attr('tabindex', -1);
143         this.$element = dom; //@!!!
144
145         this.setWlxmlTag(this.wlxmlNode.getTagName());
146         this.setWlxmlClass(this.wlxmlNode.getClass());
147
148         this.wlxmlNode.contents().forEach(function(node) {
149             container.append(this.canvas.createElement(node).dom());
150         }.bind(this));
151         return dom;
152     },
153     _container: function() {
154         return this.dom().children('[document-element-content]');
155     },
156     detach: function() {
157         this.dom().detach();
158         this.canvas = null;
159         return this;
160     },
161     append: function(params) {
162         return manipulate(this, params, 'append');
163     },
164     prepend: function(params) {
165         return manipulate(this, params, 'prepend');
166     },
167     before: function(params) {
168         return manipulate(this, params, 'before');
169
170     },
171     after: function(params) {
172         return manipulate(this, params, 'after');
173     },
174     children: function() {
175         var toret = [];
176         if(this instanceof DocumentTextElement) {
177             return toret;
178         }
179
180
181         var elementContent = this._container().contents();
182         var element = this;
183         elementContent.each(function() {
184             var childElement = element.canvas.getDocumentElement(this);
185             if(childElement === undefined) {
186                 return true;
187             }
188             toret.push(childElement);
189         });
190         return toret;
191     },
192     childIndex: function(child) {
193         var children = this.children(),
194             toret = null;
195         children.forEach(function(c, idx) {
196             if(c.sameNode(child)) {
197                 toret = idx;
198                 return false;
199             }
200         });
201         return toret;
202     },
203     getWlxmlTag: function() {
204         return this._container().attr('wlxml-tag');
205     },
206     setWlxmlTag: function(tag) {
207         this._container().attr('wlxml-tag', tag);
208     },
209     getWlxmlClass: function() {
210         var klass = this._container().attr('wlxml-class');
211         if(klass) {
212             return klass.replace(/-/g, '.');
213         }
214         return undefined;
215     },
216     setWlxmlClass: function(klass) {
217         if(klass === this.getWlxmlClass()) {
218             return;
219         }
220         if(klass) {
221             this._container().attr('wlxml-class', klass.replace(/\./g, '-'));
222         }
223         else {
224             this._container().removeAttr('wlxml-class');
225         }
226         this.manager = wlxmlManagers.getFor(this);
227         this.manager.setup();
228     },
229     is: function(what) {
230         if(what === 'list' && _.contains(['list.items', 'list.items.enum'], this.getWlxmlClass())) {
231             return true;
232         }
233         return false;
234     },
235     toggleLabel: function(toggle) {
236         var displayCss = toggle ? 'inline-block' : 'none';
237         var label = this.dom().children('.canvas-widgets').find('.canvas-widget-label');
238         label.css('display', displayCss);
239         this.toggleHighlight(toggle);
240     },
241
242     toggleHighlight: function(toggle) {
243         this._container().toggleClass('highlighted-element', toggle);
244     },
245
246     toggle: function(toggle) {
247         if(this.manager) {
248             this.manager.toggle(toggle);
249         }
250     }
251 });
252
253
254 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
255 var DocumentTextElement = function(wlxmlTextNode, canvas) {
256     DocumentElement.call(this, wlxmlTextNode, canvas);
257 };
258
259 $.extend(DocumentTextElement, {
260     isContentContainer: function(htmlElement) {
261         return htmlElement.nodeType === Node.TEXT_NODE && $(htmlElement).parent().is('[document-text-element]');
262     }
263 });
264
265 DocumentTextElement.prototype = new DocumentElement();
266
267 $.extend(DocumentTextElement.prototype, {
268     createDOM: function() {
269         return $('<div>')
270             .attr('document-text-element', '')
271             .text(this.wlxmlNode.getText() || utils.unicode.ZWS);
272     },
273     detach: function() {
274         this.dom().detach();
275         this.canvas = null;
276         return this;
277     },
278     setText: function(text) {
279         this.dom().contents()[0].data = text;
280     },
281     getText: function(options) {
282         options = _.extend({raw: false}, options || {});
283         var toret = this.dom().text();
284         if(!options.raw) {
285             toret = toret.replace(utils.unicode.ZWS, '');
286         }
287         return toret;
288     },
289     isEmpty: function() {
290         // Having at least Zero Width Space is guaranteed be Content Observer
291         return this.dom().contents()[0].data === utils.unicode.ZWS;
292     },
293     after: function(params) {
294         if(params instanceof DocumentTextElement || params.text) {
295             return false;
296         }
297         var element;
298         if(params instanceof DocumentNodeElement) {
299             element = params;
300         } else {
301             element = this.canvas.createElement(params);
302         }
303         this.dom().wrap('<div>');
304         this.dom().parent().after(element.dom());
305         this.dom().unwrap();
306         return element;
307     },
308     before: function(params) {
309         if(params instanceof DocumentTextElement || params.text) {
310             return false;
311         }
312         var element;
313         if(params instanceof DocumentNodeElement) {
314             element = params;
315         } else {
316             element = this.canvas.createElement(params);
317         }
318         this.dom().wrap('<div>');
319         this.dom().parent().before(element.dom());
320         this.dom().unwrap();
321         return element;
322     },
323
324     toggleHighlight: function() {
325         // do nothing for now
326     }
327 });
328
329 return {
330     DocumentElement: DocumentElement,
331     DocumentNodeElement: DocumentNodeElement,
332     DocumentTextElement: DocumentTextElement
333 };
334
335 });