Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scroll to element only if not in view - jQuery

I know a variation on this has been asked several times; I've been browsing SO for a while now but either I'm doing something wrong or I haven't found what I need.

I have a structure of nested comments, pretty much the same as the Facebook Comments plugin, and whenever reply is clicked, a small form with textarea and a button appear at the bottom of the comments.

Again, the behaviour is the same as the Facebook Comments plugin and I want to achieve the same when it comes to scrolling the newly add textarea into view.

I've tried the scrollTo plugin, and it works smoothly but, even if I manually scroll to the very bottom of the page, the scroll animation will always reset the scroll position and begin from the top.

For the record, this is how I'm calling scrollTo:

$.scrollTo($('#addReply_1'), 800);

where addReply_1 is the div containing the form. I've stried scrolling to the form itself and to the textarea. Same results.

Is there a way to scroll to an element only if its not already visible?

I've tried many solutions offered on SO, like Scroll to an element using jQuery, but none seem to behave as desired; even Scroll to a particular element w/ jQuery or Check if element is visible after scrolling displays the same "jumpy" behaviour.


UPDATE: Online demo to show behaviour

I've uploaded an html demo page that shows the behaviour I'm complaining about: http://www.wouldbebetter.com/demo/comment-demo.htm

Just scroll to the bottom of the page and click any of the Reply links to see the "jumpy" scrolling I'm referring to.

Note that this demo uses the scrollintoview plugin of @Robert Koritnik's answer, but the behaviour is the same if I use, for instance, ScrollTo.

like image 672
Sergi Papaseit Avatar asked Apr 16 '11 09:04

Sergi Papaseit


4 Answers

All modern Browsers support this. Visit: http://caniuse.com/#search=scrollIntoView

function scrollIntoViewIfNeeded(target) { 
    if (target.getBoundingClientRect().bottom > window.innerHeight) {
        target.scrollIntoView(false);
    }

    if (target.getBoundingClientRect().top < 0) {
        target.scrollIntoView();
    } 
}

Update

The target has to be an Element. If you use jQuery call the function like this:

scrollIntoViewIfNeeded($(".target")[0]);
like image 98
Markus Avatar answered Nov 15 '22 16:11

Markus


Yes there is a jQuery plugin that scrolls to an element only if it's not within visible boundaries of the scrollable ancestor. I've written one does exactly what you require. And you will probably find it easier to use compared to scrollTo() since you only have to provide the element that you'd like to see.

I could copy paste the code here, but since I add some additions from time to time it's better to link you to blog post where you'll find all the details related to programmatic scrolling and latest plugin code. Programmatic scrolling can be quite distracting to users and the whole user interface experience, so I suppose it will be an interesting read.

Usage

Plugin is really simple to use:

$("#ElementToScrollIntoView").scrollintoview();

Plugin automatically finds nearest scrollable ancestor and scrolls it accordingly (if at all needed). There are some additional settings to this plugin you can use and this is how they look like:

scrollintoview: function (options) {
    /// <summary>Scrolls the first element in the set into view by scrolling its closest scrollable parent.</summary>
    /// <param name="options" type="Object">Additional options that can configure scrolling:
    ///        duration (default: "fast") - jQuery animation speed (can be a duration string or number of milliseconds)
    ///        direction (default: "both") - select possible scrollings ("vertical" or "y", "horizontal" or "x", "both")
    ///        complete (default: none) - a function to call when scrolling completes (called in context of the DOM element being scrolled)
    /// </param>
    /// <return type="jQuery">Returns the same jQuery set that this function was run on.</return>

I'm using this plugin on my Sharepoint 2010 site on pages where I present long tabular data. Whenever I add a new item (row) to this table I additionally scroll to this new record and highlight it, so users can see the new record immediately.

Sharepoint was also the reason why I decided not to provide scrollable element manually but rather look for it programatically. Sharepoint uses admin costumizable master pages which means I don't know which element is going to be scrollable at runtime. But I do know which element I want to see. Hence this plugin. It's also rather simplified compared to scrollTo() plugin that supports various different scenarios. Most of the time developers tend to use only one (or a very limited number of them).

Additional observations

Default link click processing prevention

Using my plugins still makes it rather problematic, since there's some flickering when adding those reply boxes. The problem is that your link clicking actually executes. You should prevent this in order to make your page to work smooth:

  1. either set click events on your links in one of these two ways:

    <a href="javascript:void AddReplyForm(44); return false;">Reply</a>
    

    or

    <a href="#" onclick="void AddReplyForm(44); return false;">Reply</a>
    
  2. a better way would be to run this on document ready:

    $(function() {
        $("a").click(function(evt) {
            evt.preventDefault();
        });
    });
    

The main idea is to prevent browser from processing link clicks. Because this makes the browser to look for in-page anchor and since it can't find one, it auto scrolls to the top. Then you tell it to scroll to your element.

Duplicate IDs

When you create reply form, you add new and new and new elements but they're all with the same ID. You should either avoid doing this or use some other means. You could remove the need for IDs altogether by providing the element to your BindClick() function. The main reply generating function could as well look like this (this function is written in a way that completely eliminates the need for element IDs):

function AddReplyForm(topCommentID)
{
    var el = $(addReplyForm).appendTo('#comment_' + topCommentID + ' .right');
    BindClick(el); // mind this !! you provide the element to remove
    el.scrollintoview();
}
like image 33
Robert Koritnik Avatar answered Nov 15 '22 15:11

Robert Koritnik


Had the same problem... after reviewing a few answers, here's what I came up with for doing this... without pulling down another plugin.

function scrollIntoViewIfNeeded($target) {
    if ($target.position()) {
        if ($target.position().top < jQuery(window).scrollTop()){
            //scroll up
            $('html,body').animate({scrollTop: $target.position().top});
        }
        else if ($target.position().top + $target.height() >
            $(window).scrollTop() + (
                window.innerHeight || document.documentElement.clientHeight
            )) {
            //scroll down
            $('html,body').animate({scrollTop: $target.position().top -
                (window.innerHeight || document.documentElement.clientHeight)
                    + $target.height() + 15}
            );
        }
    }
}

The "15" on the last line is just extra padding - you might need to adjust it, or add it to the scroll up line.

EDIT: changed window.innerHeight to (window.innerHeight || document.documentElement.clientHeight) for IE < 8 support

like image 23
iandisme Avatar answered Nov 15 '22 16:11

iandisme


to make sure elem is in view inside a container:

let rectElem = elem.getBoundingClientRect(), rectContainer=container.getBoundingClientRect();
if (rectElem.bottom > rectContainer.bottom) elem.scrollIntoView(false);
if (rectElem.top < rectContainer.top) elem.scrollIntoView();
like image 10
kofifus Avatar answered Nov 15 '22 15:11

kofifus