- spreadsheet_actions(키 · 메뉴 → 이름) + actions_run(몸통 · 늦게): Ctrl+Shift++ · Ctrl+- (행 · 열 통째면 바로) · F5 · Ctrl+G · Ctrl+9/0 · Ctrl+Shift+9/0 · Ctrl+B/I/U/5 · Ctrl+Shift+& · Ctrl+Shift+_ · Ctrl+; · Ctrl+PageDown/PageUp · Shift+F11 · Shift+Alt+→/← - [데이터 ▾] 에 이동 · 틀 고정 ▸ · 칸 넣기/지우기 · 텍스트 나누기 · 중복 제거 · 연속 채우기 · 윤곽 ▸ - 식 편집 중 F4 참조 $ 돌리기(cycleAbs) · 접힌 묶음 행 · 열은 방향키가 건너뜀 - 크기: 수식 분석 · 자동 필터 · 찾기 패널을 처음 쓸 때 불러옴 — 화면 27.0 KB · 합 60.9 KB - M02 [스프레드시트 시험] ORCA 수치 확인 · D 규칙 시험 9 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
252 lines
8.9 KiB
TypeScript
252 lines
8.9 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_keys.ts (주인 D)
|
|
* 엑셀 단축키 지도 — 숨은 textarea · 수식 입력줄 keydown 하나로 받음.
|
|
* 편집 중: Enter/Tab 확정 이동 · Alt+Enter 줄바꿈 · Ctrl+Enter 고름 전부 · Esc · F2 방식 바꿈 ·
|
|
* enter 방식 방향키 = 확정 이동(식 참조 자리면 가리키기).
|
|
* 고름 중: 방향(Shift 늘림 · Ctrl 끝) · Home · Ctrl+Home/End · PgUp/PgDn · Enter/Tab(고름 안 돌기) ·
|
|
* F2 · Delete · Backspace · Ctrl+A · Shift/Ctrl+Space · Ctrl+Z/Y · Ctrl+D/R · 그 밖은 hooks.more.
|
|
* 식 편집 중 F4 = 참조 $ 돌리기(A1 → $A$1 → A$1 → $A1).
|
|
* 글자 키는 막지 않음 — textarea 의 input · compositionstart 가 편집을 엶(한글 첫 글자).
|
|
* 복사 · 붙여넣기(Ctrl+C/X/V)는 막지 않음 — E 클립보드가 copy · paste 사건으로 받음.
|
|
* ========================================================================== */
|
|
|
|
import { cycleAbs } from "./spreadsheet_editor";
|
|
import { fillCells } from "./spreadsheet_fill";
|
|
import {
|
|
cycle,
|
|
LAST_R,
|
|
move,
|
|
selectAll,
|
|
selectCell,
|
|
selectCols,
|
|
selectRows,
|
|
selectSpan,
|
|
usedEnd,
|
|
} from "./spreadsheet_selection";
|
|
import type { Editor } from "./spreadsheet_editor";
|
|
import type { Selection, SpreadsheetContext } from "./spreadsheet_view_types";
|
|
|
|
const ARROWS: Record<string, [number, number]> = {
|
|
ArrowUp: [-1, 0],
|
|
ArrowDown: [1, 0],
|
|
ArrowLeft: [0, -1],
|
|
ArrowRight: [0, 1],
|
|
};
|
|
|
|
/** 2단계 잇기 자리 — 부른 쪽(spreadsheet.ts)이 채움 */
|
|
export interface KeyHooks {
|
|
/** Ctrl+F · Shift+F5 찾기 · Ctrl+H 바꾸기 */
|
|
find?(replace: boolean): void;
|
|
/** Ctrl+Shift+V — 곧 올 paste 사건을 골라 붙여넣기로 */
|
|
pasteSpecial?(): void;
|
|
/** Shift+F2 메모 편집 */
|
|
comment?(): void;
|
|
/** 고름 중 Esc(서식 붓 끄기 등) */
|
|
escape?(): void;
|
|
/** Ctrl+1 셀 서식 창 */
|
|
formatDialog?(): void;
|
|
/** Alt+= 자동 합계 */
|
|
autosum?(): void;
|
|
/** Ctrl+Shift+L 필터 켜기 · 끄기 */
|
|
filter?(): void;
|
|
/** Alt+↓ — 필터 머리 칸이면 값 목록을 열고 true */
|
|
dropdown?(): boolean;
|
|
/** 고름 중 그 밖 단축키(`spreadsheet_actions.ts`) — 받았으면 true */
|
|
more?(e: KeyboardEvent): boolean;
|
|
}
|
|
|
|
export function attachKeys(ctx: SpreadsheetContext, ed: Editor, hooks: KeyHooks = {}): () => void {
|
|
const go = (sel: Selection): void => ctx.select(sel, true);
|
|
|
|
/** 확정 뒤 한 칸 — 고름이 여러 칸이면 그 안에서 돎 */
|
|
function next(dr: number, dc: number): void {
|
|
const sheet = ctx.sheet();
|
|
go(cycle(sheet, ctx.selection, dr, dc) ?? move(sheet, ctx.selection, dr, dc));
|
|
}
|
|
|
|
function editingKey(e: KeyboardEvent, inBar: boolean): boolean {
|
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
switch (e.key) {
|
|
case "Enter": {
|
|
const node = e.target as HTMLTextAreaElement;
|
|
if (e.altKey) {
|
|
node.setRangeText("\n", node.selectionStart, node.selectionEnd, "end");
|
|
node.dispatchEvent(new Event("input"));
|
|
return true;
|
|
}
|
|
if (ctrl) return (ed.commitAll(), true);
|
|
ed.commit();
|
|
if (ed.editing()) return true; // 유효성에 막힘
|
|
next(e.shiftKey ? -1 : 1, 0);
|
|
return true;
|
|
}
|
|
case "Tab":
|
|
if (!e.shiftKey && ed.accept()) return true; // 함수 자동 완성
|
|
ed.commit();
|
|
if (ed.editing()) return true;
|
|
next(0, e.shiftKey ? -1 : 1);
|
|
return true;
|
|
case "Escape":
|
|
ed.cancel();
|
|
return true;
|
|
case "F2":
|
|
ed.toggleMode();
|
|
return true;
|
|
case "F4": {
|
|
const node = e.target as HTMLTextAreaElement;
|
|
const got = cycleAbs(node.value, node.selectionStart);
|
|
if (!got) return false;
|
|
node.value = got.text;
|
|
node.setSelectionRange(got.caret, got.caret);
|
|
node.dispatchEvent(new Event("input"));
|
|
return true;
|
|
}
|
|
}
|
|
const arrow = ARROWS[e.key];
|
|
if (arrow && !arrow[1] && !e.shiftKey && ed.suggestMove(arrow[0])) return true;
|
|
if (!arrow || inBar || ed.mode() === "edit") return false;
|
|
if (ed.pointable()) {
|
|
ed.pointMove(arrow[0], arrow[1], e.shiftKey, ctrl);
|
|
return true;
|
|
}
|
|
if (e.shiftKey || ctrl) return false; // 글자 고르기 · 낱말 건너기는 textarea 에 맡김
|
|
ed.commit();
|
|
if (!ed.editing()) go(move(ctx.sheet(), ctx.selection, arrow[0], arrow[1]));
|
|
return true;
|
|
}
|
|
|
|
function navKey(e: KeyboardEvent): boolean {
|
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
const sheet = ctx.sheet();
|
|
const sel = ctx.selection;
|
|
const arrow = ARROWS[e.key];
|
|
if (e.altKey && e.key === "ArrowDown" && hooks.dropdown?.()) return true;
|
|
if (hooks.more?.(e)) return true;
|
|
if (e.altKey && e.key === "=" && hooks.autosum && !ctx.readOnly) return (hooks.autosum(), true);
|
|
if (arrow && !e.altKey) {
|
|
go(move(sheet, sel, arrow[0], arrow[1], { extend: e.shiftKey, jump: ctrl }));
|
|
return true;
|
|
}
|
|
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
if (ctrl && !e.altKey) {
|
|
switch (k) {
|
|
case "z":
|
|
e.shiftKey ? ctx.redo() : ctx.undo();
|
|
return true;
|
|
case "y":
|
|
ctx.redo();
|
|
return true;
|
|
case "a":
|
|
go(selectAll(sheet, sel.활성));
|
|
return true;
|
|
case "Home":
|
|
go(e.shiftKey ? selectSpan(sheet, sel.기준, { r: 0, c: 0 }) : selectCell(sheet, 0, 0));
|
|
return true;
|
|
case "End": {
|
|
const end = usedEnd(sheet);
|
|
go(e.shiftKey ? selectSpan(sheet, sel.기준, end) : selectCell(sheet, end.r, end.c));
|
|
return true;
|
|
}
|
|
case " ": {
|
|
const g = sel.범위[0];
|
|
go(selectCols(sheet, g.c0, g.c1, sel.활성.c));
|
|
return true;
|
|
}
|
|
case "d":
|
|
case "r":
|
|
fill(k === "d");
|
|
return true;
|
|
case "f":
|
|
case "h":
|
|
if (!hooks.find) return false;
|
|
hooks.find(k === "h");
|
|
return true;
|
|
case "1":
|
|
if (!hooks.formatDialog) return false;
|
|
hooks.formatDialog();
|
|
return true;
|
|
case "l":
|
|
if (!e.shiftKey || !hooks.filter) return false;
|
|
hooks.filter();
|
|
return true;
|
|
case "v":
|
|
if (e.shiftKey) hooks.pasteSpecial?.();
|
|
return false; // paste 사건은 그대로 — E 클립보드 · 골라 붙여넣기가 받음
|
|
}
|
|
return false;
|
|
}
|
|
switch (e.key) {
|
|
case "Enter":
|
|
next(e.shiftKey ? -1 : 1, 0);
|
|
return true;
|
|
case "Tab":
|
|
next(0, e.shiftKey ? -1 : 1);
|
|
return true;
|
|
case "F5": // Shift+F5 = 찾기(엑셀 · Ctrl+F 를 브라우저가 가로챌 때)
|
|
if (!e.shiftKey || !hooks.find) return false;
|
|
hooks.find(false);
|
|
return true;
|
|
case "F2":
|
|
if (e.shiftKey) hooks.comment?.();
|
|
else ed.begin();
|
|
return true;
|
|
case "Escape":
|
|
hooks.escape?.();
|
|
return !!hooks.escape;
|
|
case "Delete":
|
|
if (!ctx.readOnly) ctx.dispatch({ 종류: "내용지움", 시트: sheet.id, 범위: sel.범위 });
|
|
return true;
|
|
case "Backspace":
|
|
ed.begin("");
|
|
return true;
|
|
case "Home":
|
|
go(
|
|
e.shiftKey
|
|
? selectSpan(sheet, sel.기준, { r: sel.기준.r, c: 0 })
|
|
: selectCell(sheet, sel.활성.r, 0),
|
|
);
|
|
return true;
|
|
case "PageDown":
|
|
case "PageUp": {
|
|
const v = ctx.grid.visibleRange();
|
|
const n = Math.max(1, v.r1 - v.r0) * (e.key === "PageDown" ? 1 : -1);
|
|
const r = Math.min(LAST_R, Math.max(0, sel.활성.r + n));
|
|
go(selectCell(sheet, r, sel.활성.c));
|
|
return true;
|
|
}
|
|
case " ":
|
|
if (!e.shiftKey) return false;
|
|
go(selectRows(sheet, sel.범위[0].r0, sel.범위[0].r1, sel.활성.r));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Ctrl+D 아래로 · Ctrl+R 오른쪽으로 — 고름 첫 행(열)을 나머지에 */
|
|
function fill(down: boolean): void {
|
|
if (ctx.readOnly) return;
|
|
const sheet = ctx.sheet();
|
|
const g = ctx.selection.범위[0];
|
|
const src = down ? { ...g, r1: g.r0 } : { ...g, c1: g.c0 };
|
|
if ((down ? g.r1 : g.c1) === (down ? src.r1 : src.c1)) return;
|
|
ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: fillCells(ctx.book, sheet.id, src, g) });
|
|
}
|
|
|
|
function onKey(e: KeyboardEvent): void {
|
|
if (e.isComposing || e.keyCode === 229) return; // 한글 조합 중 — 브라우저 몫
|
|
const inBar = e.target === ed.formula;
|
|
const done = ed.editing() ? editingKey(e, inBar) : !inBar && navKey(e);
|
|
if (done) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
|
|
ed.ta.addEventListener("keydown", onKey);
|
|
ed.formula.addEventListener("keydown", onKey);
|
|
return () => {
|
|
ed.ta.removeEventListener("keydown", onKey);
|
|
ed.formula.removeEventListener("keydown", onKey);
|
|
};
|
|
}
|