仅当不在视图中时才滚动到元素 - jQuery

Posted

技术标签:

【中文标题】仅当不在视图中时才滚动到元素 - jQuery【英文标题】:Scroll to element only if not in view - jQuery 【发布时间】:2011-08-06 20:01:52 【问题描述】:

我知道这方面的一个变体已经被问过好几次了;我已经浏览 SO 有一段时间了,但要么我做错了什么,要么我没有找到我需要的东西。

我有一个嵌套的 cmets 结构,与 Facebook Comments plugin 几乎相同,并且每当单击 reply 时,cmets 底部会出现一个带有 textarea 和一个按钮的小表单。

同样,行为与 Facebook Comments plugin 相同,我希望在将新添加的 textarea 滚动到视图中时实现相同的效果。

我尝试了scrollTo 插件,它运行流畅,但是,即使我手动滚动到页面的最底部,滚动动画也会总是重置滚动位置并从顶部开始。

为了记录,这就是我调用 scrollTo 的方式:

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

其中addReply_1 是包含表单的div。我尝试滚动到表单本身和textarea。结果相同。

有没有办法滚动到一个元素只有当它不可见的时候?

我尝试了 SO 上提供的许多解决方案,例如 Scroll to an element using jQuery,但似乎没有一个表现符合预期;甚至 Scroll to a particular element w/ jQuery 或 Check if element is visible after scrolling 也会显示相同的“跳跃”行为。


更新:展示行为的在线演示

我上传了一个 html 演示页面,其中显示了我所抱怨的行为:http://www.wouldbebetter.com/demo/comment-demo.htm

只需滚动到页面底部并单击任何Reply 链接即可查看我所指的“跳跃”滚动。

请注意,此演示使用 @Robert Koritnik 答案的 scrollintoview 插件,但如果我使用例如 ScrollTo,则行为是相同的。

【问题讨论】:

检查元素的偏移量并测试它是否在窗口的范围内。 @jimplode - 谢谢,你能帮忙看看如何检查它是否在界限内吗? 【参考方案1】:

是的,有一个 jQuery 插件,仅当元素不在可滚动祖先的可见边界内时才会滚动到该元素。我已经写了一个完全符合您的要求。与scrollTo() 相比,您可能会发现它更易于使用,因为您只需提供您想看到的元素。

我可以在此处复制粘贴代码,但由于我不时添加一些内容,因此最好将您链接到blog post,您可以在其中找到与程序化滚动和最新插件代码相关的所有详细信息。程序化滚动可能会分散用户和整个用户界面体验的注意力,所以我想这会是一本有趣的书。

用法

插件使用起来真的很简单:

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

插件会自动找到最近的可滚动祖先并相应地滚动它(如果需要的话)。您可以使用此插件的一些附加设置,它们的外观如下:

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>

我在我的 Sharepoint 2010 网站上使用这个插件,在我展示长表格数据的页面上。每当我向此表添加新项目(行)时,我都会滚动到此新记录并突出显示它,以便用户可以立即看到新记录。

Sharepoint 也是我决定不手动提供可滚动元素而是以编程方式查找它的原因。 Sharepoint 使用管理员可定制的母版页,这意味着我不知道在运行时哪个元素可以滚动。但我确实知道我想看到哪个元素。因此这个插件。与支持各种不同场景的scrollTo() 插件相比,它也相当简化。大多数时候,开发人员倾向于只使用一个(或非常有限的数量)。

补充意见

默认链接点击处理预防

使用我的插件仍然会带来很多问题,因为添加这些回复框时会出现一些闪烁。问题是您的链接点击实际上执行了。您应该防止这种情况发生,以使您的页面运行顺畅:

    通过以下两种方式之一在您的链接上设置点击事件:

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

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

    更好的方法是在准备好的文档上运行它:

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

主要思想是防止浏览器处理链接点击。因为这会使浏览器寻找页内锚点,并且由于找不到,它会自动滚动到顶部。然后你告诉它滚动到你的元素。

