89 lines
3.5 KiB
JavaScript
89 lines
3.5 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = factory;
|
|
else root.createMobilePullRefresh = factory;
|
|
})(typeof self !== 'undefined' ? self : this, function createMobilePullRefresh(options) {
|
|
const surface = options.surface || document.querySelector('#my-work');
|
|
const indicator = options.indicator || document.querySelector('#mobile-pull-refresh');
|
|
const threshold = options.threshold || 64;
|
|
const isMobile = options.isMobile || (() => window.matchMedia('(max-width: 600px)').matches);
|
|
const getScrollY = options.getScrollY || (() => window.scrollY);
|
|
const isBlocked = options.isBlocked || (() => Array.from(options.overlays || document.querySelectorAll('[role="dialog"], #whiteboard-modal, #markdown-modal')).some(overlay =>
|
|
(overlay.checkVisibility ? overlay.checkVisibility() : !overlay.closest('[hidden]')) &&
|
|
(overlay.classList.contains('open') || overlay.getAttribute('aria-modal') === 'true')));
|
|
let gesture = null;
|
|
let inFlight = null;
|
|
|
|
function show(state, text) {
|
|
indicator.hidden = false;
|
|
indicator.dataset.state = state;
|
|
indicator.textContent = text;
|
|
}
|
|
|
|
function resetGesture() {
|
|
gesture = null;
|
|
}
|
|
|
|
function startsOnExcludedTarget(target) {
|
|
if (!target) return false;
|
|
if (target.closest?.('button, a, input, textarea, select, summary, [contenteditable="true"], [role="button"]')) return true;
|
|
for (let node = target; node && node !== surface; node = node.parentElement) {
|
|
if ((node.scrollWidth || 0) > (node.clientWidth || 0)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function onPointerDown(event) {
|
|
if (inFlight || event.pointerType !== 'touch' || !isMobile() || getScrollY() > 0 || isBlocked() || startsOnExcludedTarget(event.target)) return;
|
|
gesture = {id:event.pointerId, x:event.clientX, y:event.clientY, ready:false};
|
|
surface.setPointerCapture?.(event.pointerId);
|
|
}
|
|
|
|
function onPointerMove(event) {
|
|
if (!gesture || gesture.id !== event.pointerId) return;
|
|
const dx = event.clientX - gesture.x;
|
|
const dy = event.clientY - gesture.y;
|
|
if (dy <= 0 || Math.abs(dx) > dy) return resetGesture();
|
|
event.preventDefault();
|
|
gesture.ready = dy >= threshold;
|
|
show(gesture.ready ? 'ready' : 'pulling', gesture.ready ? 'Release to refresh' : 'Pull to refresh');
|
|
}
|
|
|
|
async function onPointerUp(event) {
|
|
if (!gesture || gesture.id !== event.pointerId) return;
|
|
const shouldRefresh = gesture.ready;
|
|
resetGesture();
|
|
surface.releasePointerCapture?.(event.pointerId);
|
|
if (!shouldRefresh || inFlight) return;
|
|
show('refreshing', 'Refreshing My Work…');
|
|
inFlight = Promise.resolve().then(options.refresh);
|
|
try {
|
|
await inFlight;
|
|
show('success', 'My Work is up to date');
|
|
} catch (_) {
|
|
show('error', 'Could not refresh · pull to retry');
|
|
} finally {
|
|
inFlight = null;
|
|
}
|
|
}
|
|
|
|
function onPointerCancel() {
|
|
resetGesture();
|
|
}
|
|
|
|
return {
|
|
start() {
|
|
surface.addEventListener('pointerdown', onPointerDown);
|
|
surface.addEventListener('pointermove', onPointerMove);
|
|
surface.addEventListener('pointerup', onPointerUp);
|
|
surface.addEventListener('pointercancel', onPointerCancel);
|
|
},
|
|
stop() {
|
|
surface.removeEventListener('pointerdown', onPointerDown);
|
|
surface.removeEventListener('pointermove', onPointerMove);
|
|
surface.removeEventListener('pointerup', onPointerUp);
|
|
surface.removeEventListener('pointercancel', onPointerCancel);
|
|
resetGesture();
|
|
},
|
|
};
|
|
});
|