UNPKG

@virtualstate/app-history

Version:

Native JavaScript [app-history](https://github.com/WICG/app-history) implementation

1,541 lines (1,438 loc) 78.9 kB
function isEvent(value) { function isLike(value) { return !!value; } return isLike(value) && (typeof value.type === "string" || typeof value.type === "symbol"); } function assertEvent(value, type) { if (!isEvent(value)) { throw new Error("Expected event"); } if (typeof type !== "undefined" && value.type !== type) { throw new Error(`Expected event type ${type}, got ${value.type.toString()}`); } } function isParallelEvent(value) { return isEvent(value) && value.parallel !== false; } class AbortError extends Error { constructor(message) { super(`AbortError${message ? `: ${message}` : ""}`); this.name = "AbortError"; } } function isAbortError(error) { return error instanceof Error && error.name === "AbortError"; } class InvalidStateError extends Error { constructor(message) { super(`InvalidStateError${message ? `: ${message}` : ""}`); this.name = "InvalidStateError"; } } function isInvalidStateError(error) { return error instanceof Error && error.name === "InvalidStateError"; } function isAbortSignal(value) { function isAbortSignalLike(value) { return typeof value === "object"; } return (isAbortSignalLike(value) && typeof value.aborted === "boolean" && typeof value.addEventListener === "function"); } function isSignalEvent(value) { function isSignalEventLike(value) { return value.hasOwnProperty("signal"); } return (isEvent(value) && isSignalEventLike(value) && isAbortSignal(value.signal)); } function isSignalHandled(event, error) { if (isSignalEvent(event) && event.signal.aborted && error instanceof Error && isAbortError(error)) { return true; } } /** * @experimental */ const EventTargetListeners$1 = Symbol.for("@opennetwork/environment/events/target/listeners"); /** * @experimental */ const EventTargetListenersIgnore = Symbol.for("@opennetwork/environment/events/target/listeners/ignore"); /** * @experimental */ const EventTargetListenersMatch = Symbol.for("@opennetwork/environment/events/target/listeners/match"); /** * @experimental */ const EventTargetListenersThis = Symbol.for("@opennetwork/environment/events/target/listeners/this"); const EventDescriptorSymbol = Symbol.for("@opennetwork/environment/events/descriptor"); function matchEventCallback(type, callback, options) { const optionsDescriptor = isOptionsDescriptor(options) ? options : undefined; return descriptor => { if (optionsDescriptor) { return optionsDescriptor === descriptor; } return (!callback || callback === descriptor.callback) && type === descriptor.type; }; function isOptionsDescriptor(options) { function isLike(options) { return !!options; } return isLike(options) && options[EventDescriptorSymbol] === true; } } function isFunctionEventCallback(fn) { return typeof fn === "function"; } class EventTargetListeners { #listeners = []; [EventTargetListenersIgnore] = new WeakSet(); get [EventTargetListeners$1]() { return [...(this.#listeners ?? [])]; } [EventTargetListenersMatch](type) { const external = this[EventTargetListeners$1]; const matched = [ ...new Set([...(external ?? []), ...(this.#listeners ?? [])]) ] .filter(descriptor => descriptor.type === type || descriptor.type === "*") .filter(descriptor => !this[EventTargetListenersIgnore]?.has(descriptor)); const listener = typeof type === "string" ? this[`on${type}`] : undefined; if (typeof listener === "function" && isFunctionEventCallback(listener)) { matched.push({ type, callback: listener, [EventDescriptorSymbol]: true }); } return matched; } addEventListener(type, callback, options) { const listener = { ...options, isListening: () => !!this.#listeners?.find(matchEventCallback(type, callback)), descriptor: { [EventDescriptorSymbol]: true, ...options, type, callback }, timestamp: Date.now() }; if (listener.isListening()) { return; } this.#listeners?.push(listener.descriptor); } removeEventListener(type, callback, options) { if (!isFunctionEventCallback(callback)) { return; } const externalListeners = this[EventTargetListeners$1] ?? this.#listeners ?? []; const externalIndex = externalListeners.findIndex(matchEventCallback(type, callback, options)); if (externalIndex === -1) { return; } const index = this.#listeners?.findIndex(matchEventCallback(type, callback, options)) ?? -1; if (index !== -1) { this.#listeners?.splice(index, 1); } const descriptor = externalListeners[externalIndex]; if (descriptor) { this[EventTargetListenersIgnore]?.add(descriptor); } } hasEventListener(type, callback) { if (callback && !isFunctionEventCallback(callback)) { return false; } const foundIndex = this.#listeners?.findIndex(matchEventCallback(type, callback)) ?? -1; return foundIndex > -1; } } class AsyncEventTarget extends EventTargetListeners { [EventTargetListenersThis]; constructor(thisValue = undefined) { super(); this[EventTargetListenersThis] = thisValue; } async dispatchEvent(event) { const listeners = this[EventTargetListenersMatch]?.(event.type) ?? []; // Don't even dispatch an aborted event if (isSignalEvent(event) && event.signal.aborted) { throw new AbortError(); } const parallel = isParallelEvent(event); const promises = []; for (let index = 0; index < listeners.length; index += 1) { const descriptor = listeners[index]; const promise = (async () => { // Remove the listener before invoking the callback // This ensures that inside of the callback causes no more additional event triggers to this // listener if (descriptor.once) { // by passing the descriptor as the options, we get an internal redirect // that forces an instance level object equals, meaning // we will only remove _this_ descriptor! this.removeEventListener(descriptor.type, descriptor.callback, descriptor); } await descriptor.callback.call(this[EventTargetListenersThis] ?? this, event); })(); if (!parallel) { try { await promise; } catch (error) { if (!isSignalHandled(event, error)) { await Promise.reject(error); } } if (isSignalEvent(event) && event.signal.aborted) { // bye return; } } else { promises.push(promise); } } if (promises.length) { // Allows for all promises to settle finish so we can stay within the event, we then // will utilise Promise.all which will reject with the first rejected promise const results = await Promise.allSettled(promises); const rejected = results.filter((result) => { return result.status === "rejected"; }); if (rejected.length) { let unhandled = rejected; // If the event was aborted, then allow abort errors to occur, and handle these as handled errors // The dispatcher does not care about this because they requested it // // There may be other unhandled errors that are more pressing to the task they are doing. // // The dispatcher can throw an abort error if they need to throw it up the chain if (isSignalEvent(event) && event.signal.aborted) { unhandled = unhandled.filter(result => !isSignalHandled(event, result.reason)); } if (unhandled.length === 1) { await Promise.reject(unhandled[0].reason); throw unhandled[0].reason; // We shouldn't get here } else if (unhandled.length > 1) { throw new AggregateError(unhandled.map(({ reason }) => reason)); } } } } } var asyncEventTarget = /*#__PURE__*/Object.freeze({ __proto__: null, AsyncEventTarget: AsyncEventTarget }); const defaultModule = { EventTarget: AsyncEventTarget, AsyncEventTarget, SyncEventTarget: AsyncEventTarget }; let module; try { module = await Promise.resolve().then(function () { return asyncEventTarget; }); console.log("Using @virtualstate/app-history/event-target", module); } catch { console.log("Using default"); module = defaultModule; } const EventTargetImplementation = module.EventTarget || module.SyncEventTarget || module.AsyncEventTarget; function assertEventTarget(target) { if (typeof target !== "function") { throw new Error("Could not load EventTarget implementation"); } } class EventTarget$1 extends AsyncEventTarget { constructor(...args) { super(); if (EventTargetImplementation) { assertEventTarget(EventTargetImplementation); const { dispatchEvent } = new EventTargetImplementation(...args); this.dispatchEvent = dispatchEvent; } } } class AppHistoryEventTarget extends EventTarget$1 { addEventListener(type, listener, options) { assertEventCallback(listener); return super.addEventListener(type, listener, typeof options === "boolean" ? { once: options } : options); function assertEventCallback(listener) { if (typeof listener !== "function") throw new Error("Please us the function variant of event listener"); } } removeEventListener(type, listener, options) { assertEventCallback(listener); return super.removeEventListener(type, listener); function assertEventCallback(listener) { if (typeof listener !== "function") throw new Error("Please us the function variant of event listener"); } } } const { v4 } = await Promise.resolve().then(function () { return _rollup_plugin_ignore_empty_module_placeholder$1; }) .catch(console.error) .catch(() => undefined) .then((mod) => mod ?? ({ v4() { return `0101010-0101010-${Math.random()}`.replace(".", ""); } })); const AppHistoryEntryNavigationType = Symbol.for("@virtualstate/app-history/entry/navigationType"); const AppHistoryEntryKnownAs = Symbol.for("@virtualstate/app-history/entry/knownAs"); const AppHistoryEntrySetState = Symbol.for("@virtualstate/app-history/entry/setState"); class AppHistoryEntry extends AppHistoryEventTarget { #index; #state; get index() { return typeof this.#index === "number" ? this.#index : this.#index(); } key; id; url; sameDocument; get [AppHistoryEntryNavigationType]() { return this.#options.navigationType; } get [AppHistoryEntryKnownAs]() { const set = new Set(this.#options[AppHistoryEntryKnownAs]); set.add(this.id); return set; } #options; get [EventTargetListeners$1]() { return [...(super[EventTargetListeners$1] ?? []), ...(this.#options[EventTargetListeners$1] ?? [])]; } constructor(init) { super(); this.#options = init; this.key = init.key || v4(); this.id = v4(); this.url = init.url ?? undefined; this.#index = init.index; this.sameDocument = init.sameDocument ?? true; this.#state = init.state; } getState() { const state = this.#state; /** * https://github.com/WICG/app-history/blob/7c0332b30746b14863f717404402bc49e497a2b2/spec.bs#L1406 * Note that in general, unless the state value is a primitive, entry.getState() !== entry.getState(), since a fresh copy is returned each time. */ if (typeof state === "undefined" || typeof state === "number" || typeof state === "boolean" || typeof state === "symbol" || typeof state === "bigint" || typeof state === "string") { return state; } if (typeof state === "function") { console.warn("State passed to appHistory.navigate was a function, this may be unintentional"); console.warn("Unless a state value is primitive, with a standard implementation of appHistory"); console.warn("your state value will be serialized and deserialized before this point, meaning"); console.warn("a function would not be usable."); } return { ...state }; } [AppHistoryEntrySetState](state) { this.#state = state; } } /** * @param handleCatch rejected promises automatically to allow free usage */ function deferred(handleCatch) { let resolve = undefined, reject = undefined; const promise = new Promise((resolveFn, rejectFn) => { resolve = resolveFn; reject = rejectFn; }); ok(resolve); ok(reject); return { resolve, reject, promise: handleCatch ? (promise.catch(handleCatch)) : promise }; } function ok(value) { if (!value) { throw new Error("Value not provided"); } } /** * @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); } /** * @author Toru Nagashima <https://github.com/mysticatea> * See LICENSE file in root directory for full license. */ /** * The signal class. * @see https://dom.spec.whatwg.org/#abortsignal */ class AbortSignal extends EventTarget { /** * AbortSignal cannot be constructed directly. */ constructor() { super(); throw new TypeError("AbortSignal cannot be constructed directly"); } /** * Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise. */ get aborted() { const aborted = abortedFlags.get(this); if (typeof aborted !== "boolean") { throw new TypeError(`Expected 'this' to be an 'AbortSignal' object, but got ${this === null ? "null" : typeof this}`); } return aborted; } } defineEventAttribute(AbortSignal.prototype, "abort"); /** * Create an AbortSignal object. */ function createAbortSignal() { const signal = Object.create(AbortSignal.prototype); EventTarget.call(signal); abortedFlags.set(signal, false); return signal; } /** * Abort a given signal. */ function abortSignal(signal) { if (abortedFlags.get(signal) !== false) { return; } abortedFlags.set(signal, true); signal.dispatchEvent({ type: "abort" }); } /** * Aborted flag for each instances. */ const abortedFlags = new WeakMap(); // Properties should be enumerable. Object.defineProperties(AbortSignal.prototype, { aborted: { enumerable: true }, }); // `toString()` should return `"[object AbortSignal]"` if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") { Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, { configurable: true, value: "AbortSignal", }); } /** * The AbortController. * @see https://dom.spec.whatwg.org/#abortcontroller */ class AbortController { /** * Initialize this controller. */ constructor() { signals.set(this, createAbortSignal()); } /** * Returns the `AbortSignal` object associated with this object. */ get signal() { return getSignal(this); } /** * Abort and signal to any observers that the associated activity is to be aborted. */ abort() { abortSignal(getSignal(this)); } } /** * Associated signals. */ const signals = new WeakMap(); /** * Get the associated signal of a given controller. */ function getSignal(controller) { const signal = signals.get(controller); if (signal == null) { throw new TypeError(`Expected 'this' to be an 'AbortController' object, but got ${controller === null ? "null" : typeof controller}`); } return signal; } // Properties should be enumerable. Object.defineProperties(AbortController.prototype, { signal: { enumerable: true }, abort: { enumerable: true }, }); if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") { Object.defineProperty(AbortController.prototype, Symbol.toStringTag, { configurable: true, value: "AbortController", }); } const Rollback = Symbol.for("@virtualstate/app-history/rollback"); const Unset = Symbol.for("@virtualstate/app-history/unset"); const AppHistoryTransitionParentEventTarget = Symbol.for("@virtualstate/app-history/transition/parentEventTarget"); const AppHistoryTransitionFinishedDeferred = Symbol.for("@virtualstate/app-history/transition/deferred/finished"); const AppHistoryTransitionCommittedDeferred = Symbol.for("@virtualstate/app-history/transition/deferred/committed"); const AppHistoryTransitionNavigationType = Symbol.for("@virtualstate/app-history/transition/navigationType"); const AppHistoryTransitionInitialEntries = Symbol.for("@virtualstate/app-history/transition/entries/initial"); const AppHistoryTransitionFinishedEntries = Symbol.for("@virtualstate/app-history/transition/entries/finished"); const AppHistoryTransitionInitialIndex = Symbol.for("@virtualstate/app-history/transition/index/initial"); const AppHistoryTransitionFinishedIndex = Symbol.for("@virtualstate/app-history/transition/index/finished"); const AppHistoryTransitionEntry = Symbol.for("@virtualstate/app-history/transition/entry"); const AppHistoryTransitionIsCommitted = Symbol.for("@virtualstate/app-history/transition/isCommitted"); const AppHistoryTransitionIsFinished = Symbol.for("@virtualstate/app-history/transition/isFinished"); const AppHistoryTransitionIsRejected = Symbol.for("@virtualstate/app-history/transition/isRejected"); const AppHistoryTransitionKnown = Symbol.for("@virtualstate/app-history/transition/known"); const AppHistoryTransitionPromises = Symbol.for("@virtualstate/app-history/transition/promises"); const AppHistoryTransitionWhile = Symbol.for("@virtualstate/app-history/transition/while"); const AppHistoryTransitionIsOngoing = Symbol.for("@virtualstate/app-history/transition/isOngoing"); const AppHistoryTransitionIsPending = Symbol.for("@virtualstate/app-history/transition/isPending"); const AppHistoryTransitionWait = Symbol.for("@virtualstate/app-history/transition/wait"); const AppHistoryTransitionPromiseResolved = Symbol.for("@virtualstate/app-history/transition/promise/resolved"); const AppHistoryTransitionRejected = Symbol.for("@virtualstate/app-history/transition/rejected"); const AppHistoryTransitionCommit = Symbol.for("@virtualstate/app-history/transition/commit"); const AppHistoryTransitionFinish = Symbol.for("@virtualstate/app-history/transition/finish"); const AppHistoryTransitionStart = Symbol.for("@virtualstate/app-history/transition/start"); const AppHistoryTransitionStartDeadline = Symbol.for("@virtualstate/app-history/transition/start/deadline"); const AppHistoryTransitionError = Symbol.for("@virtualstate/app-history/transition/error"); const AppHistoryTransitionFinally = Symbol.for("@virtualstate/app-history/transition/finally"); const AppHistoryTransitionAbort = Symbol.for("@virtualstate/app-history/transition/abort"); class AppHistoryTransition extends EventTarget$1 { finished; /** * @experimental */ committed; from; navigationType; #options; [AppHistoryTransitionFinishedDeferred] = deferred(); [AppHistoryTransitionCommittedDeferred] = deferred(); get [AppHistoryTransitionIsPending]() { return !!this.#promises.size; } get [AppHistoryTransitionNavigationType]() { return this.#options[AppHistoryTransitionNavigationType]; } get [AppHistoryTransitionInitialEntries]() { return this.#options[AppHistoryTransitionInitialEntries]; } get [AppHistoryTransitionInitialIndex]() { return this.#options[AppHistoryTransitionInitialIndex]; } [AppHistoryTransitionFinishedEntries]; [AppHistoryTransitionFinishedIndex]; [AppHistoryTransitionIsCommitted] = false; [AppHistoryTransitionIsFinished] = false; [AppHistoryTransitionIsRejected] = false; [AppHistoryTransitionIsOngoing] = false; [AppHistoryTransitionKnown] = new Set(); [AppHistoryTransitionEntry]; #promises = new Set(); #rolledBack = false; #abortController = new AbortController(); get signal() { return this.#abortController.signal; } get [AppHistoryTransitionPromises]() { return this.#promises; } constructor(init) { super(); this[AppHistoryTransitionFinishedDeferred] = init[AppHistoryTransitionFinishedDeferred] ?? this[AppHistoryTransitionFinishedDeferred]; this[AppHistoryTransitionCommittedDeferred] = init[AppHistoryTransitionCommittedDeferred] ?? this[AppHistoryTransitionCommittedDeferred]; this.#options = init; const finished = this.finished = this[AppHistoryTransitionFinishedDeferred].promise; const committed = this.committed = this[AppHistoryTransitionCommittedDeferred].promise; // Auto catching abort void finished.catch(error => error); void committed.catch(error => error); this.from = init.from; this.navigationType = init.navigationType; this[AppHistoryTransitionFinishedEntries] = init[AppHistoryTransitionFinishedEntries]; this[AppHistoryTransitionFinishedIndex] = init[AppHistoryTransitionFinishedIndex]; const known = init[AppHistoryTransitionKnown]; if (known) { for (const entry of known) { this[AppHistoryTransitionKnown].add(entry); } } this[AppHistoryTransitionEntry] = init[AppHistoryTransitionEntry]; // Event listeners { // Events to promises { this.addEventListener(AppHistoryTransitionCommit, this.#onCommitPromise, { once: true }); this.addEventListener(AppHistoryTransitionFinish, this.#onFinishPromise, { once: true }); } // Events to property setters { this.addEventListener(AppHistoryTransitionCommit, this.#onCommitSetProperty, { once: true }); this.addEventListener(AppHistoryTransitionFinish, this.#onFinishSetProperty, { once: true }); } // Rejection + Abort { this.addEventListener(AppHistoryTransitionError, this.#onError, { once: true }); this.addEventListener(AppHistoryTransitionAbort, () => { if (!this[AppHistoryTransitionIsFinished]) { return this[AppHistoryTransitionRejected](new AbortError()); } }); } // Proxy all events from this transition onto entry + the parent event target // // The parent could be another transition, or the appHistory, this allows us to // "bubble up" events layer by layer // // In this implementation, this allows individual transitions to "intercept" navigate and break the child // transition from happening // // TODO WARN this may not be desired behaviour vs standard spec'd appHistory { this.addEventListener("*", this[AppHistoryTransitionEntry].dispatchEvent.bind(this[AppHistoryTransitionEntry])); this.addEventListener("*", init[AppHistoryTransitionParentEventTarget].dispatchEvent.bind(init[AppHistoryTransitionParentEventTarget])); } } } rollback = (options) => { // console.log({ rolled: this.#rolledBack }); if (this.#rolledBack) { // TODO throw new InvalidStateError("Rollback invoked multiple times: Please raise an issue at https://github.com/virtualstate/app-history with the use case where you want to use a rollback multiple times, this may have been unexpected behaviour"); } this.#rolledBack = true; return this.#options.rollback(options); }; #onCommitSetProperty = () => { this[AppHistoryTransitionIsCommitted] = true; }; #onFinishSetProperty = () => { this[AppHistoryTransitionIsFinished] = true; }; #onFinishPromise = () => { // console.log("onFinishPromise") this[AppHistoryTransitionFinishedDeferred].resolve(this[AppHistoryTransitionEntry]); }; #onCommitPromise = () => { if (this.signal.aborted) ; else { this[AppHistoryTransitionCommittedDeferred].resolve(this[AppHistoryTransitionEntry]); } }; #onError = (event) => { return this[AppHistoryTransitionRejected](event.error); }; [AppHistoryTransitionPromiseResolved] = (...promises) => { for (const promise of promises) { this.#promises.delete(promise); } }; [AppHistoryTransitionRejected] = async (reason) => { if (this[AppHistoryTransitionIsRejected]) return; this[AppHistoryTransitionIsRejected] = true; this[AppHistoryTransitionAbort](); const navigationType = this[AppHistoryTransitionNavigationType]; // console.log({ navigationType, reason, entry: this[AppHistoryTransitionEntry] }); if ((typeof navigationType === "string" || navigationType === Rollback)) { // console.log("navigateerror", { reason, z: isInvalidStateError(reason) }); await this.dispatchEvent({ type: "navigateerror", error: reason, get message() { if (reason instanceof Error) { return reason.message; } return `${reason}`; } }); // console.log("navigateerror finishe