diff --git a/packages/common/src/interfaces/elementEventListener.interface.ts b/packages/common/src/interfaces/elementEventListener.interface.ts index 409a80691..1301edbdb 100644 --- a/packages/common/src/interfaces/elementEventListener.interface.ts +++ b/packages/common/src/interfaces/elementEventListener.interface.ts @@ -2,4 +2,5 @@ export interface ElementEventListener { element: Element; eventName: string; listener: EventListenerOrEventListenerObject; + groupName?: string; } diff --git a/packages/common/src/services/__tests__/bindingEvent.service.spec.ts b/packages/common/src/services/__tests__/bindingEvent.service.spec.ts index 9119ef7bf..be5b302d9 100644 --- a/packages/common/src/services/__tests__/bindingEvent.service.spec.ts +++ b/packages/common/src/services/__tests__/bindingEvent.service.spec.ts @@ -44,7 +44,6 @@ describe('BindingEvent Service', () => { }); it('should be able to bind an event with single listener and options to multiple elements', () => { - const mockElm = { addEventListener: jest.fn() } as unknown as HTMLElement; const mockCallback = jest.fn(); const elm1 = document.createElement('input'); const elm2 = document.createElement('input'); @@ -65,8 +64,6 @@ describe('BindingEvent Service', () => { it('should call unbindAll and expect as many removeEventListener be called', () => { const mockElm = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement; - const addEventSpy = jest.spyOn(mockElm, 'addEventListener'); - const removeEventSpy = jest.spyOn(mockElm, 'removeEventListener'); const mockCallback1 = jest.fn(); const mockCallback2 = jest.fn(); @@ -78,9 +75,42 @@ describe('BindingEvent Service', () => { service.unbindAll(); expect(service.boundedEvents.length).toBe(0); - expect(addEventSpy).toHaveBeenCalledWith('keyup', mockCallback1, undefined); - expect(addEventSpy).toHaveBeenCalledWith('click', mockCallback2, { capture: true, passive: true }); - expect(removeEventSpy).toHaveBeenCalledWith('keyup', mockCallback1); - expect(removeEventSpy).toHaveBeenCalledWith('click', mockCallback2); + expect(mockElm.addEventListener).toHaveBeenCalledWith('keyup', mockCallback1, undefined); + expect(mockElm.addEventListener).toHaveBeenCalledWith('click', mockCallback2, { capture: true, passive: true }); + expect(mockElm.removeEventListener).toHaveBeenCalledWith('keyup', mockCallback1); + expect(mockElm.removeEventListener).toHaveBeenCalledWith('click', mockCallback2); + }); + + it('should call unbindAll with a group name and expect that group listeners to be removed but others kept', () => { + const mockElm1 = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement; + const mockElm2 = { addEventListener: jest.fn(), removeEventListener: jest.fn() } as unknown as HTMLElement; + const mockCallback1 = jest.fn(); + const mockCallback2 = jest.fn(); + const mockCallback3 = jest.fn(); + const mockCallback4 = jest.fn(); + const mockCallback5 = jest.fn(); + + service = new BindingEventService(); + service.bind(mockElm1, 'keyup', mockCallback1, false, 'same-group'); + service.bind(mockElm1, 'keydown', mockCallback2, { capture: true, passive: true }, 'magic'); + service.bind(mockElm2, 'click', mockCallback3, { capture: true, passive: true }); // no group + service.bind(mockElm2, 'mouseover', mockCallback4, { capture: false, passive: true }, 'same-group'); + service.bind(mockElm2, 'mouseout', mockCallback5, { capture: false, passive: false }, 'wonderful'); + + expect(service.boundedEvents.length).toBe(5); + expect(mockElm1.addEventListener).toHaveBeenCalledWith('keyup', mockCallback1, false); // same-group + expect(mockElm1.addEventListener).toHaveBeenCalledWith('keydown', mockCallback2, { capture: true, passive: true }); + expect(mockElm2.addEventListener).toHaveBeenCalledWith('click', mockCallback3, { capture: true, passive: true }); + expect(mockElm2.addEventListener).toHaveBeenCalledWith('mouseover', mockCallback4, { capture: false, passive: true }); // same-group + expect(mockElm2.addEventListener).toHaveBeenCalledWith('mouseout', mockCallback5, { capture: false, passive: false }); + + service.unbindAll('same-group'); + + expect(service.boundedEvents.length).toBe(3); + expect(mockElm1.removeEventListener).toHaveBeenCalledWith('keyup', mockCallback1); // same-group + expect(mockElm1.removeEventListener).not.toHaveBeenCalledWith(); + expect(mockElm2.removeEventListener).not.toHaveBeenCalledWith(); + expect(mockElm2.removeEventListener).toHaveBeenCalledWith('mouseover', mockCallback4); // same-group + expect(mockElm2.removeEventListener).not.toHaveBeenCalledWith(); }); }); diff --git a/packages/common/src/services/bindingEvent.service.ts b/packages/common/src/services/bindingEvent.service.ts index e4b6bc784..25ccff589 100644 --- a/packages/common/src/services/bindingEvent.service.ts +++ b/packages/common/src/services/bindingEvent.service.ts @@ -13,43 +13,62 @@ export class BindingEventService { } /** Bind an event listener to any element */ - bind(elementOrElements: Element | NodeListOf | Window, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) { + bind(elementOrElements: Element | NodeListOf | Window, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject, listenerOptions?: boolean | AddEventListenerOptions, groupName = '') { + // convert to array for looping in next task const eventNames = (Array.isArray(eventNameOrNames)) ? eventNameOrNames : [eventNameOrNames]; if ((elementOrElements as NodeListOf)?.forEach) { - (elementOrElements as NodeListOf)?.forEach(element => { + // multiple elements to bind to + (elementOrElements as NodeListOf).forEach(element => { for (const eventName of eventNames) { - element.addEventListener(eventName, listener, options); + element.addEventListener(eventName, listener, listenerOptions); this._boundedEvents.push({ element, eventName, listener }); } }); } else { + // single elements to bind to for (const eventName of eventNames) { - (elementOrElements as Element).addEventListener(eventName, listener, options); - this._boundedEvents.push({ element: (elementOrElements as Element), eventName, listener }); + (elementOrElements as Element).addEventListener(eventName, listener, listenerOptions); + this._boundedEvents.push({ element: (elementOrElements as Element), eventName, listener, groupName }); } } } - /** Unbind all will remove every every event handlers that were bounded earlier */ + /** Unbind a specific listener that was bounded earlier */ unbind(elementOrElements: Element | NodeListOf, eventNameOrNames: string | string[], listener: EventListenerOrEventListenerObject) { + // convert to array for looping in next task const elements = (Array.isArray(elementOrElements)) ? elementOrElements : [elementOrElements]; const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames]; + for (const eventName of eventNames) { for (const element of elements) { - if (element?.removeEventListener) { + if (typeof element?.removeEventListener === 'function') { element.removeEventListener(eventName, listener); } } } } - /** Unbind all will remove every every event handlers that were bounded earlier */ - unbindAll() { - while (this._boundedEvents.length > 0) { - const boundedEvent = this._boundedEvents.pop() as ElementEventListener; - const { element, eventName, listener } = boundedEvent; - this.unbind(element, eventName, listener); + /** + * Unbind all event listeners that were bounded, optionally provide a group name to unbind all listeners assigned to that specific group only. + */ + unbindAll(groupName?: string) { + if (groupName) { + // unbind only the bounded event with a specific group + this._boundedEvents.forEach((boundedEvent, idx) => { + if (boundedEvent.groupName === groupName) { + const { element, eventName, listener } = boundedEvent; + this.unbind(element, eventName, listener); + this._boundedEvents.splice(idx, 1); + } + }); + } else { + // unbind everything + while (this._boundedEvents.length > 0) { + const boundedEvent = this._boundedEvents.pop() as ElementEventListener; + const { element, eventName, listener } = boundedEvent; + this.unbind(element, eventName, listener); + } } } }