重复 ID

当您创建回复表单时,您会添加新元素和新元素,但它们都具有相同的 ID。您应该避免这样做或使用其他方法。您可以通过将元素提供给 BindClick() 函数来完全消除对 ID 的需求。主要的回复生成函数也可以如下所示(这个函数的编写方式完全不需要元素 ID):

function AddReplyForm(topCommentID)

    var el = $(addReplyForm).appendTo('#comment_' + topCommentID + ' .right');
    BindClick(el); // mind this !! you provide the element to remove
    el.scrollintoview();

【讨论】:

@Robert - 不错的插件。我实际上看到了相同的行为:即使滚动页面以使新插入的表单在视图中,页面也会从顶部滚动。有什么想法吗? @Sergi:在这种情况下,我建议您提供一些代码,我将提供一个演示来实际了解发生了什么。这种行为不是我插件的一部分,所以我想你要么没有测试正确的页面(你使用它的地方),要么没有刷新你的脚本(Ctrl-F5)并且你的页面仍然使用以前的行为.我的插件根本没有开发从顶部滚动。但还有第三种选择:您还使用了一些其他功能(通过插件或直接操作 DOM 元素属性或调用 DOM 函数),它们首先滚动到顶部。 @Robert - 我已经刷新了所有内容,这是肯定的。我正在设置一个演示;我会发布链接。谢谢你的帮助。顺便说一句,您的插件是否依赖于任何特定版本的 jQuery?现在使用 1.4.4,但我很快就会升级到 1.5.1... @Sergi:是的。正如开头的插件评论所说:*需要 jQuery 1.4 或更新版本“所以你应该没问题。当你提供一个演示时,只需提供可以重现问题的基本内容,这样我就更容易处理了.. . 在 2015 年末仍然很好用。你应该把它扔到 github 和 npm 上!【参考方案2】:

我不知道我是否明白你想要什么,但看看是不是这样,接近还是我完全迷失了:

<!DOCTYPE html>
<html>
    <head>
        <title>Scroll To Reply</title>
        <meta charset="utf-8" />
     <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
        <script type="text/javascript">
            $(document).ready(function()
                var $contextarea = $('#contextform textarea');
                $('a.reply').live('click',function()
                    $(this).closest('p').css('margin-top':'300px;');
                    $('#contextform').appendTo($(this).closest('p'));
                    $('#contextform').slideDown(1000);//.css('display':'block');
                    $(this).closest('p').attr('id','scrolltome');
                    $('html,body').animate(slideDown: $('#scrolltome').offset().top, 2000);
                );
                $('.sendreply').live('click',function()
                    if($contextarea.val() == null || $contextarea.val() == '') 
                        alert('textarea is empty!');
                     else 
                        $('#contextform').slideUp(800);
                    
                );

                //                $('button').click(function() 
                //                    $('html,body').animate(scrollTop: $('#body').offset().top, 2000);//scrolls to the top
                //                );

            );
        </script>
        <style type="text/css">
            body
                font-size:25px;
                font-family: 'Arimo', Arial, sans-serif;
            
            #contextform 
                display:none;
                width:500px;
                height: 150px;
                background: #0489B7;
                padding: 5px
            
            #contextform textarea 
                width: 490px;
                height: 90px;
            
        </style>
    </head>
    <body id="body">
        <h1>Scroll to reply</h1>
        <div id="contextform">
            <form method="post" action="">
                <textarea id="textarea"></textarea>
                <button type="button" class="sendreply">Send Reply</button>
            </form>
            <a name="myAnchor" id="myAnchor">anchor</a>
        </div>
        <ol>
            <?php for ($i = 0; $i < 20; $i++)  ?>
                <li><p>The quick brown fox jumps over the lazy dog
                        <a href="#scrolltome" class="reply">Reply</a>
                    </p></li>
            <?php  ?>
        </ol>
    </body>
</html>

【讨论】:

