UNPKG

htmx-ext-hold

Version:

An htmx extension to trigger events on 'hold' (mousedown/touchstart for a duration)

114 lines (113 loc) 3.45 kB
// src/index.ts var DEFAULT_HOLD_DELAY = 500; var CSS_HOLD_PROGRESS_VAR = "--hold-progress"; var HOLD_ACTIVE_CLASS = "htmx-hold-active"; var START_EVENTS = ["mousedown", "touchstart"]; var CANCEL_EVENTS = [ "mouseup", "mouseleave", "touchend", "touchcancel" ]; var HOLD_TRIGGER_PATTERN = /\bhold\b/; var DATA_PROGRESS_ATTR = "holdProgress"; var processedElements = new WeakSet; function parseDelay(triggerSpec, htmx) { const match = triggerSpec.match(/hold\s+delay:(\d+(?:\.\d+)?(?:ms|s)?)/); if (!match?.[1]) return null; return htmx.parseInterval?.(match[1]) ?? null; } function registerElement(elt, triggerSpec, htmx) { if (processedElements.has(elt)) return; const delay = parseDelay(triggerSpec, htmx) ?? DEFAULT_HOLD_DELAY; let startTime = null; let animationFrameId = null; let holdTriggered = false; let isActive = false; const clearAnimation = () => { if (animationFrameId !== null) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } }; const updateProgress = () => { if (startTime === null) return; const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / delay, 1); elt.style.setProperty(CSS_HOLD_PROGRESS_VAR, progress.toString()); elt.dataset[DATA_PROGRESS_ATTR] = Math.round(progress * 100).toString(); if (progress >= 1 && !holdTriggered) { holdTriggered = true; clearAnimation(); htmx.trigger?.(elt, "hold"); } else if (!holdTriggered) { animationFrameId = requestAnimationFrame(updateProgress); } }; START_EVENTS.forEach((eventName) => { elt.addEventListener(eventName, (event) => { if (isActive) return; if (event.cancelable) event.preventDefault(); isActive = true; holdTriggered = false; startTime = Date.now(); htmx.addClass?.(elt, HOLD_ACTIVE_CLASS); elt.style.setProperty(CSS_HOLD_PROGRESS_VAR, "0"); elt.dataset[DATA_PROGRESS_ATTR] = "0"; animationFrameId = requestAnimationFrame(updateProgress); }); }); CANCEL_EVENTS.forEach((eventName) => { elt.addEventListener(eventName, () => { if (!isActive) return; isActive = false; startTime = null; holdTriggered = false; clearAnimation(); htmx.removeClass?.(elt, HOLD_ACTIVE_CLASS); elt.style.setProperty(CSS_HOLD_PROGRESS_VAR, "0"); elt.dataset[DATA_PROGRESS_ATTR] = "0"; }); }); processedElements.add(elt); } function registerHoldExtension() { const htmx = window.htmx; if (!htmx || typeof htmx.defineExtension !== "function") { console.error("htmx is not available."); return; } htmx.defineExtension("hold", { onEvent(name, evt) { if (name !== "htmx:afterProcessNode") return true; const elt = evt.detail.elt; if (!elt) return true; const triggerSpec = elt.getAttribute("hx-trigger") ?? elt.getAttribute("data-hx-trigger"); if (!triggerSpec || !HOLD_TRIGGER_PATTERN.test(triggerSpec)) return true; registerElement(elt, triggerSpec, htmx); return true; } }); } if (typeof window !== "undefined") { if (window.htmx) { registerHoldExtension(); } else if (typeof document !== "undefined") { document.addEventListener("htmx:load", registerHoldExtension, { once: true }); } } var src_default = registerHoldExtension; export { src_default as default };