12

I need a better way to calculate a scrollable div's viewport.

Under normal circumstances, I would use the following attributes: (scrollLeft, scrollTop, clientWidth, clientHeight)

Using these numbers I can accurately determine which part of a scrollable DOM element's viewport is currently visible, I use this information to asynchronously load things that are visible to the user on demand when scrolling to the content horizontally or vertically. When the content of the DIV is massive, this will avoid an embarassing browser crashing bug because of too many DOM elements being loaded.

My component has worked for a while now with no issues, this build we are introducing RTL support. Now everything is thrown off because of browser inconsistencies.

To demonstrate, I have created a simple example which will output the scrollLeft attribute of a scrollable element in a JSFiddle.

The behavior of the scrollLeft attribute on this simple scrollable element is not consistent from one browser to the next. The 3 major browsers I've tried all behaved differently.

  1. FF-latest scrollLeft starts at 0 and goes negative when scrolling left
  2. IE 9 scrollLeft starts at 0 and goes positive when scrolling left
  3. Chrome-latest scrollLeft starts at a higher number and goes to 0 when scrolling left

I want to avoid having code like if(ie){...}else if(ff){...}else if (chrome){...} that would be horrible, and not maintainable in the long run in case browsers change behavior.

Is there a better way to figure out precisely which part of the DIV is currently visible?

Perhaps there is some other reliable DOM attribute other than scrollLeft?

Maybe there is a jQuery plugin that will do it for me, keeping in mind which browser version it is?

Maybe there is a technique I can use to figure out which of the cases it is at runtime without relying on some unreliable browser detection (i.e. userAgent)

Fiddle Example (code copied below)

HTML

<div id="box"><div id="content">scroll me</div></div>
<div id="output">Scroll Left: <span id="scrollLeft"></span></div>

CSS

#box {
    width: 100px; height: 100px;
    overflow: auto;
    direction: rtl;
}
#content { width: 300px; height: 300px; }

JS

function updateScroll() {
    $('#scrollLeft').text(box.scrollLeft());
}
var box = $('#box').scroll(updateScroll);
updateScroll();
Scimonster
  • 32,893
  • 9
  • 77
  • 89
codefactor
  • 1,616
  • 2
  • 18
  • 41
  • Could you just use the absolute value of the scroll (Distance from starting position) in your calculations? – Pete TNT Jun 18 '14 at 12:01
  • @PeteTNT unfortunately that will not account for the fact that the value in FF and IE would the be essentially the scrollRight instead of left. This would be the number of pixels from the right of the viewport to the right extend of the scrollable element. In chrome it would still be scrollLeft, and my code wouldn't know the difference without unreliable browser detection code. – codefactor Jun 18 '14 at 15:22

4 Answers4

10

Here's a jQuery plugin which does not use browser detection: https://github.com/othree/jquery.rtl-scroll-type

Using this plugin you could replace jQuery's scrollLeft function with your own predictable version, like this:

var origScrollLeft = jQuery.fn.scrollLeft;
jQuery.fn.scrollLeft = function(i) {
    var value = origScrollLeft.apply(this, arguments);

    if (i === undefined) {
        switch(jQuery.support.rtlScrollType) {
            case "negative":
                return value + this[0].scrollWidth - this[0].clientWidth;
            case "reverse":
                return this[0].scrollWidth - value - this[0].clientWidth;
        }
    }

    return value;
};

I didn't include the code for setting the scroll offset, but you get the idea.

Here's the fiddle: http://jsfiddle.net/scA63/

Also, this lib may be of interest too.

