UNPKG

stickyard

Version:

Make your component sticky the easy way

213 lines (169 loc) 6.38 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = _interopDefault(require('react')); var PropTypes = _interopDefault(require('prop-types')); function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } var styleTransform = function styleTransform(style, transform) { style.transform = transform; style.WebkitTransform = transform; }; var styleTranslateY = function styleTranslateY(style, offset) { styleTransform(style, "translateY(" + offset + "px) translateZ(0)"); }; /** * Stickyard, make your component sticky the easy way using render prop */ var Stickyard = /*#__PURE__*/ function (_React$PureComponent) { _inheritsLoose(Stickyard, _React$PureComponent); function Stickyard(props) { var _this; _this = _React$PureComponent.call(this, props) || this; _this.setContainerRef = _this.setContainerRef.bind(_assertThisInitialized(_this)); _this.setStickyRef = _this.setStickyRef.bind(_assertThisInitialized(_this)); _this.updateState = _this.updateState.bind(_assertThisInitialized(_this)); _this.getStickyOffset = _this.getStickyOffset.bind(_assertThisInitialized(_this)); _this.getStickyOffsets = _this.getStickyOffsets.bind(_assertThisInitialized(_this)); _this.scrollToIndex = _this.scrollToIndex.bind(_assertThisInitialized(_this)); _this.scrollTo = _this.scrollTo.bind(_assertThisInitialized(_this)); _this.container = null; _this.stickers = []; _this.lastStickyIndex = -1; _this.updating = false; return _this; } var _proto = Stickyard.prototype; _proto.componentDidMount = function componentDidMount() { this.purgeStickers(); if (this.container) { this.container.addEventListener('scroll', this.updateState); } }; _proto.componentDidUpdate = function componentDidUpdate() { this.purgeStickers(); }; _proto.componentWillUnmount = function componentWillUnmount() { if (this.container) { this.container.removeEventListener('scroll', this.updateState); } }; _proto.setContainerRef = function setContainerRef(ref) { this.container = ref; if (ref) { // the postion should be either `relative` or `absolute` if (ref.style.position !== 'absolute') { ref.style.position = 'relative'; } ref.style.overflowY = 'auto'; ref.style.willChange = 'transform'; ref.style.WebkitOverflowScrolling = 'touch'; } }; _proto.setStickyRef = function setStickyRef(ref) { if (ref) this.stickers.push(ref); }; _proto.getStickyOffset = function getStickyOffset(sticker) { var offsetTop = sticker.offsetTop, offsetParent = sticker.offsetParent; while (this.container && offsetParent !== this.container) { offsetTop += offsetParent.offsetTop; // eslint-disable-next-line prefer-destructuring offsetParent = offsetParent.offsetParent; } return offsetTop; }; _proto.getStickyOffsets = function getStickyOffsets() { return this.stickers.map(this.getStickyOffset); }; _proto.scrollTo = function scrollTo(offset) { if (this.container) { this.container.scrollTop = offset; } }; _proto.scrollToIndex = function scrollToIndex(index) { if (index >= 0 && index < this.getStickyOffsets().length) { this.scrollTo(this.getStickyOffsets()[index]); } }; _proto.updateState = function updateState() { if (this.updating || !this.container || this.stickers.length === 0) return; this.updating = true; var _this$container = this.container, scrollTop = _this$container.scrollTop, scrollHeight = _this$container.scrollHeight; var offsets = this.getStickyOffsets().concat(scrollHeight); var stickyIndex = 0; while (scrollTop >= offsets[stickyIndex]) { stickyIndex += 1; } stickyIndex -= 1; var sticker = stickyIndex >= 0 ? this.stickers[stickyIndex] : null; if (sticker) { if (scrollTop < offsets[stickyIndex + 1] - sticker.offsetHeight) { styleTranslateY(sticker.style, scrollTop - offsets[stickyIndex]); } else { styleTranslateY(sticker.style, offsets[stickyIndex + 1] - offsets[stickyIndex] - sticker.offsetHeight); } } var _this$props = this.props, stickyClassName = _this$props.stickyClassName, onSticky = _this$props.onSticky; if (stickyIndex !== this.lastStickyIndex) { var lastSticker = this.lastStickyIndex >= 0 ? this.stickers[this.lastStickyIndex] : null; if (lastSticker) styleTransform(lastSticker.style, ''); if (stickyClassName) { sticker && sticker.classList && sticker.classList.add(stickyClassName); lastSticker && lastSticker.classList && lastSticker.classList.remove(stickyClassName); } onSticky && onSticky(stickyIndex); this.lastStickyIndex = stickyIndex; } this.updating = false; }; _proto.purgeStickers = function purgeStickers() { var _this2 = this; this.stickers = this.stickers.filter(function (sticker) { return sticker && sticker.offsetHeight; }).sort(function (a, b) { return _this2.getStickyOffset(a) - _this2.getStickyOffset(b); }); this.updateState(); }; _proto.render = function render() { var children = this.props.children; return children({ registerContainer: this.setContainerRef, registerSticky: this.setStickyRef, updateState: this.updateState, getStickyOffsets: this.getStickyOffsets, scrollToIndex: this.scrollToIndex, scrollTo: this.scrollTo }); }; return Stickyard; }(React.PureComponent); Stickyard.propTypes = { /** * Render whatever you want, it's called with an object */ children: PropTypes.func.isRequired, /** * The className to be attached to the element when it's sticky. */ stickyClassName: PropTypes.string, /** * It's called when a element becomes sticky, `-1` means there is no sticky element. */ onSticky: PropTypes.func }; module.exports = Stickyard;