Skip to content

Commit 3e7331f

Browse files
committed
feat: add hold as a trigger event modifier
1 parent 81a6e25 commit 3e7331f

File tree

5 files changed

+184
-4
lines changed

5 files changed

+184
-4
lines changed

src/htmx.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ var htmx = (function() {
699699
* @property {boolean} [triggeredOnce]
700700
* @property {number} [delayed]
701701
* @property {number|null} [throttle]
702+
* @property {number|null} [holdTimer]
702703
* @property {WeakMap<HtmxTriggerSpecification,WeakMap<EventTarget,string>>} [lastValue]
703704
* @property {boolean} [loaded]
704705
* @property {string} [path]
@@ -2290,6 +2291,9 @@ var htmx = (function() {
22902291
} else if (token === 'throttle' && tokens[0] === ':') {
22912292
tokens.shift()
22922293
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA))
2294+
} else if (token === 'hold' && tokens[0] === ':') {
2295+
tokens.shift()
2296+
triggerSpec.hold = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA))
22932297
} else if (token === 'queue' && tokens[0] === ':') {
22942298
tokens.shift()
22952299
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA)
@@ -2492,6 +2496,7 @@ var htmx = (function() {
24922496
* @param {boolean} [explicitCancel]
24932497
*/
24942498
function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
2499+
/** @type {HtmxNodeInternalData} */
24952500
const elementData = getInternalData(elt)
24962501
/** @type {(Node|Window)[]} */
24972502
let eltsToListenOn
@@ -2513,11 +2518,12 @@ var htmx = (function() {
25132518
elementData.lastValue.get(triggerSpec).set(eltToListenOn, eltToListenOn.value)
25142519
})
25152520
}
2521+
const actualTrigger = triggerSpec.hold ? 'pointerdown' : triggerSpec.trigger
25162522
forEach(eltsToListenOn, function(eltToListenOn) {
25172523
/** @type EventListener */
25182524
const eventListener = function(evt) {
25192525
if (!bodyContains(elt)) {
2520-
eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener)
2526+
eltToListenOn.removeEventListener(actualTrigger, eventListener)
25212527
return
25222528
}
25232529
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
@@ -2568,7 +2574,23 @@ var htmx = (function() {
25682574
return
25692575
}
25702576

2571-
if (triggerSpec.throttle > 0) {
2577+
if (triggerSpec.hold > 0) {
2578+
if (elementData.holdTimer) clearTimeout(elementData.holdTimer)
2579+
elementData.holdTimer = getWindow().setTimeout(function() {
2580+
triggerEvent(elt, 'htmx:trigger')
2581+
handler(elt, evt)
2582+
}, triggerSpec.hold)
2583+
const cancelListener = function() {
2584+
if (elementData.holdTimer) {
2585+
clearTimeout(elementData.holdTimer)
2586+
elementData.holdTimer = null
2587+
}
2588+
}
2589+
eltToListenOn.addEventListener('pointerup', cancelListener, { once: true })
2590+
nodeData.listenerInfos.push({ trigger: 'pointerup', listener: cancelListener, on: eltToListenOn })
2591+
eltToListenOn.addEventListener('pointercancel', cancelListener, { once: true })
2592+
nodeData.listenerInfos.push({ trigger: 'pointercancel', listener: cancelListener, on: eltToListenOn })
2593+
} else if (triggerSpec.throttle > 0) {
25722594
if (!elementData.throttle) {
25732595
triggerEvent(elt, 'htmx:trigger')
25742596
handler(elt, evt)
@@ -2591,11 +2613,29 @@ var htmx = (function() {
25912613
nodeData.listenerInfos = []
25922614
}
25932615
nodeData.listenerInfos.push({
2594-
trigger: triggerSpec.trigger,
2616+
trigger: actualTrigger,
25952617
listener: eventListener,
25962618
on: eltToListenOn
25972619
})
2598-
eltToListenOn.addEventListener(triggerSpec.trigger, eventListener)
2620+
eltToListenOn.addEventListener(actualTrigger, eventListener)
2621+
if (triggerSpec.hold) {
2622+
if (actualTrigger !== 'mousedown') {
2623+
nodeData.listenerInfos.push({
2624+
trigger: 'mousedown',
2625+
listener: eventListener,
2626+
on: eltToListenOn
2627+
})
2628+
eltToListenOn.addEventListener('mousedown', eventListener)
2629+
}
2630+
if (actualTrigger !== 'touchstart') {
2631+
nodeData.listenerInfos.push({
2632+
trigger: 'touchstart',
2633+
listener: eventListener,
2634+
on: eltToListenOn
2635+
})
2636+
eltToListenOn.addEventListener('touchstart', eventListener)
2637+
}
2638+
}
25992639
})
26002640
}
26012641

@@ -5209,6 +5249,7 @@ var htmx = (function() {
52095249
* @property {string} [queue]
52105250
* @property {string} [root]
52115251
* @property {string} [threshold]
5252+
* @property {number} [hold]
52125253
*/
52135254

52145255
/**

test/attributes/hx-trigger.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ describe('hx-trigger attribute', function() {
297297
'event throttle:0s': [{ trigger: 'event', throttle: 0 }],
298298
'event throttle:0ms': [{ trigger: 'event', throttle: 0 }],
299299
'event throttle:1s, foo': [{ trigger: 'event', throttle: 1000 }, { trigger: 'foo' }],
300+
'event hold:1s': [{ trigger: 'event', hold: 1000 }],
301+
'event hold:0s': [{ trigger: 'event', hold: 0 }],
302+
'event hold:0ms': [{ trigger: 'event', hold: 0 }],
300303
'event delay:1s': [{ trigger: 'event', delay: 1000 }],
301304
'event delay:1s, foo': [{ trigger: 'event', delay: 1000 }, { trigger: 'foo' }],
302305
'event delay:0s, foo': [{ trigger: 'event', delay: 0 }, { trigger: 'foo' }],
@@ -1370,4 +1373,137 @@ describe('hx-trigger attribute', function() {
13701373
complete()
13711374
}, 30)
13721375
})
1376+
1377+
it('hold does not trigger before time', function(done) {
1378+
this.server.respondWith('GET', '/test', 'Held!')
1379+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1380+
var event = htmx._('makeEvent')('pointerdown')
1381+
div.dispatchEvent(event)
1382+
setTimeout(function() {
1383+
div.innerHTML.should.equal('Not Held')
1384+
done()
1385+
}, 25)
1386+
})
1387+
1388+
it('hold triggers after specified time', function(done) {
1389+
var server = this.server
1390+
server.respondWith('GET', '/test', 'Held!')
1391+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1392+
var event = htmx._('makeEvent')('pointerdown')
1393+
div.dispatchEvent(event)
1394+
setTimeout(function() {
1395+
server.respond()
1396+
div.innerHTML.should.equal('Held!')
1397+
done()
1398+
}, 75)
1399+
})
1400+
1401+
it('hold cancels on pointerup before time', function(done) {
1402+
this.server.respondWith('GET', '/test', 'Held!')
1403+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1404+
var downEvent = htmx._('makeEvent')('pointerdown')
1405+
var upEvent = htmx._('makeEvent')('pointerup')
1406+
div.dispatchEvent(downEvent)
1407+
setTimeout(function() {
1408+
div.dispatchEvent(upEvent)
1409+
}, 25)
1410+
setTimeout(function() {
1411+
div.innerHTML.should.equal('Not Held')
1412+
done()
1413+
}, 75)
1414+
})
1415+
1416+
it('hold cancels on pointercancel before time', function(done) {
1417+
this.server.respondWith('GET', '/test', 'Held!')
1418+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1419+
var downEvent = htmx._('makeEvent')('pointerdown')
1420+
var cancelEvent = htmx._('makeEvent')('pointercancel')
1421+
div.dispatchEvent(downEvent)
1422+
setTimeout(function() {
1423+
div.dispatchEvent(cancelEvent)
1424+
}, 25)
1425+
setTimeout(function() {
1426+
div.innerHTML.should.equal('Not Held')
1427+
done()
1428+
}, 75)
1429+
})
1430+
1431+
it('hold started with touchstart cancels on pointerup', function(done) {
1432+
var server = this.server
1433+
server.respondWith('GET', '/test', 'Held!')
1434+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1435+
var touchStart = htmx._('makeEvent')('touchstart')
1436+
var pointerUp = htmx._('makeEvent')('pointerup')
1437+
div.dispatchEvent(touchStart)
1438+
setTimeout(function() {
1439+
div.dispatchEvent(pointerUp)
1440+
}, 25)
1441+
setTimeout(function() {
1442+
div.innerHTML.should.equal('Not Held')
1443+
done()
1444+
}, 75)
1445+
})
1446+
1447+
it('hold started with mousedown cancels on pointerup', function(done) {
1448+
var server = this.server
1449+
server.respondWith('GET', '/test', 'Held!')
1450+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1451+
var mouseDown = htmx._('makeEvent')('mousedown')
1452+
var pointerUp = htmx._('makeEvent')('pointerup')
1453+
div.dispatchEvent(mouseDown)
1454+
setTimeout(function() {
1455+
div.dispatchEvent(pointerUp)
1456+
}, 25)
1457+
setTimeout(function() {
1458+
div.innerHTML.should.equal('Not Held')
1459+
done()
1460+
}, 75)
1461+
})
1462+
1463+
it('hold works with mouse events', function(done) {
1464+
var server = this.server
1465+
server.respondWith('GET', '/test', 'Held!')
1466+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1467+
var event = htmx._('makeEvent')('mousedown')
1468+
div.dispatchEvent(event)
1469+
setTimeout(function() {
1470+
server.respond()
1471+
div.innerHTML.should.equal('Held!')
1472+
done()
1473+
}, 75)
1474+
})
1475+
1476+
it('hold works with touch events', function(done) {
1477+
var server = this.server
1478+
server.respondWith('GET', '/test', 'Held!')
1479+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1480+
var event = htmx._('makeEvent')('touchstart')
1481+
div.dispatchEvent(event)
1482+
setTimeout(function() {
1483+
server.respond()
1484+
div.innerHTML.should.equal('Held!')
1485+
done()
1486+
}, 75)
1487+
})
1488+
1489+
it('hold works with pointer events', function(done) {
1490+
var server = this.server
1491+
server.respondWith('GET', '/test', 'Held!')
1492+
var div = make('<div hx-get="/test" hx-trigger="click hold:50ms">Not Held</div>')
1493+
var event = htmx._('makeEvent')('pointerdown')
1494+
div.dispatchEvent(event)
1495+
setTimeout(function() {
1496+
server.respond()
1497+
div.innerHTML.should.equal('Held!')
1498+
done()
1499+
}, 75)
1500+
})
1501+
1502+
it('hold:0ms triggers immediately', function() {
1503+
this.server.respondWith('GET', '/test', 'Held!')
1504+
var div = make('<div hx-get="/test" hx-trigger="click hold:0ms">Not Held</div>')
1505+
div.click()
1506+
this.server.respond()
1507+
div.innerHTML.should.equal('Held!')
1508+
})
13731509
})

www/content/attributes/hx-trigger.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Standard events can also have modifiers that change how they behave. The modifi
6161
is seen again it will reset the delay.
6262
* `throttle:<timing declaration>` - a throttle will occur after an event triggers a request. If the event
6363
is seen again before the delay completes, it is ignored, the element will trigger at the end of the delay.
64+
* `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.
6465
* `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)
6566
* A standard CSS selector resolves to all elements matching that selector. Thus, `from:input` would listen on every input on the page.
6667
* 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.

www/static/src/htmx.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5209,6 +5209,7 @@ var htmx = (function() {
52095209
* @property {string} [queue]
52105210
* @property {string} [root]
52115211
* @property {string} [threshold]
5212+
* @property {number} [hold]
52125213
*/
52135214

52145215
/**

www/themes/htmx-theme/static/js/htmx.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5209,6 +5209,7 @@ var htmx = (function() {
52095209
* @property {string} [queue]
52105210
* @property {string} [root]
52115211
* @property {string} [threshold]
5212+
* @property {number} [hold]
52125213
*/
52135214

52145215
/**

0 commit comments

Comments
 (0)