|
|
|
|
@ -32,24 +32,35 @@ import { SafePolygon } from '../../layers/floating/safe-polygon';
|
|
|
|
|
|
|
|
|
|
import { contextMenuMorfo } from '../../../morfo/components/context-menu';
|
|
|
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
|
import type { ActiveTimers, TimerHandle } from '$timer';
|
|
|
|
|
|
|
|
|
|
const attrs = createAttrs(contextMenuMorfo);
|
|
|
|
|
|
|
|
|
|
// ── Typeahead helper ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class Typeahead {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly timers: ActiveTimers,
|
|
|
|
|
private readonly key: () => string
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
private search = '';
|
|
|
|
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
private timer: TimerHandle | null = null;
|
|
|
|
|
|
|
|
|
|
handle(key: string, items: HTMLElement[]): HTMLElement | null {
|
|
|
|
|
if (key.length !== 1) return null;
|
|
|
|
|
|
|
|
|
|
this.search += key.toLowerCase();
|
|
|
|
|
if (this.timer) clearTimeout(this.timer);
|
|
|
|
|
this.timer = setTimeout(() => {
|
|
|
|
|
this.search = '';
|
|
|
|
|
this.timer = null;
|
|
|
|
|
}, 500);
|
|
|
|
|
this.cancelTimer();
|
|
|
|
|
this.timer = this.timers.schedule(
|
|
|
|
|
this.key(),
|
|
|
|
|
500,
|
|
|
|
|
() => {
|
|
|
|
|
this.search = '';
|
|
|
|
|
this.timer = null;
|
|
|
|
|
},
|
|
|
|
|
{ replace: true, meta: { component: 'context-menu', action: 'typeahead-reset' } }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const match = items.find((el) => {
|
|
|
|
|
const text = el.getAttribute('data-text-value') || el.textContent || '';
|
|
|
|
|
@ -61,8 +72,12 @@ class Typeahead {
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
this.search = '';
|
|
|
|
|
this.cancelTimer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cancelTimer() {
|
|
|
|
|
if (this.timer) {
|
|
|
|
|
clearTimeout(this.timer);
|
|
|
|
|
this.timer.cancel();
|
|
|
|
|
this.timer = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -102,7 +117,7 @@ export class ContextMenuProvider {
|
|
|
|
|
|
|
|
|
|
readonly floatingProvider: FloatingProvider;
|
|
|
|
|
readonly contentPresence: Presence;
|
|
|
|
|
readonly typeahead = new Typeahead();
|
|
|
|
|
readonly typeahead: Typeahead;
|
|
|
|
|
|
|
|
|
|
contentId = state('');
|
|
|
|
|
triggerId = state('');
|
|
|
|
|
@ -120,6 +135,10 @@ export class ContextMenuProvider {
|
|
|
|
|
private constructor(opts: ContextMenuOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.soma = Soma.require();
|
|
|
|
|
this.typeahead = new Typeahead(
|
|
|
|
|
this.soma.uix.timers,
|
|
|
|
|
() => `soma:context-menu:${this.opts.id.current}:typeahead`
|
|
|
|
|
);
|
|
|
|
|
this.runtime = this.soma.runtime(contextMenuMorfo, {});
|
|
|
|
|
this.runtimePart = this.runtime.part('provider', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
@ -788,7 +807,7 @@ export class ContextMenuSubProvider {
|
|
|
|
|
readonly contentPresence: Presence;
|
|
|
|
|
readonly floatingProvider: FloatingProvider;
|
|
|
|
|
readonly safePolygon: SafePolygon;
|
|
|
|
|
readonly typeahead = new Typeahead();
|
|
|
|
|
readonly typeahead: Typeahead;
|
|
|
|
|
|
|
|
|
|
contentId = state('');
|
|
|
|
|
subTriggerId = state('');
|
|
|
|
|
@ -798,6 +817,11 @@ export class ContextMenuSubProvider {
|
|
|
|
|
private constructor(opts: ContextMenuSubOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
this.provider = ContextMenuProvider.require();
|
|
|
|
|
this.typeahead = new Typeahead(
|
|
|
|
|
this.provider.soma.uix.timers,
|
|
|
|
|
() =>
|
|
|
|
|
`soma:context-menu:${this.provider.opts.id.current}:${this.subTriggerId.current || this.contentId.current || 'sub'}:typeahead`
|
|
|
|
|
);
|
|
|
|
|
this.floatingProvider = FloatingProvider.create();
|
|
|
|
|
|
|
|
|
|
this.contentPresence = new Presence({
|
|
|
|
|
@ -845,7 +869,7 @@ export class ContextMenuSubTriggerProvider {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly sub: ContextMenuSubProvider;
|
|
|
|
|
private openTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
private openTimer: TimerHandle | null = null;
|
|
|
|
|
|
|
|
|
|
private constructor(opts: ContextMenuSubTriggerOpts) {
|
|
|
|
|
// A30: register subTriggerRef via `onRefChange` (attachRef fires at
|
|
|
|
|
@ -873,19 +897,24 @@ export class ContextMenuSubTriggerProvider {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private clearOpenTimeout() {
|
|
|
|
|
if (this.openTimeout) {
|
|
|
|
|
clearTimeout(this.openTimeout);
|
|
|
|
|
this.openTimeout = null;
|
|
|
|
|
if (this.openTimer) {
|
|
|
|
|
this.openTimer.cancel();
|
|
|
|
|
this.openTimer = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly onpointerenter = () => {
|
|
|
|
|
if (this.opts.disabled.current) return;
|
|
|
|
|
this.clearOpenTimeout();
|
|
|
|
|
this.openTimeout = setTimeout(() => {
|
|
|
|
|
this.openTimeout = null;
|
|
|
|
|
this.sub.handleOpen();
|
|
|
|
|
}, 100);
|
|
|
|
|
this.openTimer = this.sub.provider.soma.uix.timers.schedule(
|
|
|
|
|
this.openTimerKey,
|
|
|
|
|
100,
|
|
|
|
|
() => {
|
|
|
|
|
this.openTimer = null;
|
|
|
|
|
this.sub.handleOpen();
|
|
|
|
|
},
|
|
|
|
|
{ replace: true, meta: { component: 'context-menu', action: 'submenu-open' } }
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
readonly onpointerleave = () => {
|
|
|
|
|
@ -911,6 +940,10 @@ export class ContextMenuSubTriggerProvider {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private get openTimerKey() {
|
|
|
|
|
return `soma:context-menu:${this.opts.id.current}:submenu-open`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly props = $derived.by(() =>
|
|
|
|
|
this.runtimePart.assert({
|
|
|
|
|
...this.runtimePart.props,
|
|
|
|
|
|