|
|
|
|
@ -1,4 +1,5 @@
|
|
|
|
|
import type { ActiveDom } from '$adom';
|
|
|
|
|
import type { TimerHandle, TimerScheduler } from '$timer';
|
|
|
|
|
import type { AnnouncePriority, AnnounceApi } from './types';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -32,10 +33,11 @@ type RegionPair = {
|
|
|
|
|
a: HTMLElement;
|
|
|
|
|
b: HTMLElement;
|
|
|
|
|
toggle: boolean;
|
|
|
|
|
timer: ReturnType<typeof setTimeout> | null;
|
|
|
|
|
timer: AnnounceTimer | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type AnnouncerCache = WeakMap<Document, Map<string, RegionPair>>;
|
|
|
|
|
type AnnounceTimer = Pick<TimerHandle, 'cancel'>;
|
|
|
|
|
|
|
|
|
|
function ensurePair(
|
|
|
|
|
dom: ActiveDom,
|
|
|
|
|
@ -91,7 +93,35 @@ function ensurePair(
|
|
|
|
|
return pair;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function write(dom: ActiveDom, pair: RegionPair, text: string, timeout: number) {
|
|
|
|
|
function scheduleClear(
|
|
|
|
|
key: string,
|
|
|
|
|
timeout: number,
|
|
|
|
|
task: () => void,
|
|
|
|
|
timers?: TimerScheduler
|
|
|
|
|
): AnnounceTimer {
|
|
|
|
|
if (timers) {
|
|
|
|
|
return timers.schedule(key, timeout, task, {
|
|
|
|
|
replace: true,
|
|
|
|
|
meta: { component: 'announce', action: 'global-clear' }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const native = setTimeout(task, timeout);
|
|
|
|
|
return {
|
|
|
|
|
cancel() {
|
|
|
|
|
clearTimeout(native);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function write(
|
|
|
|
|
dom: ActiveDom,
|
|
|
|
|
pair: RegionPair,
|
|
|
|
|
text: string,
|
|
|
|
|
timeout: number,
|
|
|
|
|
timers?: TimerScheduler
|
|
|
|
|
) {
|
|
|
|
|
pair.toggle = !pair.toggle;
|
|
|
|
|
if (pair.toggle) {
|
|
|
|
|
dom.writeText(pair.a, text);
|
|
|
|
|
@ -100,12 +130,17 @@ function write(dom: ActiveDom, pair: RegionPair, text: string, timeout: number)
|
|
|
|
|
dom.writeText(pair.a, '');
|
|
|
|
|
dom.writeText(pair.b, text);
|
|
|
|
|
}
|
|
|
|
|
if (pair.timer) clearTimeout(pair.timer);
|
|
|
|
|
pair.timer = setTimeout(() => {
|
|
|
|
|
dom.writeText(pair.a, '');
|
|
|
|
|
dom.writeText(pair.b, '');
|
|
|
|
|
pair.timer = null;
|
|
|
|
|
}, timeout);
|
|
|
|
|
if (pair.timer) pair.timer.cancel();
|
|
|
|
|
pair.timer = scheduleClear(
|
|
|
|
|
`soma:announce:global:${pair.a.id}`,
|
|
|
|
|
timeout,
|
|
|
|
|
() => {
|
|
|
|
|
dom.writeText(pair.a, '');
|
|
|
|
|
dom.writeText(pair.b, '');
|
|
|
|
|
pair.timer = null;
|
|
|
|
|
},
|
|
|
|
|
timers
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -122,13 +157,19 @@ function write(dom: ActiveDom, pair: RegionPair, text: string, timeout: number)
|
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* @param defaultTimeout Ms before messages are cleared. @default 1000
|
|
|
|
|
* @param timers Optional UIX/App scheduler. When omitted, the global fallback
|
|
|
|
|
* uses native timers because this API is intentionally usable outside UIX.
|
|
|
|
|
*/
|
|
|
|
|
export function createAnnouncer(dom: ActiveDom, defaultTimeout = 1000): AnnounceApi {
|
|
|
|
|
export function createAnnouncer(
|
|
|
|
|
dom: ActiveDom,
|
|
|
|
|
defaultTimeout = 1000,
|
|
|
|
|
timers?: TimerScheduler
|
|
|
|
|
): AnnounceApi {
|
|
|
|
|
const announce: AnnounceApi['announce'] = (message, priority = 'polite', timeout) => {
|
|
|
|
|
const baseId = priority === 'assertive' ? ASSERTIVE_ID : POLITE_ID;
|
|
|
|
|
const pair = ensurePair(dom, priority, baseId);
|
|
|
|
|
if (!pair) return;
|
|
|
|
|
write(dom, pair, message, timeout ?? defaultTimeout);
|
|
|
|
|
write(dom, pair, message, timeout ?? defaultTimeout, timers);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clear: AnnounceApi['clear'] = () => {
|
|
|
|
|
@ -147,7 +188,7 @@ export function createAnnouncer(dom: ActiveDom, defaultTimeout = 1000): Announce
|
|
|
|
|
for (const pair of cache.values()) {
|
|
|
|
|
dom.writeText(pair.a, '');
|
|
|
|
|
dom.writeText(pair.b, '');
|
|
|
|
|
if (pair.timer) clearTimeout(pair.timer);
|
|
|
|
|
if (pair.timer) pair.timer.cancel();
|
|
|
|
|
pair.timer = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|