【问题标题】:Event to detect when position:sticky is triggered检测 position:sticky 何时触发的事件
【发布时间】:2013-04-24 12:37:51
【问题描述】:

我正在使用新的position: sticky (info) 创建类似 iOS 的内容列表。

它运行良好,并且比以前的 JavaScript 替代方案 (example) 好得多,但是据我所知,当它被触发时没有触发任何事件,这意味着当栏到达页面顶部时我什么也做不了,与之前的解决方案不同。

当带有position: sticky 的元素到达页面顶部时,我想添加一个类(例如stuck)。有没有办法用 JavaScript 来监听这个? jQuery的使用很好。

【问题讨论】:

  • 这很有趣,因为对那篇文章的评价最高的评论正好解决了你的问题。那家伙说得对,这应该是媒体查询,而不是属性。这样你就可以在元素卡住时改变样式(我们经常这样做)。哦,好吧,一个人可以做梦。
  • 是的,我注意到了那个评论,他的提议似乎要好得多。不过,position: sticky 是 Chrome 实现的,所以我正在寻找一种使其可用的方法!
  • 您找到解决方案了吗?
  • 我笨吗?!第一个评论者在说什么文章?!
  • @katerlouis 不!我认为这是链接失效或评论删除的情况。

标签: javascript jquery html css position


【解决方案1】:

DemoIntersectionObserver(使用技巧):

// get the sticky element
const stickyElm = document.querySelector('header')

const observer = new IntersectionObserver( 
  ([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
  {threshold: [1]}
);

observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; }

section{
  background: lightblue;
  padding: 2em 1em;
}

header{
  position: sticky;
  top: -1px;                       /* ➜ the trick */

  padding: 1em;
  padding-top: calc(1em + 1px);    /* ➜ compensate for the trick */

  background: salmon;
  transition: .1s;
}

/* styles for when the header is in sticky mode */
header.isSticky{
  font-size: .8em;
  opacity: .5;
}
<section>Space</section>
<header>Sticky Header</header>

top 的值必须是-1px,否则元素将永远不会与浏览器窗口的顶部相交(因此永远不会触发相交观察者)。

为了应对这种1px 的隐藏内容,应在粘性元素的边框或填充中添加额外的1px 空间。

? 或者,如果您希望保持 CSS 原样 (top:0),那么您可以通过添加设置 rootMargin: '-1px 0px 0px 0px' (正如 @mattrick 在他的回答中显示的那样)

带有老式 scroll 事件监听器的演示:

  1. auto-detecting first scrollable parent
  2. Throttling the scroll event
  3. 关注点分离的功能组合
  4. 事件回调缓存:scrollCallback(如果需要可以解绑)

// get the sticky element
const stickyElm = document.querySelector('header');

// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);

// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;


// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)

// Act if sticky or not
const onSticky = isSticky => {
   console.clear()
   console.log(isSticky)
   
   stickyElm.classList.toggle('isSticky', isSticky)
}

// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)

const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)



// OPTIONAL CODE BELOW ///////////////////

// find-first-scrollable-parent
// Credit: https://stackoverflow.com/a/42543908/104380
function getScrollParent(element, includeHidden) {
    var style = getComputedStyle(element),
        excludeStaticParent = style.position === "absolute",
        overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;

    if (style.position !== "fixed") 
      for (var parent = element; (parent = parent.parentElement); ){
          style = getComputedStyle(parent);
          if (excludeStaticParent && style.position === "static") 
              continue;
          if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) 
            return parent;
      }

    return window
}

// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
    var wait = false;                  // Initially, we're not waiting
    return function () {               // We return a throttled function
        if (!wait) {                   // If we're not waiting
            callback.call();           // Execute users function
            wait = true;               // Prevent future invocations
            setTimeout(function () {   // After a period of time
                wait = false;          // And allow future invocations
            }, limit);
        }
    }
}
header{
  position: sticky;
  top: 0;

  /* not important styles */
  background: salmon;
  padding: 1em;
  transition: .1s;
}

header.isSticky{
  /* styles for when the header is in sticky mode */
  font-size: .8em;
  opacity: .5;
}