Lucas Trzesniewski
  • 50,214
  • 11
  • 107
  • 158
  • 1
    The plugin creates a DOM element on the fly and tests it for which type of scroll it is, which is much more reliable. Thank you for this! Do you per chance know how to do the same thing, but this time for the window's scroll? It seems the window scroll detection is just as inconsistent (and is not the same as the DIV scroll) - for example on Chrome the scrollLeft of a DIV is not inversed, but the body it is. http://jsfiddle.net/g3gyZ/3/ – codefactor Jun 24 '14 at 23:43
  • I just tested Chrome, Firefox and IE, and they all seem to calculate the window scroll offset the same way. – Lucas Trzesniewski Jun 24 '14 at 23:53
  • At least in my testing IE have the window scroll as a positive number. All others gave as negative. For my situation I will just do the absolute value and assume it will give the same value in all browsers. I think that should work. – codefactor Jun 25 '14 at 16:55
  • That's weird... I've tested on IE 11 and it behaves just like the other browsers (nagative on the left side, zero on the right side). Which version do you have? – Lucas Trzesniewski Jun 25 '14 at 17:18
  • I guess that explains it... So IE changed its behavior at v10 or 11. You have to take that into account. Unfortunately, I won't be able to help you with this particular issue as I don't have IE9, and IE11 in IE9 mode works just like IE11... – Lucas Trzesniewski Jun 25 '14 at 22:06
7

You can try this:-

         var initialScrollLeft = $('#box').scrollLeft(), negativeToZero, startFromZero;
            if(initialScrollLeft === 0){
                startFromZero = true; 
            } else if(initialScrollLeft < 0){
                negativeToZero = true;
            } 
            var box = $('#box').scroll(function(){
                if(startFromZero){
                    if(box.scrollLeft()>0){
                        $('#scrollLeft').text(- (box.scrollLeft()));
                    }else {
                        $('#scrollLeft').text(box.scrollLeft()); 
                    }
                } else if(negativeToZero){
                    $('#scrollLeft').text(box.scrollLeft()+(box[0].scrollWidth - box[0].clientWidth));  
                } else{
                    $('#scrollLeft').text(box.scrollLeft()-(box[0].scrollWidth - box[0].clientWidth));  
                }

            });
Indranil Mondal
  • 2,799
  • 3
  • 25
  • 40
  • 1
    Can you clean up the code, and provide a working example? The goal would be to have a reusable function that could return the proper scroll left of a div. – codefactor Jun 24 '14 at 23:40
  • Just replace your javascript code with my code, it should work? Isn't it working? Or you need the explanation? – Indranil Mondal Jun 25 '14 at 06:22
  • I've edited my code, you can try it. In this code, if you run this any browser, you'll see the value of scroll left 0 to negative in the span you're showing. – Indranil Mondal Jun 25 '14 at 07:46
4

Problem: (Ex. Scroll Width = 100)

Chrome - Most Right: 100 Most Left: 0.

IE- Most Right: 0 Most Left: 100.

Firefox - Most Right: 0 Most Left: -100.

Solution #1

As mentioned by @Lucas Trzesniewski.

You could use this Jquery plugin:

https://github.com/othree/jquery.rtl-scroll-type

The plugin is used to detect which type is the browser are using. Assign the result to jQuery's support object named 'rtlScrollType'. You will need the scrollWidth of the element to transform between these three types of value

Solution #2

Credits: jQuery.scrollLeft() when direction is rtl - different values in different browsers

I know you didn't want to include browser detection individually for each browser. With this example, only 2 extra lines of code are added for Safari and Chrome and it works like a charm!

Modified it to demonstrate it better for you.

$('div.Container').scroll(function () {
    st = $("div.Container").scrollLeft() + ' ' + GetScrollLeft($("div.Container"));
    $('#scrollLeft').html(st);
});

function GetScrollLeft(elem) {
    var scrollLeft = elem.scrollLeft();
    if ($("body").css("direction").toLowerCase() == "rtl") {
        // Absolute value - gets IE and FF to return the same values
        var scrollLeft = Math.abs(scrollLeft);

        // Get Chrome and Safari to return the same value as well
        if ($.browser.webkit) {
            scrollLeft = elem[0].scrollWidth - elem[0].clientWidth - scrollLeft;
        }
    }
    return scrollLeft;
}

JSFiddle:

http://jsfiddle.net/SSZRd/1/

The value on the left should be the same for all browser while the value on the right is the older value which is different on all browser. (Tested on Firefox, Safari, Chrome, IE9).

Community
  • 1
  • 1
