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
This commit is contained in:
@@ -1,19 +1,34 @@
|
||||
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 label = ref("");
|
||||
const labelPos = ref({ x: 0, y: 0 });
|
||||
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;
|
||||
@@ -23,31 +38,54 @@ export function usePhyLongPress(options: Options) {
|
||||
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;
|
||||
labelPos.value = { x: event.clientX, y: event.clientY };
|
||||
label.value = `${options.parentLabel(candidates.value[1]!)} 2/${candidates.value.length}`;
|
||||
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;
|
||||
const id = candidates.value[index.value]!;
|
||||
const name = index.value === 0 ? "キー" : options.parentLabel(id);
|
||||
label.value = `${name} ${index.value + 1}/${candidates.value.length}`;
|
||||
}, 450);
|
||||
}, 500);
|
||||
step.value++;
|
||||
}, HOLD_INTERVAL);
|
||||
}, HOLD_DELAY);
|
||||
}
|
||||
|
||||
function cycling() {
|
||||
return label.value !== "";
|
||||
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 && !label.value) {
|
||||
if (timer && !open.value) {
|
||||
window.clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
@@ -55,16 +93,17 @@ export function usePhyLongPress(options: Options) {
|
||||
|
||||
function take(): LongPressPick {
|
||||
const id = candidates.value[index.value]!;
|
||||
if (cycle) window.clearInterval(cycle);
|
||||
label.value = "";
|
||||
stopCycle();
|
||||
open.value = false;
|
||||
return { id, shift };
|
||||
}
|
||||
|
||||
function finish(): LongPressPick | null {
|
||||
if (timer) window.clearTimeout(timer);
|
||||
if (cycle) window.clearInterval(cycle);
|
||||
const id = label.value ? candidates.value[index.value] : undefined;
|
||||
label.value = "";
|
||||
timer = undefined;
|
||||
stopCycle();
|
||||
const id = open.value ? candidates.value[index.value] : undefined;
|
||||
open.value = false;
|
||||
return id ? { id, shift } : null;
|
||||
}
|
||||
|
||||
@@ -73,13 +112,19 @@ export function usePhyLongPress(options: Options) {
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
labelPos,
|
||||
open,
|
||||
mode,
|
||||
pos,
|
||||
candidates,
|
||||
names,
|
||||
index,
|
||||
step,
|
||||
pointing,
|
||||
reset,
|
||||
start,
|
||||
cycling,
|
||||
picking,
|
||||
point,
|
||||
cancelPending,
|
||||
take,
|
||||
finish,
|
||||
|
||||
Reference in New Issue
Block a user