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