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