Output formatting wip: keeping excessive white space at the end of text intact
[fnpeditor.git] / modules / documentCanvas / canvas / documentElement.js
1 define([
2 'libs/jquery-1.9.1.min',
3 'libs/underscore-min',
4 'modules/documentCanvas/classAttributes'
5 ], function($, _, classAttributes) {
6     
7 'use strict';
8
9
10 // DocumentElement represents a text or an element node from WLXML document rendered inside Canvas
11 var DocumentElement = function(htmlElement, canvas) {
12     if(arguments.length === 0)
13         return;
14     this.canvas = canvas;
15     this._setupDOMHandler(htmlElement);
16 }
17
18 var elementTypeFromParams = function(params) {
19     return params.text !== undefined ? DocumentTextElement : DocumentNodeElement;
20
21 };
22
23 $.extend(DocumentElement, {
24     create: function(params, canvas) {
25         return elementTypeFromParams(params).create(params);
26     },
27
28     createDOM: function(params) {
29         return elementTypeFromParams(params).createDOM(params);
30     },
31
32     fromHTMLElement: function(htmlElement, canvas) {
33         var $element = $(htmlElement);
34         if(htmlElement.nodeType === Node.ELEMENT_NODE && $element.attr('wlxml-tag'))
35             return DocumentNodeElement.fromHTMLElement(htmlElement, canvas);
36         if($element.attr('wlxml-text') !== undefined || (htmlElement.nodeType === Node.TEXT_NODE && $element.parent().attr('wlxml-text') !== undefined))
37             return DocumentTextElement.fromHTMLElement(htmlElement, canvas);
38         return undefined;
39     }
40 });
41
42 $.extend(DocumentElement.prototype, {
43     _setupDOMHandler: function(htmlElement) {
44         this.$element = $(htmlElement);
45     },
46     dom: function() {
47         return this.$element;
48     },
49     parent: function() {
50         var parents = this.$element.parents('[wlxml-tag]');
51         if(parents.length)
52             return DocumentElement.fromHTMLElement(parents[0], this.canvas);
53         return null;
54     },
55
56     parents: function() {
57         var parents = [],
58             parent = this.parent();
59         while(parent) {
60             parents.push(parent);
61             parent = parent.parent();
62         }
63         return parents;
64     },
65
66     sameNode: function(other) {
67         return other && (typeof other === typeof this) && other.dom()[0] === this.dom()[0];
68     },
69
70     wrapWithNodeElement: function(wlxmlNode) {
71         var wrapper = DocumentNodeElement.create({tag: wlxmlNode.tag, klass: wlxmlNode.klass});
72         this.dom().replaceWith(wrapper.dom());
73         wrapper.append(this);
74         return wrapper;
75     },
76
77     detach: function() {
78         this.dom().detach();
79         this.canvas = null;
80     },
81
82     markAsCurrent: function() {
83         this.canvas.markAsCurrent(this);
84     },
85
86     getVerticallyFirstTextElement: function() {
87         var toret;
88         this.children().some(function(child) {
89             if(!child.isVisible())
90                 return false; // continue
91             if(child instanceof DocumentTextElement) {
92                 toret = child;
93                 return true; // break
94             } else {
95                 toret = child.getVerticallyFirstTextElement();
96                 if(toret)
97                     return true; // break
98             }
99         });
100         return toret;
101     },
102
103     isVisible: function() {
104         return this instanceof DocumentTextElement || this.getWlxmlTag() !== 'metadata';
105     }
106 });
107
108
109 // DocumentNodeElement represents an element node from WLXML document rendered inside Canvas
110 var DocumentNodeElement = function(htmlElement, canvas) {
111     DocumentElement.call(this, htmlElement, canvas);
112 };
113
114 $.extend(DocumentNodeElement, {
115     createDOM: function(params) {
116         var dom = $('<div>')
117             .attr('wlxml-tag', params.tag);
118         if(params.klass)
119             dom.attr('wlxml-class', params.klass.replace(/\./g, '-'));
120         if(params.meta) {
121             _.keys(params.meta).forEach(function(key) {
122                 dom.attr('wlxml-meta-'+key, params.meta[key]);
123             });
124         }
125         dom.data('other-attrs', params.others);
126         return dom;
127     },
128
129     create: function(params, canvas) {
130         return this.fromHTMLElement(this.createDOM(params)[0]);
131     },
132
133     fromHTMLElement: function(htmlElement, canvas) {
134         return new this(htmlElement, canvas);
135     }
136 });
137
138 var manipulate = function(e, params, action) {
139     var element;
140     if(params instanceof DocumentElement) {
141         element = params;
142     } else {
143         element = DocumentElement.create(params);
144     }
145     e.dom()[action](element.dom());
146     return element;
147 };
148
149 DocumentNodeElement.prototype = new DocumentElement();
150
151 $.extend(DocumentNodeElement.prototype, {
152     data: function() {
153         var dom = this.dom(),
154             args = Array.prototype.slice.call(arguments, 0);
155         if(args.length === 2 && args[1] === undefined)
156             return dom.removeData(args[1]);
157         return dom.data.apply(dom, arguments);
158     },
159     toXML: function(level) {
160         var node = $('<' + this.getWlxmlTag() + '>'),
161             toret = $('<div>');
162         if(this.getWlxmlClass())
163             node.attr('class', this.getWlxmlClass());
164         var meta = this.getWlxmlMetaAttrs();
165         meta.forEach(function(attr) {
166             if(attr.value)
167                 node.attr('meta-' + attr.name, attr.value);
168         });
169         _.keys(this.data('other-attrs') || {}).forEach(function(key) {
170             node.attr(key, this.data('other-attrs')[key]);
171         }, this);
172
173         if(this.data('orig-before') !== undefined) {
174             toret.append(document.createTextNode(this.data('orig-before')));
175         } else if(level && this.getWlxmlTag() !== 'span') {
176             toret.append('\n' + (new Array(level * 2 + 1)).join(' '));
177         }
178         if(this.data('orig-append') !== undefined) {
179             node.append(this.data('orig-append'));
180             //toret = toret.prepend(document.createTextNode(this.data('orig-prepend')));
181         } else if(this.getWlxmlTag() !== 'span'){
182             node.append('\n' + (new Array(level * 2 + 1)).join(' '));
183         }
184
185         toret.append(node);
186         return toret.contents();
187     },
188     append: function(params) {
189         if(params.tag !== 'span')
190             this.data('orig-append', undefined);
191         return manipulate(this, params, 'append');
192     },
193     before: function(params) {
194         return manipulate(this, params, 'before');
195
196     },
197     after: function(params) {
198         return manipulate(this, params, 'after');
199     },
200     children: function() {
201         var toret = [];
202         if(this instanceof DocumentTextElement)
203             return toret;
204
205
206         var elementContent = this.dom().contents();
207         var element = this;
208         elementContent.each(function(idx) {
209             var childElement = DocumentElement.fromHTMLElement(this, element.canvas);
210             if(idx === 0 && elementContent.length > 1 && elementContent[1].nodeType === Node.ELEMENT_NODE && (childElement instanceof DocumentTextElement) && $.trim($(this).text()) === '')
211                 return true;
212             if(idx > 0 && childElement instanceof DocumentTextElement) {
213                 if(toret[toret.length-1] instanceof DocumentNodeElement && $.trim($(this).text()) === '')
214                     return true;
215             }
216             toret.push(childElement);
217         });
218         return toret;
219     },
220     childIndex: function(child) {
221         var children = this.children(),
222             toret = null;
223         children.forEach(function(c, idx) {
224             if(c.sameNode(child)) {
225                 toret = idx;
226                 return false;
227             }
228         });
229         return toret;
230     },
231     getWlxmlTag: function() {
232         return this.dom().attr('wlxml-tag');
233     },
234     setWlxmlTag: function(tag) {
235         this.dom().attr('wlxml-tag', tag);
236     },
237     getWlxmlClass: function() {
238         var klass = this.dom().attr('wlxml-class');
239         if(klass)
240             return klass.replace(/-/g, '.');
241         return undefined;
242     },
243     setWlxmlClass: function(klass) {
244         this.getWlxmlMetaAttrs().forEach(function(attr) {
245             if(!classAttributes.hasMetaAttr(klass, attr.name))
246                 this.dom().removeAttr('wlxml-meta-' + attr.name);
247         }, this);
248
249         if(klass)
250             this.dom().attr('wlxml-class', klass.replace(/\./g, '-'));
251         else
252             this.dom().removeAttr('wlxml-class');
253     },
254     is: function(what) {
255         if(what === 'list' && _.contains(['list-items', 'list-items-enum'], this.dom().attr('wlxml-class')))
256             return true;
257         return false;
258     },
259
260
261     getWlxmlMetaAttr: function(attr) {
262         return this.dom().attr('wlxml-meta-'+attr);
263     },
264     getWlxmlMetaAttrs: function() {
265         var toret = [];
266         var attrList = classAttributes.getMetaAttrsList(this.getWlxmlClass());
267         attrList.all.forEach(function(attr) {
268             toret.push({name: attr.name, value: this.getWlxmlMetaAttr(attr.name) || ''});
269         }, this);
270         return toret;
271     },
272     setWlxmlMetaAttr: function(attr, value) {
273         this.dom().attr('wlxml-meta-'+attr, value);
274     }
275 });
276
277
278 // DocumentNodeElement represents a text node from WLXML document rendered inside Canvas
279 var DocumentTextElement = function(htmlElement, canvas) {
280     DocumentElement.call(this, htmlElement, canvas);
281 };
282
283 $.extend(DocumentTextElement, {
284     createDOM: function(params) {
285         return $('<div>')
286             .attr('wlxml-text', '')
287             .text(params.text);
288     },
289
290     create: function(params, canvas) {
291         return this.fromHTMLElement(this.createDOM(params)[0]);
292     },
293
294     fromHTMLElement: function(htmlElement, canvas) {
295         return new this(htmlElement, canvas);
296     }
297 });
298
299 DocumentTextElement.prototype = new DocumentElement();
300
301 $.extend(DocumentTextElement.prototype, {
302     toXML: function() {
303         return this.getText();
304     },
305     _setupDOMHandler: function(htmlElement) {
306         var $element = $(htmlElement);
307         if(htmlElement.nodeType === Node.TEXT_NODE)
308             this.$element = $element.parent();
309         else
310             this.$element = $element;
311     },
312     setText: function(text) {
313         this.dom().contents()[0].data = text;
314     },
315     getText: function() {
316         return this.dom().text();
317     },
318     after: function(params) {
319         if(params instanceof DocumentTextElement || params.text)
320             return false;
321         var element;
322         if(params instanceof DocumentNodeElement) {
323             element = params;
324         } else {
325             element = DocumentNodeElement.create(params);
326         }
327         this.dom().wrap('<div>');
328         this.dom().parent().after(element.dom());
329         this.dom().unwrap();
330         return element;
331     },
332     before: function(params) {
333         if(params instanceof DocumentTextElement || params.text)
334             return false;
335         var element;
336         if(params instanceof DocumentNodeElement) {
337             element = params;
338         } else {
339             element = DocumentNodeElement.create(params);
340         }
341         this.dom().wrap('<div>');
342         this.dom().parent().before(element.dom());
343         this.dom().unwrap();
344         return element;
345     },
346     wrapWithNodeElement: function(wlxmlNode) {
347         if(typeof wlxmlNode.start === 'number' && typeof wlxmlNode.end === 'number') {
348             return this.canvas.wrapText({
349                 inside: this.parent(),
350                 textNodeIdx: this.parent().childIndex(this),
351                 offsetStart: Math.min(wlxmlNode.start, wlxmlNode.end),
352                 offsetEnd: Math.max(wlxmlNode.start, wlxmlNode.end),
353                 _with: {tag: wlxmlNode.tag, klass: wlxmlNode.klass}
354             });
355         } else {
356             return DocumentElement.prototype.wrapWithNodeElement.call(this, wlxmlNode);
357         }
358     },
359     unwrap: function() {
360         var parent = this.parent(),
361             toret;
362         if(parent.children().length === 1) {
363             toret = parent.parent();
364             var grandParent = parent.parent();
365             if(grandParent) {
366                 var grandParentChildren = grandParent.children(),
367                     idx = grandParent.childIndex(parent),
368                     prev = idx - 1 > -1 ? grandParentChildren[idx-1] : null,
369                     next = idx + 1 < grandParentChildren.length ? grandParentChildren[idx+1] : null;
370                 if(prev && next) {
371                     prev.setText(prev.getText() + this.getText() + next.getText());
372                     next.detach();
373                 } else if (prev || next) {
374                     var target = prev ? prev : next;
375                     target.setText(target.getText() + this.getText());
376                 } else {
377                     parent.after(this);
378                 }
379             } else {
380                 parent.after(this);
381             }
382             parent.detach();
383             return toret;
384         }
385     },
386     split: function(params) {
387         var parentElement = this.parent(),
388             myIdx = parentElement.childIndex(this),
389             myCanvas = this.canvas,
390             passed = false,
391             succeedingChildren = [],
392             thisElement = this,
393             prefix = this.getText().substr(0, params.offset),
394             suffix = this.getText().substr(params.offset);
395
396         parentElement.children().forEach(function(child) {
397             if(passed)
398                 succeedingChildren.push(child);
399             if(child.sameNode(thisElement))
400                 passed = true;
401         });
402
403         if(prefix.length > 0)
404             this.setText(prefix);
405         else
406             this.detach();
407         
408         var newElement = DocumentNodeElement.create({tag: parentElement.getWlxmlTag(), klass: parentElement.getWlxmlClass()}, myCanvas);
409         parentElement.after(newElement);
410
411         if(suffix.length > 0)
412             newElement.append({text: suffix});
413         succeedingChildren.forEach(function(child) {
414             newElement.append(child);
415         });
416
417         return {first: parentElement, second: newElement};
418     },
419 });
420
421 return {
422     DocumentElement: DocumentElement,
423     DocumentNodeElement: DocumentNodeElement,
424     DocumentTextElement: DocumentTextElement
425 };
426
427 });