Files
Beyond-KLE/mock/src/ui/phy/usePhyLongPress.ts
T
ewdernaandClaude Opus 5.5 b383fcf844 mock: 長押しにループ方式とリスト方式を入れ、ツールバーで切り替える(T-30 単位7b)
どちらも押した位置に円形インジケータを、少し離して選択先のリストを出す。
間隔は600msで始まり1000msごとに進む(500ms/450msは早いという所感への対応)。
リスト方式は、ポインタがリストに入ると巡回を止め、離したときに指している行で確定する。
リストへ向かう動きをドラッグと取り違えないよう、リスト方式では長押し後のドラッグをしない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShoEXYqi8PWTjB9E86Ltot
2026-10-23 21:07:04 +09:00

134 lines
3.8 KiB
TypeScript

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<HoldMode>("loop");
const pos = ref({ x: 0, y: 0 });
const candidates = ref<string[]>([]);
const names = ref<string[]>([]);
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,
};
}