Files
sgauthier 6e64e138e2
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s
planning
2024-10-14 09:15:30 +02:00

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;