UNPKG

element-vir

Version:

Heroic. Reactive. Declarative. Type safe. Web components without compromise.

92 lines (91 loc) 3.72 kB
import { AsyncDirective, directive, noChange, } from '../../lit-exports/all-lit-exports.js'; import { extractElement } from './directive-helpers.js'; export function listen(eventType, listener) { return listenDirective(eventType, listener); } /** * The directive generics here are not strong enough to maintain their values. Thus, the directive * call is wrapped in the function above. */ const listenDirective = directive(class extends AsyncDirective { element; lastListenerMetaData; isPartDisconnected = false; constructor(partInfo) { super(partInfo); this.element = extractElement(partInfo, 'listen'); } resetListener(listenerMetaData) { this.removeCurrentListener(); this.lastListenerMetaData = listenerMetaData; /** While disconnected, `reconnected` is responsible for attaching the listener. */ if (this.isConnected) { this.addCurrentListener(); } } addCurrentListener() { if (this.lastListenerMetaData) { this.element.addEventListener(this.lastListenerMetaData.eventType, this.lastListenerMetaData.listener); } } removeCurrentListener() { if (this.lastListenerMetaData) { this.element.removeEventListener(this.lastListenerMetaData.eventType, this.lastListenerMetaData.listener); } } /** * An event's propagation path is fixed when its dispatch begins, so an element that gets * removed from the document _during_ a dispatch still has connected nodes in the path of * that event. Plain `addEventListener` listeners keep firing for such an event, so these * must too: otherwise an earlier listener that removes the element (a pop-up closing itself * on the very click it is responding to, for example) silently swallows every later * listener for that same event. */ isMidDispatchDisconnect(event) { return (!this.element.isConnected && event .composedPath() .some((eventTarget) => eventTarget instanceof Node && eventTarget.isConnected)); } createListenerMetaData(eventType, callback) { return { eventType, callback, listener: (event) => { if (this.isPartDisconnected && !this.isMidDispatchDisconnect(event)) { return undefined; } return this.lastListenerMetaData?.callback(event); }, }; } render(eventTypeInput, callback) { const eventType = typeof eventTypeInput === 'string' ? eventTypeInput : eventTypeInput.type; if (typeof eventType !== 'string') { throw new TypeError(`Cannot listen to an event with a name that is not a string. Given event name: '${String(eventType)}'`); } if (this.lastListenerMetaData && this.lastListenerMetaData.eventType === eventType) { /** * Store the callback here so we don't have to update the attached listener every * time the callback is updated. */ this.lastListenerMetaData.callback = callback; } else { this.resetListener(this.createListenerMetaData(eventType, callback)); } return noChange; } disconnected() { /** * The listener stays attached so that events which were already dispatching still reach * it (see `isMidDispatchDisconnect`); this flag blocks everything dispatched * afterwards. */ this.isPartDisconnected = true; } reconnected() { this.isPartDisconnected = false; this.addCurrentListener(); } });