Anki/ts/lib/sveltelib/handler-list.ts
Damien Elmes 9f55cf26fc
Switch to SvelteKit (#3077)
* Update to latest Node LTS

* Add sveltekit

* Split tslib into separate @generated and @tslib components

SvelteKit's path aliases don't support multiple locations, so our old
approach of using @tslib to refer to both ts/lib and out/ts/lib will no
longer work. Instead, all generated sources and their includes are
placed in a separate out/ts/generated folder, and imported via @generated
instead. This also allows us to generate .ts files, instead of needing
to output separate .d.ts and .js files.

* Switch package.json to module type

* Avoid usage of baseUrl

Incompatible with SvelteKit

* Move sass into ts; use relative links

SvelteKit's default sass support doesn't allow overriding loadPaths

* jest->vitest, graphs example working with yarn dev

* most pages working in dev mode

* Some fixes after rebasing

* Fix/silence some svelte-check errors

* Get image-occlusion working with Fabric types

* Post-rebase lock changes

* Editor is now checked

* SvelteKit build integrated into ninja

* Use the new SvelteKit entrypoint for pages like congrats/deck options/etc

* Run eslint once for ts/**; fix some tests

* Fix a bunch of issues introduced when rebasing over latest main

* Run eslint fix

* Fix remaining eslint+pylint issues; tests now all pass

* Fix some issues with a clean build

* Latest bufbuild no longer requires @__PURE__ hack

* Add a few missed dependencies

* Add yarn.bat to fix Windows build

* Fix pages failing to show when ANKI_API_PORT not defined

* Fix svelte-check and vitest on Windows

* Set node path in ./yarn

* Move svelte-kit output to ts/.svelte-kit

Sadly, I couldn't figure out a way to store it in out/ if out/ is
a symlink, as it breaks module resolution when SvelteKit is run.

* Allow HMR inside Anki

* Skip SvelteKit build when HMR is defined

* Fix some post-rebase issues

I should have done a normal merge instead.
2024-03-31 09:16:31 +01:00

174 lines
4.5 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { Callback } from "@tslib/typing";
import type { Readable, Writable } from "svelte/store";
import { writable } from "svelte/store";
type Handler<T> = (args: T) => Promise<void>;
interface HandlerAccess<T> {
callback: Handler<T>;
clear(): void;
}
class TriggerItem<T> {
#active: Writable<boolean>;
constructor(
private setter: (handler: Handler<T>, clear: Callback) => void,
private clear: Callback,
) {
this.#active = writable(false);
}
/**
* A store which indicates whether the trigger is currently turned on.
*/
get active(): Readable<boolean> {
return this.#active;
}
/**
* Deactivate the trigger. Can be safely called multiple times.
*/
off(): void {
this.#active.set(false);
this.clear();
}
on(handler: Handler<T>): void {
this.setter(handler, () => this.off());
this.#active.set(true);
}
}
interface HandlerOptions {
once: boolean;
}
export class HandlerList<T> {
#list: HandlerAccess<T>[] = [];
/**
* Returns a `TriggerItem`, which can be used to attach event handlers.
* This TriggerItem exposes an additional `active` store. This can be
* useful, if other components need to react to the input handler being active.
*/
trigger(options?: Partial<HandlerOptions>): TriggerItem<T> {
const once = options?.once ?? false;
let handler: Handler<T> | null = null;
return new TriggerItem(
(callback: Handler<T>, doClear: Callback): void => {
const handlerAccess = {
callback(args: T): Promise<void> {
const result = callback(args);
if (once) {
doClear();
}
return result;
},
clear(): void {
if (once) {
doClear();
}
},
};
this.#list.push(handlerAccess);
handler = handlerAccess.callback;
},
() => {
if (handler) {
this.off(handler);
handler = null;
}
},
);
}
/**
* Attaches an event handler.
* @returns a callback, which removes the event handler. Alternatively,
* you can call `off` on the HandlerList.
*/
on(handler: Handler<T>, options?: Partial<HandlerOptions>): Callback {
const once = options?.once ?? false;
let offHandler: Handler<T> | null = null;
const off = (): void => {
if (offHandler) {
this.off(offHandler);
offHandler = null;
}
};
const handlerAccess = {
callback: (args: T): Promise<void> => {
const result = handler(args);
if (once) {
off();
}
return result;
},
clear(): void {
if (once) {
off();
}
},
};
offHandler = handlerAccess.callback;
this.#list.push(handlerAccess);
return off;
}
private off(handler: Handler<T>): void {
const index = this.#list.findIndex(
(value: HandlerAccess<T>): boolean => value.callback === handler,
);
if (index >= 0) {
this.#list.splice(index, 1);
}
}
get length(): number {
return this.#list.length;
}
dispatch(args: T): Promise<void> {
const promises: Promise<void>[] = [];
for (const { callback } of [...this]) {
promises.push(callback(args));
}
return Promise.all(promises) as unknown as Promise<void>;
}
clear(): void {
for (const { clear } of [...this]) {
clear();
}
}
[Symbol.iterator](): Iterator<HandlerAccess<T>, null, unknown> {
const list = this.#list;
let step = 0;
return {
next(): IteratorResult<HandlerAccess<T>, null> {
if (step >= list.length) {
return { value: null, done: true };
}
return { value: list[step++], done: false };
},
};
}
}
export type { TriggerItem };