imbondbaby
  • 6,351
  • 3
  • 21
  • 54
  • Works great on IE 9 too :) Please let me know if you have any questions! – imbondbaby Jun 25 '14 at 19:57
  • The op said he wants to avoid code like `$.browser.webkit` – Lucas Trzesniewski Jun 25 '14 at 20:05
  • "I know you didn't want to include browser detection individually for each browser. With this example, only 2 extra lines of code are added for Safari and Chrome and it works like a charm!". I included that as part of my answer. Also, that is why I posted 2 solutions. Both work great but if I understand it right, he didn't want a browser validation for every single browser as he mentioned "I want to avoid having code like `if(ie){...}else if(ff){...}else if (chrome){...}"` but 2 lines of extra code shouldn't hurt anyone :). – imbondbaby Jun 25 '14 at 20:10
  • The `$.browser.webkit` conditional throws a wrench in the code. It is not the number of lines that I am concerned with, it is the logic that must be maintained. What if in the future webkit decides that scrollLeft should act like other browsers then your code will break. Also `$.browser` was deprecated and removed in the latest versions of jQuery. – codefactor Jun 25 '14 at 22:01
0
 1. FF-latest scrollLeft starts at 0 and goes negative when scrolling left
 2. IE 9 scrollLeft starts at 0 and goes positive when scrolling left
 3. Chrome-latest scrollLeft starts at a higher number and goes to when scrolling left

I want to avoid having code like if(ie){...}else if(ff){...}else if(chrome){...}
that would be horrible, and not maintainable in the long run in case browsers change behavior

FYI: Chrome 85 (final shipping Aug. 2020) fixed this bug and aligns behaviour with Firefox and Safari and the spec.
See https://www.chromestatus.com/feature/5759578031521792

Is there a feature detection available for this?

Yes, e.g. use one of two scrips (from Frédéric Wang) available here:

https://people.igalia.com/fwang/scrollable-elements-in-non-default-writing-modes/

either this

function scroll_coordinates_behavior_with_scrollIntoView() {
    /* Append a RTL scrollable 1px square containing two 1px-wide descendants on
       the same line, reveal each of them successively and compare their
       scrollLeft coordinates. The scrollable square has 'position: fixed' so
       that scrollIntoView() calls don't scroll the viewport. */
    document.body.insertAdjacentHTML("beforeend", "<div style='direction: rtl;\
position: fixed; left: 0; top: 0; overflow: hidden; width: 1px; height: 1px;'>\
<div style='width: 2px; height: 1px;'><div style='display: inline-block;\
width: 1px;'></div><div style='display: inline-block; width: 1px;'></div>\
3</div></div>");
    var scroller = document.body.lastElementChild;
    scroller.firstElementChild.children[0].scrollIntoView();
    var right = scroller.scrollLeft;
    scroller.firstElementChild.children[1].scrollIntoView();
    var left = scroller.scrollLeft;

    /* Per the CSSOM specification, the standard behavior is:
       - decreasing coordinates when scrolling leftward.
       - nonpositive coordinates for scroller with leftward overflow. */
    var result = { "decreasing": left < right, "nonpositive": left < 0 };
    document.body.removeChild(scroller);
    return result;
}

or that

function scroll_coordinates_behavior_by_setting_nonpositive_scrollLeft() {
    /* Append a RTL scrollable 1px square containing a 2px-wide child and check
       the initial scrollLeft and whether it's possible to set a negative one.*/
    document.body.insertAdjacentHTML("beforeend", "<div style='direction: rtl;\
position: absolute; left: 0; top: 0; overflow: hidden; width: 1px;\
height: 1px;'><div style='width: 2px; height: 1px;'></div></div>");
    var scroller = document.body.lastElementChild;
    var initially_positive = scroller.scrollLeft > 0;
    scroller.scrollLeft = -1;
    var has_negative = scroller.scrollLeft < 0;

    /* Per the CSSOM specification, the standard behavio999r is:
       - decreasing coordinates when scrolling leftward.
       - nonpositive coordinates for scroller with leftward overflow. */
    var result = { "decreasing": has_negative ||
                   initially_positive, "nonpositive": has_negative };
    document.body.removeChild(scroller);
    return result;
}
j.j.
  • 1,914
  • 15
  • 12