5 * var selection = new wysihtml5.Selection(editor);
8 var dom = wysihtml5.dom;
10 function _getCumulativeOffsetTop(element) {
12 if (element.parentNode) {
14 top += element.offsetTop || 0;
15 element = element.offsetParent;
21 // Provides the depth of ``descendant`` relative to ``ancestor``
22 function getDepth(ancestor, descendant) {
24 while (descendant !== ancestor) {
26 descendant = descendant.parentNode;
28 throw new Error("not a descendant of ancestor!");
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;
38 var common = range.commonAncestorContainer,
39 start_depth = getDepth(common, range.startContainer),
40 end_depth = getDepth(common, range.endContainer);
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);
49 range.setEndAfter(range.endContainer);
50 end_depth = getDepth(common, range.endContainer);
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
62 this.composer = editor.composer;
63 this.doc = this.composer.doc;
64 this.contain = contain;
65 this.unselectableClass = unselectableClass || false;
69 * Get the current selection as a bookmark to be able to later restore it
71 * @return {Object} An object that represents the current selection
73 getBookmark: function() {
74 var range = this.getRange();
75 if (range) expandRangeToSurround(range);
76 return range && range.cloneRange();
80 * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
82 * @param {Object} bookmark An object that represents the current selection
84 setBookmark: function(bookmark) {
89 this.setSelection(bookmark);
93 * Set the caret in front of the given node
95 * @param {Object} node The element or text node where to position the caret in front of
97 * selection.setBefore(myElement);
99 setBefore: function(node) {
100 var range = rangy.createRange(this.doc);
101 range.setStartBefore(node);
102 range.setEndBefore(node);
103 return this.setSelection(range);
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
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);
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);
133 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
138 delayedPlaceholderRemover = function (event) {
139 if (caretPlaceholder && caretPlaceholder.parentNode) {
140 setTimeout(placeholderRemover, 0);
143 keyDownHandler = function(event) {
144 if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) {
145 placeholderRemover();
149 caretPlaceholder.style.position = 'absolute';
150 caretPlaceholder.style.display = 'block';
151 caretPlaceholder.style.minWidth = '1px';
152 caretPlaceholder.style.zIndex = '99999';
153 caretPlaceholder.appendChild(caretPlaceholderText);
155 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
156 this.setBefore(caretPlaceholderText);
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);
168 return caretPlaceholder;
172 * Set the caret after the given node
174 * @param {Object} node The element or text node where to position the caret in front of
176 * selection.setBefore(myElement);
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,
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);
190 // Webkit fails to add selection if there are no textnodes in that region
191 // (like an uneditable container at the end of content).
193 this.creteTemporaryCaretSpaceAfter(node);
199 * Ability to select/mark nodes
201 * @param {Element} node The node/element to select
203 * selection.selectNode(document.getElementById("my-image"));
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");
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) {}
220 range.selectNodeContents(node);
222 range.selectNode(node);
225 if (canHaveHTML && isEmpty && isElement) {
226 range.collapse(isBlockElement);
227 } else if (canHaveHTML && isEmpty) {
228 range.setStartAfter(node);
229 range.setEndAfter(node);
232 this.setSelection(range);
236 * Get the node which contains the selection
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
241 * var nodeThatContainsCaret = selection.getSelectedNode();
243 getSelectedNode: function(controlRange) {
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);
254 selection = this.getSelection(this.doc);
255 if (selection.focusNode === selection.anchorNode) {
256 return selection.focusNode;
258 range = this.getRange(this.doc);
259 return range ? range.commonAncestorContainer : this.doc.body;
263 fixSelBorders: function() {
264 var range = this.getRange();
265 expandRangeToSurround(range);
266 this.setSelection(range);
269 getSelectedOwnNodes: function(controlRange) {
271 ranges = this.getOwnRanges(),
274 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
275 ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
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);
287 nodes = nodes.concat(curNodes);
292 containsUneditable: function() {
293 var uneditables = this.getOwnUneditables(),
294 selection = this.getSelection();
296 for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
297 if (selection.containsNode(uneditables[i])) {
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;
311 if (this.unselectableClass) {
312 if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { className: this.unselectableClass }, false, this.contain))) {
313 range.setStartBefore(startParent);
315 if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { className: this.unselectableClass }, false, this.contain))) {
316 range.setEndAfter(endParent);
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);
323 for (var i = uneditables.length; i--;) {
325 ev = new CustomEvent("wysihtml5:uneditable:delete");
326 uneditables[i].dispatchEvent(ev);
331 range.deleteContents();
332 this.setSelection(range);
335 getPreviousNode: function(node, ignoreEmpty) {
338 var selection = this.getSelection();
339 node = selection.anchorNode;
342 if (node === this.contain) {
346 var ret = node.previousSibling,
349 if (ret === this.contain) {
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);
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)
368 ret = this.getPreviousNode(ret, ignoreEmpty);
370 } else if (!ret && node !== this.contain) {
371 parent = node.parentNode;
372 if (parent !== this.contain) {
373 ret = this.getPreviousNode(parent, ignoreEmpty);
377 return (ret !== this.contain) ? ret : false;
380 getSelectionParentsByTag: function(tagName) {
381 var nodes = this.getSelectedOwnNodes(),
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);
390 return (parents.length) ? parents : null;
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);
400 lastR.selectNodeContents(sNode);
401 lastR.setStart(sNode, pos);
406 caretIsLastInSelection: function() {
407 var r = rangy.createRange(this.doc),
408 s = this.getSelection(),
409 endc = this.getRangeToNodeEnd().cloneContents(),
410 endtxt = endc.textContent;
412 return (/^\s*$/).test(endtxt);
415 caretIsFirstInSelection: function() {
416 var r = rangy.createRange(this.doc),
417 s = this.getSelection(),
418 range = this.getRange(),
419 startNode = range.startContainer;
422 if (startNode.nodeType === wysihtml5.TEXT_NODE) {
423 return this.isCollapsed() && (startNode.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/).test(startNode.data.substr(0,range.startOffset)));
425 r.selectNodeContents(this.getRange().commonAncestorContainer);
427 return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
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)));
439 return (offset === 0 && !this.getPreviousNode(node, true));
443 caretIsBeforeUneditable: function() {
444 var selection = this.getSelection(),
445 node = selection.anchorNode,
446 offset = selection.anchorOffset,
448 range, contentNodes, lastNode;
452 var prevNode = this.getPreviousNode(node, true),
453 prevLeaf = prevNode ? wysihtml5.dom.domNode(prevNode).lastLeafNode((this.unselectableClass) ? {leafClasses: [this.unselectableClass]} : false) : null;
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];
463 range = selection.getRangeAt(0);
464 range.setStart(range.startContainer, range.startOffset - 1);
465 // TODO: make getting children on range a separate funtion
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]);
474 lastNode = childNodes.length > 0 ? childNodes[childNodes.length -1] : null;
475 if (lastNode && lastNode.nodeType === 1 && wysihtml5.dom.hasClass(lastNode, this.unselectableClass)) {
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);
495 setTimeout(function() { throw e; }, 0);
498 rangy.restoreSelection(sel);
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),
511 nextSibling, prevSibling,
515 // Nothing selected, execute and say goodbye
521 if (!range.collapsed) {
522 range2 = range.cloneRange();
523 node2 = range2.createContextualFragment(placeholderHtml);
524 range2.collapse(false);
525 range2.insertNode(node2);
529 node = range.createContextualFragment(placeholderHtml);
530 range.insertNode(node);
533 caretPlaceholder = this.contain.querySelectorAll("." + className);
534 range.setStartBefore(caretPlaceholder[0]);
535 range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
537 this.setSelection(range);
539 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
541 method(range.startContainer, range.endContainer);
543 setTimeout(function() { throw e; }, 0);
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;
552 if (prevSibling && nextSibling) {
553 newRange.setStartBefore(nextSibling);
554 newRange.setEndAfter(prevSibling);
556 newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
557 dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
558 newRange.setStartBefore(newCaretPlaceholder);
559 newRange.setEndAfter(newCaretPlaceholder);
561 this.setSelection(newRange);
562 for (var i = caretPlaceholder.length; i--;) {
563 caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
567 // fallback for when all hell breaks loose
568 this.contain.focus();
571 if (restoreScrollPosition) {
572 body.scrollTop = oldScrollTop;
573 body.scrollLeft = oldScrollLeft;
576 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
578 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
582 set: function(node, offset) {
583 var newRange = rangy.createRange(this.doc);
584 newRange.setStart(node, offset || 0);
585 this.setSelection(newRange);
589 * Insert html at the caret position and move the cursor after the inserted html
591 * @param {String} html HTML string to insert
593 * selection.insertHTML("<p>foobar</p>");
595 insertHTML: function(html) {
596 var range = rangy.createRange(this.doc),
597 node = this.doc.createElement('DIV'),
598 fragment = this.doc.createDocumentFragment(),
601 node.innerHTML = html;
602 lastChild = node.lastChild;
604 while (node.firstChild) {
605 fragment.appendChild(node.firstChild);
607 this.insertNode(fragment);
610 this.setAfter(lastChild);
615 * Insert a node at the caret position and move the cursor behind it
617 * @param {Object} node HTML string to insert
619 * selection.insertNode(document.createTextNode("foobar"));
621 insertNode: function(node) {
622 var range = this.getRange();
624 range.insertNode(node);
629 * Wraps current selection with the given node
631 * @param {Object} node The node to surround the selected elements with
633 surround: function(nodeOptions) {
634 var ranges = this.getOwnRanges(),
636 if (ranges.length == 0) {
640 for (var i = ranges.length; i--;) {
641 node = this.doc.createElement(nodeOptions.nodeName);
643 if (nodeOptions.className) {
644 node.className = nodeOptions.className;
646 if (nodeOptions.cssStyle) {
647 node.setAttribute('style', nodeOptions.cssStyle);
650 // This only works when the range boundaries are not overlapping other elements
651 ranges[i].surroundContents(node);
652 this.selectNode(node);
655 node.appendChild(ranges[i].extractContents());
656 ranges[i].insertNode(node);
662 deblockAndSurround: function(nodeOptions) {
663 var tempElement = this.doc.createElement('div'),
664 range = rangy.createRange(this.doc),
669 tempElement.className = nodeOptions.className;
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]);
676 range.setStartBefore(tempDivElements[0]);
677 range.setEndAfter(tempDivElements[tempDivElements.length - 1]);
678 tempElements = range.extractContents();
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);
686 if (firstChild.nodeName !== "BR") { tempElement.appendChild(this.doc.createElement('br')); }
687 tempElements.removeChild(firstChild);
689 tempElement.appendChild(firstChild);
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
704 * selection.scrollIntoView();
706 scrollIntoView: function() {
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;
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;
729 * Select line where the caret is in
731 selectLine: function() {
732 if (wysihtml5.browser.supportsSelectionModify()) {
733 this._selectLine_W3C();
734 } else if (this.doc.selection) {
735 this._selectLine_MSIE();
740 * See https://developer.mozilla.org/en/DOM/Selection/modify
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");
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();
756 selection.modify("extend", location, "lineboundary");
758 if (location === "left") {
759 selection.collapseToStart();
760 } else if (location === "right") {
761 selection.collapseToEnd();
767 _selectLine_MSIE: function() {
768 var range = this.doc.selection.createRange(),
769 rangeTop = range.boundingTop,
770 scrollWidth = this.doc.body.scrollWidth,
777 if (!range.moveToPoint) {
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);
792 for (i=-10; i<scrollWidth; i+=2) {
794 range.moveToPoint(i, rangeTop);
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--) {
805 rangeEnd.moveToPoint(j, rangeBottom);
810 range.setEndPoint("EndToEnd", rangeEnd);
814 getText: function() {
815 var selection = this.getSelection();
816 return selection ? selection.toString() : "";
819 getNodes: function(nodeType, filter) {
820 var range = this.getRange();
822 return range.getNodes([nodeType], filter);
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);
835 if (containment === 0) {
836 range.setEndAfter(this.contain.lastChild);
838 if (containment === 3) {
839 range.setStartBefore(this.contain.firstChild);
840 range.setEndAfter(this.contain.lastChild);
842 } else if (this._detectInlineRangeProblems(range)) {
843 var previousElementSibling = range.endContainer.previousElementSibling;
844 if (previousElementSibling) {
845 range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
851 _endOffsetForNode: function(node) {
852 var range = document.createRange();
853 range.selectNodeContents(node);
854 return range.endOffset;
857 _detectInlineRangeProblems: function(range) {
858 var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
860 range.endOffset == 0 &&
861 position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
865 getRange: function(dontFix) {
866 var selection = this.getSelection(),
867 range = selection && selection.rangeCount && selection.getRangeAt(0);
869 if (dontFix !== true) {
870 this.fixRangeOverflow(range);
876 getOwnUneditables: function() {
877 var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
878 deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
880 return wysihtml5.lang.array(allUneditables).without(deepUneditables);
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() {
891 if (r) { ranges.push(r); }
893 if (this.unselectableClass && this.contain && r) {
894 var uneditables = this.getOwnUneditables(),
896 if (uneditables.length > 0) {
897 for (var i = 0, imax = uneditables.length; i < imax; i++) {
899 for (var j = 0, jmax = ranges.length; j < jmax; j++) {
901 switch (ranges[j].compareNode(uneditables[i])) {
903 // all selection inside uneditable. remove
906 //section begins before and ends after uneditable. spilt
907 tmpRange = ranges[j].cloneRange();
908 tmpRange.setEndBefore(uneditables[i]);
909 tmpRanges.push(tmpRange);
911 tmpRange = ranges[j].cloneRange();
912 tmpRange.setStartAfter(uneditables[i]);
913 tmpRanges.push(tmpRange);
916 // in all other cases uneditable does not touch selection. dont modify
917 tmpRanges.push(ranges[j]);
928 getSelection: function() {
929 return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
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;
942 createRange: function() {
943 return rangy.createRange(this.doc);
946 isCollapsed: function() {
947 return this.getSelection().isCollapsed;
950 getHtml: function() {
951 return this.getSelection().toHtml();
954 getPlainText: function () {
955 return this.getSelection().toString();
958 isEndToEndInNode: function(nodeNames) {
959 var range = this.getRange(),
960 parentElement = range.commonAncestorContainer,
961 startNode = range.startContainer,
962 endNode = range.endContainer;
965 if (parentElement.nodeType === wysihtml5.TEXT_NODE) {
966 parentElement = parentElement.parentNode;
969 if (startNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
973 if (endNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
977 while (startNode && startNode !== parentElement) {
978 if (startNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, startNode)) {
981 if (wysihtml5.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
984 startNode = startNode.parentNode;
987 while (endNode && endNode !== parentElement) {
988 if (endNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, endNode)) {
991 if (wysihtml5.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
994 endNode = endNode.parentNode;
997 return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
1000 deselect: function() {
1001 var sel = this.getSelection();
1002 sel && sel.removeAllRanges();