80

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

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

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

4

11 回答 11

97

使用IntersectionObserver进行演示(使用技巧):

// 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级别应用“更正” (如@mattrick在他的回答中所示)rootMargin: '-1px 0px 0px 0px'

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

  1. 自动检测第一个可滚动的父级
  2. 限制滚动事件
  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 组件演示

于 2019-09-18T11:17:24.293 回答
35

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

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

observer.observe(element);
于 2020-04-09T06:19:52.673 回答
24

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

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

于 2017-10-02T18:00:10.193 回答
3

只需使用香草 JS 即可。您也可以使用 lodash 的油门功能来防止一些性能问题。

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

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

于 2020-01-29T20:24:55.813 回答
2

Chrome 添加后position: sticky,发现准备不够充分降级为 --enable-experimental-webkit-features 标志。Paul Irish在 2 月份表示“功能处于一种奇怪的边缘状态 atm”。

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

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

于 2013-05-01T14:59:22.020 回答
2

目前没有原生解决方案。请参阅定位位置:当前处于“卡住”状态的粘性元素。但是,我有一个 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)

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

于 2014-10-03T19:22:01.360 回答
2

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

不需要额外的元素。

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

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

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

于 2018-10-29T06:03:40.660 回答
1

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

于 2017-07-27T09:14:30.570 回答
0

我在我的主题中使用这个片段来添加.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);

于 2021-09-13T15:16:56.873 回答
-1

@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未更改

于 2020-07-02T15:21:10.707 回答
-2

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

// 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');
});
于 2021-05-23T08:10:58.593 回答