All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s
579 lines
18 KiB
JavaScript
579 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
var _inheritsLoose = require('@babel/runtime/helpers/inheritsLoose');
|
|
var consolidatedEvents = require('consolidated-events');
|
|
var PropTypes = require('prop-types');
|
|
var React = require('react');
|
|
var reactIs = require('react-is');
|
|
|
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
|
|
var _inheritsLoose__default = /*#__PURE__*/_interopDefaultLegacy(_inheritsLoose);
|
|
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
|
|
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
|
|
|
|
/**
|
|
* Attempts to parse the offset provided as a prop as a percentage. For
|
|
* instance, if the component has been provided with the string "20%" as
|
|
* a value of one of the offset props. If the value matches, then it returns
|
|
* a numeric version of the prop. For instance, "20%" would become `0.2`.
|
|
* If `str` isn't a percentage, then `undefined` will be returned.
|
|
*
|
|
* @param {string} str The value of an offset prop to be converted to a
|
|
* number.
|
|
* @return {number|undefined} The numeric version of `str`. Undefined if `str`
|
|
* was not a percentage.
|
|
*/
|
|
function parseOffsetAsPercentage(str) {
|
|
if (str.slice(-1) === '%') {
|
|
return parseFloat(str.slice(0, -1)) / 100;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Attempts to parse the offset provided as a prop as a pixel value. If
|
|
* parsing fails, then `undefined` is returned. Three examples of values that
|
|
* will be successfully parsed are:
|
|
* `20`
|
|
* "20px"
|
|
* "20"
|
|
*
|
|
* @param {string|number} str A string of the form "{number}" or "{number}px",
|
|
* or just a number.
|
|
* @return {number|undefined} The numeric version of `str`. Undefined if `str`
|
|
* was neither a number nor string ending in "px".
|
|
*/
|
|
function parseOffsetAsPixels(str) {
|
|
if (!isNaN(parseFloat(str)) && isFinite(str)) {
|
|
return parseFloat(str);
|
|
}
|
|
|
|
if (str.slice(-2) === 'px') {
|
|
return parseFloat(str.slice(0, -2));
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @param {string|number} offset
|
|
* @param {number} contextHeight
|
|
* @return {number} A number representing `offset` converted into pixels.
|
|
*/
|
|
|
|
function computeOffsetPixels(offset, contextHeight) {
|
|
var pixelOffset = parseOffsetAsPixels(offset);
|
|
|
|
if (typeof pixelOffset === 'number') {
|
|
return pixelOffset;
|
|
}
|
|
|
|
var percentOffset = parseOffsetAsPercentage(offset);
|
|
|
|
if (typeof percentOffset === 'number') {
|
|
return percentOffset * contextHeight;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
var ABOVE = 'above';
|
|
var INSIDE = 'inside';
|
|
var BELOW = 'below';
|
|
var INVISIBLE = 'invisible';
|
|
|
|
function debugLog() {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
var _console;
|
|
|
|
(_console = console).log.apply(_console, arguments); // eslint-disable-line no-console
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When an element's type is a string, it represents a DOM node with that tag name
|
|
* https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html#dom-elements
|
|
*
|
|
* @param {React.element} Component
|
|
* @return {bool} Whether the component is a DOM Element
|
|
*/
|
|
function isDOMElement(Component) {
|
|
return typeof Component.type === 'string';
|
|
}
|
|
|
|
var errorMessage = '<Waypoint> needs a DOM element to compute boundaries. The child you passed is neither a ' + 'DOM element (e.g. <div>) nor does it use the innerRef prop.\n\n' + 'See https://goo.gl/LrBNgw for more info.';
|
|
/**
|
|
* Raise an error if "children" is not a DOM Element and there is no ref provided to Waypoint
|
|
*
|
|
* @param {?React.element} children
|
|
* @param {?HTMLElement} ref
|
|
* @return {undefined}
|
|
*/
|
|
|
|
function ensureRefIsProvidedByChild(children, ref) {
|
|
if (children && !isDOMElement(children) && !ref) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {object} bounds An object with bounds data for the waypoint and
|
|
* scrollable parent
|
|
* @return {string} The current position of the waypoint in relation to the
|
|
* visible portion of the scrollable parent. One of the constants `ABOVE`,
|
|
* `BELOW`, `INSIDE` or `INVISIBLE`.
|
|
*/
|
|
|
|
function getCurrentPosition(bounds) {
|
|
if (bounds.viewportBottom - bounds.viewportTop === 0) {
|
|
return INVISIBLE;
|
|
} // top is within the viewport
|
|
|
|
|
|
if (bounds.viewportTop <= bounds.waypointTop && bounds.waypointTop <= bounds.viewportBottom) {
|
|
return INSIDE;
|
|
} // bottom is within the viewport
|
|
|
|
|
|
if (bounds.viewportTop <= bounds.waypointBottom && bounds.waypointBottom <= bounds.viewportBottom) {
|
|
return INSIDE;
|
|
} // top is above the viewport and bottom is below the viewport
|
|
|
|
|
|
if (bounds.waypointTop <= bounds.viewportTop && bounds.viewportBottom <= bounds.waypointBottom) {
|
|
return INSIDE;
|
|
}
|
|
|
|
if (bounds.viewportBottom < bounds.waypointTop) {
|
|
return BELOW;
|
|
}
|
|
|
|
if (bounds.waypointTop < bounds.viewportTop) {
|
|
return ABOVE;
|
|
}
|
|
|
|
return INVISIBLE;
|
|
}
|
|
|
|
var timeout;
|
|
var timeoutQueue = [];
|
|
function onNextTick(cb) {
|
|
timeoutQueue.push(cb);
|
|
|
|
if (!timeout) {
|
|
timeout = setTimeout(function () {
|
|
timeout = null; // Drain the timeoutQueue
|
|
|
|
var item; // eslint-disable-next-line no-cond-assign
|
|
|
|
while (item = timeoutQueue.shift()) {
|
|
item();
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
var isSubscribed = true;
|
|
return function unsubscribe() {
|
|
if (!isSubscribed) {
|
|
return;
|
|
}
|
|
|
|
isSubscribed = false;
|
|
var index = timeoutQueue.indexOf(cb);
|
|
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
|
|
timeoutQueue.splice(index, 1);
|
|
|
|
if (!timeoutQueue.length && timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
function resolveScrollableAncestorProp(scrollableAncestor) {
|
|
// When Waypoint is rendered on the server, `window` is not available.
|
|
// To make Waypoint easier to work with, we allow this to be specified in
|
|
// string form and safely convert to `window` here.
|
|
if (scrollableAncestor === 'window') {
|
|
return global.window;
|
|
}
|
|
|
|
return scrollableAncestor;
|
|
}
|
|
|
|
var hasWindow = typeof window !== 'undefined';
|
|
var defaultProps = {
|
|
debug: false,
|
|
scrollableAncestor: undefined,
|
|
children: undefined,
|
|
topOffset: '0px',
|
|
bottomOffset: '0px',
|
|
horizontal: false,
|
|
onEnter: function onEnter() {},
|
|
onLeave: function onLeave() {},
|
|
onPositionChange: function onPositionChange() {},
|
|
fireOnRapidScroll: true
|
|
}; // Calls a function when you scroll to the element.
|
|
|
|
var Waypoint = /*#__PURE__*/function (_React$PureComponent) {
|
|
_inheritsLoose__default['default'](Waypoint, _React$PureComponent);
|
|
|
|
function Waypoint(props) {
|
|
var _this;
|
|
|
|
_this = _React$PureComponent.call(this, props) || this;
|
|
|
|
_this.refElement = function (e) {
|
|
_this._ref = e;
|
|
};
|
|
|
|
return _this;
|
|
}
|
|
|
|
var _proto = Waypoint.prototype;
|
|
|
|
_proto.componentDidMount = function componentDidMount() {
|
|
var _this2 = this;
|
|
|
|
if (!hasWindow) {
|
|
return;
|
|
} // this._ref may occasionally not be set at this time. To help ensure that
|
|
// this works smoothly and to avoid layout thrashing, we want to delay the
|
|
// initial execution until the next tick.
|
|
|
|
|
|
this.cancelOnNextTick = onNextTick(function () {
|
|
_this2.cancelOnNextTick = null;
|
|
var _this2$props = _this2.props,
|
|
children = _this2$props.children,
|
|
debug = _this2$props.debug; // Berofe doing anything, we want to check that this._ref is avaliable in Waypoint
|
|
|
|
ensureRefIsProvidedByChild(children, _this2._ref);
|
|
_this2._handleScroll = _this2._handleScroll.bind(_this2);
|
|
_this2.scrollableAncestor = _this2._findScrollableAncestor();
|
|
|
|
if (process.env.NODE_ENV !== 'production' && debug) {
|
|
debugLog('scrollableAncestor', _this2.scrollableAncestor);
|
|
}
|
|
|
|
_this2.scrollEventListenerUnsubscribe = consolidatedEvents.addEventListener(_this2.scrollableAncestor, 'scroll', _this2._handleScroll, {
|
|
passive: true
|
|
});
|
|
_this2.resizeEventListenerUnsubscribe = consolidatedEvents.addEventListener(window, 'resize', _this2._handleScroll, {
|
|
passive: true
|
|
});
|
|
|
|
_this2._handleScroll(null);
|
|
});
|
|
};
|
|
|
|
_proto.componentDidUpdate = function componentDidUpdate() {
|
|
var _this3 = this;
|
|
|
|
if (!hasWindow) {
|
|
return;
|
|
}
|
|
|
|
if (!this.scrollableAncestor) {
|
|
// The Waypoint has not yet initialized.
|
|
return;
|
|
} // The element may have moved, so we need to recompute its position on the
|
|
// page. This happens via handleScroll in a way that forces layout to be
|
|
// computed.
|
|
//
|
|
// We want this to be deferred to avoid forcing layout during render, which
|
|
// causes layout thrashing. And, if we already have this work enqueued, we
|
|
// can just wait for that to happen instead of enqueueing again.
|
|
|
|
|
|
if (this.cancelOnNextTick) {
|
|
return;
|
|
}
|
|
|
|
this.cancelOnNextTick = onNextTick(function () {
|
|
_this3.cancelOnNextTick = null;
|
|
|
|
_this3._handleScroll(null);
|
|
});
|
|
};
|
|
|
|
_proto.componentWillUnmount = function componentWillUnmount() {
|
|
if (!hasWindow) {
|
|
return;
|
|
}
|
|
|
|
if (this.scrollEventListenerUnsubscribe) {
|
|
this.scrollEventListenerUnsubscribe();
|
|
}
|
|
|
|
if (this.resizeEventListenerUnsubscribe) {
|
|
this.resizeEventListenerUnsubscribe();
|
|
}
|
|
|
|
if (this.cancelOnNextTick) {
|
|
this.cancelOnNextTick();
|
|
}
|
|
}
|
|
/**
|
|
* Traverses up the DOM to find an ancestor container which has an overflow
|
|
* style that allows for scrolling.
|
|
*
|
|
* @return {Object} the closest ancestor element with an overflow style that
|
|
* allows for scrolling. If none is found, the `window` object is returned
|
|
* as a fallback.
|
|
*/
|
|
;
|
|
|
|
_proto._findScrollableAncestor = function _findScrollableAncestor() {
|
|
var _this$props = this.props,
|
|
horizontal = _this$props.horizontal,
|
|
scrollableAncestor = _this$props.scrollableAncestor;
|
|
|
|
if (scrollableAncestor) {
|
|
return resolveScrollableAncestorProp(scrollableAncestor);
|
|
}
|
|
|
|
var node = this._ref;
|
|
|
|
while (node.parentNode) {
|
|
node = node.parentNode;
|
|
|
|
if (node === document.body) {
|
|
// We've reached all the way to the root node.
|
|
return window;
|
|
}
|
|
|
|
var style = window.getComputedStyle(node);
|
|
var overflowDirec = horizontal ? style.getPropertyValue('overflow-x') : style.getPropertyValue('overflow-y');
|
|
var overflow = overflowDirec || style.getPropertyValue('overflow');
|
|
|
|
if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {
|
|
return node;
|
|
}
|
|
} // A scrollable ancestor element was not found, which means that we need to
|
|
// do stuff on window.
|
|
|
|
|
|
return window;
|
|
}
|
|
/**
|
|
* @param {Object} event the native scroll event coming from the scrollable
|
|
* ancestor, or resize event coming from the window. Will be undefined if
|
|
* called by a React lifecyle method
|
|
*/
|
|
;
|
|
|
|
_proto._handleScroll = function _handleScroll(event) {
|
|
if (!this._ref) {
|
|
// There's a chance we end up here after the component has been unmounted.
|
|
return;
|
|
}
|
|
|
|
var bounds = this._getBounds();
|
|
|
|
var currentPosition = getCurrentPosition(bounds);
|
|
var previousPosition = this._previousPosition;
|
|
var _this$props2 = this.props,
|
|
debug = _this$props2.debug,
|
|
onPositionChange = _this$props2.onPositionChange,
|
|
onEnter = _this$props2.onEnter,
|
|
onLeave = _this$props2.onLeave,
|
|
fireOnRapidScroll = _this$props2.fireOnRapidScroll;
|
|
|
|
if (process.env.NODE_ENV !== 'production' && debug) {
|
|
debugLog('currentPosition', currentPosition);
|
|
debugLog('previousPosition', previousPosition);
|
|
} // Save previous position as early as possible to prevent cycles
|
|
|
|
|
|
this._previousPosition = currentPosition;
|
|
|
|
if (previousPosition === currentPosition) {
|
|
// No change since last trigger
|
|
return;
|
|
}
|
|
|
|
var callbackArg = {
|
|
currentPosition: currentPosition,
|
|
previousPosition: previousPosition,
|
|
event: event,
|
|
waypointTop: bounds.waypointTop,
|
|
waypointBottom: bounds.waypointBottom,
|
|
viewportTop: bounds.viewportTop,
|
|
viewportBottom: bounds.viewportBottom
|
|
};
|
|
onPositionChange.call(this, callbackArg);
|
|
|
|
if (currentPosition === INSIDE) {
|
|
onEnter.call(this, callbackArg);
|
|
} else if (previousPosition === INSIDE) {
|
|
onLeave.call(this, callbackArg);
|
|
}
|
|
|
|
var isRapidScrollDown = previousPosition === BELOW && currentPosition === ABOVE;
|
|
var isRapidScrollUp = previousPosition === ABOVE && currentPosition === BELOW;
|
|
|
|
if (fireOnRapidScroll && (isRapidScrollDown || isRapidScrollUp)) {
|
|
// If the scroll event isn't fired often enough to occur while the
|
|
// waypoint was visible, we trigger both callbacks anyway.
|
|
onEnter.call(this, {
|
|
currentPosition: INSIDE,
|
|
previousPosition: previousPosition,
|
|
event: event,
|
|
waypointTop: bounds.waypointTop,
|
|
waypointBottom: bounds.waypointBottom,
|
|
viewportTop: bounds.viewportTop,
|
|
viewportBottom: bounds.viewportBottom
|
|
});
|
|
onLeave.call(this, {
|
|
currentPosition: currentPosition,
|
|
previousPosition: INSIDE,
|
|
event: event,
|
|
waypointTop: bounds.waypointTop,
|
|
waypointBottom: bounds.waypointBottom,
|
|
viewportTop: bounds.viewportTop,
|
|
viewportBottom: bounds.viewportBottom
|
|
});
|
|
}
|
|
};
|
|
|
|
_proto._getBounds = function _getBounds() {
|
|
var _this$props3 = this.props,
|
|
horizontal = _this$props3.horizontal,
|
|
debug = _this$props3.debug;
|
|
|
|
var _this$_ref$getBoundin = this._ref.getBoundingClientRect(),
|
|
left = _this$_ref$getBoundin.left,
|
|
top = _this$_ref$getBoundin.top,
|
|
right = _this$_ref$getBoundin.right,
|
|
bottom = _this$_ref$getBoundin.bottom;
|
|
|
|
var waypointTop = horizontal ? left : top;
|
|
var waypointBottom = horizontal ? right : bottom;
|
|
var contextHeight;
|
|
var contextScrollTop;
|
|
|
|
if (this.scrollableAncestor === window) {
|
|
contextHeight = horizontal ? window.innerWidth : window.innerHeight;
|
|
contextScrollTop = 0;
|
|
} else {
|
|
contextHeight = horizontal ? this.scrollableAncestor.offsetWidth : this.scrollableAncestor.offsetHeight;
|
|
contextScrollTop = horizontal ? this.scrollableAncestor.getBoundingClientRect().left : this.scrollableAncestor.getBoundingClientRect().top;
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== 'production' && debug) {
|
|
debugLog('waypoint top', waypointTop);
|
|
debugLog('waypoint bottom', waypointBottom);
|
|
debugLog('scrollableAncestor height', contextHeight);
|
|
debugLog('scrollableAncestor scrollTop', contextScrollTop);
|
|
}
|
|
|
|
var _this$props4 = this.props,
|
|
bottomOffset = _this$props4.bottomOffset,
|
|
topOffset = _this$props4.topOffset;
|
|
var topOffsetPx = computeOffsetPixels(topOffset, contextHeight);
|
|
var bottomOffsetPx = computeOffsetPixels(bottomOffset, contextHeight);
|
|
var contextBottom = contextScrollTop + contextHeight;
|
|
return {
|
|
waypointTop: waypointTop,
|
|
waypointBottom: waypointBottom,
|
|
viewportTop: contextScrollTop + topOffsetPx,
|
|
viewportBottom: contextBottom - bottomOffsetPx
|
|
};
|
|
}
|
|
/**
|
|
* @return {Object}
|
|
*/
|
|
;
|
|
|
|
_proto.render = function render() {
|
|
var _this4 = this;
|
|
|
|
var children = this.props.children;
|
|
|
|
if (!children) {
|
|
// We need an element that we can locate in the DOM to determine where it is
|
|
// rendered relative to the top of its context.
|
|
return /*#__PURE__*/React__default['default'].createElement("span", {
|
|
ref: this.refElement,
|
|
style: {
|
|
fontSize: 0
|
|
}
|
|
});
|
|
}
|
|
|
|
if (isDOMElement(children) || reactIs.isForwardRef(children)) {
|
|
var ref = function ref(node) {
|
|
_this4.refElement(node);
|
|
|
|
if (children.ref) {
|
|
if (typeof children.ref === 'function') {
|
|
children.ref(node);
|
|
} else {
|
|
children.ref.current = node;
|
|
}
|
|
}
|
|
};
|
|
|
|
return /*#__PURE__*/React__default['default'].cloneElement(children, {
|
|
ref: ref
|
|
});
|
|
}
|
|
|
|
return /*#__PURE__*/React__default['default'].cloneElement(children, {
|
|
innerRef: this.refElement
|
|
});
|
|
};
|
|
|
|
return Waypoint;
|
|
}(React__default['default'].PureComponent);
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
Waypoint.propTypes = {
|
|
children: PropTypes__default['default'].element,
|
|
debug: PropTypes__default['default'].bool,
|
|
onEnter: PropTypes__default['default'].func,
|
|
onLeave: PropTypes__default['default'].func,
|
|
onPositionChange: PropTypes__default['default'].func,
|
|
fireOnRapidScroll: PropTypes__default['default'].bool,
|
|
// eslint-disable-next-line react/forbid-prop-types
|
|
scrollableAncestor: PropTypes__default['default'].any,
|
|
horizontal: PropTypes__default['default'].bool,
|
|
// `topOffset` can either be a number, in which case its a distance from the
|
|
// top of the container in pixels, or a string value. Valid string values are
|
|
// of the form "20px", which is parsed as pixels, or "20%", which is parsed
|
|
// as a percentage of the height of the containing element.
|
|
// For instance, if you pass "-20%", and the containing element is 100px tall,
|
|
// then the waypoint will be triggered when it has been scrolled 20px beyond
|
|
// the top of the containing element.
|
|
topOffset: PropTypes__default['default'].oneOfType([PropTypes__default['default'].string, PropTypes__default['default'].number]),
|
|
// `bottomOffset` can either be a number, in which case its a distance from the
|
|
// bottom of the container in pixels, or a string value. Valid string values are
|
|
// of the form "20px", which is parsed as pixels, or "20%", which is parsed
|
|
// as a percentage of the height of the containing element.
|
|
// For instance, if you pass "20%", and the containing element is 100px tall,
|
|
// then the waypoint will be triggered when it has been scrolled 20px beyond
|
|
// the bottom of the containing element.
|
|
// Similar to `topOffset`, but for the bottom of the container.
|
|
bottomOffset: PropTypes__default['default'].oneOfType([PropTypes__default['default'].string, PropTypes__default['default'].number])
|
|
};
|
|
}
|
|
|
|
Waypoint.above = ABOVE;
|
|
Waypoint.below = BELOW;
|
|
Waypoint.inside = INSIDE;
|
|
Waypoint.invisible = INVISIBLE;
|
|
Waypoint.defaultProps = defaultProps;
|
|
Waypoint.displayName = 'Waypoint';
|
|
|
|
exports.Waypoint = Waypoint;
|