|
import type { ReactiveController, ReactiveControllerHost } from 'lit'; |
|
|
|
export interface ClickPressOptions { |
|
/** |
|
* An optional selector to designate an element in shadow root where listeners will be attached. If unset, the host |
|
* element will be used. |
|
*/ |
|
selector: string; |
|
/** |
|
* The time in milliseconds to wait after the first press and before the interval will start counting. Useful for |
|
* simulating a click press in the same way many keyboards work when a key is held down. |
|
*/ |
|
delayAfterFirstPress: number; |
|
/** The time in milliseconds to wait before initiating another press. Defaults to 250. */ |
|
interval: number; |
|
/** |
|
* Called when pressed and called again every interval while the control remains pressed. The event will initially be |
|
* the mousedown event that initiated the press. Subsequent events will be the most recent mousemove event, or the |
|
* original mousedown event if the cursor hasn't moved. |
|
*/ |
|
press: (event: MouseEvent) => unknown; |
|
/** |
|
* Called when the press is finally released. The event will be a mouseup event or a mouseleave event if the cursor |
|
* exited the target element during a press. |
|
*/ |
|
release: (event: MouseEvent) => unknown; |
|
} |
|
|
|
export class ClickPressController implements ReactiveController { |
|
host: ReactiveControllerHost & Element; |
|
options: Partial<ClickPressOptions>; |
|
target: Element; |
|
firstPressTimeout: number; |
|
nextInterval: number; |
|
lastMoveEvent: MouseEvent | undefined; |
|
|
|
constructor(host: ReactiveControllerHost & Element, options: Partial<ClickPressOptions>) { |
|
(this.host = host).addController(this); |
|
this.options = options; |
|
|
|
this.handleMove = this.handleMove.bind(this); |
|
this.handlePress = this.handlePress.bind(this); |
|
this.handleExit = this.handleExit.bind(this); |
|
this.handleRelease = this.handleRelease.bind(this); |
|
} |
|
|
|
hostConnected() { |
|
this.host.updateComplete.then(() => { |
|
if (this.options.selector) { |
|
try { |
|
const target = this.host.shadowRoot!.querySelector(this.options.selector); |
|
if (target === null) { |
|
throw new Error(`No target found using the "${this.options.selector}" query selector.`); |
|
} |
|
this.target = target; |
|
} catch { |
|
throw new Error(`Invalid query selector "${this.options.selector}" in ClickPressController.`); |
|
} |
|
} else { |
|
this.target = this.host; |
|
} |
|
|
|
this.target.addEventListener('mousedown', this.handlePress); |
|
this.target.addEventListener('touchstart', this.handlePress); |
|
}); |
|
} |
|
|
|
hostDisconnected() { |
|
this.target.removeEventListener('mousedown', this.handlePress); |
|
this.target.removeEventListener('touchstart', this.handlePress); |
|
this.removePressedListeners(); |
|
} |
|
|
|
private handlePress(event: MouseEvent) { |
|
this.addPressedListeners(); |
|
|
|
// First press |
|
if (this.options.press) { |
|
this.options.press(event); |
|
} |
|
|
|
// Interval presses (after first press delay) |
|
this.firstPressTimeout = window.setTimeout(() => { |
|
this.nextInterval = window.setInterval(() => { |
|
if (this.options.press) { |
|
this.options.press(this.lastMoveEvent ?? event); |
|
} |
|
}, this.options.interval ?? 250); |
|
}, this.options.delayAfterFirstPress ?? 0); |
|
} |
|
|
|
private handleExit(event: MouseEvent) { |
|
// If the mouse leaves the target element, trigger a release |
|
this.handleRelease(event); |
|
} |
|
|
|
private handleMove(event: MouseEvent) { |
|
// Remember the last mouse move event so we can send it along in the next interval. |
|
this.lastMoveEvent = event; |
|
} |
|
|
|
private handleRelease(event: MouseEvent) { |
|
this.removePressedListeners(); |
|
window.clearTimeout(this.firstPressTimeout); |
|
window.clearInterval(this.nextInterval); |
|
this.lastMoveEvent = undefined; |
|
|
|
// Release |
|
if (this.options.release) { |
|
this.options.release(event); |
|
} |
|
} |
|
|
|
private addPressedListeners() { |
|
this.target.addEventListener('mouseleave', this.handleExit); |
|
document.addEventListener('mousemove', this.handleMove); |
|
document.addEventListener('mouseup', this.handleRelease); |
|
document.addEventListener('touchend', this.handleRelease); |
|
document.addEventListener('touchmove', this.handleExit); |
|
} |
|
|
|
private removePressedListeners() { |
|
this.target.removeEventListener('mouseleave', this.handleExit); |
|
document.removeEventListener('mousemove', this.handleMove); |
|
document.removeEventListener('mouseup', this.handleRelease); |
|
document.removeEventListener('touchend', this.handleRelease); |
|
document.removeEventListener('touchmove', this.handleExit); |
|
} |
|
} |