- 캔버스 · 명령줄에서 Space(수정키 없음 · 길게 눌러 반복 아님) = Enter · 글 단계(TEXT · MTEXT · 표 칸 · 이름 등)는 띄어쓰기 - 글 단계 판정 = sequence-tool 단계 meta kind 'text'(activeToolWantsFreeText) - 빈 입력 + 선택 도구 대기에서 키보드 Enter · Space = repeatLastCommand · 빈 우클릭은 확정만 - 한글 조합 중 Space · Shift/Ctrl+Space 무시 - 시험 ime-sink: 「L␣」 = LINE · TEXT 띄어쓰기 · 조합 중 · 수정키 · 반복 무시 · 빈 Space · Enter 반복 · 우클릭 반복 안 함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
/**
|
|
* 한글 IME 받개 — 캔버스에는 글 칸이 없어 조합(compositionstart/update/end)이 생기지 않는다.
|
|
* 문자를 기다리는 단계(TEXT · MTEXT · 표 칸 · 지시선 …)에서만 숨은 input 에 초점을 줘 조합을 받고,
|
|
* 끝난 글자를 넘긴다. 명령 단계에서는 초점을 풀어 한글 자판이어도 L · REC 같은 영문 명령이 들어간다.
|
|
*/
|
|
import { getActiveToolActor } from '../state.ts';
|
|
|
|
/** 조합 중 키 — 단축키·글자 처리가 먹지 않고 IME 에 넘긴다 */
|
|
export const isImeKey = (evt: KeyboardEvent): boolean =>
|
|
evt.isComposing || evt.keyCode === 229 || evt.key === 'Process';
|
|
|
|
/** 활성 도구의 지금 단계가 TEXT_INPUT 을 받는가 */
|
|
export function activeToolAcceptsText(): boolean {
|
|
const snapshot = getActiveToolActor()?.getSnapshot();
|
|
return !!snapshot?.machine?.states?.[snapshot?.value]?.config?.on?.TEXT_INPUT;
|
|
}
|
|
|
|
/** 지금 단계가 자유 글(TEXT · MTEXT · 표 칸 · 이름 …)을 받는가 — Space 가 띄어쓰기(아니면 Enter) */
|
|
export function activeToolWantsFreeText(): boolean {
|
|
const meta = getActiveToolActor()?.getSnapshot()?.getMeta?.() as
|
|
| Record<string, { kind?: string } | undefined>
|
|
| undefined;
|
|
return Object.values(meta ?? {}).some((stateMeta) => stateMeta?.kind === 'text');
|
|
}
|
|
|
|
/** Enter 로 받을 Space — 수정키(Shift · Ctrl · Alt · Meta: 한/영 전환 등) · 길게 눌러 반복은 아님 */
|
|
export const isPlainSpace = (
|
|
evt: Pick<KeyboardEvent, 'key' | 'shiftKey' | 'ctrlKey' | 'altKey' | 'metaKey' | 'repeat'>
|
|
): boolean =>
|
|
evt.key === ' ' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey && !evt.metaKey && !evt.repeat;
|
|
|
|
export class ImeSink {
|
|
/** 조합 중인 글자 — 커서 옆 입력 칸에 미리 보인다 */
|
|
public composing = '';
|
|
public element: HTMLInputElement | null = null;
|
|
|
|
constructor(private readonly onCommit: (text: string) => void) {}
|
|
|
|
public attach() {
|
|
const el = document.createElement('input');
|
|
el.className = 'cad-ime-sink';
|
|
el.tabIndex = -1;
|
|
el.autocomplete = 'off';
|
|
el.setAttribute('aria-hidden', 'true');
|
|
el.style.cssText =
|
|
'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;border:0;padding:0;pointer-events:none';
|
|
el.addEventListener('compositionupdate', (evt) => this.handleCompositionUpdate(evt.data));
|
|
el.addEventListener('compositionend', (evt) => this.handleCompositionEnd(evt.data));
|
|
el.addEventListener('input', (evt) => this.handleInput(evt as InputEvent));
|
|
document.body.appendChild(el);
|
|
this.element = el;
|
|
}
|
|
|
|
public handleCompositionUpdate(data: string) {
|
|
this.composing = data ?? '';
|
|
}
|
|
|
|
public handleCompositionEnd(data: string) {
|
|
this.composing = '';
|
|
if (this.element) this.element.value = '';
|
|
if (data) this.onCommit(data);
|
|
}
|
|
|
|
/** 조합 직후 IME 가 바로 넣는 글자(띄어쓰기 · 숫자) — 조합 중 input 은 compositionend 가 맡는다 */
|
|
public handleInput(evt: Pick<InputEvent, 'isComposing' | 'data'>) {
|
|
if (evt.isComposing || !evt.data) return;
|
|
if (this.element) this.element.value = '';
|
|
this.onCommit(evt.data);
|
|
}
|
|
|
|
/** 문자를 기다리면 받개에 초점 · 아니면 놓음. 다른 입력 칸(명령줄 · 대화상자)의 초점은 뺏지 않는다 */
|
|
public sync(wantsText: boolean) {
|
|
const el = this.element;
|
|
if (!el) return;
|
|
const active = document.activeElement;
|
|
if (wantsText && !active?.closest('input, textarea, select, [contenteditable]')) {
|
|
el.focus({ preventScroll: true });
|
|
} else if (!wantsText && active === el && !this.composing) {
|
|
el.blur();
|
|
}
|
|
}
|
|
}
|