stickyard
Version:
Make your component sticky the easy way
209 lines (167 loc) • 6.21 kB
JavaScript
import React from 'react';
import PropTypes from '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
};
export default Stickyard;