【参考方案3】:

遇到了同样的问题...在查看了几个答案之后,这就是我想出的解决方法...没有拉下另一个插件。

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
            );
        
    

最后一行的“15”只是额外的填充 - 您可能需要调整它,或者将其添加到向上滚动行。

编辑:将 window.innerHeight 更改为 (window.innerHeight || document.documentElement.clientHeight) 以支持 IE

【讨论】:

@iandisme 不应该 $("selector").position() 是 $("selector").offset() 吗? @sedran 可能是; 4年发生了很多变化。我记得这是我使用的实际代码(obv。我更改了选择器)。 你从哪里得到 $target.position()? $target 是一个 jQuery 对象,作为参数传递给方法。 position() 是该对象的一个​​方法。 $&lt;name&gt; 是命名 jQuery 对象的常用约定。 @TheCrazyProfessor api.jquery.com/position 您必须将position() 替换为offset()(正如@iandisme 指出的那样)才能处理定位元素【参考方案4】:

我稍微修改了@iandisme 的答案,并将其封装为一个小型 jquery 插件:

(function ($) 
'use strict';

$.fn.scrollToSimple = function ($target) 
    var $container = this.first();      // Only scrolls the first matched container

    var pos = $target.position(), height = $target.outerHeight();
    var containerScrollTop = $container.scrollTop(), containerHeight = $container.height();
    var top = pos.top + containerScrollTop;     // position.top is relative to the scrollTop of the containing element

    var paddingPx = containerHeight * 0.15;      // padding keeps the target from being butted up against the top / bottom of the container after scroll

    if (top < containerScrollTop)      // scroll up                
        $container.scrollTop(top - paddingPx);
    
    else if (top + height > containerScrollTop + containerHeight)      // scroll down
        $container.scrollTop(top + height - containerHeight + paddingPx);
    
;
)(jQuery);

我删除了对 .animate 的调用,因为我正在寻找即时滚动。我还添加了滚动任何(可滚动)容器的功能,而不仅仅是窗口。示例用法:

// scroll the window so #target  is visible
$(window).scrollToSimple( $("#target") );

// scroll the scrollable element #container so that #target is visible
$("#container").scrollToSimple( $("#target") );

【讨论】:

【参考方案5】:

所有现代浏览器都支持这一点。访问:http://caniuse.com/#search=scrollIntoView

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

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

更新

目标必须是一个元素。如果你使用 jQuery 调用这样的函数:

scrollIntoViewIfNeeded($(".target")[0]);

【讨论】:

Uncaught TypeError: target.getBoundingClientRect is not a function @AeroWang 哪个浏览器和浏览器版本? Chrome 60.0.3100.0 @Magu 我昨天已经尝试过了,最终我认为 scrollIntoView 不适用于移动 safari - 特别是在遇到可能整个 html 元素的幽灵边距时(如果使用中文拼音 9 按钮输入)。在这里查看我的问题:***.com/questions/43996535/… 如果目标比窗口高,这可以通过在第一次 scrollIntoView 发生后第二次为 rect.top 调用 getBoundingClientRect() 来改善。这将使目标的顶部与窗口边缘对齐,这可能比对齐底部更可取。【参考方案6】:

确保元素在容器内可见:

let rectElem = elem.getBoundingClientRect(), rectContainer=container.getBoundingClientRect();
if (rectElem.bottom > rectContainer.bottom) elem.scrollIntoView(false);
if (rectElem.top < rectContainer.top) elem.scrollIntoView();

【讨论】:

这个答案对我来说效果最好,因为它更准确地跟随熟悉元素的滚动,如select【参考方案7】:

JQuery 不是必需的。

这个函数只显示指定的元素:

function scrollIntoView(elm) 
    if(elm) 
        let bnd=elm.getBoundingClientRect();
        if     (bnd.top<0                    )  elm.scrollIntoView(true );      
        else if(bnd.bottom>window.innerHeight)  elm.scrollIntoView(bnd.top<=0); 
        
    return elm;
    