/* not important styles*/

body{ height: 200vh; font:20px Arial; }

section{
  background: lightblue;
  padding: 2em 1em;
}
<section>Space</section>
<header>Sticky Header</header>

这是使用第一种技术的React component demo

【讨论】:

  • 为什么需要“技巧”?如果你使用top: 0padding-top: 1em 会发生什么?
  • 好吧,如果你设置top: 0那么它永远不会与浏览器窗口的顶部相交。设置top: -1px 将允许1px 的交集(尽管1px 的内容将不可见,因此需要补偿这个技巧)。聪明?
  • 你可以通过使用border-top: 1px solid transparent来避免计算函数。
  • 我做了一些修改。我澄清了“技巧”是什么以及为什么需要它。我还向人们澄清了 CSS 的哪些部分是必需的,哪些是多余的(为了清楚起见,我正在考虑删除不必要的东西)。最后,将“演示”一词作为链接使人们更容易以更直观的方式访问,而不是一直滚动到答案的末尾以访问现场演示。所有这些都使答案对人们更有用。
  • 这样做的问题是,如果元素在页面下方看不见,也会应用 isSticky。如果您依赖类在滚动时显示/隐藏内容,这可能会导致问题和内容闪烁。
【解决方案2】:

我找到了一个类似于@vsync 的答案的解决方案,但它不需要您需要添加到样式表中的“hack”。您可以简单地更改 IntersectionObserver 的边界,以避免需要将元素本身移动到视口之外:

const observer = new IntersectionObserver(callback, {
  rootMargin: '-1px 0px 0px 0px',
  threshold: [1],
});

observer.observe(element);

【讨论】:

  • 你能举个例子吗?这似乎对我不起作用。
  • 最优雅的答案恕我直言
  • 当我的sticky在右边时这有效,但当sticky元素在左边时它不起作用(水平滚动,显然调整了rootMargins)。当它在左边时,它总是相交,所以我永远无法确定它何时粘住。根边距没有区别。
  • 混合垂直同步响应;工作完美:)
  • 嗨,它可以工作,但是因为这需要更多的代码,并且由于无论如何都必须编写 top CSS 属性,所以只需将 top:0 更改为 top:-1 和收工 ;) 除非这个小的 CSS 更改会导致您的设计出现一些视觉错误
【解决方案3】:

如果有人通过 Google 来到这里,他们自己的工程师有一个使用 IntersectionObserver、自定义事件和哨兵的解决方案:

https://developers.google.com/web/updates/2017/09/sticky-headers

【讨论】:

  • 这是一个解释不清的答案,但是,如果用户可以使用仅限 chrome 的解决方案,该链接显示了这个问题的一个很好的答案。
  • 使用 JavaScript。我们需要纯 CSS 方法。
  • @Green 纯 CSS 解决方案会很棒,但是(还没有)。加上 OP 要求提供 JavaScript 解决方案,所以这应该是公认的答案。
  • 纯 CSS 解决方案会违反 CSS 工作方式的一些基本规则。想象一个规则会导致一个元素变得粘滞,但随后一些新的:stuck 选择器会更改 CSS,因此它不会被粘住。你会有一个无限循环。 CSS 是经过精心设计的,以避免出现这种情况。
  • CSS 不是经过精心设计的,从其各种(例如模块)规范随附的脚注可以明显看出,这些规范追溯纠正了自 CSS 构思以来所犯的一整串错误。这与 CSS 是否应该能够解决“无限”循环无关。已经有很多情况下你会得到“跳跃”的布局——例如:hover 规则重置元素的大小,导致 :hover 不再适用,这再次重置大小,再次应用 :hover,依此类推。 :stuck 肯定不会让事情变得更糟。
【解决方案4】:

只需使用 vanilla JS 即可。您也可以使用 lodash 的节流功能来防止一些性能问题。

const element = document.getElementById("element-id");

document.addEventListener(
  "scroll",
  _.throttle(e => {
    element.classList.toggle(
      "is-sticky",
      element.offsetTop <= window.scrollY
    );
  }, 500)
);

