adaptive-speech-recognizer
Version:
Adaptive dictation-mode speech recognizer ponyfill compatible with WebChat that gives the user time to think and stutter/stammer.
870 lines (783 loc) • 21.7 kB
JavaScript
/**
*
* @see (https://npmjs.com/package/event-target-shim-es5)
* @see https://github.com/mysticatea/event-target-shim/blob/HEAD/dist/event-target-shim.mjs
* @license (MIT)
*/
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* @copyright 2015 Toru Nagashima. All rights reserved.
* See LICENSE file in root directory for full license.
*/
/**
* @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<Event, PrivateData>}
* @private
*/
const privateData = new WeakMap();
/**
* Cache for wrapper classes.
* @type {WeakMap<Object, Function>}
* @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<object, Map<string, ListenerNode>>}
* @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<string, ListenerNode>} 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);
}
export default EventTarget;
export { defineEventAttribute, EventTarget };
// >>>># sourceMappingURL=event-target-shim.mjs.map