Skip to content
Open
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
49 changes: 45 additions & 4 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ var htmx = (function() {
* @property {boolean} [triggeredOnce]
* @property {number} [delayed]
* @property {number|null} [throttle]
* @property {number|null} [holdTimer]
* @property {WeakMap<HtmxTriggerSpecification,WeakMap<EventTarget,string>>} [lastValue]
* @property {boolean} [loaded]
* @property {string} [path]
Expand Down Expand Up @@ -2290,6 +2291,9 @@ var htmx = (function() {
} else if (token === 'throttle' && tokens[0] === ':') {
tokens.shift()
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA))
} else if (token === 'hold' && tokens[0] === ':') {
tokens.shift()
triggerSpec.hold = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA))
} else if (token === 'queue' && tokens[0] === ':') {
tokens.shift()
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA)
Expand Down Expand Up @@ -2492,6 +2496,7 @@ var htmx = (function() {
* @param {boolean} [explicitCancel]
*/
function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
/** @type {HtmxNodeInternalData} */
const elementData = getInternalData(elt)
/** @type {(Node|Window)[]} */
let eltsToListenOn
Expand All @@ -2513,11 +2518,12 @@ var htmx = (function() {
elementData.lastValue.get(triggerSpec).set(eltToListenOn, eltToListenOn.value)
})
}
const actualTrigger = triggerSpec.hold ? 'pointerdown' : triggerSpec.trigger
forEach(eltsToListenOn, function(eltToListenOn) {
/** @type EventListener */
const eventListener = function(evt) {
if (!bodyContains(elt)) {
eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener)
eltToListenOn.removeEventListener(actualTrigger, eventListener)
return
}
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
Expand Down Expand Up @@ -2568,7 +2574,23 @@ var htmx = (function() {
return
}

if (triggerSpec.throttle > 0) {
if (triggerSpec.hold > 0) {
if (elementData.holdTimer) clearTimeout(elementData.holdTimer)
elementData.holdTimer = getWindow().setTimeout(function() {
triggerEvent(elt, 'htmx:trigger')
handler(elt, evt)
}, triggerSpec.hold)
const cancelListener = function() {
if (elementData.holdTimer) {
clearTimeout(elementData.holdTimer)
elementData.holdTimer = null
}
}
eltToListenOn.addEventListener('pointerup', cancelListener, { once: true })
nodeData.listenerInfos.push({ trigger: 'pointerup', listener: cancelListener, on: eltToListenOn })
eltToListenOn.addEventListener('pointercancel', cancelListener, { once: true })
nodeData.listenerInfos.push({ trigger: 'pointercancel', listener: cancelListener, on: eltToListenOn })
} else if (triggerSpec.throttle > 0) {
if (!elementData.throttle) {
triggerEvent(elt, 'htmx:trigger')
handler(elt, evt)
Expand All @@ -2591,11 +2613,29 @@ var htmx = (function() {
nodeData.listenerInfos = []
}
nodeData.listenerInfos.push({
trigger: triggerSpec.trigger,
trigger: actualTrigger,
listener: eventListener,
on: eltToListenOn
})
eltToListenOn.addEventListener(triggerSpec.trigger, eventListener)
eltToListenOn.addEventListener(actualTrigger, eventListener)
if (triggerSpec.hold) {
if (actualTrigger !== 'mousedown') {
nodeData.listenerInfos.push({
trigger: 'mousedown',
listener: eventListener,
on: eltToListenOn
})
eltToListenOn.addEventListener('mousedown', eventListener)
}
if (actualTrigger !== 'touchstart') {
nodeData.listenerInfos.push({
trigger: 'touchstart',
listener: eventListener,
on: eltToListenOn
})
eltToListenOn.addEventListener('touchstart', eventListener)
}
}
})
}

Expand Down Expand Up @@ -5209,6 +5249,7 @@ var htmx = (function() {
* @property {string} [queue]
* @property {string} [root]
* @property {string} [threshold]
* @property {number} [hold]
*/

/**
Expand Down
136 changes: 136 additions & 0 deletions test/attributes/hx-trigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ describe('hx-trigger attribute', function() {
'event throttle:0s': [{ trigger: 'event', throttle: 0 }],
'event throttle:0ms': [{ trigger: 'event', throttle: 0 }],
'event throttle:1s, foo': [{ trigger: 'event', throttle: 1000 }, { trigger: 'foo' }],
'event hold:1s': [{ trigger: 'event', hold: 1000 }],
'event hold:0s': [{ trigger: 'event', hold: 0 }],
'event hold:0ms': [{ trigger: 'event', hold: 0 }],
'event delay:1s': [{ trigger: 'event', delay: 1000 }],
'event delay:1s, foo': [{ trigger: 'event', delay: 1000 }, { trigger: 'foo' }],
'event delay:0s, foo': [{ trigger: 'event', delay: 0 }, { trigger: 'foo' }],
Expand Down Expand Up @@ -1370,4 +1373,137 @@ describe('hx-trigger attribute', function() {
complete()
}, 30)
})

it('hold does not trigger before time', function(done) {
this.server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var event = htmx._('makeEvent')('pointerdown')
div.dispatchEvent(event)
setTimeout(function() {
div.innerHTML.should.equal('Not Held')
done()
}, 25)
})

it('hold triggers after specified time', function(done) {
var server = this.server
server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var event = htmx._('makeEvent')('pointerdown')
div.dispatchEvent(event)
setTimeout(function() {
server.respond()
div.innerHTML.should.equal('Held!')
done()
}, 75)
})

it('hold cancels on pointerup before time', function(done) {
this.server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var downEvent = htmx._('makeEvent')('pointerdown')
var upEvent = htmx._('makeEvent')('pointerup')
div.dispatchEvent(downEvent)
setTimeout(function() {
div.dispatchEvent(upEvent)
}, 25)
setTimeout(function() {
div.innerHTML.should.equal('Not Held')
done()
}, 75)
})

it('hold cancels on pointercancel before time', function(done) {
this.server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var downEvent = htmx._('makeEvent')('pointerdown')
var cancelEvent = htmx._('makeEvent')('pointercancel')
div.dispatchEvent(downEvent)
setTimeout(function() {
div.dispatchEvent(cancelEvent)
}, 25)
setTimeout(function() {
div.innerHTML.should.equal('Not Held')
done()
}, 75)
})

it('hold started with touchstart cancels on pointerup', function(done) {
var server = this.server
server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var touchStart = htmx._('makeEvent')('touchstart')
var pointerUp = htmx._('makeEvent')('pointerup')
div.dispatchEvent(touchStart)
setTimeout(function() {
div.dispatchEvent(pointerUp)
}, 25)
setTimeout(function() {
div.innerHTML.should.equal('Not Held')
done()
}, 75)
})

it('hold started with mousedown cancels on pointerup', function(done) {
var server = this.server
server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var mouseDown = htmx._('makeEvent')('mousedown')
var pointerUp = htmx._('makeEvent')('pointerup')
div.dispatchEvent(mouseDown)
setTimeout(function() {
div.dispatchEvent(pointerUp)
}, 25)
setTimeout(function() {
div.innerHTML.should.equal('Not Held')
done()
}, 75)
})

it('hold works with mouse events', function(done) {
var server = this.server
server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var event = htmx._('makeEvent')('mousedown')
div.dispatchEvent(event)
setTimeout(function() {
server.respond()
div.innerHTML.should.equal('Held!')
done()
}, 75)
})

it('hold works with touch events', function(done) {
var server = this.server
server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var event = htmx._('makeEvent')('touchstart')
div.dispatchEvent(event)
setTimeout(function() {
server.respond()
div.innerHTML.should.equal('Held!')
done()
}, 75)
})

it('hold works with pointer events', function(done) {
var server = this.server
server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
var event = htmx._('makeEvent')('pointerdown')
div.dispatchEvent(event)
setTimeout(function() {
server.respond()
div.innerHTML.should.equal('Held!')
done()
}, 75)
})

it('hold:0ms triggers immediately', function() {
this.server.respondWith('GET', '/test', 'Held!')
var div = make('<div hx-get="/test" hx-trigger="click hold:0ms">Not Held</div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Held!')
})
})
1 change: 1 addition & 0 deletions www/content/attributes/hx-trigger.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Standard events can also have modifiers that change how they behave. The modifi
is seen again it will reset the delay.
* `throttle:<timing declaration>` - a throttle will occur after an event triggers a request. If the event
is seen again before the delay completes, it is ignored, the element will trigger at the end of the delay.
* `hold:<timing declaration>` - requires the user to hold the pointer down for the specified duration before triggering the request. Cancels on pointer up or cancel.
* `from:<Extended CSS selector>` - allows the event that triggers a request to come from another element in the document (e.g. listening to a key event on the body, to support hot keys)
* A standard CSS selector resolves to all elements matching that selector. Thus, `from:input` would listen on every input on the page.
* The CSS selector is only evaluated once and is not re-evaluated when the page changes. If you need to detect dynamically added elements use a [standard event filter](#standard-event-filters), for example `hx-trigger="click[event.target.matches('button')] from:body"` which would [catch](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Event_bubbling) click events from every button on the page.
Expand Down
1 change: 1 addition & 0 deletions www/static/src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -5209,6 +5209,7 @@ var htmx = (function() {
* @property {string} [queue]
* @property {string} [root]
* @property {string} [threshold]
* @property {number} [hold]
*/

/**
Expand Down
1 change: 1 addition & 0 deletions www/themes/htmx-theme/static/js/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -5209,6 +5209,7 @@ var htmx = (function() {
* @property {string} [queue]
* @property {string} [root]
* @property {string} [threshold]
* @property {number} [hold]
*/

/**
Expand Down