htmx-ext-hold
Version:
An htmx extension to trigger events on 'hold' (mousedown/touchstart for a duration)
114 lines (113 loc) • 3.45 kB
JavaScript
// 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
};