UNPKG

@shopify/react-async

Version:

Tools for creating powerful, asynchronously-loaded React components

231 lines (223 loc) 7.11 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var prefetch = require('./context/prefetch.js'); var EventListener = require('./EventListener.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); const INTENTION_DELAY_MS = 150; const SENSITIVITY = 15; class ConnectedPrefetcher extends React__default["default"].PureComponent { constructor(...args) { super(...args); this.state = {}; this.prefetchAgressively = shouldPrefetchAggressively(); // Initial position of the mouse this.iX = 0; this.iY = 0; // Final position of the mouse this.fX = 0; this.fY = 0; this.handleMouseMove = ({ clientX, clientY }) => { this.iX = clientX; this.iY = clientY; }; this.handlePressStart = ({ target }) => { this.clearTimeout(); if (target == null) { return; } const url = closestUrlFromNode(target); if (url != null) { this.setState({ url }); } }; this.compare = url => { const { iX, iY } = this; this.clearTimeout(); // Calculate the change of the mouse position // If it is smaller than the sensitivity, we can assume that the user is intending on visiting the link if (Math.hypot(this.fX - iX, this.fY - iY) < SENSITIVITY) { this.setState({ url }); } else { this.fX = iX; this.fY = iY; this.timeout = setTimeout(() => this.compare(url), INTENTION_DELAY_MS); } }; this.handlePointerLeave = ({ target, relatedTarget }) => { const { url } = this.state; const { timeout, timeoutUrl } = this; if (target == null) { if (timeout) { this.clearTimeout(); } return; } if (url == null && timeout == null) { return; } const closestUrl = closestUrlFromNode(target); const relatedUrl = relatedTarget && closestUrlFromNode(relatedTarget); if (timeout != null && urlsEqual(closestUrl, timeoutUrl) && !urlsEqual(relatedUrl, timeoutUrl)) { this.clearTimeout(); } if (urlsEqual(closestUrl, url) && !urlsEqual(relatedUrl, url)) { this.setState({ url: undefined }); } }; this.handlePointerEnter = event => { const { target } = event; if (target == null) { return; } const { timeoutUrl, timeout } = this; const url = closestUrlFromNode(target); if (url == null) { return; } if (timeout) { if (urlsEqual(url, timeoutUrl)) { return; } else { this.clearTimeout(); } } this.timeoutUrl = url; // If the event is a mouse event, record initial mouse position upon entering the element this.timeout = setTimeout(() => { this.clearTimeout(); if ('clientX' in event && 'clientY' in event) { this.compare(url); } else { this.setState({ url }); } }, INTENTION_DELAY_MS); }; } render() { const { url } = this.state; const { manager } = this.props; const preloadMarkup = url ? /*#__PURE__*/React__default["default"].createElement("div", { style: { visibility: 'hidden' } }, findMatches(manager.registered, url).map(({ render, path }, index) => { // eslint-disable-next-line react/no-array-index-key return /*#__PURE__*/React__default["default"].createElement("div", { key: `${path}${index}` }, render(url)); })) : null; const expensiveListeners = this.prefetchAgressively ? /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "mouseover", handler: this.handlePointerEnter }), /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "focusin", handler: this.handlePointerEnter }), /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "mouseout", handler: this.handlePointerLeave }), /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "focusout", handler: this.handlePointerLeave }), /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "mousemove", handler: this.handleMouseMove })) : null; return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "mousedown", handler: this.handlePressStart }), /*#__PURE__*/React__default["default"].createElement(EventListener.EventListener, { passive: true, event: "touchstart", handler: this.handlePressStart }), expensiveListeners, preloadMarkup); } clearTimeout() { if (this.timeout != null) { clearTimeout(this.timeout); this.timeout = undefined; this.timeoutUrl = undefined; } } } function Prefetcher(props) { return /*#__PURE__*/React__default["default"].createElement(prefetch.PrefetchContext.Consumer, null, manager => /*#__PURE__*/React__default["default"].createElement(ConnectedPrefetcher, Object.assign({}, props, { manager: manager }))); } function shouldPrefetchAggressively() { return typeof navigator === 'undefined' || !('connection' in navigator) || !navigator.connection.saveData; } function urlsEqual(first, second) { return first == null && first === second || first != null && second != null && first.href === second.href; } function findMatches(records, url) { return [...records].filter(({ path: match }) => matches(url, match)); } function matches(url, matcher) { return typeof matcher === 'string' ? matcher === url.pathname : matcher.test(url.pathname); } function closestUrlFromNode(element) { if (!(element instanceof HTMLElement)) { return undefined; } // data-href is a hack for resource list doing the <a> as a sibling const closestUrl = element.closest('[href], [data-href]'); if (closestUrl == null || !(closestUrl instanceof HTMLElement)) { return undefined; } const url = closestUrl.getAttribute('href') || closestUrl.getAttribute('data-href'); try { return url ? new URL(url, window.location.href) : undefined; } catch (error) { return undefined; } } exports.INTENTION_DELAY_MS = INTENTION_DELAY_MS; exports.Prefetcher = Prefetcher; exports.SENSITIVITY = SENSITIVITY;