{"id":2802,"date":"2013-02-13T17:54:53","date_gmt":"2013-02-13T23:54:53","guid":{"rendered":"http:\/\/bililite.com\/blog\/?p=2802"},"modified":"2014-01-13T16:05:29","modified_gmt":"2014-01-13T22:05:29","slug":"scrolling-to-cross-browser-ranges","status":"publish","type":"post","link":"https:\/\/bililite.com\/blog\/2013\/02\/13\/scrolling-to-cross-browser-ranges\/","title":{"rendered":"Scrolling to Cross-browser Ranges"},"content":{"rendered":"<p>One thing I wanted to add to my <a href=\"http:\/\/bililite.com\/blog\/2011\/01\/17\/cross-browser-text-ranges-and-selections\/\" title=\"Cross-Browser Text Ranges and Selections\"><code>bililiteRange<\/code><\/a> was the ability to scroll the range into view without changing the selection. As with all things having to do with ranges, there's no consistent way to do it, and <em>Internet Explorer does it best<\/em> (this is the only time you'll hear me say that).<\/p>\r\n<!--more-->\r\n<p>For IE, the <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ie\/ms535872(v=vs.85).aspx\"><code>TextRange<\/code><\/a> object supports <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ie\/ms536730(v=vs.85).aspx\"><code>scrollIntoView<\/code><\/a>, so all you need to do is:<\/p>\r\n<pre><code class=\"language-javascript\" >range.scrollIntoView()<\/code><\/pre>\r\n<p>and you're done.<\/p>\r\n<p>In <code class=\"language-javascript\" >bililiteRange<\/code> I don't actually use this, since <code class=\"language-javascript\" >scrollIntoView()<\/code> scrolls to the top of the screen no matter what, and I want to only scroll if the range isn't already visible. To do that, I need to set the <code class=\"language-javascript\" >scrollTop<\/code> of the element appropriately; the formula is:<\/p>\r\n<pre><code class=\"language-javascript\" >var top = range.boundingTop - element.offsetTop + 2*element.scrollTop;<\/code><\/pre>\r\n<p>The double adding <code class=\"language-javascript\" >element.scrollTop<\/code> is a <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ie\/ms533540(v=vs.85).aspx\">documented IE bug<\/a>. Then we can test to scroll if needed:<\/p>\r\n<pre><code class=\"language-javascript\" >if (element.scrollTop &gt; top || element.scrollTop+element.clientHeight &lt; top){\r\n  element.scrollTop = top;\r\n}<\/code><\/pre>\r\n\r\n<p>Standards-based browsers use <a href=\"http:\/\/www.w3.org\/TR\/DOM-Level-2-Traversal-Range\/ranges.html\">Range<\/a> objects, and they have no <code>scrollIntoView<\/code>. <del>But we can create a dummy element, insert it into the range, and scroll to that:<\/del><\/p>\r\n<pre><code class=\"language-javascript\" >\r\n\/\/ Note: don't do this! It messes up the selection\r\nvar span = document.createElement('span');\r\nrange.insertNode(span);\r\nspan.scrollIntoView();\r\nspan.parentNode.removeChild(span);<\/code><\/pre>\r\n\r\n<p><strong>Edit 2014-01-09<\/strong>: It turns out that there's <a href=\"\/blog\/2014\/01\/09\/odd-bug-with-document-ranges-and-selections\/\">odd behavior with the selection if you modify the DOM<\/a>. While the above approach is simple if that's not a problem, the right way is with (where <code class=\"language-javascript\" >element<\/code> is the parent element that does the scrolling):<\/p>\r\n\r\n<pre><code class=\"language-javascript\" >var top = range.getBoundingClientRect().top - element.offsetTop + element.scrollTop;<\/code><\/pre>\r\n<p>And set <code class=\"language-javascript\" >element.scrollTop<\/code> if necessary, as above.<\/p>\r\n\r\n<p>If the range is empty, you have to temporarily insert some text to make it have a <code class=\"language-javascript\" >clientRect<\/code>, os the actual code is:<\/p>\r\n\r\n<pre><code class=\"language-javascript\" >if (range.toString() == ''){\r\n\tvar textnode = this._doc.createTextNode('X');\r\n\trng.insertNode (textnode);\r\n}\r\nvar top = range.getBoundingClientRect().top - element.offsetTop + element.scrollTop;\r\nif (textnode) textnode.parentNode.removeChild(textnode);<\/code><\/pre>\r\n<p>This method fails, unfortunately, if the parent element is absolutely positioned. As of 2014-01-13, I use an easier method that creates a dummy range at the beginning of the element and subtract its <code class=\"language-javascript\" >getBoundingClientRect().top<\/code> from that of the target range. See the <a href=\"https:\/\/github.com\/dwachss\/bililiteRange\/blob\/master\/bililiteRange.js\">bililiteRange source<\/a> for IERange.prototype._nativeTop() and <code class=\"language-javascript\" >W3CRange.prototype._nativeTop()<\/code> for details\r\n\r\n\r\n\r\n<p>But the hardest part is scrolling a <code class=\"language-html\" >&lt;textarea&gt;<\/code>. There's no good way to scroll to a selection in a text area, and there's no equivalent of <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/DOM\/range.getBoundingClientRect\"><code>getBoundingClientRect<\/code><\/a> so we can use <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/DOM\/element.scrollTop\"><code>scrollTop<\/code><\/a> to scroll to the right place. I don't know where I found this clever hack, but it solves that problem: <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/DOM\/element.scrollHeight\"><code>scrollHeight<\/code><\/a> gives the total height of the text in a <code class=\"language-html\" >&lt;textarea&gt;<\/code> if it overflows, so we can set the height to 1px (forcing the overflow), truncate the <code>value<\/code> to the text <em>before<\/em> the range we care about, and measure <code>scrollHeight<\/code>. This will be the bottom of the beginning of the range, in pixels from the top of the <code class=\"language-html\" >&lt;textarea&gt;<\/code>. We want the top of the range, so subtract the line height (which we get as the <code>scrollHeight<\/code> of a one-line <code class=\"language-html\" >&lt;textarea&gt;<\/code>):<\/p>\r\n<pre><code class=\"language-javascript\" >var style = getComputedStyle(textareaElement);\r\nvar oldheight = style.height;\r\nvar oldval = textareaElement.value;\r\ntextareaElement.style.height = '1px';\r\ntextareaElement.value = oldval.slice(0, rangeStart);\r\nvar top = textareaElement.scrollHeight;\r\ntextareaElement.value = 'X';\r\ntop -= textareaElement.scrollHeight;\r\ntextareaElement.style.height = oldheight; \/\/ restore the element before anyone notices. May have a brief flash\r\ntextareaElement.value = oldval;\r\n\/\/ scroll into position if necessary as above\r\nif (textareaElement.scrollTop &gt; top || textareaElement.scrollTop+textareaElement.clientHeight &lt; top){\r\n\ttextareaElement.scrollTop = top;\r\n}<\/code><\/pre>\r\n<p>Now the <code class=\"language-html\" >&lt;textarea&gt;<\/code> is scrolled correctly.<\/p>\r\n\r\n\r\n<p>But we may still want to make sure that the element <em>itself<\/em> is scrolled into view. We can't just use <code>scrollIntoView<\/code> straight, since that shows the top of the element, and the range may be at the bottom. In <code class=\"language-javascript\" >bililiteRange<\/code> I don't worry about that (<code class=\"language-javascript\" >bililiteRange.scrollIntoView()<\/code> only scrolls within the element). If you want to take care of that, create a dummy element as above, absolutely position it at the desired location, and scroll to that:<\/p>\r\n<pre><code class=\"language-javascript\" >\/\/ get the position as in jQuery.offset\r\nvar rect = textareaElement.getBoundingClientRect();\r\nrect.top += window.pageYOffset - document.documentElement.clientTop;\r\nrect.left += window.pageXOffset - document.documentElement.clientLeft;\r\n \/\/ create an element to scroll to\r\nvar div = document.createElement('div');\r\ndiv.style.position = 'absolute';\r\ndiv.style.top = (rect.top+top-textareaElement.scrollTop)+'px'; \/\/ adjust for how far in the range is; it may not have scrolled all the way to the top\r\ndiv.style.left = rect.left+'px';\r\ndiv.innerHTML = '&amp;nbsp;'; \/\/ scrollIntoViewIfNeeded won't scroll if there's no content (it's not needed!)\r\ndocument.body.appendChild(div);\r\ndiv.scrollIntoViewIfNeeded ? div.scrollIntoViewIfNeeded() : div.scrollIntoView();\r\ndiv.parentNode.removeChild(div);<\/code><\/pre>\r\n<p>For <code class=\"language-html\" >&lt;textarea&gt;<\/code>s you may also want to save the original selection with <code>selectionStart<\/code> and <code>selectionEnd<\/code> and restore it with <code>setSelectionRange<\/code>, since it will change when the text is truncated. In <code class=\"language-javascript\" >bililiteRange<\/code> I create a dummy element that is a clone of the original, so the selection doesn't change.<\/p>\r\n<p>And this ugly mess seems to work correctly. Hope some of this analysis is useful to someone.<\/p>","protected":false},"excerpt":{"rendered":"One thing I wanted to add to my bililiteRange was the ability to scroll the range into view without changing the selection. As with all things having to do with ranges, there's no consistent way to do it, and Internet Explorer does it best (this is the only time you'll hear me say that).","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10],"tags":[],"_links":{"self":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2802"}],"collection":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/comments?post=2802"}],"version-history":[{"count":20,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2802\/revisions"}],"predecessor-version":[{"id":3163,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2802\/revisions\/3163"}],"wp:attachment":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/media?parent=2802"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/categories?post=2802"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/tags?post=2802"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}