以下功能更强大的功能允许将所需元素的容器滚动到视图中,还可以轻松确保整个容器可见,从而避免出现半遮挡的视觉效果。

/**
  * Scroll the specified element into view, optionally first searching for a specific container and
  * first making that visible. This function does it's best to scroll the entire container into view
  * but ultimately ensures that as much of the element as fits in the viewport will be visible.
  *
  * #### Arguments:
  *
  *     elm (DOMElement)    The element to make visible.
  *     contag (string)     Optional name of a container tag. Ignored if blank/null/omitted.
  *     conprp (string)     Optional name of a container property to also match. Ignored if blank/null/omitted.
  *     conval (string)     Optional value of the container property. Ignored if `conprp` is not supplied; defaults to "" if omitted.
  */
function scrollIntoView(elm,contag,conprp,conval) 
    if(elm) 
        if(contag || conprp) 
            let con;
            if(conval==null)  conval=""; 
            for(con=elm; con!=null && con.tagName!="BODY"; con=con.parentNode) 
                if((!contag || contag==con.tagName) && (!conprp || con[conprp]==conval)) 
                    break;                                                                          // matched container tag and property
                    
                
            if(con)  scrollIntoView(con); 
            

        let bnd=elm.getBoundingClientRect();

        if     (bnd.top<0                    )  elm.scrollIntoView(true );      
        else if(bnd.bottom>window.innerHeight)  elm.scrollIntoView(bnd.top<=0); 
        
    return elm;
    

这使得例如在向上滚动时显示它变得容易:

而不是这个:

【讨论】:

【参考方案8】:

这里的每个答案要么似乎已经过时并且不再适用于现代版本的 jQuery(可能是由于 position() 和 offset() 函数的变化),要么在我需要它们的情况下工作太有限。例如,如果您的代码在 iframe 中,上述答案似乎都不起作用。

我注意到的最重要的事情之一是它们都只使用了容器对象的正常高度,只要整个容器对象在窗口中可见,它就可以正常工作,但是当你的容器对象是 html 对象本身时并且高度远远低于显示的高度,代码的向下滚动部分不再起作用。相反,该算法需要使用对象在屏幕上的可见高度才能正常工作(请参阅Get the visible height of a div with jQuery)。

我最终编写了自己的解决方案,该解决方案看起来更加强大并且适用于更多情况:

function scrollIntoViewIfNeeded($target, options) 

    var options = options ? options : ,
    $win = $($target[0].ownerDocument.defaultView), //get the window object of the $target, don't use "window" because the element could possibly be in a different iframe than the one calling the function
    $container = options.$container ? options.$container : $win,        
    padding = options.padding ? options.padding : 20,
    elemTop = $target.offset().top,
    elemHeight = $target.outerHeight(),
    containerTop = $container.scrollTop(),
    //Everything past this point is used only to get the container's visible height, which is needed to do this accurately
    containerHeight = $container.outerHeight(),
    winTop = $win.scrollTop(),
    winBot = winTop + $win.height(),
    containerVisibleTop = containerTop < winTop ? winTop : containerTop,
    containerVisibleBottom = containerTop + containerHeight > winBot ? winBot : containerTop + containerHeight,
    containerVisibleHeight = containerVisibleBottom - containerVisibleTop;

    if (elemTop < containerTop) 
        //scroll up
        if (options.instant) 
            $container.scrollTop(elemTop - padding);
         else 
            $container.animate(scrollTop: elemTop - padding, options.animationOptions);
        
     else if (elemTop + elemHeight > containerTop + containerVisibleHeight) 
        //scroll down
        if (options.instant) 
            $container.scrollTop(elemTop + elemHeight - containerVisibleHeight + padding);
         else 
            $container.animate(scrollTop: elemTop + elemHeight - containerVisibleHeight + padding, options.animationOptions);
        
    

