4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
13 return s.replace(/[\n\r]+/g, '');
17 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;
20 * This class handles text and control selection it's an crossbrowser utility class.
21 * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
23 * @class tinymce.dom.Selection
25 * // Getting the currently selected node for the active editor
26 * alert(tinymce.activeEditor.selection.getNode().nodeName);
28 tinymce.create('tinymce.dom.Selection', {
30 * Constructs a new selection instance.
34 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
35 * @param {Window} win Window to bind the selection object to.
36 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
38 Selection : function(dom, win, serializer) {
43 t.serializer = serializer;
48 * This event gets executed before contents is extracted from the selection.
50 * @event onBeforeSetContent
51 * @param {tinymce.dom.Selection} selection Selection object that fired the event.
52 * @param {Object} args Contains things like the contents that will be returned.
57 * This event gets executed before contents is inserted into selection.
59 * @event onBeforeGetContent
60 * @param {tinymce.dom.Selection} selection Selection object that fired the event.
61 * @param {Object} args Contains things like the contents that will be inserted.
66 * This event gets executed when contents is inserted into selection.
69 * @param {tinymce.dom.Selection} selection Selection object that fired the event.
70 * @param {Object} args Contains things like the contents that will be inserted.
75 * This event gets executed when contents is extracted from the selection.
78 * @param {tinymce.dom.Selection} selection Selection object that fired the event.
79 * @param {Object} args Contains things like the contents that will be returned.
83 t[e] = new tinymce.util.Dispatcher(t);
86 // No W3C Range support
87 if (!t.win.getSelection)
88 t.tridentSel = new tinymce.dom.TridentSelection(t);
90 if (tinymce.isIE && dom.boxModel)
91 this._fixIESelection();
94 tinymce.addUnload(t.destroy, t);
98 * Move the selection cursor range to the specified node and offset.
99 * @param node Node to put the cursor in.
100 * @param offset Offset from the start of the node to put the cursor at.
102 setCursorLocation: function(node, offset) {
103 var t = this; var r = t.dom.createRng();
104 r.setStart(node, offset);
105 r.setEnd(node, offset);
110 * Returns the selected contents using the DOM serializer passed in to this class.
113 * @param {Object} s Optional settings class with for example output format text or html.
114 * @return {String} Selected contents in for example HTML format.
116 * // Alerts the currently selected contents
117 * alert(tinyMCE.activeEditor.selection.getContent());
119 * // Alerts the currently selected contents as plain text
120 * alert(tinyMCE.activeEditor.selection.getContent({format : 'text'}));
122 getContent : function(s) {
123 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
128 s.format = s.format || 'html';
129 s.forced_root_block = '';
130 t.onBeforeGetContent.dispatch(t, s);
132 if (s.format == 'text')
133 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
135 if (r.cloneContents) {
136 n = r.cloneContents();
140 } else if (is(r.item) || is(r.htmlText))
141 e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText;
143 e.innerHTML = r.toString();
145 // Keep whitespace before and after
146 if (/^\s/.test(e.innerHTML))
149 if (/\s+$/.test(e.innerHTML))
154 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
155 t.onGetContent.dispatch(t, s);
161 * Sets the current selection to the specified content. If any contents is selected it will be replaced
162 * with the contents passed in to this function. If there is no selection the contents will be inserted
163 * where the caret is placed in the editor/page.
166 * @param {String} content HTML contents to set could also be other formats depending on settings.
167 * @param {Object} args Optional settings object with for example data format.
169 * // Inserts some HTML contents at the current selection
170 * tinyMCE.activeEditor.selection.setContent('<strong>Some contents</strong>');
172 setContent : function(content, args) {
173 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
175 args = args || {format : 'html'};
177 content = args.content = content;
179 // Dispatch before set content event
181 self.onBeforeSetContent.dispatch(self, args);
183 content = args.content;
185 if (rng.insertNode) {
186 // Make caret marker since insertNode places the caret in the beginning of text after insert
187 content += '<span id="__caret">_</span>';
189 // Delete and insert new node
190 if (rng.startContainer == doc && rng.endContainer == doc) {
191 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
192 doc.body.innerHTML = content;
194 rng.deleteContents();
196 if (doc.body.childNodes.length == 0) {
197 doc.body.innerHTML = content;
199 // createContextualFragment doesn't exists in IE 9 DOMRanges
200 if (rng.createContextualFragment) {
201 rng.insertNode(rng.createContextualFragment(content));
203 // Fake createContextualFragment call in IE 9
204 frag = doc.createDocumentFragment();
205 temp = doc.createElement('div');
207 frag.appendChild(temp);
208 temp.outerHTML = content;
210 rng.insertNode(frag);
215 // Move to caret marker
216 caretNode = self.dom.get('__caret');
218 // Make sure we wrap it compleatly, Opera fails with a simple select call
219 rng = doc.createRange();
220 rng.setStartBefore(caretNode);
221 rng.setEndBefore(caretNode);
224 // Remove the caret position
225 self.dom.remove('__caret');
230 // Might fail on Opera for some odd reason
234 // Delete content and get caret text selection
235 doc.execCommand('Delete', false, null);
239 rng.pasteHTML(content);
242 // Dispatch set content event
244 self.onSetContent.dispatch(self, args);
248 * Returns the start element of a selection range. If the start is in a text
249 * node the parent element will be returned.
252 * @return {Element} Start element of selection range.
254 getStart : function() {
255 var rng = this.getRng(), startElement, parentElement, checkRng, node;
257 if (rng.duplicate || rng.item) {
258 // Control selection, return first item
263 checkRng = rng.duplicate();
264 checkRng.collapse(1);
265 startElement = checkRng.parentElement();
267 // Check if range parent is inside the start element, then return the inner parent element
268 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
269 parentElement = node = rng.parentElement();
270 while (node = node.parentNode) {
271 if (node == startElement) {
272 startElement = parentElement;
279 startElement = rng.startContainer;
281 if (startElement.nodeType == 1 && startElement.hasChildNodes())
282 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
284 if (startElement && startElement.nodeType == 3)
285 return startElement.parentNode;
292 * Returns the end element of a selection range. If the end is in a text
293 * node the parent element will be returned.
296 * @return {Element} End element of selection range.
298 getEnd : function() {
299 var t = this, r = t.getRng(), e, eo;
301 if (r.duplicate || r.item) {
307 e = r.parentElement();
309 if (e && e.nodeName == 'BODY')
310 return e.lastChild || e;
317 if (e.nodeType == 1 && e.hasChildNodes())
318 e = e.childNodes[eo > 0 ? eo - 1 : eo];
320 if (e && e.nodeType == 3)
328 * Returns a bookmark location for the current selection. This bookmark object
329 * can then be used to restore the selection after some content modification to the document.
331 * @method getBookmark
332 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
333 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
334 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
336 * // Stores a bookmark of the current selection
337 * var bm = tinyMCE.activeEditor.selection.getBookmark();
339 * tinyMCE.activeEditor.setContent(tinyMCE.activeEditor.getContent() + 'Some new content');
341 * // Restore the selection bookmark
342 * tinyMCE.activeEditor.selection.moveToBookmark(bm);
344 getBookmark : function(type, normalized) {
345 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;
347 function findIndex(name, element) {
350 each(dom.select(name), function(node, i) {
359 function getLocation() {
360 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};
362 function getPoint(rng, start) {
363 var container = rng[start ? 'startContainer' : 'endContainer'],
364 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
366 if (container.nodeType == 3) {
368 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
369 offset += node.nodeValue.length;
374 childNodes = container.childNodes;
376 if (offset >= childNodes.length && childNodes.length) {
378 offset = Math.max(0, childNodes.length - 1);
381 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
384 for (; container && container != root; container = container.parentNode)
385 point.push(t.dom.nodeIndex(container, normalized));
390 bookmark.start = getPoint(rng, true);
392 if (!t.isCollapsed())
393 bookmark.end = getPoint(rng);
399 return t.tridentSel.getBookmark(type);
401 return getLocation();
404 // Handle simple range
406 return {rng : t.getRng()};
410 collapsed = tinyMCE.activeEditor.selection.isCollapsed();
411 styles = 'overflow:hidden;line-height:0px';
414 if (rng.duplicate || rng.item) {
417 rng2 = rng.duplicate();
420 // Insert start marker
422 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
426 rng2.collapse(false);
428 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p>
429 rng.moveToElementText(rng2.parentElement());
430 if (rng.compareEndPoints('StartToEnd', rng2) == 0)
431 rng2.move('character', -1);
433 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
436 // IE might throw unspecified error so lets ignore it
441 element = rng.item(0);
442 name = element.nodeName;
444 return {name : name, index : findIndex(name, element)};
447 element = t.getNode();
448 name = element.nodeName;
450 return {name : name, index : findIndex(name, element)};
453 rng2 = rng.cloneRange();
457 rng2.collapse(false);
458 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr));
462 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr));
465 t.moveToBookmark({id : id, keep : 1});
471 * Restores the selection to the specified bookmark.
473 * @method moveToBookmark
474 * @param {Object} bookmark Bookmark to restore selection from.
475 * @return {Boolean} true/false if it was successful or not.
477 * // Stores a bookmark of the current selection
478 * var bm = tinyMCE.activeEditor.selection.getBookmark();
480 * tinyMCE.activeEditor.setContent(tinyMCE.activeEditor.getContent() + 'Some new content');
482 * // Restore the selection bookmark
483 * tinyMCE.activeEditor.selection.moveToBookmark(bm);
485 moveToBookmark : function(bookmark) {
486 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;
489 if (bookmark.start) {
490 rng = dom.createRng();
491 root = dom.getRoot();
493 function setEndPoint(start) {
494 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
499 // Find container node
500 for (node = root, i = point.length - 1; i >= 1; i--) {
501 children = node.childNodes;
503 if (point[i] > children.length - 1)
506 node = children[point[i]];
509 // Move text offset to best suitable location
510 if (node.nodeType === 3)
511 offset = Math.min(point[0], node.nodeValue.length);
513 // Move element offset to best suitable location
514 if (node.nodeType === 1)
515 offset = Math.min(point[0], node.childNodes.length);
517 // Set offset within container node
519 rng.setStart(node, offset);
521 rng.setEnd(node, offset);
528 return t.tridentSel.moveToBookmark(bookmark);
530 if (setEndPoint(true) && setEndPoint()) {
533 } else if (bookmark.id) {
534 function restoreEndPoint(suffix) {
535 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
538 node = marker.parentNode;
540 if (suffix == 'start') {
542 idx = dom.nodeIndex(marker);
544 node = marker.firstChild;
548 startContainer = endContainer = node;
549 startOffset = endOffset = idx;
552 idx = dom.nodeIndex(marker);
554 node = marker.firstChild;
563 prev = marker.previousSibling;
564 next = marker.nextSibling;
566 // Remove all marker text nodes
567 each(tinymce.grep(marker.childNodes), function(node) {
568 if (node.nodeType == 3)
569 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
572 // Remove marker but keep children if for example contents where inserted into the marker
573 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
574 while (marker = dom.get(bookmark.id + '_' + suffix))
575 dom.remove(marker, 1);
577 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
578 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact
579 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) {
580 idx = prev.nodeValue.length;
581 prev.appendData(next.nodeValue);
584 if (suffix == 'start') {
585 startContainer = endContainer = prev;
586 startOffset = endOffset = idx;
596 function addBogus(node) {
597 // Adds a bogus BR element for empty block elements or just a space on IE since it renders BR elements incorrectly
598 if (dom.isBlock(node) && !node.innerHTML)
599 node.innerHTML = !isIE ? '<br data-mce-bogus="1" />' : ' ';
604 // Restore start/end points
605 restoreEndPoint('start');
606 restoreEndPoint('end');
608 if (startContainer) {
609 rng = dom.createRng();
610 rng.setStart(addBogus(startContainer), startOffset);
611 rng.setEnd(addBogus(endContainer), endOffset);
614 } else if (bookmark.name) {
615 t.select(dom.select(bookmark.name)[bookmark.index]);
616 } else if (bookmark.rng)
617 t.setRng(bookmark.rng);
622 * Selects the specified element. This will place the start and end of the selection range around the element.
625 * @param {Element} node HMTL DOM element to select.
626 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
627 * @return {Element} Selected element the same element as the one that got passed in.
629 * // Select the first paragraph in the active editor
630 * tinyMCE.activeEditor.selection.select(tinyMCE.activeEditor.dom.select('p')[0]);
632 select : function(node, content) {
633 var t = this, dom = t.dom, rng = dom.createRng(), idx;
636 idx = dom.nodeIndex(node);
637 rng.setStart(node.parentNode, idx);
638 rng.setEnd(node.parentNode, idx + 1);
640 // Find first/last text node or BR element
642 function setPoint(node, start) {
643 var walker = new tinymce.dom.TreeWalker(node, node);
647 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
649 rng.setStart(node, 0);
651 rng.setEnd(node, node.nodeValue.length);
657 if (node.nodeName == 'BR') {
659 rng.setStartBefore(node);
661 rng.setEndBefore(node);
665 } while (node = (start ? walker.next() : walker.prev()));
679 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
681 * @method isCollapsed
682 * @return {Boolean} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
684 isCollapsed : function() {
685 var t = this, r = t.getRng(), s = t.getSel();
690 if (r.compareEndPoints)
691 return r.compareEndPoints('StartToEnd', r) === 0;
693 return !s || r.collapsed;
697 * Collapse the selection to start or end of range.
700 * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start.
702 collapse : function(to_start) {
703 var self = this, rng = self.getRng(), node;
705 // Control range on IE
708 rng = self.win.document.body.createTextRange();
709 rng.moveToElementText(node);
712 rng.collapse(!!to_start);
717 * Returns the browsers internal selection object.
720 * @return {Selection} Internal browser selection object.
722 getSel : function() {
723 var t = this, w = this.win;
725 return w.getSelection ? w.getSelection() : w.document.selection;
729 * Returns the browsers internal range object.
732 * @param {Boolean} w3c Forces a compatible W3C range on IE.
733 * @return {Range} Internal browser range object.
734 * @see http://www.quirksmode.org/dom/range_intro.html
735 * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
737 getRng : function(w3c) {
738 var t = this, s, r, elm, doc = t.win.document;
740 // Found tridentSel object then we need to use that one
741 if (w3c && t.tridentSel)
742 return t.tridentSel.getRangeAt(0);
746 r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : doc.createRange());
748 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
751 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
752 if (tinymce.isIE && r && r.setStart && doc.selection.createRange().item) {
753 elm = doc.selection.createRange().item(0);
754 r = doc.createRange();
755 r.setStartBefore(elm);
759 // No range found then create an empty one
760 // This can occur when the editor is placed in a hidden container element on Gecko
761 // Or on IE when there was an exception
763 r = doc.createRange ? doc.createRange() : doc.body.createTextRange();
765 if (t.selectedRange && t.explicitRange) {
766 if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {
767 // Safari, Opera and Chrome only ever select text which causes the range to change.
768 // This lets us use the originally set range if the selection hasn't been changed by the user.
771 t.selectedRange = null;
772 t.explicitRange = null;
780 * Changes the selection to the specified DOM range.
783 * @param {Range} r Range to select.
785 setRng : function(r) {
797 // IE9 might throw errors here don't know why
801 t.selectedRange = s.getRangeAt(0);
806 t.tridentSel.addRange(r);
810 // Is IE specific range
814 // Needed for some odd IE bug #1843306
820 * Sets the current selection to the specified DOM element.
823 * @param {Element} n Element to set as the contents of the selection.
824 * @return {Element} Returns the element that got passed in.
826 * // Inserts a DOM node at current selection/caret location
827 * tinyMCE.activeEditor.selection.setNode(tinyMCE.activeEditor.dom.create('img', {src : 'some.gif', title : 'some title'}));
829 setNode : function(n) {
832 t.setContent(t.dom.getOuterHTML(n));
838 * Returns the currently selected element or the common ancestor element for both start and end of the selection.
841 * @return {Element} Currently selected element or common ancestor element.
843 * // Alerts the currently selected elements node name
844 * alert(tinyMCE.activeEditor.selection.getNode().nodeName);
846 getNode : function() {
847 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer;
849 // Range maybe lost after the editor is made visible again
851 return t.dom.getRoot();
854 elm = rng.commonAncestorContainer;
856 // Handle selection a image or other control like element such as anchors
857 if (!rng.collapsed) {
858 if (rng.startContainer == rng.endContainer) {
859 if (rng.endOffset - rng.startOffset < 2) {
860 if (rng.startContainer.hasChildNodes())
861 elm = rng.startContainer.childNodes[rng.startOffset];
865 // If the anchor node is a element instead of a text node then return this element
866 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
867 // return sel.anchorNode.childNodes[sel.anchorOffset];
869 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
870 // This happens when you double click an underlined word in FireFox.
871 if (start.nodeType === 3 && end.nodeType === 3) {
872 function skipEmptyTextNodes(n, forwards) {
874 while (n && n.nodeType === 3 && n.length === 0) {
875 n = forwards ? n.nextSibling : n.previousSibling;
879 if (start.length === rng.startOffset) {
880 start = skipEmptyTextNodes(start.nextSibling, true);
882 start = start.parentNode;
884 if (rng.endOffset === 0) {
885 end = skipEmptyTextNodes(end.previousSibling, false);
887 end = end.parentNode;
890 if (start && start === end)
895 if (elm && elm.nodeType == 3)
896 return elm.parentNode;
901 return rng.item ? rng.item(0) : rng.parentElement();
904 getSelectedBlocks : function(st, en) {
905 var t = this, dom = t.dom, sb, eb, n, bl = [];
907 sb = dom.getParent(st || t.getStart(), dom.isBlock);
908 eb = dom.getParent(en || t.getEnd(), dom.isBlock);
913 if (sb && eb && sb != eb) {
916 while ((n = n.nextSibling) && n != eb) {
928 normalize : function() {
929 var self = this, rng, normalized;
931 // Normalize only on non IE browsers for now
935 function normalizeEndPoint(start) {
936 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node;
938 container = rng[(start ? 'start' : 'end') + 'Container'];
939 offset = rng[(start ? 'start' : 'end') + 'Offset'];
941 // If the container is a document move it to the body element
942 if (container.nodeType === 9) {
943 container = container.body;
947 // If the container is body try move it into the closest text node or position
948 // TODO: Add more logic here to handle element selection cases
949 if (container === body) {
951 if (container.hasChildNodes()) {
952 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)];
955 // Walk the DOM to find a text node to place the caret at or a BR
957 walker = new tinymce.dom.TreeWalker(container, body);
959 // Found a text node use that position
960 if (node.nodeType === 3) {
961 offset = start ? 0 : node.nodeValue.length - 1;
966 // Found a BR element that we can place the caret before
967 if (node.nodeName === 'BR') {
968 offset = dom.nodeIndex(node);
969 container = node.parentNode;
972 } while (node = (start ? walker.next() : walker.prev()));
978 // Set endpoint if it was normalized
980 rng['set' + (start ? 'Start' : 'End')](container, offset);
985 // Normalize the end points
986 normalizeEndPoint(true);
991 // Set the selection if it was normalized
993 //console.log(self.dom.dumpRng(rng));
998 destroy : function(s) {
1003 // Manual destroy then remove unload handler
1005 tinymce.removeUnload(t.destroy);
1008 // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode
1009 _fixIESelection : function() {
1010 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm;
1012 // Make HTML element unselectable since we are going to handle selection by hand
1013 doc.documentElement.unselectable = true;
1015 // Return range from point or null if it failed
1016 function rngFromPoint(x, y) {
1017 var rng = body.createTextRange();
1020 rng.moveToPoint(x, y);
1022 // IE sometimes throws and exception, so lets just ignore it
1029 // Fires while the selection is changing
1030 function selectionChange(e) {
1033 // Check if the button is down or not
1035 // Create range from mouse position
1036 pointRng = rngFromPoint(e.x, e.y);
1039 // Check if pointRange is before/after selection then change the endPoint
1040 if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
1041 pointRng.setEndPoint('StartToStart', startRng);
1043 pointRng.setEndPoint('EndToEnd', startRng);
1051 // Removes listeners
1052 function endSelection() {
1053 var rng = doc.selection.createRange();
1055 // If the range is collapsed then use the last start range
1056 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0)
1059 dom.unbind(doc, 'mouseup', endSelection);
1060 dom.unbind(doc, 'mousemove', selectionChange);
1061 startRng = started = 0;
1064 // Detect when user selects outside BODY
1065 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) {
1066 if (e.target.nodeName === 'HTML') {
1070 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
1071 htmlElm = doc.documentElement;
1072 if (htmlElm.scrollHeight > htmlElm.clientHeight)
1076 // Setup start position
1077 startRng = rngFromPoint(e.x, e.y);
1079 // Listen for selection change events
1080 dom.bind(doc, 'mouseup', endSelection);
1081 dom.bind(doc, 'mousemove', selectionChange);