element-vir
Version:
Heroic. Reactive. Declarative. Type safe. Web components without compromise.
92 lines (91 loc) • 3.72 kB
JavaScript
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();
}
});