$target 是一个 jQuery 对象,其中包含您希望在需要时滚动到视图中的对象。

options(可选)可以在对象中包含以下选项:

options.$container - 一个 jQuery 对象,指向 $target 的包含元素(换句话说,DOM 中带有滚动条的元素)。默认为包含 $target 元素的窗口,并且足够智能以选择 iframe 窗口。请记住在属性名称中包含 $。

options.padding - 当对象滚动到视图中时添加到对象上方或下方的填充(以像素为单位)。这样它就不会靠在窗户的边缘。默认为 20。

options.instant - 如果设置为 true,则不会使用 jQuery animate,并且滚动条会立即弹出到正确的位置。默认为 false。

options.animationOptions - 您希望传递给 jQuery animate 函数的任何 jQuery 选项(请参阅http://api.jquery.com/animate/)。这样,您可以更改动画的持续时间或在滚动完成时执行回调函数。这仅在 options.instant 设置为 false 时有效。如果您需要即时动画但需要回调,请设置options.animationOptions.duration = 0 而不是使用options.instant = true

【讨论】:

【参考方案9】:

Element.scrollIntoViewIfNeeded()

根据MDN Web Docs:

Element.scrollIntoViewIfNeeded() 方法将当前元素滚动到浏览器窗口的可见区域(如果它尚未在浏览器窗口的可见区域内)。如果元素已经在浏览器窗口的可见区域内,则不会发生滚动。此方法是标准Element.scrollIntoView() 方法的专有变体。

示例(使用 jQuery):

$('#example')[0].scrollIntoViewIfNeeded();

注意:此功能是非标准的,不在标准轨道上,因此在生产中使用此功能之前要小心。 浏览器支持见Can I use...。

【讨论】:

天哪。这甚至不会出现在 G 搜索中。【参考方案10】:

我使用来自 kofifus (https://***.com/a/43010437/1075062) 的答案,但在许多情况下我不知道容器是什么,所以我使用来自 (Find first scrollable parent) 的答案来找出答案。我使用 jQuery UI,所以我可以使用 .scrollParent() 方法(如果需要,可以在链接的问题中找到它的一个端口)。我还使用专有的 scrollIntoViewIfNeeded(如果存在),它存在于许多现代浏览器中,因此目前只有 FireFox 和 Opera Mini(以及旧浏览器)需要自定义代码 (https://caniuse.com/#feat=scrollintoviewifneeded)。

(代码是 TypeScript)

/**
 * Scroll the element into view if not already visible.
 *
 * https://caniuse.com/#feat=scrollintoviewifneeded
 * https://***.com/questions/5685589/scroll-to-element-only-if-not-in-view-jquery
 * https://***.com/questions/35939886/find-first-scrollable-parent
 */
public static ScrollIntoViewIfNeeded(element: Element): void

    if (element)
    
        // Proprietary method, available in many modern browsers
        if ((<any>element).scrollIntoViewIfNeeded)
        
            (<any>element).scrollIntoViewIfNeeded();
        
        else
        
            let $element = $(element);

            // jQuery UI scrollParent method available?
            if ($element.scrollParent)
            
                let $parent = $(element).scrollParent();
                let rectElem = element.getBoundingClientRect();
                let rectContainer = $parent[0].getBoundingClientRect();

                if (rectElem.bottom > rectContainer.bottom || rectElem.top < rectContainer.top)
                
                    element.scrollIntoView();
                
            
            else if (element.scrollIntoView)
            
                element.scrollIntoView();
            
        
    

【讨论】:

以上是关于仅当不在视图中时才滚动到元素 - jQuery的主要内容,如果未能解决你的问题,请参考以下文章

Apache mod_rewrite:仅当不在本地主机中时才强制 www

MySQL:仅当不在另一张表中时才从一张表中选择电子邮件?

google不在视图范围内需要滚动

元素在视口中时的jquery触发功能

ScrollViewer - 指示子元素滚动到视图中

JQuery:当元素在视图中时触发动作