Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 109 additions & 138 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -693,151 +693,123 @@ var htmx = (() => {
this.__onTrigger(elt, specString, initialHandler)
}

// Wire up trigger listeners with full modifier support (delay, throttle, once, etc.)
// Wire up event listeners with full modifier support (once, prevent, stop,
// delay, throttle, changed, capture, passive, from, filter, etc.)
__onTrigger(elt, specString, handler) {
let specs = this.__parseTriggerSpecs(specString)
this.__htmxProp(elt).triggerSpecs.push(...specs)

for (let spec of specs) {
spec.handler = handler
spec.listeners = []
spec.values = new WeakMap()

let [eventName, filter] = this.__extractFilter(spec.name);

// should be first so logic is called only when all other filters pass
if (spec.once) {
let original = spec.handler
spec.handler = (evt) => {
original(evt)
for (let listenerInfo of spec.listeners) {
listenerInfo.fromElt.removeEventListener(listenerInfo.eventName, listenerInfo.handler)
}
// Resolve from: elements (self listens on elt but filters by event.target in guard)
let fromElts = [elt];
if (spec.from === 'outside') fromElts = [document];
else if (spec.from && spec.from !== 'self') fromElts = this.__findAllExt(elt, spec.from);

// Inner: runs after delay/throttle resolves
let inner = (evt) => {
if (spec.halt || spec.prevent) evt.preventDefault();
if (spec.halt || spec.stop || spec.consume) evt.stopPropagation();
if (spec.once) {
for (let info of spec.listeners) info.fromElt.removeEventListener(info.eventName, info.handler, info);
}
}

if (eventName === 'intersect' || eventName === "revealed") {
let observerOptions = {}
if (spec.root) observerOptions.root = this.__findOrWarn(elt, spec.root)
if (spec.threshold) observerOptions.threshold = parseFloat(spec.threshold)
let isRevealed = eventName === "revealed"
spec.observer = new IntersectionObserver((entries) => {
for (let i = 0; i < entries.length; i++) {
let entry = entries[i]
if (entry.isIntersecting) {
this.trigger(elt, 'intersect', {}, false)
if (isRevealed) {
spec.observer.disconnect()
}
break;
}
}
}, observerOptions)
eventName = "intersect"
spec.observer.observe(elt)
}
handler(evt);
};

// Wrap inner with delay/throttle if needed
let timed = inner;
if (spec.delay) {
let original = spec.handler
spec.handler = evt => {
clearTimeout(spec.timeout)
spec.timeout = setTimeout(() => original(evt),
this.parseInterval(spec.delay));
}
}

if (spec.throttle) {
let original = spec.handler
spec.handler = evt => {
timed = evt => {
clearTimeout(spec.timeout);
spec.timeout = setTimeout(() => inner(evt), this.parseInterval(spec.delay));
};
} else if (spec.throttle) {
timed = evt => {
if (spec.throttled) {
spec.throttledEvent = evt
spec.throttledEvent = evt;
} else {
spec.throttled = true
original(evt);
spec.throttled = true;
inner(evt);
spec.throttleTimeout = setTimeout(() => {
spec.throttled = false
spec.throttled = false;
if (spec.throttledEvent) {
// implement trailing-edge throttling
let throttledEvent = spec.throttledEvent;
spec.throttledEvent = null
spec.handler(throttledEvent);
let e = spec.throttledEvent;
spec.throttledEvent = null;
timed(e);
}
}, this.parseInterval(spec.throttle))
}, this.parseInterval(spec.throttle));
}
}
};
}

if (spec.target) {
let original = spec.handler
spec.handler = evt => {
if (evt.target?.matches?.(spec.target)) {
original(evt)
// Guarded: pre-timing checks that determine if event should proceed
spec.handler = (evt) => {
if (spec.from === 'self' && evt.target !== elt) return;
if (spec.from === 'outside' && elt.contains(evt.target)) return;
if (spec.target && !evt.target?.matches?.(spec.target)) return;
if (spec.changed) {
let values = spec.values ??= new WeakMap();
let changed = false;
for (let fromElt of fromElts) {
if (values.get(fromElt) !== fromElt.value) {
changed = true;
values.set(fromElt, fromElt.value);
}
}
if (!changed) return;
}
if (filter) {
if (this.__shouldCancel(evt)) evt.preventDefault();
let evtArgs = {}; for (let k in evt) evtArgs[k] = evt[k];
if (!this.__executeJavaScript(elt, evtArgs, filter, true, false)) return;
}
timed(evt);
};

// Intersect/revealed: set up observer
if (eventName === 'intersect' || eventName === 'revealed') {
let observerOptions = {rootMargin: spec.rootMargin};
if (spec.root) observerOptions.root = this.__findOrWarn(elt, spec.root);
if (spec.threshold) observerOptions.threshold = parseFloat(spec.threshold);
let isRevealed = eventName === 'revealed';
spec.observer = new IntersectionObserver((entries) => {
for (let i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) {
this.trigger(elt, 'intersect', {}, false);
if (isRevealed) spec.observer.disconnect();
break;
}
}
}, observerOptions);
eventName = 'intersect';
spec.observer.observe(elt);
}

// Every: set up interval
if (eventName === "every") {
let interval = Object.keys(spec).find(k => k !== 'name');
spec.interval = setInterval(() => {
if (elt.isConnected) {
this.__trigger(elt, 'every', {}, false);
} else {
clearInterval(spec.interval)
}
if (elt.isConnected) this.__trigger(elt, 'every', {}, false);
else clearInterval(spec.interval);
}, this.parseInterval(interval));
}

if (spec.consume) {
let original = spec.handler
spec.handler = (evt) => {
evt.stopPropagation()
original(evt)
}
}

if (filter) {
let original = spec.handler
spec.handler = (evt) => {
if (this.__shouldCancel(evt)) evt.preventDefault()
let evtArgs = {}; for (let k in evt) evtArgs[k] = evt[k];
if (this.__executeJavaScript(elt, evtArgs, filter, true, false)) {
original(evt)
}
}
}

let fromElts = [elt];
if (spec.from) {
fromElts = this.__findAllExt(elt, spec.from)
}

if (spec.changed) {
let original = spec.handler
spec.handler = (evt) => {
let trigger = false
for (let fromElt of fromElts) {
if (spec.values.get(fromElt) !== fromElt.value) {
trigger = true
spec.values.set(fromElt, fromElt.value);
}
}
if (trigger) {
original(evt)
}
}
}

// load: fire handler directly (no listener needed)
// Load: fire immediately, no listener needed
if (eventName === 'load') {
spec.handler(new CustomEvent('load'))
continue
spec.handler(new CustomEvent('load'));
continue;
}

// Register listeners
for (let fromElt of fromElts) {
let listenerInfo = {fromElt, eventName, handler: spec.handler};
elt._htmx.listeners.push(listenerInfo)
spec.listeners.push(listenerInfo)
fromElt.addEventListener(eventName, spec.handler);
let listenerInfo = {fromElt, eventName, handler: spec.handler,
capture: !!spec.capture, passive: !!spec.passive};
elt._htmx.listeners.push(listenerInfo);
spec.listeners.push(listenerInfo);
fromElt.addEventListener(eventName, spec.handler, listenerInfo);
}
}
}
Expand Down Expand Up @@ -976,7 +948,7 @@ var htmx = (() => {
spec.observer?.disconnect()
}
for (let listenerInfo of elt._htmx.listeners || []) {
listenerInfo.fromElt.removeEventListener(listenerInfo.eventName, listenerInfo.handler);
listenerInfo.fromElt.removeEventListener(listenerInfo.eventName, listenerInfo.handler, listenerInfo);
}
this.__trigger(elt, "htmx:after:cleanup")
}
Expand Down Expand Up @@ -1651,36 +1623,35 @@ var htmx = (() => {

// hx-on:<event> binds to <event> directly
// hx-on::<event> is shorthand for hx-on:htmx:<event> (htmx events)
// Modifiers (dot-separated): .prevent .stop .halt .once .self .outside .capture .passive .cc
__handleHxOnAttributes(node) {
let searchStrings = this.__prefixes("hx-on:").map(p => this.__maybeAdjustMetaCharacter(p));
let hxOnNames = this.__prefixes("hx-on");
let mc = this.config.metaCharacter || ':';
let handler = (code) => async (evt) => {
try {
await this.__executeJavaScript(node, { event: evt },
`with(event?.detail||{}){${code}}`, false);
} catch (e) {
if (typeof e !== 'symbol') this.__trigger(node, 'htmx:error', { error: e });
}
};
for (let attr of node.getAttributeNames()) {
let searchString = searchStrings.find(s => attr.startsWith(s));
if (!searchString) continue;
let [evtName, ...mods] = attr.substring(searchString.length).split('.');
let has = m => mods.includes(m);
if (evtName.startsWith(mc)) evtName = 'htmx' + evtName;
if (has('cc')) evtName = evtName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
let code = node.getAttribute(attr);
let target = has('outside') ? document : node;
let opts = { capture: has('capture'), passive: has('passive') };
let halt = has('halt');
let handler = async (evt) => {
if (has('self') && evt.target !== node) return;
if (has('outside') && node.contains(evt.target)) return;
if (halt || has('prevent')) evt.preventDefault();
if (halt || has('stop')) evt.stopPropagation();
if (has('once')) target.removeEventListener(evtName, handler, opts);
try {
await this.__executeJavaScript(node, { event: evt },
`with(event?.detail||{}){${code}}`, false);
} catch (e) {
if (typeof e !== 'symbol') this.__trigger(node, 'htmx:error', { error: e });
let prefix = hxOnNames.find(p => attr.startsWith(p));
if (!prefix) continue;
let rest = attr.substring(prefix.length);
let value = node.getAttribute(attr);
// hx-on="click once -> doA(); blur -> doB()"
if (!rest) {
for (let part of value.split(/;(?=[^;]*->)/)) {
let idx = part.indexOf('->');
if (idx !== -1) this.__onTrigger(node, part.substring(0, idx).trim(), handler(part.substring(idx + 2).trim()));
}
};
target.addEventListener(evtName, handler, opts);
this.__htmxProp(node).listeners.push({fromElt: target, eventName: evtName, handler});
continue;
}
// hx-on:click="code" or hx-on::before:request="code"
if (rest[0] !== mc) continue;
let eventName = rest.substring(1);
if (eventName.startsWith(mc)) eventName = 'htmx' + mc + eventName.substring(1);
this.__onTrigger(node, eventName, handler(value));
}
}

Expand Down
Loading