WIP: Note editing, markdown to html
[oweals/karmaworld.git] / karmaworld / apps / wysihtml5 / static / wysihtml5 / wysihtml-0.4.17 / src / selection / selection.js
1 /**
2  * Selection API
3  *
4  * @example
5  *    var selection = new wysihtml5.Selection(editor);
6  */
7 (function(wysihtml5) {
8   var dom = wysihtml5.dom;
9
10   function _getCumulativeOffsetTop(element) {
11     var top = 0;
12     if (element.parentNode) {
13       do {
14         top += element.offsetTop || 0;
15         element = element.offsetParent;
16       } while (element);
17     }
18     return top;
19   }
20
21   // Provides the depth of ``descendant`` relative to ``ancestor``
22   function getDepth(ancestor, descendant) {
23       var ret = 0;
24       while (descendant !== ancestor) {
25           ret++;
26           descendant = descendant.parentNode;
27           if (!descendant)
28               throw new Error("not a descendant of ancestor!");
29       }
30       return ret;
31   }
32
33   // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
34   // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
35   function expandRangeToSurround(range) {
36       if (range.canSurroundContents()) return;
37
38       var common = range.commonAncestorContainer,
39           start_depth = getDepth(common, range.startContainer),
40           end_depth = getDepth(common, range.endContainer);
41
42       while(!range.canSurroundContents()) {
43         // In the following branches, we cannot just decrement the depth variables because the setStartBefore/setEndAfter may move the start or end of the range more than one level relative to ``common``. So we need to recompute the depth.
44         if (start_depth > end_depth) {
45             range.setStartBefore(range.startContainer);
46             start_depth = getDepth(common, range.startContainer);
47         }
48         else {
49             range.setEndAfter(range.endContainer);
50             end_depth = getDepth(common, range.endContainer);
51         }
52       }
53   }
54
55   wysihtml5.Selection = Base.extend(
56     /** @scope wysihtml5.Selection.prototype */ {
57     constructor: function(editor, contain, unselectableClass) {
58       // Make sure that our external range library is initialized
59       window.rangy.init();
60
61       this.editor   = editor;
62       this.composer = editor.composer;
63       this.doc      = this.composer.doc;
64       this.contain = contain;
65       this.unselectableClass = unselectableClass || false;
66     },
67
68     /**
69      * Get the current selection as a bookmark to be able to later restore it
70      *
71      * @return {Object} An object that represents the current selection
72      */
73     getBookmark: function() {
74       var range = this.getRange();
75       if (range) expandRangeToSurround(range);
76       return range && range.cloneRange();
77     },
78
79     /**
80      * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
81      *
82      * @param {Object} bookmark An object that represents the current selection
83      */
84     setBookmark: function(bookmark) {
85       if (!bookmark) {
86         return;
87       }
88
89       this.setSelection(bookmark);
90     },
91
92     /**
93      * Set the caret in front of the given node
94      *
95      * @param {Object} node The element or text node where to position the caret in front of
96      * @example
97      *    selection.setBefore(myElement);
98      */
99     setBefore: function(node) {
100       var range = rangy.createRange(this.doc);
101       range.setStartBefore(node);
102       range.setEndBefore(node);
103       return this.setSelection(range);
104     },
105
106     // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail.
107     // Webkit has an issue with placing caret into places where there are no textnodes near by.
108     creteTemporaryCaretSpaceAfter: function (node) {
109       var caretPlaceholder = this.doc.createElement('span'),
110           caretPlaceholderText = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE),
111           placeholderRemover = (function(event) {
112             // Self-destructs the caret and keeps the text inserted into it by user
113             var lastChild;
114
115             this.contain.removeEventListener('mouseup', placeholderRemover);
116             this.contain.removeEventListener('keydown', keyDownHandler);
117             this.contain.removeEventListener('touchstart', placeholderRemover);
118             this.contain.removeEventListener('focus', placeholderRemover);
119             this.contain.removeEventListener('blur', placeholderRemover);
120             this.contain.removeEventListener('paste', delayedPlaceholderRemover);
121             this.contain.removeEventListener('drop', delayedPlaceholderRemover);
122             this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover);
123
124             // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack
125             // Otherwise the wrapper can just be removed
126             if (caretPlaceholder && caretPlaceholder.parentNode) {
127               caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, "");
128               if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) {
129                 lastChild = caretPlaceholder.lastChild;
130                 wysihtml5.dom.unwrap(caretPlaceholder);
131                 this.setAfter(lastChild);
132               } else {
133                 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
134               }
135
136             }
137           }).bind(this),
138           delayedPlaceholderRemover = function (event) {
139             if (caretPlaceholder && caretPlaceholder.parentNode) {
140               setTimeout(placeholderRemover, 0);
141             }
142           },
143           keyDownHandler = function(event) {
144             if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) {
145               placeholderRemover();
146             }
147           };
148
149       caretPlaceholder.style.position = 'absolute';
150       caretPlaceholder.style.display = 'block';
151       caretPlaceholder.style.minWidth = '1px';
152       caretPlaceholder.style.zIndex = '99999';
153       caretPlaceholder.appendChild(caretPlaceholderText);
154
155       node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
156       this.setBefore(caretPlaceholderText);
157
158       // Remove the caret fix on any of the following events (some are delayed as content change happens after event)
159       this.contain.addEventListener('mouseup', placeholderRemover);
160       this.contain.addEventListener('keydown', keyDownHandler);
161       this.contain.addEventListener('touchstart', placeholderRemover);
162       this.contain.addEventListener('focus', placeholderRemover);
163       this.contain.addEventListener('blur', placeholderRemover);
164       this.contain.addEventListener('paste', delayedPlaceholderRemover);
165       this.contain.addEventListener('drop', delayedPlaceholderRemover);
166       this.contain.addEventListener('beforepaste', delayedPlaceholderRemover);
167
168       return caretPlaceholder;
169     },
170
171     /**
172      * Set the caret after the given node
173      *
174      * @param {Object} node The element or text node where to position the caret in front of
175      * @example
176      *    selection.setBefore(myElement);
177      */
178     setAfter: function(node) {
179       var range = rangy.createRange(this.doc),
180           originalScrollTop = this.doc.documentElement.scrollTop || this.doc.body.scrollTop || this.doc.defaultView.pageYOffset,
181           originalScrollLeft = this.doc.documentElement.scrollLeft || this.doc.body.scrollLeft || this.doc.defaultView.pageXOffset,
182           sel;
183
184       range.setStartAfter(node);
185       range.setEndAfter(node);
186       this.composer.element.focus();
187       this.doc.defaultView.scrollTo(originalScrollLeft, originalScrollTop);
188       sel = this.setSelection(range);
189
190       // Webkit fails to add selection if there are no textnodes in that region
191       // (like an uneditable container at the end of content).
192       if (!sel) {
193         this.creteTemporaryCaretSpaceAfter(node);
194       }
195       return sel;
196     },
197
198     /**
199      * Ability to select/mark nodes
200      *
201      * @param {Element} node The node/element to select
202      * @example
203      *    selection.selectNode(document.getElementById("my-image"));
204      */
205     selectNode: function(node, avoidInvisibleSpace) {
206       var range           = rangy.createRange(this.doc),
207           isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
208           canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
209           content         = isElement ? node.innerHTML : node.data,
210           isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
211           displayStyle    = dom.getStyle("display").from(node),
212           isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
213
214       if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
215         // Make sure that caret is visible in node by inserting a zero width no breaking space
216         try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
217       }
218
219       if (canHaveHTML) {
220         range.selectNodeContents(node);
221       } else {
222         range.selectNode(node);
223       }
224
225       if (canHaveHTML && isEmpty && isElement) {
226         range.collapse(isBlockElement);
227       } else if (canHaveHTML && isEmpty) {
228         range.setStartAfter(node);
229         range.setEndAfter(node);
230       }
231
232       this.setSelection(range);
233     },
234
235     /**
236      * Get the node which contains the selection
237      *
238      * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
239      * @return {Object} The node that contains the caret
240      * @example
241      *    var nodeThatContainsCaret = selection.getSelectedNode();
242      */
243     getSelectedNode: function(controlRange) {
244       var selection,
245           range;
246
247       if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
248         range = this.doc.selection.createRange();
249         if (range && range.length) {
250           return range.item(0);
251         }
252       }
253
254       selection = this.getSelection(this.doc);
255       if (selection.focusNode === selection.anchorNode) {
256         return selection.focusNode;
257       } else {
258         range = this.getRange(this.doc);
259         return range ? range.commonAncestorContainer : this.doc.body;
260       }
261     },
262
263     fixSelBorders: function() {
264       var range = this.getRange();
265       expandRangeToSurround(range);
266       this.setSelection(range);
267     },
268
269     getSelectedOwnNodes: function(controlRange) {
270       var selection,
271           ranges = this.getOwnRanges(),
272           ownNodes = [];
273
274       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
275           ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
276       }
277       return ownNodes;
278     },
279
280     findNodesInSelection: function(nodeTypes) {
281       var ranges = this.getOwnRanges(),
282           nodes = [], curNodes;
283       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
284         curNodes = ranges[i].getNodes([1], function(node) {
285             return wysihtml5.lang.array(nodeTypes).contains(node.nodeName);
286         });
287         nodes = nodes.concat(curNodes);
288       }
289       return nodes;
290     },
291
292     containsUneditable: function() {
293       var uneditables = this.getOwnUneditables(),
294           selection = this.getSelection();
295
296       for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
297         if (selection.containsNode(uneditables[i])) {
298           return true;
299         }
300       }
301
302       return false;
303     },
304
305     // Deletes selection contents making sure uneditables/unselectables are not partially deleted
306     // Triggers wysihtml5:uneditable:delete custom event on all deleted uneditables if customevents suppoorted
307     deleteContents: function()  {
308       var range = this.getRange(),
309           startParent, endParent, uneditables, ev;
310
311       if (this.unselectableClass) {
312         if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { className: this.unselectableClass }, false, this.contain))) {
313           range.setStartBefore(startParent);
314         }
315         if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { className: this.unselectableClass }, false, this.contain))) {
316           range.setEndAfter(endParent);
317         }
318
319         // If customevents present notify uneditable elements of being deleted
320         uneditables = range.getNodes([1], (function (node) {
321           return wysihtml5.dom.hasClass(node, this.unselectableClass);
322         }).bind(this));
323         for (var i = uneditables.length; i--;) {
324           try {
325             ev = new CustomEvent("wysihtml5:uneditable:delete");
326             uneditables[i].dispatchEvent(ev);
327           } catch (err) {}
328         }
329
330       }
331       range.deleteContents();
332       this.setSelection(range);
333     },
334
335     getPreviousNode: function(node, ignoreEmpty) {
336       var displayStyle;
337       if (!node) {
338         var selection = this.getSelection();
339         node = selection.anchorNode;
340       }
341
342       if (node === this.contain) {
343           return false;
344       }
345
346       var ret = node.previousSibling,
347           parent;
348
349       if (ret === this.contain) {
350           return false;
351       }
352
353       if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
354          // do not count comments and other node types
355          ret = this.getPreviousNode(ret, ignoreEmpty);
356       } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
357         // do not count empty textnodes as previous nodes
358         ret = this.getPreviousNode(ret, ignoreEmpty);
359       } else if (ignoreEmpty && ret && ret.nodeType === 1) {
360         // Do not count empty nodes if param set.
361         // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like
362         displayStyle = wysihtml5.dom.getStyle("display").from(ret);
363         if (
364             !wysihtml5.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) &&
365             !wysihtml5.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) &&
366             (/^[\s]*$/).test(ret.innerHTML)
367           ) {
368             ret = this.getPreviousNode(ret, ignoreEmpty);
369           }
370       } else if (!ret && node !== this.contain) {
371         parent = node.parentNode;
372         if (parent !== this.contain) {
373             ret = this.getPreviousNode(parent, ignoreEmpty);
374         }
375       }
376
377       return (ret !== this.contain) ? ret : false;
378     },
379
380     getSelectionParentsByTag: function(tagName) {
381       var nodes = this.getSelectedOwnNodes(),
382           curEl, parents = [];
383
384       for (var i = 0, maxi = nodes.length; i < maxi; i++) {
385         curEl = (nodes[i].nodeName &&  nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { nodeName: ['LI']}, false, this.contain);
386         if (curEl) {
387           parents.push(curEl);
388         }
389       }
390       return (parents.length) ? parents : null;
391     },
392
393     getRangeToNodeEnd: function() {
394       if (this.isCollapsed()) {
395         var range = this.getRange(),
396             sNode = range.startContainer,
397             pos = range.startOffset,
398             lastR = rangy.createRange(this.doc);
399
400         lastR.selectNodeContents(sNode);
401         lastR.setStart(sNode, pos);
402         return lastR;
403       }
404     },
405
406     caretIsLastInSelection: function() {
407       var r = rangy.createRange(this.doc),
408           s = this.getSelection(),
409           endc = this.getRangeToNodeEnd().cloneContents(),
410           endtxt = endc.textContent;
411
412       return (/^\s*$/).test(endtxt);
413     },
414
415     caretIsFirstInSelection: function() {
416       var r = rangy.createRange(this.doc),
417           s = this.getSelection(),
418           range = this.getRange(),
419           startNode = range.startContainer;
420       
421       if (startNode) {
422         if (startNode.nodeType === wysihtml5.TEXT_NODE) {
423           return this.isCollapsed() && (startNode.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/).test(startNode.data.substr(0,range.startOffset)));
424         } else {
425           r.selectNodeContents(this.getRange().commonAncestorContainer);
426           r.collapse(true);
427           return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
428         }
429       }
430     },
431
432     caretIsInTheBeginnig: function(ofNode) {
433         var selection = this.getSelection(),
434             node = selection.anchorNode,
435             offset = selection.anchorOffset;
436         if (ofNode && node) {
437           return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { nodeName: ofNode }, 1)));
438         } else if (node) {
439           return (offset === 0 && !this.getPreviousNode(node, true));
440         }
441     },
442
443     caretIsBeforeUneditable: function() {
444       var selection = this.getSelection(),
445           node = selection.anchorNode,
446           offset = selection.anchorOffset,
447           childNodes = [],
448           range, contentNodes, lastNode;
449
450       if (node) {
451         if (offset === 0) {
452           var prevNode = this.getPreviousNode(node, true),
453               prevLeaf = prevNode ? wysihtml5.dom.domNode(prevNode).lastLeafNode((this.unselectableClass) ? {leafClasses: [this.unselectableClass]} : false) : null;
454           if (prevLeaf) {
455             var uneditables = this.getOwnUneditables();
456             for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
457               if (prevLeaf === uneditables[i]) {
458                 return uneditables[i];
459               }
460             }
461           }
462         } else {
463           range = selection.getRangeAt(0);
464           range.setStart(range.startContainer, range.startOffset - 1);
465           // TODO: make getting children on range a separate funtion
466           if (range) {
467             contentNodes = range.getNodes([1,3]);
468             for (var n = 0, max = contentNodes.length; n < max; n++) {
469               if (contentNodes[n].parentNode && contentNodes[n].parentNode === node) {
470                 childNodes.push(contentNodes[n]);
471               }
472             }
473           }
474           lastNode = childNodes.length > 0 ? childNodes[childNodes.length -1] : null;
475           if (lastNode && lastNode.nodeType === 1 && wysihtml5.dom.hasClass(lastNode, this.unselectableClass)) {
476             return lastNode;
477           }
478
479         }
480       }
481       return false;
482     },
483
484     // TODO: Figure out a method from following 2 that would work universally
485     executeAndRestoreRangy: function(method, restoreScrollPosition) {
486       var win = this.doc.defaultView || this.doc.parentWindow,
487           sel = rangy.saveSelection(win);
488
489       if (!sel) {
490         method();
491       } else {
492         try {
493           method();
494         } catch(e) {
495           setTimeout(function() { throw e; }, 0);
496         }
497       }
498       rangy.restoreSelection(sel);
499     },
500
501     // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
502     executeAndRestore: function(method, restoreScrollPosition) {
503       var body                  = this.doc.body,
504           oldScrollTop          = restoreScrollPosition && body.scrollTop,
505           oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
506           className             = "_wysihtml5-temp-placeholder",
507           placeholderHtml       = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
508           range                 = this.getRange(true),
509           caretPlaceholder,
510           newCaretPlaceholder,
511           nextSibling, prevSibling,
512           node, node2, range2,
513           newRange;
514
515       // Nothing selected, execute and say goodbye
516       if (!range) {
517         method(body, body);
518         return;
519       }
520
521       if (!range.collapsed) {
522         range2 = range.cloneRange();
523         node2 = range2.createContextualFragment(placeholderHtml);
524         range2.collapse(false);
525         range2.insertNode(node2);
526         range2.detach();
527       }
528
529       node = range.createContextualFragment(placeholderHtml);
530       range.insertNode(node);
531
532       if (node2) {
533         caretPlaceholder = this.contain.querySelectorAll("." + className);
534         range.setStartBefore(caretPlaceholder[0]);
535         range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
536       }
537       this.setSelection(range);
538
539       // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
540       try {
541         method(range.startContainer, range.endContainer);
542       } catch(e) {
543         setTimeout(function() { throw e; }, 0);
544       }
545       caretPlaceholder = this.contain.querySelectorAll("." + className);
546       if (caretPlaceholder && caretPlaceholder.length) {
547         newRange = rangy.createRange(this.doc);
548         nextSibling = caretPlaceholder[0].nextSibling;
549         if (caretPlaceholder.length > 1) {
550           prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
551         }
552         if (prevSibling && nextSibling) {
553           newRange.setStartBefore(nextSibling);
554           newRange.setEndAfter(prevSibling);
555         } else {
556           newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
557           dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
558           newRange.setStartBefore(newCaretPlaceholder);
559           newRange.setEndAfter(newCaretPlaceholder);
560         }
561         this.setSelection(newRange);
562         for (var i = caretPlaceholder.length; i--;) {
563          caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
564         }
565
566       } else {
567         // fallback for when all hell breaks loose
568         this.contain.focus();
569       }
570
571       if (restoreScrollPosition) {
572         body.scrollTop  = oldScrollTop;
573         body.scrollLeft = oldScrollLeft;
574       }
575
576       // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
577       try {
578         caretPlaceholder.parentNode.removeChild(caretPlaceholder);
579       } catch(e2) {}
580     },
581
582     set: function(node, offset) {
583       var newRange = rangy.createRange(this.doc);
584       newRange.setStart(node, offset || 0);
585       this.setSelection(newRange);
586     },
587
588     /**
589      * Insert html at the caret position and move the cursor after the inserted html
590      *
591      * @param {String} html HTML string to insert
592      * @example
593      *    selection.insertHTML("<p>foobar</p>");
594      */
595     insertHTML: function(html) {
596       var range     = rangy.createRange(this.doc),
597           node = this.doc.createElement('DIV'),
598           fragment = this.doc.createDocumentFragment(),
599           lastChild;
600
601       node.innerHTML = html;
602       lastChild = node.lastChild;
603
604       while (node.firstChild) {
605         fragment.appendChild(node.firstChild);
606       }
607       this.insertNode(fragment);
608
609       if (lastChild) {
610         this.setAfter(lastChild);
611       }
612     },
613
614     /**
615      * Insert a node at the caret position and move the cursor behind it
616      *
617      * @param {Object} node HTML string to insert
618      * @example
619      *    selection.insertNode(document.createTextNode("foobar"));
620      */
621     insertNode: function(node) {
622       var range = this.getRange();
623       if (range) {
624         range.insertNode(node);
625       }
626     },
627
628     /**
629      * Wraps current selection with the given node
630      *
631      * @param {Object} node The node to surround the selected elements with
632      */
633     surround: function(nodeOptions) {
634       var ranges = this.getOwnRanges(),
635           node, nodes = [];
636       if (ranges.length == 0) {
637         return nodes;
638       }
639
640       for (var i = ranges.length; i--;) {
641         node = this.doc.createElement(nodeOptions.nodeName);
642         nodes.push(node);
643         if (nodeOptions.className) {
644           node.className = nodeOptions.className;
645         }
646         if (nodeOptions.cssStyle) {
647           node.setAttribute('style', nodeOptions.cssStyle);
648         }
649         try {
650           // This only works when the range boundaries are not overlapping other elements
651           ranges[i].surroundContents(node);
652           this.selectNode(node);
653         } catch(e) {
654           // fallback
655           node.appendChild(ranges[i].extractContents());
656           ranges[i].insertNode(node);
657         }
658       }
659       return nodes;
660     },
661
662     deblockAndSurround: function(nodeOptions) {
663       var tempElement = this.doc.createElement('div'),
664           range = rangy.createRange(this.doc),
665           tempDivElements,
666           tempElements,
667           firstChild;
668
669       tempElement.className = nodeOptions.className;
670
671       this.composer.commands.exec("formatBlock", nodeOptions.nodeName, nodeOptions.className);
672       tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className);
673       if (tempDivElements[0]) {
674         tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]);
675
676         range.setStartBefore(tempDivElements[0]);
677         range.setEndAfter(tempDivElements[tempDivElements.length - 1]);
678         tempElements = range.extractContents();
679
680         while (tempElements.firstChild) {
681           firstChild = tempElements.firstChild;
682           if (firstChild.nodeType == 1 && wysihtml5.dom.hasClass(firstChild, nodeOptions.className)) {
683             while (firstChild.firstChild) {
684               tempElement.appendChild(firstChild.firstChild);
685             }
686             if (firstChild.nodeName !== "BR") { tempElement.appendChild(this.doc.createElement('br')); }
687             tempElements.removeChild(firstChild);
688           } else {
689             tempElement.appendChild(firstChild);
690           }
691         }
692       } else {
693         tempElement = null;
694       }
695
696       return tempElement;
697     },
698
699     /**
700      * Scroll the current caret position into the view
701      * FIXME: This is a bit hacky, there might be a smarter way of doing this
702      *
703      * @example
704      *    selection.scrollIntoView();
705      */
706     scrollIntoView: function() {
707       var doc           = this.doc,
708           tolerance     = 5, // px
709           hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
710           tempElement   = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
711             var element = doc.createElement("span");
712             // The element needs content in order to be able to calculate it's position properly
713             element.innerHTML = wysihtml5.INVISIBLE_SPACE;
714             return element;
715           })(),
716           offsetTop;
717
718       if (hasScrollBars) {
719         this.insertNode(tempElement);
720         offsetTop = _getCumulativeOffsetTop(tempElement);
721         tempElement.parentNode.removeChild(tempElement);
722         if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
723           doc.body.scrollTop = offsetTop;
724         }
725       }
726     },
727
728     /**
729      * Select line where the caret is in
730      */
731     selectLine: function() {
732       if (wysihtml5.browser.supportsSelectionModify()) {
733         this._selectLine_W3C();
734       } else if (this.doc.selection) {
735         this._selectLine_MSIE();
736       }
737     },
738
739     /**
740      * See https://developer.mozilla.org/en/DOM/Selection/modify
741      */
742     _selectLine_W3C: function() {
743       var win       = this.doc.defaultView,
744           selection = win.getSelection();
745       selection.modify("move", "left", "lineboundary");
746       selection.modify("extend", "right", "lineboundary");
747     },
748
749     // collapses selection to current line beginning or end
750     toLineBoundary: function (location, collapse) {
751       collapse = (typeof collapse === 'undefined') ? false : collapse;
752       if (wysihtml5.browser.supportsSelectionModify()) {
753         var win = this.doc.defaultView,
754             selection = win.getSelection();
755
756         selection.modify("extend", location, "lineboundary");
757         if (collapse) {
758           if (location === "left") {
759             selection.collapseToStart();
760           } else if (location === "right") {
761             selection.collapseToEnd();
762           }
763         }
764       }
765     },
766
767     _selectLine_MSIE: function() {
768       var range       = this.doc.selection.createRange(),
769           rangeTop    = range.boundingTop,
770           scrollWidth = this.doc.body.scrollWidth,
771           rangeBottom,
772           rangeEnd,
773           measureNode,
774           i,
775           j;
776
777       if (!range.moveToPoint) {
778         return;
779       }
780
781       if (rangeTop === 0) {
782         // Don't know why, but when the selection ends at the end of a line
783         // range.boundingTop is 0
784         measureNode = this.doc.createElement("span");
785         this.insertNode(measureNode);
786         rangeTop = measureNode.offsetTop;
787         measureNode.parentNode.removeChild(measureNode);
788       }
789
790       rangeTop += 1;
791
792       for (i=-10; i<scrollWidth; i+=2) {
793         try {
794           range.moveToPoint(i, rangeTop);
795           break;
796         } catch(e1) {}
797       }
798
799       // Investigate the following in order to handle multi line selections
800       // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
801       rangeBottom = rangeTop;
802       rangeEnd = this.doc.selection.createRange();
803       for (j=scrollWidth; j>=0; j--) {
804         try {
805           rangeEnd.moveToPoint(j, rangeBottom);
806           break;
807         } catch(e2) {}
808       }
809
810       range.setEndPoint("EndToEnd", rangeEnd);
811       range.select();
812     },
813
814     getText: function() {
815       var selection = this.getSelection();
816       return selection ? selection.toString() : "";
817     },
818
819     getNodes: function(nodeType, filter) {
820       var range = this.getRange();
821       if (range) {
822         return range.getNodes([nodeType], filter);
823       } else {
824         return [];
825       }
826     },
827
828     fixRangeOverflow: function(range) {
829       if (this.contain && this.contain.firstChild && range) {
830         var containment = range.compareNode(this.contain);
831         if (containment !== 2) {
832           if (containment === 1) {
833             range.setStartBefore(this.contain.firstChild);
834           }
835           if (containment === 0) {
836             range.setEndAfter(this.contain.lastChild);
837           }
838           if (containment === 3) {
839             range.setStartBefore(this.contain.firstChild);
840             range.setEndAfter(this.contain.lastChild);
841           }
842         } else if (this._detectInlineRangeProblems(range)) {
843           var previousElementSibling = range.endContainer.previousElementSibling;
844           if (previousElementSibling) {
845             range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
846           }
847         }
848       }
849     },
850
851     _endOffsetForNode: function(node) {
852       var range = document.createRange();
853       range.selectNodeContents(node);
854       return range.endOffset;
855     },
856
857     _detectInlineRangeProblems: function(range) {
858       var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
859       return (
860         range.endOffset == 0 &&
861         position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
862       );
863     },
864
865     getRange: function(dontFix) {
866       var selection = this.getSelection(),
867           range = selection && selection.rangeCount && selection.getRangeAt(0);
868
869       if (dontFix !== true) {
870         this.fixRangeOverflow(range);
871       }
872
873       return range;
874     },
875
876     getOwnUneditables: function() {
877       var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
878           deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
879
880       return wysihtml5.lang.array(allUneditables).without(deepUneditables);
881     },
882
883     // Returns an array of ranges that belong only to this editable
884     // Needed as uneditable block in contenteditabel can split range into pieces
885     // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
886     getOwnRanges: function()  {
887       var ranges = [],
888           r = this.getRange(),
889           tmpRanges;
890
891       if (r) { ranges.push(r); }
892
893       if (this.unselectableClass && this.contain && r) {
894           var uneditables = this.getOwnUneditables(),
895               tmpRange;
896           if (uneditables.length > 0) {
897             for (var i = 0, imax = uneditables.length; i < imax; i++) {
898               tmpRanges = [];
899               for (var j = 0, jmax = ranges.length; j < jmax; j++) {
900                 if (ranges[j]) {
901                   switch (ranges[j].compareNode(uneditables[i])) {
902                     case 2:
903                       // all selection inside uneditable. remove
904                     break;
905                     case 3:
906                       //section begins before and ends after uneditable. spilt
907                       tmpRange = ranges[j].cloneRange();
908                       tmpRange.setEndBefore(uneditables[i]);
909                       tmpRanges.push(tmpRange);
910
911                       tmpRange = ranges[j].cloneRange();
912                       tmpRange.setStartAfter(uneditables[i]);
913                       tmpRanges.push(tmpRange);
914                     break;
915                     default:
916                       // in all other cases uneditable does not touch selection. dont modify
917                       tmpRanges.push(ranges[j]);
918                   }
919                 }
920                 ranges = tmpRanges;
921               }
922             }
923           }
924       }
925       return ranges;
926     },
927
928     getSelection: function() {
929       return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
930     },
931
932     // Sets selection in document to a given range
933     // Set selection method detects if it fails to set any selection in document and returns null on fail
934     // (especially needed in webkit where some ranges just can not create selection for no reason)
935     setSelection: function(range) {
936       var win       = this.doc.defaultView || this.doc.parentWindow,
937           selection = rangy.getSelection(win);
938       selection.setSingleRange(range);
939       return (selection && selection.anchorNode && selection.focusNode) ? selection : null;
940     },
941
942     createRange: function() {
943       return rangy.createRange(this.doc);
944     },
945
946     isCollapsed: function() {
947         return this.getSelection().isCollapsed;
948     },
949
950     getHtml: function() {
951       return this.getSelection().toHtml();
952     },
953
954     getPlainText: function () {
955       return this.getSelection().toString();
956     },
957
958     isEndToEndInNode: function(nodeNames) {
959       var range = this.getRange(),
960           parentElement = range.commonAncestorContainer,
961           startNode = range.startContainer,
962           endNode = range.endContainer;
963
964
965         if (parentElement.nodeType === wysihtml5.TEXT_NODE) {
966           parentElement = parentElement.parentNode;
967         }
968
969         if (startNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
970           return false;
971         }
972
973         if (endNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
974           return false;
975         }
976
977         while (startNode && startNode !== parentElement) {
978           if (startNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, startNode)) {
979             return false;
980           }
981           if (wysihtml5.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
982             return false;
983           }
984           startNode = startNode.parentNode;
985         }
986
987         while (endNode && endNode !== parentElement) {
988           if (endNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, endNode)) {
989             return false;
990           }
991           if (wysihtml5.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
992             return false;
993           }
994           endNode = endNode.parentNode;
995         }
996
997         return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
998     },
999
1000     deselect: function() {
1001       var sel = this.getSelection();
1002       sel && sel.removeAllRanges();
1003     }
1004   });
1005
1006 })(wysihtml5);