diff --git a/src/event-manager.js b/src/event-manager.js index 49cb88e9..368f0064 100644 --- a/src/event-manager.js +++ b/src/event-manager.js @@ -67,41 +67,45 @@ class CapturedHandlerEntry { } } -function handleDelegatedEvent(event) { - event.propagationStopped = false; - let target = findOriginalEventTarget(event); +class DelegateHandlerEntry { + constructor(eventName, eventManager) { + this.eventName = eventName; + this.count = 0; + this.eventManager = eventManager; + } - while (target && !event.propagationStopped) { - if (target.delegatedCallbacks) { - let callback = target.delegatedCallbacks[event.type]; - if (callback) { - if (event.stopPropagation !== stopPropagation) { - event.standardStopPropagation = event.stopPropagation; - event.stopPropagation = stopPropagation; - } - if ('handleEvent' in callback) { - callback.handleEvent(event); - } else { - callback(event); + handleEvent(event) { + event.propagationStopped = false; + let target = findOriginalEventTarget(event); + + while (target && !event.propagationStopped) { + if (target.delegatedCallbacks) { + let callback = target.delegatedCallbacks[event.type]; + if (callback) { + if (event.stopPropagation !== stopPropagation) { + event.standardStopPropagation = event.stopPropagation; + event.stopPropagation = stopPropagation; + } + if ('handleEvent' in callback) { + callback.handleEvent(event); + } else { + callback(event); + } } } - } - target = target.parentNode; - } -} + const parent = target.parentNode; + const shouldEscapeShadowRoot = this.eventManager.escapeShadowRoot && parent instanceof ShadowRoot; -class DelegateHandlerEntry { - constructor(eventName) { - this.eventName = eventName; - this.count = 0; + target = shouldEscapeShadowRoot ? parent.host : parent; + } } increment() { this.count++; if (this.count === 1) { - DOM.addEventListener(this.eventName, handleDelegatedEvent, false); + DOM.addEventListener(this.eventName, this, false); } } @@ -109,7 +113,7 @@ class DelegateHandlerEntry { if (this.count === 0) { emLogger.warn('The same EventListener was disposed multiple times.'); } else if (--this.count === 0) { - DOM.removeEventListener(this.eventName, handleDelegatedEvent, false); + DOM.removeEventListener(this.eventName, this, false); } } } @@ -163,6 +167,10 @@ class DefaultEventStrategy { delegatedHandlers = {}; capturedHandlers = {}; + constructor(eventManager) { + this.eventManager = eventManager; + } + /** * @param {Element} target * @param {string} targetEvent @@ -177,7 +185,7 @@ class DefaultEventStrategy { if (strategy === delegationStrategy.bubbling) { delegatedHandlers = this.delegatedHandlers; - handlerEntry = delegatedHandlers[targetEvent] || (delegatedHandlers[targetEvent] = new DelegateHandlerEntry(targetEvent)); + handlerEntry = delegatedHandlers[targetEvent] || (delegatedHandlers[targetEvent] = new DelegateHandlerEntry(targetEvent, this.eventManager)); let delegatedCallbacks = target.delegatedCallbacks || (target.delegatedCallbacks = {}); if (!delegatedCallbacks[targetEvent]) { handlerEntry.increment(); @@ -235,9 +243,10 @@ export const delegationStrategy = { }; export class EventManager { - constructor() { + constructor(escapeShadowRoot = false) { this.elementHandlerLookup = {}; this.eventStrategyLookup = {}; + this.escapeShadowRoot = escapeShadowRoot; this.registerElementConfig({ tagName: 'input', @@ -277,7 +286,7 @@ export class EventManager { } }); - this.defaultEventStrategy = new DefaultEventStrategy(); + this.defaultEventStrategy = new DefaultEventStrategy(this); } registerElementConfig(config) { diff --git a/test/event-manager.spec.js b/test/event-manager.spec.js index e134cedb..8f4bcc60 100644 --- a/test/event-manager.spec.js +++ b/test/event-manager.spec.js @@ -5,11 +5,11 @@ import * as LogManager from 'aurelia-logging'; describe('EventManager', () => { describe('getElementHandler', () => { - var em; + let em; beforeAll(() => em = new EventManager()); it('handles input', () => { - var element = DOM.createElement('input'); + let element = DOM.createElement('input'); expect(em.getElementHandler(element, 'value')).not.toBeNull(); expect(em.getElementHandler(element, 'checked')).not.toBeNull(); @@ -18,21 +18,21 @@ describe('EventManager', () => { }); it('handles textarea', () => { - var element = DOM.createElement('textarea'); + let element = DOM.createElement('textarea'); expect(em.getElementHandler(element, 'value')).not.toBeNull(); expect(em.getElementHandler(element, 'id')).toBeNull(); }); it('handles select', () => { - var element = DOM.createElement('select'); + let element = DOM.createElement('select'); expect(em.getElementHandler(element, 'value')).not.toBeNull(); expect(em.getElementHandler(element, 'id')).toBeNull(); }); it('handles textContent and innerHTML properties', () => { - var element = DOM.createElement('div'); + let element = DOM.createElement('div'); expect(em.getElementHandler(element, 'textContent')).not.toBeNull(); expect(em.getElementHandler(element, 'innerHTML')).not.toBeNull(); @@ -40,7 +40,7 @@ describe('EventManager', () => { }); it('handles scrollTop and scrollLeft properties', () => { - var element = DOM.createElement('div'); + let element = DOM.createElement('div'); expect(em.getElementHandler(element, 'scrollTop')).not.toBeNull(); expect(em.getElementHandler(element, 'scrollLeft')).not.toBeNull(); @@ -48,11 +48,11 @@ describe('EventManager', () => { }); it('can subscribe', () => { - var element = DOM.createElement('input'), - handler = em.getElementHandler(element, 'value'), - dispose, - callback = jasmine.createSpy('callback'), - inputEvent = DOM.createCustomEvent('input'); + let element = DOM.createElement('input'), + handler = em.getElementHandler(element, 'value'), + dispose, + callback = jasmine.createSpy('callback'), + inputEvent = DOM.createCustomEvent('input'); element.value = 'foo'; expect(handler).toBeDefined(); expect(handler.subscribe).toBeDefined(); @@ -72,29 +72,40 @@ describe('EventManager', () => { }); describe('addEventListener', () => { - const em = new EventManager(); - const one = document.createElement('div'); - const two = document.createElement('div'); - const three = document.createElement('div'); - - const oneClick = jasmine.createSpy('one-click'); - const threeClick = jasmine.createSpy('three-click'); - const oneDelegate = jasmine.createSpy('one-delegate'); - const threeDelegate = jasmine.createSpy('three-delegate'); + let em, one, two, three, shadowHost, shadowRoot, shadowButton, oneClick, threeClick, oneDelegate, threeDelegate, delegationEntryHandlers; beforeEach(() => { + em = new EventManager(); + one = document.createElement('div'); + two = document.createElement('div'); + three = document.createElement('div'); + shadowHost = document.createElement('div'); + shadowButton = document.createElement('button'); + + oneClick = jasmine.createSpy('one-click'); + threeClick = jasmine.createSpy('three-click'); + oneDelegate = jasmine.createSpy('one-delegate'); + threeDelegate = jasmine.createSpy('three-delegate'); + document.body.appendChild(one); one.appendChild(two); two.appendChild(three); + one.appendChild(shadowHost); - em.addEventListener(one, 'click', oneClick, delegationStrategy.none); - em.addEventListener(three, 'click', threeClick, delegationStrategy.none); - em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling); - em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling); + shadowRoot = shadowHost.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(shadowButton); + + delegationEntryHandlers = [ + em.addEventListener(one, 'click', oneClick, delegationStrategy.none, true), + em.addEventListener(three, 'click', threeClick, delegationStrategy.none, true), + em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling, true), + em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling, true) + ]; }); afterEach(() => { - one.parentNode.removeChild(one); + delegationEntryHandlers.forEach(delegationEntryHandler => delegationEntryHandler.dispose()); + one.remove(); }); it('bubbles properly when not delegated', () => { @@ -116,8 +127,21 @@ describe('EventManager', () => { one.dispatchEvent(oneClickEvent); expect(threeClick).not.toHaveBeenCalledWith(threeClickEvent); expect(oneClick).toHaveBeenCalledWith(oneClickEvent); - oneClick.calls.reset(); - threeClick.calls.reset(); + }); + + + it('bubbles properly out of shadow dom when not delegated with composed flag', () => { + em.escapeShadowRoot = true; + const shadowButtonClickEvent = DOM.createCustomEvent('click', { bubbles: true, composed: true }); + shadowButton.dispatchEvent(shadowButtonClickEvent); + expect(oneClick).toHaveBeenCalledWith(shadowButtonClickEvent); + }); + + it('should not bubble out of shadow dom when not delegated without composed flag', () => { + em.escapeShadowRoot = true; + const shadowButtonClickEvent = DOM.createCustomEvent('click', { bubbles: true }); + shadowButton.dispatchEvent(shadowButtonClickEvent); + expect(oneClick).not.toHaveBeenCalledWith(shadowButtonClickEvent); }); it('bubbles properly when delegated', () => { @@ -139,8 +163,26 @@ describe('EventManager', () => { one.dispatchEvent(oneDelegateEvent); expect(threeDelegate).not.toHaveBeenCalledWith(threeDelegateEvent); expect(oneDelegate).toHaveBeenCalledWith(oneDelegateEvent); - oneDelegate.calls.reset(); - threeDelegate.calls.reset(); + }); + + it('should not bubble out of shadow dom when escapeShadowRoot is not explicitly set', () => { + const shadowButtonDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true, composed: true }); + shadowButton.dispatchEvent(shadowButtonDelegateEvent); + expect(oneDelegate).not.toHaveBeenCalled(); + }); + + it('bubbles properly out of shadow dom when delegated with composed flag', () => { + em.escapeShadowRoot = true; + const shadowButtonDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true, composed: true }); + shadowButton.dispatchEvent(shadowButtonDelegateEvent); + expect(oneDelegate).toHaveBeenCalledWith(shadowButtonDelegateEvent); + }); + + it('should not bubble out of shadow dom when delegated without composed flag', () => { + em.escapeShadowRoot = true; + const shadowButtonDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true }); + shadowButton.dispatchEvent(shadowButtonDelegateEvent); + expect(oneDelegate).not.toHaveBeenCalledWith(shadowButtonDelegateEvent); }); it('stops bubbling when asked', () => { @@ -184,7 +226,7 @@ describe('EventManager', () => { let originalWarn; beforeEach(() => { originalWarn = LogManager.Logger.prototype.warn; - spyOn(LogManager.Logger.prototype, 'warn') + spyOn(LogManager.Logger.prototype, 'warn'); }); afterEach(() => { LogManager.Logger.prototype.warn = originalWarn; @@ -207,7 +249,7 @@ describe('EventManager', () => { em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling, true), em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling, true), em.addEventListener(one, 'delegate', oneCapture, delegationStrategy.capturing, true), - em.addEventListener(three, 'delegate', threeCapture, delegationStrategy.capturing, true), + em.addEventListener(three, 'delegate', threeCapture, delegationStrategy.capturing, true) ]; let threeClickEvent = DOM.createCustomEvent('click', { bubbles: true }); @@ -217,24 +259,24 @@ describe('EventManager', () => { oneClick.calls.reset(); threeClick.calls.reset(); - let delegateBubblingCount = em.defaultEventStrategy.delegatedHandlers['delegate'].count; - + let delegateBubblingCount = em.defaultEventStrategy.delegatedHandlers.delegate.count; + handlers.push( em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling, true), em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling, true) ); - let delegateBubblingCountAfterDoubleSubscription = em.defaultEventStrategy.delegatedHandlers['delegate'].count; - expect(delegateBubblingCountAfterDoubleSubscription).toEqual(delegateBubblingCount,"allows double subscription for bubbling"); + let delegateBubblingCountAfterDoubleSubscription = em.defaultEventStrategy.delegatedHandlers.delegate.count; + expect(delegateBubblingCountAfterDoubleSubscription).toEqual(delegateBubblingCount, 'allows double subscription for bubbling'); expect(LogManager.Logger.prototype.warn).toHaveBeenCalled(); - let delegateCaptureCount = em.defaultEventStrategy.delegatedHandlers['delegate'].count; - + let delegateCaptureCount = em.defaultEventStrategy.delegatedHandlers.delegate.count; + handlers.push( em.addEventListener(one, 'delegate', oneCapture, delegationStrategy.capturing, true), em.addEventListener(three, 'delegate', threeCapture, delegationStrategy.capturing, true), - ) - expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(2) - let delegateCaptureCountAfterDoubleSubscription = em.defaultEventStrategy.capturedHandlers['delegate'].count; - expect(delegateCaptureCountAfterDoubleSubscription).toEqual(delegateCaptureCount,"allows double subscription for capture"); + ); + expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(2); + let delegateCaptureCountAfterDoubleSubscription = em.defaultEventStrategy.capturedHandlers.delegate.count; + expect(delegateCaptureCountAfterDoubleSubscription).toEqual(delegateCaptureCount, 'allows double subscription for capture'); handlers[0].dispose(); handlers[1].dispose(); @@ -257,26 +299,26 @@ describe('EventManager', () => { threeCapture.calls.reset(); oneCapture.calls.reset(); - handlers[2].dispose(); - let delegateBubblingCountBeforeDisposingDisposed = em.defaultEventStrategy.delegatedHandlers['delegate'].count; - handlers[2].dispose(); - let delegateBubblingCountAfterDisposingDisposed = em.defaultEventStrategy.delegatedHandlers['delegate'].count; - expect(delegateBubblingCountAfterDisposingDisposed).toEqual(delegateBubblingCountBeforeDisposingDisposed,"allows double disposing for bubbling"); - expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(3) + handlers[2].dispose(); + let delegateBubblingCountBeforeDisposingDisposed = em.defaultEventStrategy.delegatedHandlers.delegate.count; + handlers[2].dispose(); + let delegateBubblingCountAfterDisposingDisposed = em.defaultEventStrategy.delegatedHandlers.delegate.count; + expect(delegateBubblingCountAfterDisposingDisposed).toEqual(delegateBubblingCountBeforeDisposingDisposed, 'allows double disposing for bubbling'); + expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(3); handlers[3].dispose(); - - handlers[4].dispose(); - let delegateCaptureCountBeforeDisposingDisposed = em.defaultEventStrategy.capturedHandlers['delegate'].count; - handlers[4].dispose(); - let delegateCaptureCountAfterDisposingDisposed = em.defaultEventStrategy.capturedHandlers['delegate'].count; - expect(delegateCaptureCountAfterDisposingDisposed).toEqual(delegateCaptureCountBeforeDisposingDisposed,"allows double disposing for bubbling"); - expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(4) + + handlers[4].dispose(); + let delegateCaptureCountBeforeDisposingDisposed = em.defaultEventStrategy.capturedHandlers.delegate.count; + handlers[4].dispose(); + let delegateCaptureCountAfterDisposingDisposed = em.defaultEventStrategy.capturedHandlers.delegate.count; + expect(delegateCaptureCountAfterDisposingDisposed).toEqual(delegateCaptureCountBeforeDisposingDisposed, 'allows double disposing for bubbling'); + expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(4); handlers[5].dispose(); - em.defaultEventStrategy.capturedHandlers['delegate'].decrement(); + em.defaultEventStrategy.capturedHandlers.delegate.decrement(); expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(5); - em.defaultEventStrategy.delegatedHandlers['delegate'].decrement(); - expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(6) + em.defaultEventStrategy.delegatedHandlers.delegate.decrement(); + expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(6); threeDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true }); three.dispatchEvent(threeDelegateEvent);