diff --git a/CHANGELOG.md b/CHANGELOG.md index 5039f7fa47..9c28d99853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fix [#2360](https://github.com/microsoft/BotFramework-WebChat/issues/2360). Timestamp should update on language change, by [@compulim](https://github.com/compulim) in PR [#2414](https://github.com/microsoft/BotFramework-WebChat/pull/2414) - Fix [#2428](https://github.com/microsoft/BotFramework-WebChat/issues/2428). Should interrupt speech synthesis after microphone button is clicked, by [@compulim](https://github.com/compulim) in PR [#2429](https://github.com/microsoft/BotFramework-WebChat/pull/2429) - Fix [#2422](https://github.com/microsoft/BotFramework-WebChat/issues/2422). Store thumbnail URL using the activity's `attachment.thumbnailUrl` field, by [@compulim](https://github.com/compulim) in PR [#2433](https://github.com/microsoft/BotFramework-WebChat/pull/2433) +- Fix [#2435](https://github.com/microsoft/BotFramework-WebChat/issues/2435). Fix microphone button getting stuck on voice-triggered expecting input hint without a speech synthesis engine, by [@compulim](https://github.com/compulim) in PR [#2445](https://github.com/microsoft/BotFramework-WebChat/pull/2445) ### Added diff --git a/__tests__/speech.synthesis.js b/__tests__/speech.synthesis.js index f10b141096..8ce634d30e 100644 --- a/__tests__/speech.synthesis.js +++ b/__tests__/speech.synthesis.js @@ -124,4 +124,28 @@ describe('speech synthesis', () => { await expect(speechRecognitionStartCalled().fn(driver)).resolves.toBeTruthy(); await driver.wait(negateCondition(speechSynthesisUtterancePended()), timeouts.ui); }); + + describe('without speech synthesis', () => { + test('should start recognition immediately after receiving expected input hint', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + webSpeechPonyfillFactory: () => { + const { SpeechGrammarList, SpeechRecognition } = window.WebSpeechMock; + + return { + SpeechGrammarList, + SpeechRecognition + }; + } + } + }); + + await pageObjects.sendMessageViaMicrophone('input hint expected'); + + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + await expect(speechRecognitionStartCalled().fn(driver)).resolves.toBeTruthy(); + await driver.wait(negateCondition(speechSynthesisUtterancePended()), timeouts.ui); + }); + }); }); diff --git a/packages/component/src/BasicTranscript.js b/packages/component/src/BasicTranscript.js index 8a80c50614..8a6b83020e 100644 --- a/packages/component/src/BasicTranscript.js +++ b/packages/component/src/BasicTranscript.js @@ -9,6 +9,11 @@ import connectToWebChat from './connectToWebChat'; import ScrollToEndButton from './Activity/ScrollToEndButton'; import SpeakActivity from './Activity/Speak'; +import { + speechSynthesis as bypassSpeechSynthesis, + SpeechSynthesisUtterance as BypassSpeechSynthesisUtterance +} from './Speech/BypassSpeechSynthesisPonyfill'; + const ROOT_CSS = css({ overflow: 'hidden', position: 'relative' @@ -85,7 +90,11 @@ const BasicTranscript = ({
- +
    {activityElements.map(({ activity, element }, index) => (
  • {element} {// TODO: [P2] We should use core/definitions/speakingActivity for this predicate instead - speechSynthesis && activity.channelData && activity.channelData.speak && ( - - )} + activity.channelData && activity.channelData.speak && }
  • ))}
diff --git a/packages/component/src/Speech/BypassSpeechSynthesisPonyfill.js b/packages/component/src/Speech/BypassSpeechSynthesisPonyfill.js new file mode 100644 index 0000000000..26a993dae4 --- /dev/null +++ b/packages/component/src/Speech/BypassSpeechSynthesisPonyfill.js @@ -0,0 +1,164 @@ +// Since this is a bypass, we will relax some ESLint rules. +// All classes/properties defined here are in W3C Web Speech API. + +/* eslint class-methods-use-this: "off" */ +/* eslint getter-return: "off" */ +/* eslint max-classes-per-file: ["error", 4] */ +/* eslint no-empty-function: "off" */ + +import EventTarget, { defineEventAttribute } from '../external/event-target-shim'; + +class SpeechSynthesisEvent { + constructor(type, utterance) { + this._type = type; + this._utterance = utterance; + } + + get charIndex() { + return 0; + } + + get elapsedTime() { + return 0; + } + + get name() {} + + get type() { + return this._type; + } + + get utterance() { + return this._utterance; + } +} + +class SpeechSynthesisUtterance extends EventTarget { + constructor(text) { + super(); + + this._lang = 'en-US'; + this._pitch = 1; + this._rate = 1; + this._text = text; + this._voice = null; + this._volume = 1; + } + + get lang() { + return this._lang; + } + + set lang(value) { + this._lang = value; + } + + get pitch() { + return this._pitch; + } + + set pitch(value) { + this._pitch = value; + } + + get rate() { + return this._rate; + } + + set rate(value) { + this._rate = value; + } + + get text() { + return this._text; + } + + set text(value) { + this._text = value; + } + + get voice() { + return this._voice; + } + + set voice(value) { + this._voice = value; + } + + get volume() { + return this._volume; + } + + set volume(value) { + this._volume = value; + } +} + +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'boundary'); +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'end'); +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'error'); +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'mark'); +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'pause'); +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'resume'); +defineEventAttribute(SpeechSynthesisUtterance.prototype, 'start'); + +class SpeechSynthesisVoice { + get default() { + return true; + } + + get lang() { + return 'en-US'; + } + + get localService() { + return true; + } + + get name() { + return 'English (US)'; + } + + get voiceURI() { + return 'English (US)'; + } +} + +class SpeechSynthesis extends EventTarget { + get paused() { + return false; + } + + get pending() { + return false; + } + + get speaking() { + return false; + } + + cancel() {} + + getVoices() { + return [new SpeechSynthesisVoice()]; + } + + pause() { + throw new Error('pause is not implemented.'); + } + + resume() { + throw new Error('resume is not implemented.'); + } + + speak(utterance) { + utterance.dispatchEvent(new SpeechSynthesisEvent('start', utterance)); + utterance.dispatchEvent(new SpeechSynthesisEvent('end', utterance)); + } +} + +defineEventAttribute(SpeechSynthesis.prototype, 'voiceschanged'); + +const speechSynthesis = new SpeechSynthesis(); + +export { speechSynthesis, SpeechSynthesisEvent, SpeechSynthesisUtterance, SpeechSynthesisVoice }; diff --git a/packages/component/src/external/event-target-shim.js b/packages/component/src/external/event-target-shim.js new file mode 100644 index 0000000000..bb71ce05a6 --- /dev/null +++ b/packages/component/src/external/event-target-shim.js @@ -0,0 +1,863 @@ +// This is adopted from event-target-shim@5.0.1 under MIT License. +// The source code is copied here because the original package does not support ES5 browsers. + +// Webpack assumes all code under node_modules are correctly transpiled to ES5. +// But since this package did not transpile, thus, the output bundle will contains non-ES5 code which break older browsers. + +/* eslint-disable */ + +/*! + * The MIT License (MIT) + * + * Copyright (c) 2015 Toru Nagashima + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * @author Toru Nagashima + * @copyright 2015 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +/** + * @typedef {object} PrivateData + * @property {EventTarget} eventTarget The event target. + * @property {{type:string}} event The original event object. + * @property {number} eventPhase The current event phase. + * @property {EventTarget|null} currentTarget The current event target. + * @property {boolean} canceled The flag to prevent default. + * @property {boolean} stopped The flag to stop propagation. + * @property {boolean} immediateStopped The flag to stop propagation immediately. + * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null. + * @property {number} timeStamp The unix time. + * @private + */ + +/** + * Private data for event wrappers. + * @type {WeakMap} + * @private + */ +const privateData = new WeakMap(); + +/** + * Cache for wrapper classes. + * @type {WeakMap} + * @private + */ +const wrappers = new WeakMap(); + +/** + * Get private data. + * @param {Event} event The event object to get private data. + * @returns {PrivateData} The private data of the event. + * @private + */ +function pd(event) { + const retv = privateData.get(event); + console.assert(retv != null, "'this' is expected an Event object, but got", event); + return retv; +} + +/** + * https://dom.spec.whatwg.org/#set-the-canceled-flag + * @param data {PrivateData} private data. + */ +function setCancelFlag(data) { + if (data.passiveListener != null) { + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error('Unable to preventDefault inside passive event listener invocation.', data.passiveListener); + } + return; + } + if (!data.event.cancelable) { + return; + } + + data.canceled = true; + if (typeof data.event.preventDefault === 'function') { + data.event.preventDefault(); + } +} + +/** + * @see https://dom.spec.whatwg.org/#interface-event + * @private + */ +/** + * The event wrapper. + * @constructor + * @param {EventTarget} eventTarget The event target of this dispatching. + * @param {Event|{type:string}} event The original event to wrap. + */ +function Event(eventTarget, event) { + privateData.set(this, { + eventTarget, + event, + eventPhase: 2, + currentTarget: eventTarget, + canceled: false, + stopped: false, + immediateStopped: false, + passiveListener: null, + timeStamp: event.timeStamp || Date.now() + }); + + // https://heycam.github.io/webidl/#Unforgeable + Object.defineProperty(this, 'isTrusted', { value: false, enumerable: true }); + + // Define accessors + const keys = Object.keys(event); + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!(key in this)) { + Object.defineProperty(this, key, defineRedirectDescriptor(key)); + } + } +} + +// Should be enumerable, but class methods are not enumerable. +Event.prototype = { + /** + * The type of this event. + * @type {string} + */ + get type() { + return pd(this).event.type; + }, + + /** + * The target of this event. + * @type {EventTarget} + */ + get target() { + return pd(this).eventTarget; + }, + + /** + * The target of this event. + * @type {EventTarget} + */ + get currentTarget() { + return pd(this).currentTarget; + }, + + /** + * @returns {EventTarget[]} The composed path of this event. + */ + composedPath() { + const currentTarget = pd(this).currentTarget; + if (currentTarget == null) { + return []; + } + return [currentTarget]; + }, + + /** + * Constant of NONE. + * @type {number} + */ + get NONE() { + return 0; + }, + + /** + * Constant of CAPTURING_PHASE. + * @type {number} + */ + get CAPTURING_PHASE() { + return 1; + }, + + /** + * Constant of AT_TARGET. + * @type {number} + */ + get AT_TARGET() { + return 2; + }, + + /** + * Constant of BUBBLING_PHASE. + * @type {number} + */ + get BUBBLING_PHASE() { + return 3; + }, + + /** + * The target of this event. + * @type {number} + */ + get eventPhase() { + return pd(this).eventPhase; + }, + + /** + * Stop event bubbling. + * @returns {void} + */ + stopPropagation() { + const data = pd(this); + + data.stopped = true; + if (typeof data.event.stopPropagation === 'function') { + data.event.stopPropagation(); + } + }, + + /** + * Stop event bubbling. + * @returns {void} + */ + stopImmediatePropagation() { + const data = pd(this); + + data.stopped = true; + data.immediateStopped = true; + if (typeof data.event.stopImmediatePropagation === 'function') { + data.event.stopImmediatePropagation(); + } + }, + + /** + * The flag to be bubbling. + * @type {boolean} + */ + get bubbles() { + return Boolean(pd(this).event.bubbles); + }, + + /** + * The flag to be cancelable. + * @type {boolean} + */ + get cancelable() { + return Boolean(pd(this).event.cancelable); + }, + + /** + * Cancel this event. + * @returns {void} + */ + preventDefault() { + setCancelFlag(pd(this)); + }, + + /** + * The flag to indicate cancellation state. + * @type {boolean} + */ + get defaultPrevented() { + return pd(this).canceled; + }, + + /** + * The flag to be composed. + * @type {boolean} + */ + get composed() { + return Boolean(pd(this).event.composed); + }, + + /** + * The unix time of this event. + * @type {number} + */ + get timeStamp() { + return pd(this).timeStamp; + }, + + /** + * The target of this event. + * @type {EventTarget} + * @deprecated + */ + get srcElement() { + return pd(this).eventTarget; + }, + + /** + * The flag to stop event bubbling. + * @type {boolean} + * @deprecated + */ + get cancelBubble() { + return pd(this).stopped; + }, + set cancelBubble(value) { + if (!value) { + return; + } + const data = pd(this); + + data.stopped = true; + if (typeof data.event.cancelBubble === 'boolean') { + data.event.cancelBubble = true; + } + }, + + /** + * The flag to indicate cancellation state. + * @type {boolean} + * @deprecated + */ + get returnValue() { + return !pd(this).canceled; + }, + set returnValue(value) { + if (!value) { + setCancelFlag(pd(this)); + } + }, + + /** + * Initialize this event object. But do nothing under event dispatching. + * @param {string} type The event type. + * @param {boolean} [bubbles=false] The flag to be possible to bubble up. + * @param {boolean} [cancelable=false] The flag to be possible to cancel. + * @deprecated + */ + initEvent() { + // Do nothing. + } +}; + +// `constructor` is not enumerable. +Object.defineProperty(Event.prototype, 'constructor', { + value: Event, + configurable: true, + writable: true +}); + +// Ensure `event instanceof window.Event` is `true`. +if (typeof window !== 'undefined' && typeof window.Event !== 'undefined') { + Object.setPrototypeOf(Event.prototype, window.Event.prototype); + + // Make association for wrappers. + wrappers.set(window.Event.prototype, Event); +} + +/** + * Get the property descriptor to redirect a given property. + * @param {string} key Property name to define property descriptor. + * @returns {PropertyDescriptor} The property descriptor to redirect the property. + * @private + */ +function defineRedirectDescriptor(key) { + return { + get() { + return pd(this).event[key]; + }, + set(value) { + pd(this).event[key] = value; + }, + configurable: true, + enumerable: true + }; +} + +/** + * Get the property descriptor to call a given method property. + * @param {string} key Property name to define property descriptor. + * @returns {PropertyDescriptor} The property descriptor to call the method property. + * @private + */ +function defineCallDescriptor(key) { + return { + value() { + const event = pd(this).event; + return event[key].apply(event, arguments); + }, + configurable: true, + enumerable: true + }; +} + +/** + * Define new wrapper class. + * @param {Function} BaseEvent The base wrapper class. + * @param {Object} proto The prototype of the original event. + * @returns {Function} The defined wrapper class. + * @private + */ +function defineWrapper(BaseEvent, proto) { + const keys = Object.keys(proto); + if (keys.length === 0) { + return BaseEvent; + } + + /** CustomEvent */ + function CustomEvent(eventTarget, event) { + BaseEvent.call(this, eventTarget, event); + } + + CustomEvent.prototype = Object.create(BaseEvent.prototype, { + constructor: { value: CustomEvent, configurable: true, writable: true } + }); + + // Define accessors. + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (!(key in BaseEvent.prototype)) { + const descriptor = Object.getOwnPropertyDescriptor(proto, key); + const isFunc = typeof descriptor.value === 'function'; + Object.defineProperty( + CustomEvent.prototype, + key, + isFunc ? defineCallDescriptor(key) : defineRedirectDescriptor(key) + ); + } + } + + return CustomEvent; +} + +/** + * Get the wrapper class of a given prototype. + * @param {Object} proto The prototype of the original event to get its wrapper. + * @returns {Function} The wrapper class. + * @private + */ +function getWrapper(proto) { + if (proto == null || proto === Object.prototype) { + return Event; + } + + let wrapper = wrappers.get(proto); + if (wrapper == null) { + wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto); + wrappers.set(proto, wrapper); + } + return wrapper; +} + +/** + * Wrap a given event to management a dispatching. + * @param {EventTarget} eventTarget The event target of this dispatching. + * @param {Object} event The event to wrap. + * @returns {Event} The wrapper instance. + * @private + */ +function wrapEvent(eventTarget, event) { + const Wrapper = getWrapper(Object.getPrototypeOf(event)); + return new Wrapper(eventTarget, event); +} + +/** + * Get the immediateStopped flag of a given event. + * @param {Event} event The event to get. + * @returns {boolean} The flag to stop propagation immediately. + * @private + */ +function isStopped(event) { + return pd(event).immediateStopped; +} + +/** + * Set the current event phase of a given event. + * @param {Event} event The event to set current target. + * @param {number} eventPhase New event phase. + * @returns {void} + * @private + */ +function setEventPhase(event, eventPhase) { + pd(event).eventPhase = eventPhase; +} + +/** + * Set the current target of a given event. + * @param {Event} event The event to set current target. + * @param {EventTarget|null} currentTarget New current target. + * @returns {void} + * @private + */ +function setCurrentTarget(event, currentTarget) { + pd(event).currentTarget = currentTarget; +} + +/** + * Set a passive listener of a given event. + * @param {Event} event The event to set current target. + * @param {Function|null} passiveListener New passive listener. + * @returns {void} + * @private + */ +function setPassiveListener(event, passiveListener) { + pd(event).passiveListener = passiveListener; +} + +/** + * @typedef {object} ListenerNode + * @property {Function} listener + * @property {1|2|3} listenerType + * @property {boolean} passive + * @property {boolean} once + * @property {ListenerNode|null} next + * @private + */ + +/** + * @type {WeakMap>} + * @private + */ +const listenersMap = new WeakMap(); + +// Listener types +const CAPTURE = 1; +const BUBBLE = 2; +const ATTRIBUTE = 3; + +/** + * Check whether a given value is an object or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if the value is an object. + */ +function isObject(x) { + return x !== null && typeof x === 'object'; //eslint-disable-line no-restricted-syntax +} + +/** + * Get listeners. + * @param {EventTarget} eventTarget The event target to get. + * @returns {Map} The listeners. + * @private + */ +function getListeners(eventTarget) { + const listeners = listenersMap.get(eventTarget); + if (listeners == null) { + throw new TypeError("'this' is expected an EventTarget object, but got another value."); + } + return listeners; +} + +/** + * Get the property descriptor for the event attribute of a given event. + * @param {string} eventName The event name to get property descriptor. + * @returns {PropertyDescriptor} The property descriptor. + * @private + */ +function defineEventAttributeDescriptor(eventName) { + return { + get() { + const listeners = getListeners(this); + let node = listeners.get(eventName); + while (node != null) { + if (node.listenerType === ATTRIBUTE) { + return node.listener; + } + node = node.next; + } + return null; + }, + + set(listener) { + if (typeof listener !== 'function' && !isObject(listener)) { + listener = null; // eslint-disable-line no-param-reassign + } + const listeners = getListeners(this); + + // Traverse to the tail while removing old value. + let prev = null; + let node = listeners.get(eventName); + while (node != null) { + if (node.listenerType === ATTRIBUTE) { + // Remove old value. + if (prev !== null) { + prev.next = node.next; + } else if (node.next !== null) { + listeners.set(eventName, node.next); + } else { + listeners.delete(eventName); + } + } else { + prev = node; + } + + node = node.next; + } + + // Add new value. + if (listener !== null) { + const newNode = { + listener, + listenerType: ATTRIBUTE, + passive: false, + once: false, + next: null + }; + if (prev === null) { + listeners.set(eventName, newNode); + } else { + prev.next = newNode; + } + } + }, + configurable: true, + enumerable: true + }; +} + +/** + * Define an event attribute (e.g. `eventTarget.onclick`). + * @param {Object} eventTargetPrototype The event target prototype to define an event attrbite. + * @param {string} eventName The event name to define. + * @returns {void} + */ +function defineEventAttribute(eventTargetPrototype, eventName) { + Object.defineProperty(eventTargetPrototype, `on${eventName}`, defineEventAttributeDescriptor(eventName)); +} + +/** + * Define a custom EventTarget with event attributes. + * @param {string[]} eventNames Event names for event attributes. + * @returns {EventTarget} The custom EventTarget. + * @private + */ +function defineCustomEventTarget(eventNames) { + /** CustomEventTarget */ + function CustomEventTarget() { + EventTarget.call(this); + } + + CustomEventTarget.prototype = Object.create(EventTarget.prototype, { + constructor: { + value: CustomEventTarget, + configurable: true, + writable: true + } + }); + + for (let i = 0; i < eventNames.length; ++i) { + defineEventAttribute(CustomEventTarget.prototype, eventNames[i]); + } + + return CustomEventTarget; +} + +/** + * EventTarget. + * + * - This is constructor if no arguments. + * - This is a function which returns a CustomEventTarget constructor if there are arguments. + * + * For example: + * + * class A extends EventTarget {} + * class B extends EventTarget("message") {} + * class C extends EventTarget("message", "error") {} + * class D extends EventTarget(["message", "error"]) {} + */ +function EventTarget() { + /*eslint-disable consistent-return */ + if (this instanceof EventTarget) { + listenersMap.set(this, new Map()); + return; + } + if (arguments.length === 1 && Array.isArray(arguments[0])) { + return defineCustomEventTarget(arguments[0]); + } + if (arguments.length > 0) { + const types = new Array(arguments.length); + for (let i = 0; i < arguments.length; ++i) { + types[i] = arguments[i]; + } + return defineCustomEventTarget(types); + } + throw new TypeError('Cannot call a class as a function'); + /*eslint-enable consistent-return */ +} + +// Should be enumerable, but class methods are not enumerable. +EventTarget.prototype = { + /** + * Add a given listener to this event target. + * @param {string} eventName The event name to add. + * @param {Function} listener The listener to add. + * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. + * @returns {void} + */ + addEventListener(eventName, listener, options) { + if (listener == null) { + return; + } + if (typeof listener !== 'function' && !isObject(listener)) { + throw new TypeError("'listener' should be a function or an object."); + } + + const listeners = getListeners(this); + const optionsIsObj = isObject(options); + const capture = optionsIsObj ? Boolean(options.capture) : Boolean(options); + const listenerType = capture ? CAPTURE : BUBBLE; + const newNode = { + listener, + listenerType, + passive: optionsIsObj && Boolean(options.passive), + once: optionsIsObj && Boolean(options.once), + next: null + }; + + // Set it as the first node if the first node is null. + let node = listeners.get(eventName); + if (node === undefined) { + listeners.set(eventName, newNode); + return; + } + + // Traverse to the tail while checking duplication.. + let prev = null; + while (node != null) { + if (node.listener === listener && node.listenerType === listenerType) { + // Should ignore duplication. + return; + } + prev = node; + node = node.next; + } + + // Add it. + prev.next = newNode; + }, + + /** + * Remove a given listener from this event target. + * @param {string} eventName The event name to remove. + * @param {Function} listener The listener to remove. + * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener. + * @returns {void} + */ + removeEventListener(eventName, listener, options) { + if (listener == null) { + return; + } + + const listeners = getListeners(this); + const capture = isObject(options) ? Boolean(options.capture) : Boolean(options); + const listenerType = capture ? CAPTURE : BUBBLE; + + let prev = null; + let node = listeners.get(eventName); + while (node != null) { + if (node.listener === listener && node.listenerType === listenerType) { + if (prev !== null) { + prev.next = node.next; + } else if (node.next !== null) { + listeners.set(eventName, node.next); + } else { + listeners.delete(eventName); + } + return; + } + + prev = node; + node = node.next; + } + }, + + /** + * Dispatch a given event. + * @param {Event|{type:string}} event The event to dispatch. + * @returns {boolean} `false` if canceled. + */ + dispatchEvent(event) { + if (event == null || typeof event.type !== 'string') { + throw new TypeError('"event.type" should be a string.'); + } + + // If listeners aren't registered, terminate. + const listeners = getListeners(this); + const eventName = event.type; + let node = listeners.get(eventName); + if (node == null) { + return true; + } + + // Since we cannot rewrite several properties, so wrap object. + const wrappedEvent = wrapEvent(this, event); + + // This doesn't process capturing phase and bubbling phase. + // This isn't participating in a tree. + let prev = null; + while (node != null) { + // Remove this listener if it's once + if (node.once) { + if (prev !== null) { + prev.next = node.next; + } else if (node.next !== null) { + listeners.set(eventName, node.next); + } else { + listeners.delete(eventName); + } + } else { + prev = node; + } + + // Call this listener + setPassiveListener(wrappedEvent, node.passive ? node.listener : null); + if (typeof node.listener === 'function') { + try { + node.listener.call(this, wrappedEvent); + } catch (err) { + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error(err); + } + } + } else if (node.listenerType !== ATTRIBUTE && typeof node.listener.handleEvent === 'function') { + node.listener.handleEvent(wrappedEvent); + } + + // Break if `event.stopImmediatePropagation` was called. + if (isStopped(wrappedEvent)) { + break; + } + + node = node.next; + } + setPassiveListener(wrappedEvent, null); + setEventPhase(wrappedEvent, 0); + setCurrentTarget(wrappedEvent, null); + + return !wrappedEvent.defaultPrevented; + } +}; + +// `constructor` is not enumerable. +Object.defineProperty(EventTarget.prototype, 'constructor', { + value: EventTarget, + configurable: true, + writable: true +}); + +// Ensure `eventTarget instanceof window.EventTarget` is `true`. +if (typeof window !== 'undefined' && typeof window.EventTarget !== 'undefined') { + Object.setPrototypeOf(EventTarget.prototype, window.EventTarget.prototype); +} + +exports.defineEventAttribute = defineEventAttribute; +exports.EventTarget = EventTarget; +exports.default = EventTarget; + +module.exports = EventTarget; +module.exports.EventTarget = module.exports['default'] = EventTarget; +module.exports.defineEventAttribute = defineEventAttribute; +//# sourceMappingURL=event-target-shim.js.map