import { ref } from "vue"; export type HoldMode = "loop" | "list"; // feedback「長押しでの選択の切り替え」で 500ms / 450ms は早いとされたため延ばす。 export const HOLD_DELAY = 600; export const HOLD_INTERVAL = 1000; type Options = { parentLabel: (id: string | null) => string; pressTargets: (id: string) => string[]; mode: () => HoldMode; }; export type LongPressPick = { id: string; shift: boolean }; // キーの長押しで、キー → 親 → … の順に選択先を巡回する。 // loop: 巡回だけで選び、長押し後に動かすと選んだ先をドラッグする。 // list: 巡回は同じだが、ポインタがリストに入ると巡回をやめ、指している行を離した時点で選ぶ。ドラッグはしない。 // ポインタの扱い(ドラッグ・クリック)は呼び出し側が持ち、ここは候補と表示だけを持つ。 export function usePhyLongPress(options: Options) { const open = ref(false); const mode = ref("loop"); const pos = ref({ x: 0, y: 0 }); const candidates = ref([]); const names = ref([]); const index = ref(0); // 巡回が進むたびに増やし、インジケータのアニメーションをやり直させる const step = ref(0); // list 方式で、ポインタがリストに入ったあと const pointing = ref(false); let timer: number | undefined; let cycle: number | undefined; let shift = false; let activated = false; function reset() { activated = false; } function stopCycle() { if (cycle) window.clearInterval(cycle); cycle = undefined; } function start(event: PointerEvent, keyId: string) { candidates.value = options.pressTargets(keyId); index.value = 0; shift = event.shiftKey; timer = window.setTimeout(() => { timer = undefined; if (candidates.value.length <= 1) return; activated = true; mode.value = options.mode(); names.value = candidates.value.map((id, i) => (i === 0 ? "キー" : options.parentLabel(id))); index.value = 1; step.value++; pointing.value = false; pos.value = { x: event.clientX, y: event.clientY }; open.value = true; cycle = window.setInterval(() => { index.value = (index.value + 1) % candidates.value.length; step.value++; }, HOLD_INTERVAL); }, HOLD_DELAY); } function cycling() { return open.value && mode.value === "loop"; } function picking() { return open.value && mode.value === "list"; } // list 方式: ポインタの下にあるリストの行を指す。リストの外では直前の状態を保つ。 function point(clientX: number, clientY: number) { const row = document .elementsFromPoint(clientX, clientY) .find((element) => element instanceof HTMLElement && element.dataset.holdIndex !== undefined); if (!(row instanceof HTMLElement)) return; stopCycle(); pointing.value = true; index.value = Number(row.dataset.holdIndex); } function cancelPending() { if (timer && !open.value) { window.clearTimeout(timer); timer = undefined; } } function take(): LongPressPick { const id = candidates.value[index.value]!; stopCycle(); open.value = false; return { id, shift }; } function finish(): LongPressPick | null { if (timer) window.clearTimeout(timer); timer = undefined; stopCycle(); const id = open.value ? candidates.value[index.value] : undefined; open.value = false; return id ? { id, shift } : null; } function wasActivated() { return activated; } return { open, mode, pos, candidates, names, index, step, pointing, reset, start, cycling, picking, point, cancelPending, take, finish, wasActivated, }; }