@avatijs/listener
Version:
Listener package part of Avati project
326 lines (316 loc) • 11.6 kB
JavaScript
/*!
* @avatijs/listener 0.1.1
* Copyright (c) 2024 Khaled Sameer <khaled.smq@hotmail.com>
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://avati.io/ for details.
*/
/* eslint-disable */
import * as __WEBPACK_EXTERNAL_MODULE__avatijs_debounce_970167a9__ from "@avatijs/debounce";
import * as __WEBPACK_EXTERNAL_MODULE__avatijs_throttle_db391ff6__ from "@avatijs/throttle";
/******/ // The require scope
/******/ var __webpack_require__ = {};
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
W: () => (/* reexport */ EventListenerManager)
});
;// external "@avatijs/debounce"
var x = (y) => {
var x = {}; __webpack_require__.d(x, y); return x
}
var y = (x) => (() => (x))
const debounce_namespaceObject = x({ ["debounce"]: () => (__WEBPACK_EXTERNAL_MODULE__avatijs_debounce_970167a9__.debounce) });
;// external "@avatijs/throttle"
var throttle_x = (y) => {
var x = {}; __webpack_require__.d(x, y); return x
}
var throttle_y = (x) => (() => (x))
const throttle_namespaceObject = throttle_x({ ["throttle"]: () => (__WEBPACK_EXTERNAL_MODULE__avatijs_throttle_db391ff6__.throttle) });
;// ./src/EventListenerManager.ts
var _a, _b, _c;
// Private symbol declarations
const _listeners = Symbol('listeners');
const _weakRefMap = Symbol('weakRefMap');
const _handleWeakRef = Symbol('handleWeakRef');
const _validateParams = Symbol('validateParams');
const _generateEventId = Symbol('generateEventId');
const _eventIdCounter = Symbol('eventIdCounter');
class EventListenerManager {
constructor() {
Object.defineProperty(this, "defaultOptions", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, _a, {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, _b, {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, _c, {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// TODO - Add more event mappings
Object.defineProperty(this, "EVENT_MAPPINGS", {
enumerable: true,
configurable: true,
writable: true,
value: {
debounce: new Set(['input', 'change', 'keyup', 'keydown', 'focus', 'blur', 'click']),
throttle: new Set(['mousemove', 'scroll', 'resize', 'wheel']),
}
});
this[_listeners] = new Map();
this[_weakRefMap] = new WeakMap();
this[_eventIdCounter] = 0;
// Bind methods to preserve context
this.add = this.add.bind(this);
this.remove = this.remove.bind(this);
this.defaultOptions = Object.freeze({
capture: false,
passive: true,
once: false,
async: false,
});
}
/**
* Adds an event listener with advanced options.
* @param element - The target element to attach the event listener.
* @param eventType - The type of the event to listen for.
* @param callback - The callback function to execute when the event is triggered.
* @param options - Additional options for the event listener.
* @returns A unique EventId representing the event listener.
*/
add(element, eventType, callback, options = {}) {
this[_validateParams](element, eventType, callback);
const eventId = this[_generateEventId]();
const finalOptions = { ...this.defaultOptions, ...options };
let enhancedCallback = callback;
if (finalOptions.debounce && finalOptions.throttle) {
throw new Error('Cannot specify both debounce and throttle options');
}
// Create the async wrapper first if needed
if (finalOptions.async) {
const asyncCallback = async (event) => {
try {
await callback(event);
}
catch (error) {
if (finalOptions.onError && error instanceof Error) {
finalOptions.onError(error);
}
else {
throw error;
}
}
};
enhancedCallback = asyncCallback;
}
// Apply debounce/throttle after async wrapper
if (finalOptions.debounce) {
// check recommendation
this.recommendation(eventType, 'debounce');
enhancedCallback = (0,debounce_namespaceObject.debounce)(enhancedCallback, {
wait: finalOptions.debounce,
leading: finalOptions.leading,
trailing: finalOptions.trailing,
debug: finalOptions.debug,
onError: finalOptions.onError,
});
}
else if (finalOptions.throttle) {
this.recommendation(eventType, 'throttle');
enhancedCallback = (0,throttle_namespaceObject.throttle)(enhancedCallback, finalOptions.throttle, {
leading: finalOptions.leading,
trailing: finalOptions.trailing,
onError: finalOptions.onError,
});
}
// Create wrapper that includes event metadata and error handling
const wrappedCallback = async (event) => {
if (finalOptions.metadata) {
event.metadata = {
timestamp: Date.now(),
eventId,
originalCallback: callback.name || 'anonymous',
};
}
try {
await enhancedCallback.call(element, event);
}
catch (error) {
if (finalOptions.onError && error instanceof Error) {
finalOptions.onError(error);
}
else {
throw error;
}
}
// Handle 'once' option
if (finalOptions.once) {
this.remove(eventId);
}
};
// Store listener details
const listenerDetails = {
element: new WeakRef(element),
eventType,
callback: wrappedCallback,
originalCallback: callback,
options: finalOptions,
timestamp: Date.now(),
};
this[_listeners].set(eventId, listenerDetails);
this[_handleWeakRef](element, eventId);
element.addEventListener(eventType, wrappedCallback, finalOptions);
return eventId;
}
/**
* Removes an event listener
*/
remove(eventId) {
const listener = this[_listeners].get(eventId);
if (!listener)
return false;
const element = listener.element.deref();
if (element) {
element.removeEventListener(listener.eventType, listener.callback, listener.options);
// Remove eventId from _weakRefMap
const elementRefs = this[_weakRefMap].get(element);
if (elementRefs) {
elementRefs.delete(eventId);
if (elementRefs.size === 0) {
this[_weakRefMap].delete(element);
}
}
}
this[_listeners].delete(eventId);
return true;
}
/**
* Adds event listener with automatic cleanup
*/
addWithCleanup(element, eventType, callback, options = {}) {
const eventId = this.add(element, eventType, callback, options);
return () => this.remove(eventId);
}
/**
* Gets all active listeners for an element
*/
getListeners(element) {
const elementRefs = this[_weakRefMap].get(element);
if (!elementRefs)
return [];
return Array.from(elementRefs)
.map((eventId) => {
const listener = this[_listeners].get(eventId);
if (!listener)
return null;
const el = listener.element.deref();
if (!el || el !== element)
return null;
return {
eventId,
eventType: listener.eventType,
options: listener.options,
timestamp: listener.timestamp,
};
})
.filter((item) => item !== null);
}
/**
* Removes all listeners from an element
*/
removeAll(element) {
const elementRefs = this[_weakRefMap].get(element);
if (!elementRefs)
return;
for (const eventId of elementRefs) {
this.remove(eventId);
}
this[_weakRefMap].delete(element);
}
/**
* Creates a one-time event listener that auto-removes
*/
once(element, eventType, callback, options = {}) {
return this.add(element, eventType, callback, { ...options, once: true });
}
/**
* Validates input parameters
* @private
*/
[(_a = _listeners, _b = _weakRefMap, _c = _eventIdCounter, _validateParams)](element, eventType, callback) {
if (!(element instanceof Element ||
element instanceof Window ||
element instanceof Document)) {
throw new TypeError('Element must be an instance of Element, Window, or Document');
}
if (typeof eventType !== 'string') {
throw new TypeError('Event type must be a string');
}
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function');
}
}
/**
* Generates a unique event ID
* @private
*/
[_generateEventId]() {
return `event_${++this[_eventIdCounter]}`;
}
/**
* Handles WeakRef management
* @private
*/
[_handleWeakRef](element, eventId) {
let elementRefs = this[_weakRefMap].get(element);
if (!elementRefs) {
elementRefs = new Set();
this[_weakRefMap].set(element, elementRefs);
}
elementRefs.add(eventId);
}
recommendation(eventType, limiterType) {
const oppositeType = limiterType === 'throttle' ? 'debounce' : 'throttle';
if (this.EVENT_MAPPINGS[oppositeType].has(eventType)) {
console.warn(`Event type '${eventType}' is recommended to be ${oppositeType}d instead of ${limiterType}d.`);
}
}
}
;// ./src/index.ts
var __webpack_exports__EventListenerManager = __webpack_exports__.W;
export { __webpack_exports__EventListenerManager as EventListenerManager };
//# sourceMappingURL=index.js.map