@shopify/react-async
Version:
Tools for creating powerful, asynchronously-loaded React components
231 lines (223 loc) • 7.11 kB
JavaScript
'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;