WIP: Note editing, markdown to html
[oweals/karmaworld.git] / karmaworld / apps / wysihtml5 / static / wysihtml5 / wysihtml-0.4.17 / src / selection / html_applier.js
1 /**
2  * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
3  * http://code.google.com/p/rangy/
4  *
5  * changed in order to be able ...
6  *    - to use custom tags
7  *    - to detect and replace similar css classes via reg exp
8  */
9 (function(wysihtml5, rangy) {
10   var defaultTagName = "span";
11
12   var REG_EXP_WHITE_SPACE = /\s+/g;
13
14   function hasClass(el, cssClass, regExp) {
15     if (!el.className) {
16       return false;
17     }
18
19     var matchingClassNames = el.className.match(regExp) || [];
20     return matchingClassNames[matchingClassNames.length - 1] === cssClass;
21   }
22
23   function hasStyleAttr(el, regExp) {
24     if (!el.getAttribute || !el.getAttribute('style')) {
25       return false;
26     }
27     var matchingStyles = el.getAttribute('style').match(regExp);
28     return  (el.getAttribute('style').match(regExp)) ? true : false;
29   }
30
31   function addStyle(el, cssStyle, regExp) {
32     if (el.getAttribute('style')) {
33       removeStyle(el, regExp);
34       if (el.getAttribute('style') && !(/^\s*$/).test(el.getAttribute('style'))) {
35         el.setAttribute('style', cssStyle + ";" + el.getAttribute('style'));
36       } else {
37         el.setAttribute('style', cssStyle);
38       }
39     } else {
40       el.setAttribute('style', cssStyle);
41     }
42   }
43
44   function addClass(el, cssClass, regExp) {
45     if (el.className) {
46       removeClass(el, regExp);
47       el.className += " " + cssClass;
48     } else {
49       el.className = cssClass;
50     }
51   }
52
53   function removeClass(el, regExp) {
54     if (el.className) {
55       el.className = el.className.replace(regExp, "");
56     }
57   }
58
59   function removeStyle(el, regExp) {
60     var s,
61         s2 = [];
62     if (el.getAttribute('style')) {
63       s = el.getAttribute('style').split(';');
64       for (var i = s.length; i--;) {
65         if (!s[i].match(regExp) && !(/^\s*$/).test(s[i])) {
66           s2.push(s[i]);
67         }
68       }
69       if (s2.length) {
70         el.setAttribute('style', s2.join(';'));
71       } else {
72         el.removeAttribute('style');
73       }
74     }
75   }
76
77   function getMatchingStyleRegexp(el, style) {
78     var regexes = [],
79         sSplit = style.split(';'),
80         elStyle = el.getAttribute('style');
81
82     if (elStyle) {
83       elStyle = elStyle.replace(/\s/gi, '').toLowerCase();
84       regexes.push(new RegExp("(^|\\s|;)" + style.replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi"));
85
86       for (var i = sSplit.length; i-- > 0;) {
87         if (!(/^\s*$/).test(sSplit[i])) {
88           regexes.push(new RegExp("(^|\\s|;)" + sSplit[i].replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi"));
89         }
90       }
91       for (var j = 0, jmax = regexes.length; j < jmax; j++) {
92         if (elStyle.match(regexes[j])) {
93           return regexes[j];
94         }
95       }
96     }
97
98     return false;
99   }
100
101   function isMatchingAllready(node, tags, style, className) {
102     if (style) {
103       return getMatchingStyleRegexp(node, style);
104     } else if (className) {
105       return wysihtml5.dom.hasClass(node, className);
106     } else {
107       return rangy.dom.arrayContains(tags, node.tagName.toLowerCase());
108     }
109   }
110
111   function areMatchingAllready(nodes, tags, style, className) {
112     for (var i = nodes.length; i--;) {
113       if (!isMatchingAllready(nodes[i], tags, style, className)) {
114         return false;
115       }
116     }
117     return nodes.length ? true : false;
118   }
119
120   function removeOrChangeStyle(el, style, regExp) {
121
122     var exactRegex = getMatchingStyleRegexp(el, style);
123     if (exactRegex) {
124       // adding same style value on property again removes style
125       removeStyle(el, exactRegex);
126       return "remove";
127     } else {
128       // adding new style value changes value
129       addStyle(el, style, regExp);
130       return "change";
131     }
132   }
133
134   function hasSameClasses(el1, el2) {
135     return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
136   }
137
138   function replaceWithOwnChildren(el) {
139     var parent = el.parentNode;
140     while (el.firstChild) {
141       parent.insertBefore(el.firstChild, el);
142     }
143     parent.removeChild(el);
144   }
145
146   function elementsHaveSameNonClassAttributes(el1, el2) {
147     if (el1.attributes.length != el2.attributes.length) {
148       return false;
149     }
150     for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
151       attr1 = el1.attributes[i];
152       name = attr1.name;
153       if (name != "class") {
154         attr2 = el2.attributes.getNamedItem(name);
155         if (attr1.specified != attr2.specified) {
156           return false;
157         }
158         if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
159           return false;
160         }
161       }
162     }
163     return true;
164   }
165
166   function isSplitPoint(node, offset) {
167     if (rangy.dom.isCharacterDataNode(node)) {
168       if (offset == 0) {
169         return !!node.previousSibling;
170       } else if (offset == node.length) {
171         return !!node.nextSibling;
172       } else {
173         return true;
174       }
175     }
176
177     return offset > 0 && offset < node.childNodes.length;
178   }
179
180   function splitNodeAt(node, descendantNode, descendantOffset, container) {
181     var newNode;
182     if (rangy.dom.isCharacterDataNode(descendantNode)) {
183       if (descendantOffset == 0) {
184         descendantOffset = rangy.dom.getNodeIndex(descendantNode);
185         descendantNode = descendantNode.parentNode;
186       } else if (descendantOffset == descendantNode.length) {
187         descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
188         descendantNode = descendantNode.parentNode;
189       } else {
190         newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
191       }
192     }
193     if (!newNode) {
194       if (!container || descendantNode !== container) {
195
196         newNode = descendantNode.cloneNode(false);
197         if (newNode.id) {
198           newNode.removeAttribute("id");
199         }
200         var child;
201         while ((child = descendantNode.childNodes[descendantOffset])) {
202           newNode.appendChild(child);
203         }
204         rangy.dom.insertAfter(newNode, descendantNode);
205
206       }
207     }
208     return (descendantNode == node) ? newNode :  splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode), container);
209   }
210
211   function Merge(firstNode) {
212     this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
213     this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
214     this.textNodes = [this.firstTextNode];
215   }
216
217   Merge.prototype = {
218     doMerge: function() {
219       var textBits = [], textNode, parent, text;
220       for (var i = 0, len = this.textNodes.length; i < len; ++i) {
221         textNode = this.textNodes[i];
222         parent = textNode.parentNode;
223         textBits[i] = textNode.data;
224         if (i) {
225           parent.removeChild(textNode);
226           if (!parent.hasChildNodes()) {
227             parent.parentNode.removeChild(parent);
228           }
229         }
230       }
231       this.firstTextNode.data = text = textBits.join("");
232       return text;
233     },
234
235     getLength: function() {
236       var i = this.textNodes.length, len = 0;
237       while (i--) {
238         len += this.textNodes[i].length;
239       }
240       return len;
241     },
242
243     toString: function() {
244       var textBits = [];
245       for (var i = 0, len = this.textNodes.length; i < len; ++i) {
246         textBits[i] = "'" + this.textNodes[i].data + "'";
247       }
248       return "[Merge(" + textBits.join(",") + ")]";
249     }
250   };
251
252   function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize, cssStyle, similarStyleRegExp, container) {
253     this.tagNames = tagNames || [defaultTagName];
254     this.cssClass = cssClass || ((cssClass === false) ? false : "");
255     this.similarClassRegExp = similarClassRegExp;
256     this.cssStyle = cssStyle || "";
257     this.similarStyleRegExp = similarStyleRegExp;
258     this.normalize = normalize;
259     this.applyToAnyTagName = false;
260     this.container = container;
261   }
262
263   HTMLApplier.prototype = {
264     getAncestorWithClass: function(node) {
265       var cssClassMatch;
266       while (node) {
267         cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : (this.cssStyle !== "") ? false : true;
268         if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" &&  rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
269           return node;
270         }
271         node = node.parentNode;
272       }
273       return false;
274     },
275
276     // returns parents of node with given style attribute
277     getAncestorWithStyle: function(node) {
278       var cssStyleMatch;
279       while (node) {
280         cssStyleMatch = this.cssStyle ? hasStyleAttr(node, this.similarStyleRegExp) : false;
281
282         if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) {
283           return node;
284         }
285         node = node.parentNode;
286       }
287       return false;
288     },
289
290     getMatchingAncestor: function(node) {
291       var ancestor = this.getAncestorWithClass(node),
292           matchType = false;
293
294       if (!ancestor) {
295         ancestor = this.getAncestorWithStyle(node);
296         if (ancestor) {
297           matchType = "style";
298         }
299       } else {
300         if (this.cssStyle) {
301           matchType = "class";
302         }
303       }
304
305       return {
306         "element": ancestor,
307         "type": matchType
308       };
309     },
310
311     // Normalizes nodes after applying a CSS class to a Range.
312     postApply: function(textNodes, range) {
313       var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
314
315       var merges = [], currentMerge;
316
317       var rangeStartNode = firstNode, rangeEndNode = lastNode;
318       var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
319
320       var textNode, precedingTextNode;
321
322       for (var i = 0, len = textNodes.length; i < len; ++i) {
323         textNode = textNodes[i];
324         precedingTextNode = null;
325         if (textNode && textNode.parentNode) {
326           precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
327         }
328         if (precedingTextNode) {
329           if (!currentMerge) {
330             currentMerge = new Merge(precedingTextNode);
331             merges.push(currentMerge);
332           }
333           currentMerge.textNodes.push(textNode);
334           if (textNode === firstNode) {
335             rangeStartNode = currentMerge.firstTextNode;
336             rangeStartOffset = rangeStartNode.length;
337           }
338           if (textNode === lastNode) {
339             rangeEndNode = currentMerge.firstTextNode;
340             rangeEndOffset = currentMerge.getLength();
341           }
342         } else {
343           currentMerge = null;
344         }
345       }
346       // Test whether the first node after the range needs merging
347       if(lastNode && lastNode.parentNode) {
348         var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
349         if (nextTextNode) {
350           if (!currentMerge) {
351             currentMerge = new Merge(lastNode);
352             merges.push(currentMerge);
353           }
354           currentMerge.textNodes.push(nextTextNode);
355         }
356       }
357       // Do the merges
358       if (merges.length) {
359         for (i = 0, len = merges.length; i < len; ++i) {
360           merges[i].doMerge();
361         }
362         // Set the range boundaries
363         range.setStart(rangeStartNode, rangeStartOffset);
364         range.setEnd(rangeEndNode, rangeEndOffset);
365       }
366     },
367
368     getAdjacentMergeableTextNode: function(node, forward) {
369         var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
370         var el = isTextNode ? node.parentNode : node;
371         var adjacentNode;
372         var propName = forward ? "nextSibling" : "previousSibling";
373         if (isTextNode) {
374           // Can merge if the node's previous/next sibling is a text node
375           adjacentNode = node[propName];
376           if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
377             return adjacentNode;
378           }
379         } else {
380           // Compare element with its sibling
381           adjacentNode = el[propName];
382           if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
383             return adjacentNode[forward ? "firstChild" : "lastChild"];
384           }
385         }
386         return null;
387     },
388
389     areElementsMergeable: function(el1, el2) {
390       return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
391         && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
392         && hasSameClasses(el1, el2)
393         && elementsHaveSameNonClassAttributes(el1, el2);
394     },
395
396     createContainer: function(doc) {
397       var el = doc.createElement(this.tagNames[0]);
398       if (this.cssClass) {
399         el.className = this.cssClass;
400       }
401       if (this.cssStyle) {
402         el.setAttribute('style', this.cssStyle);
403       }
404       return el;
405     },
406
407     applyToTextNode: function(textNode) {
408       var parent = textNode.parentNode;
409       if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
410
411         if (this.cssClass) {
412           addClass(parent, this.cssClass, this.similarClassRegExp);
413         }
414         if (this.cssStyle) {
415           addStyle(parent, this.cssStyle, this.similarStyleRegExp);
416         }
417       } else {
418         var el = this.createContainer(rangy.dom.getDocument(textNode));
419         textNode.parentNode.insertBefore(el, textNode);
420         el.appendChild(textNode);
421       }
422     },
423
424     isRemovable: function(el) {
425       return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) &&
426               wysihtml5.lang.string(el.className).trim() === "" &&
427               (
428                 !el.getAttribute('style') ||
429                 wysihtml5.lang.string(el.getAttribute('style')).trim() === ""
430               );
431     },
432
433     undoToTextNode: function(textNode, range, ancestorWithClass, ancestorWithStyle) {
434       var styleMode = (ancestorWithClass) ? false : true,
435           ancestor = ancestorWithClass || ancestorWithStyle,
436           styleChanged = false;
437       if (!range.containsNode(ancestor)) {
438         // Split out the portion of the ancestor from which we can remove the CSS class
439         var ancestorRange = range.cloneRange();
440             ancestorRange.selectNode(ancestor);
441
442         if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
443             splitNodeAt(ancestor, range.endContainer, range.endOffset, this.container);
444             range.setEndAfter(ancestor);
445         }
446         if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
447             ancestor = splitNodeAt(ancestor, range.startContainer, range.startOffset, this.container);
448         }
449       }
450
451       if (!styleMode && this.similarClassRegExp) {
452         removeClass(ancestor, this.similarClassRegExp);
453       }
454
455       if (styleMode && this.similarStyleRegExp) {
456         styleChanged = (removeOrChangeStyle(ancestor, this.cssStyle, this.similarStyleRegExp) === "change");
457       }
458       if (this.isRemovable(ancestor) && !styleChanged) {
459         replaceWithOwnChildren(ancestor);
460       }
461     },
462
463     applyToRange: function(range) {
464         var textNodes;
465         for (var ri = range.length; ri--;) {
466             textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
467
468             if (!textNodes.length) {
469               try {
470                 var node = this.createContainer(range[ri].endContainer.ownerDocument);
471                 range[ri].surroundContents(node);
472                 this.selectNode(range[ri], node);
473                 return;
474               } catch(e) {}
475             }
476
477             range[ri].splitBoundaries();
478             textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
479             if (textNodes.length) {
480               var textNode;
481
482               for (var i = 0, len = textNodes.length; i < len; ++i) {
483                 textNode = textNodes[i];
484                 if (!this.getMatchingAncestor(textNode).element) {
485                   this.applyToTextNode(textNode);
486                 }
487               }
488
489               range[ri].setStart(textNodes[0], 0);
490               textNode = textNodes[textNodes.length - 1];
491               range[ri].setEnd(textNode, textNode.length);
492
493               if (this.normalize) {
494                 this.postApply(textNodes, range[ri]);
495               }
496             }
497
498         }
499     },
500
501     undoToRange: function(range) {
502       var textNodes, textNode, ancestorWithClass, ancestorWithStyle, ancestor;
503       for (var ri = range.length; ri--;) {
504
505           textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
506           if (textNodes.length) {
507             range[ri].splitBoundaries();
508             textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
509           } else {
510             var doc = range[ri].endContainer.ownerDocument,
511                 node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
512             range[ri].insertNode(node);
513             range[ri].selectNode(node);
514             textNodes = [node];
515           }
516
517           for (var i = 0, len = textNodes.length; i < len; ++i) {
518             if (range[ri].isValid()) {
519               textNode = textNodes[i];
520
521               ancestor = this.getMatchingAncestor(textNode);
522               if (ancestor.type === "style") {
523                 this.undoToTextNode(textNode, range[ri], false, ancestor.element);
524               } else if (ancestor.element) {
525                 this.undoToTextNode(textNode, range[ri], ancestor.element);
526               }
527             }
528           }
529
530           if (len == 1) {
531             this.selectNode(range[ri], textNodes[0]);
532           } else {
533             range[ri].setStart(textNodes[0], 0);
534             textNode = textNodes[textNodes.length - 1];
535             range[ri].setEnd(textNode, textNode.length);
536
537             if (this.normalize) {
538               this.postApply(textNodes, range[ri]);
539             }
540           }
541
542       }
543     },
544
545     selectNode: function(range, node) {
546       var isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
547           canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : true,
548           content         = isElement ? node.innerHTML : node.data,
549           isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
550
551       if (isEmpty && isElement && canHaveHTML) {
552         // Make sure that caret is visible in node by inserting a zero width no breaking space
553         try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
554       }
555       range.selectNodeContents(node);
556       if (isEmpty && isElement) {
557         range.collapse(false);
558       } else if (isEmpty) {
559         range.setStartAfter(node);
560         range.setEndAfter(node);
561       }
562     },
563
564     getTextSelectedByRange: function(textNode, range) {
565       var textRange = range.cloneRange();
566       textRange.selectNodeContents(textNode);
567
568       var intersectionRange = textRange.intersection(range);
569       var text = intersectionRange ? intersectionRange.toString() : "";
570       textRange.detach();
571
572       return text;
573     },
574
575     isAppliedToRange: function(range) {
576       var ancestors = [],
577           appliedType = "full",
578           ancestor, styleAncestor, textNodes;
579
580       for (var ri = range.length; ri--;) {
581
582         textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
583         if (!textNodes.length) {
584           ancestor = this.getMatchingAncestor(range[ri].startContainer).element;
585
586           return (ancestor) ? {
587             "elements": [ancestor],
588             "coverage": appliedType
589           } : false;
590         }
591
592         for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
593           selectedText = this.getTextSelectedByRange(textNodes[i], range[ri]);
594           ancestor = this.getMatchingAncestor(textNodes[i]).element;
595           if (ancestor && selectedText != "") {
596             ancestors.push(ancestor);
597
598             if (wysihtml5.dom.getTextNodes(ancestor, true).length === 1) {
599               appliedType = "full";
600             } else if (appliedType === "full") {
601               appliedType = "inline";
602             }
603           } else if (!ancestor) {
604             appliedType = "partial";
605           }
606         }
607
608       }
609
610       return (ancestors.length) ? {
611         "elements": ancestors,
612         "coverage": appliedType
613       } : false;
614     },
615
616     toggleRange: function(range) {
617       var isApplied = this.isAppliedToRange(range),
618           parentsExactMatch;
619
620       if (isApplied) {
621         if (isApplied.coverage === "full") {
622           this.undoToRange(range);
623         } else if (isApplied.coverage === "inline") {
624           parentsExactMatch = areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass);
625           this.undoToRange(range);
626           if (!parentsExactMatch) {
627             this.applyToRange(range);
628           }
629         } else {
630           // partial
631           if (!areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass)) {
632             this.undoToRange(range);
633           }
634           this.applyToRange(range);
635         }
636       } else {
637         this.applyToRange(range);
638       }
639     }
640   };
641
642   wysihtml5.selection.HTMLApplier = HTMLApplier;
643
644 })(wysihtml5, rangy);