【讨论】:

  • 使用 _ 并不完全是普通的 JS。
  • @SơnTrần-Nguyễn 正如我所指定的,这是一个防止可能的性能问题的 lodash 函数
  • 应该是window.addEventListener
【解决方案5】:

Chrome添加position: sticky后发现是not ready enoughrelegated to to --enable-experimental-webkit-features flag。 Paul Irish said in February“功能处于奇怪的边缘状态 atm”。

我一直在使用the polyfill,直到它变得非常头疼。它运行良好,但存在一些极端情况,例如 CORS 问题,并且通过对所有 CSS 链接执行 XHR 请求并将它们重新解析为浏览器忽略的“位置:粘性”声明,会减慢页面加载速度。

现在我使用ScrollToFixed,我比StickyJS 更喜欢它,因为它不会用包装器弄乱我的布局。

【讨论】:

    【解决方案6】:

    目前没有原生解决方案。见Targeting position:sticky elements that are currently in a 'stuck' state。不过,我有一个 CoffeeScript 解决方案,它既适用于原生 position: sticky,也适用于实现粘性行为的 polyfill。

    为您想要粘性的元素添加“粘性”类:

    .sticky {
      position: -webkit-sticky;
      position: -moz-sticky;
      position: -ms-sticky;
      position: -o-sticky;
      position: sticky;
      top: 0px;
      z-index: 1;
    }
    

    CoffeeScript 监控“粘性”元素位置并在它们处于“粘性”状态时添加“卡住”类:

    $ -> new StickyMonitor
    
    class StickyMonitor
    
      SCROLL_ACTION_DELAY: 50
    
      constructor: ->
        $(window).scroll @scroll_handler if $('.sticky').length > 0
    
      scroll_handler: =>
        @scroll_timer ||= setTimeout(@scroll_handler_throttled, @SCROLL_ACTION_DELAY)
    
      scroll_handler_throttled: =>
        @scroll_timer = null
        @toggle_stuck_state_for_sticky_elements()
    
      toggle_stuck_state_for_sticky_elements: =>
        $('.sticky').each ->
          $(this).toggleClass('stuck', this.getBoundingClientRect().top - parseInt($(this).css('top')) <= 1)
    

    注意:此代码仅适用于垂直粘性位置。

    【讨论】:

    • 这个解决方案的优点是(在我看来)可以处理窗口大小调整和页面重排,这与检查和保存offsetTop 不同。
    【解决方案7】:

    我想出了这个解决方案,它就像一个魅力,而且非常小。 :)

    不需要额外的元素。

    它确实在窗口滚动事件上运行,但这是一个小缺点。

    apply_stickies()
    
    window.addEventListener('scroll', function() {
        apply_stickies()
    })
    
    function apply_stickies() {
        var _$stickies = [].slice.call(document.querySelectorAll('.sticky'))
        _$stickies.forEach(function(_$sticky) {
            if (CSS.supports && CSS.supports('position', 'sticky')) {
                apply_sticky_class(_$sticky)
            }
        })
    }
    
    function apply_sticky_class(_$sticky) {
        var currentOffset = _$sticky.getBoundingClientRect().top
        var stickyOffset = parseInt(getComputedStyle(_$sticky).top.replace('px', ''))
        var isStuck = currentOffset <= stickyOffset
    
        _$sticky.classList.toggle('js-is-sticky', isStuck)
    }
    

    注意:此解决方案不考虑具有底部粘性的元素。这仅适用于粘性标题之类的东西。不过,它可能可以适应底部粘性。

    【讨论】:

    • 很好的答案。谷歌提出的答案很好,但对于大多数目的来说它是矫枉过正的。此外,它还支持 IE11。
    • 我真的不会称之为 IE11 支持。如果在 IE11 中查看,它更像不会导致网站崩溃。 IE11 不支持position: sticky
    • ▶ 滚动事件回调可能应该被限制,因为那里正在进行密集计算。此外,开发人员必须主动清理事件绑定并重新绑定 DOM 更改,因为粘性元素可能会在此函数运行后 * 附加到 DOM。另外.. 这段代码距离最优远*,并且可以针对性能和可读性进行大量重构
    • 如果你想要一个平滑的过渡,它不能被限制。根据我的经验,它似乎并没有表现得那么糟糕。 DOM 元素可能被附加。绝大多数时候情况并非如此。如果您知道做同样事情的更好方法,请随时提供您自己的答案。
    • 好点@Jordan,我已经根据你的建议更新了我的答案。
    【解决方案8】:

    我知道这个问题已经有一段时间了,但我找到了一个很好的解决方案。插件stickybits 在支持的情况下使用position: sticky,并在元素“卡住”时将类应用于元素。我最近使用它并取得了不错的效果,并且在撰写本文时,它正在积极开发(这对我来说是一个加分):)

    【讨论】:

    • stickybits 的一个问题是我发现当你有其他 js 监听滚动事件时它不能很好地工作。当我在不支持position: sticky(ie11,edge)的浏览器中进行测试时,这些问题才变得明显。
    • 这个库像 **** 一样有问题
    【解决方案9】:

    我在我的主题中使用这个 sn-p 将 .is-stuck 类添加到 .site-header 当它处于卡住位置时:

    // noinspection JSUnusedLocalSymbols
    (function (document, window, undefined) {
    
        let windowScroll;
    
        /**
         *
         * @param element {HTMLElement|Window|Document}
         * @param event {string}
         * @param listener {function}
         * @returns {HTMLElement|Window|Document}
         */
        function addListener(element, event, listener) {
            if (element.addEventListener) {
                element.addEventListener(event, listener);
            } else {
                // noinspection JSUnresolvedVariable
                if (element.attachEvent) {
                    element.attachEvent('on' + event, listener);
                } else {
                    console.log('Failed to attach event.');
                }
            }
            return element;
        }
    
        /**
         * Checks if the element is in a sticky position.
         *
         * @param element {HTMLElement}
         * @returns {boolean}
         */
        function isSticky(element) {
            if ('sticky' !== getComputedStyle(element).position) {
                return false;
            }
            return (1 >= (element.getBoundingClientRect().top - parseInt(getComputedStyle(element).top)));
        }
    
        /**
         * Toggles is-stuck class if the element is in sticky position.
         *
         * @param element {HTMLElement}
         * @returns {HTMLElement}
         */
        function toggleSticky(element) {
            if (isSticky(element)) {
                element.classList.add('is-stuck');
            } else {
                element.classList.remove('is-stuck');
            }
            return element;
        }
    
        /**
         * Toggles stuck state for sticky header.
         */
        function toggleStickyHeader() {
            toggleSticky(document.querySelector('.site-header'));
        }
    
        /**
         * Listen to window scroll.
         */
        addListener(window, 'scroll', function () {
            clearTimeout(windowScroll);
            windowScroll = setTimeout(toggleStickyHeader, 50);
        });
    
        /**
         * Check if the header is not stuck already.
         */
        toggleStickyHeader();
    
    
    })(document, window);
    
    

    【讨论】:

    • 正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center
    【解决方案10】:

    @vsync 的出色答案几乎是我所需要的,只是我通过 Grunt “丑化”了我的代码,而 Grunt 需要一些较旧的 JavaScript 代码样式。这是我改用的调整后的脚本:

    var stickyElm = document.getElementById('header');
    var observer = new IntersectionObserver(function (_ref) {
        var e = _ref[0];
        return e.target.classList.toggle('isSticky', e.intersectionRatio < 1);
    }, {
        threshold: [1]
    });
    observer.observe( stickyElm );
    
    

    那个答案的 CSS 没有改变

    【讨论】:

      【解决方案11】:

      这样的东西也适用于固定的滚动高度:

      // select the header
      const header = document.querySelector('header');
      // add an event listener for scrolling
      window.addEventListener('scroll', () => {
        // add the 'stuck' class
        if (window.scrollY >= 80) navbar.classList.add('stuck');
        // remove the 'stuck' class
        else navbar.classList.remove('stuck');
      });
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-06-27
        • 2023-04-09
        相关资源
        最近更新 更多