/* ============================================================================= * spreadsheet_view_menu.ts (2단계 · 주인 sub_laptop_1) * 이동 대화상자(F5 · 주소 · 정의된 이름) · 틀 고정 메뉴(첫 행 · 첫 열 · 현재 칸 기준 · 풀기). * * 정의된 이름 목록은 `Sheet.이름정의`(3단계 · sub_laptop_4 계약 · 안 이어짐) 가 있으면 * 그대로 보여 줌 — 없으면 빈 목록(주소 입력만 됨). 이름으로 다른 시트를 못 가리키는 * 지금 계약대로 활성 시트 안에서만 찾음. * * 잇는 법(D · sub7) — F5 → `openGoToDialog(ctx)` 를 `spreadsheet_keys.ts` navKey 에 더함. * 틀 고정 단추(E 도구 모음)는 눌렀을 때 `openFreezePanesMenu(ctx, button)` 를 부르면 그 * 단추 아래 작은 메뉴가 뜸. * ========================================================================== */ import { createButton, createInputField, el } from "@ui/ui_template_elements"; import { parseRange, rangeToA1 } from "./spreadsheet_address"; import type { CellRange } from "./spreadsheet_types"; import type { SpreadsheetContext } from "./spreadsheet_view_types"; const TEXT = { goToTitle: "이동", addressLabel: "참조", namedRanges: "정의된 이름", ok: "확인", cancel: "취소", notFound: "주소를 찾을 수 없음", freezeFirstRow: "첫 행 고정", freezeFirstColumn: "첫 열 고정", freezeAtActive: "틀 고정(현재 칸 기준)", unfreeze: "틀 고정 풀기", }; // ═════════════════════════════════════════════════════════════════════════════ // 이동(F5) // ═════════════════════════════════════════════════════════════════════════════ /** 범위(또는 칸) 글을 읽어 그 범위로 고름을 옮기고 보이게 스크롤함. */ export function goTo(ctx: SpreadsheetContext, target: CellRange): void { const top = { r: target.r0, c: target.c0 }; ctx.select({ 시트: ctx.selection.시트, 범위: [target], 활성: top, 기준: top }, true); } export function openGoToDialog(ctx: SpreadsheetContext): void { const overlay = el("div", { className: "ui-confirm" }); const panel = el("div", { className: "ui-confirm__panel" }); const close = (): void => overlay.remove(); overlay.addEventListener("click", (ev) => { if (ev.target === overlay) close(); }); const errorMsg = el("p", { text: "" }); errorMsg.style.cssText = "color:var(--color-danger, red);font-size:0.8rem;min-height:1em;"; const addressField = createInputField({ label: TEXT.addressLabel, value: rangeToA1( ctx.selection.범위[0] ?? { r0: ctx.selection.활성.r, c0: ctx.selection.활성.c, r1: ctx.selection.활성.r, c1: ctx.selection.활성.c, }, ), }); const goToAddress = (): void => { const target = parseRange(addressField.input.value); if (!target) { errorMsg.textContent = TEXT.notFound; return; } goTo(ctx, target); close(); }; addressField.input.addEventListener("keydown", (ev) => { if (ev.key === "Enter") goToAddress(); }); const names = ctx.sheet().이름정의 ?? []; const namesList = el("div", { children: names.map((n) => createButton({ label: `${n.이름} (${rangeToA1(n.범위)})`, variant: "ghost", onClick: () => { goTo(ctx, n.범위); close(); }, }), ), }); namesList.style.cssText = "display:flex;flex-direction:column;gap:2px;max-height:160px;overflow:auto;"; panel.append( el("h3", { text: TEXT.goToTitle }), addressField.root, errorMsg, ...(names.length > 0 ? [el("p", { text: TEXT.namedRanges }), namesList] : []), el("div", { className: "ui-confirm__actions", children: [ createButton({ label: TEXT.cancel, variant: "ghost", onClick: close }), createButton({ label: TEXT.ok, variant: "filled", onClick: goToAddress }), ], }), ); overlay.append(panel); document.body.append(overlay); addressField.input.focus(); addressField.input.select(); } // ═════════════════════════════════════════════════════════════════════════════ // 틀 고정 // ═════════════════════════════════════════════════════════════════════════════ export function freezeFirstRow(ctx: SpreadsheetContext): void { ctx.dispatch({ 종류: "틀고정", 시트: ctx.selection.시트, 행: 1, 열: ctx.sheet().틀고정?.열 ?? 0, }); } export function freezeFirstColumn(ctx: SpreadsheetContext): void { ctx.dispatch({ 종류: "틀고정", 시트: ctx.selection.시트, 행: ctx.sheet().틀고정?.행 ?? 0, 열: 1, }); } /** 활성 칸의 왼위 자리를 기준으로 그 위 · 왼쪽을 고정(활성 칸이 A1 이면 사실상 풀기와 같음). */ export function freezeAtActiveCell(ctx: SpreadsheetContext): void { const at = ctx.selection.활성; ctx.dispatch({ 종류: "틀고정", 시트: ctx.selection.시트, 행: at.r, 열: at.c }); } export function unfreezePanes(ctx: SpreadsheetContext): void { ctx.dispatch({ 종류: "틀고정", 시트: ctx.selection.시트, 행: 0, 열: 0 }); } /** 도구 모음 단추 아래(또는 화면 가운데, `anchor` 없으면) 뜨는 작은 메뉴 — 누르면 바로 적용하고 닫힘. */ export function openFreezePanesMenu(ctx: SpreadsheetContext, anchor?: HTMLElement): void { const menu = el("div", { className: "ui-confirm__panel" }); menu.style.cssText = "position:fixed;display:flex;flex-direction:column;gap:2px;min-width:180px;z-index:1000;padding:6px;"; const items: [string, () => void][] = [ [TEXT.freezeFirstRow, () => freezeFirstRow(ctx)], [TEXT.freezeFirstColumn, () => freezeFirstColumn(ctx)], [TEXT.freezeAtActive, () => freezeAtActiveCell(ctx)], [TEXT.unfreeze, () => unfreezePanes(ctx)], ]; const close = (): void => { menu.remove(); document.removeEventListener("mousedown", onOutside, true); document.removeEventListener("keydown", onEscape); }; const onOutside = (ev: MouseEvent): void => { if (!menu.contains(ev.target as Node)) close(); }; const onEscape = (ev: KeyboardEvent): void => { if (ev.key === "Escape") close(); }; menu.append( ...items.map(([label, action]) => createButton({ label, variant: "ghost", onClick: () => { action(); close(); }, }), ), ); document.body.append(menu); if (anchor) { const box = anchor.getBoundingClientRect(); menu.style.top = `${box.bottom + 4}px`; menu.style.left = `${box.left}px`; } else { menu.style.top = "40%"; menu.style.left = "50%"; menu.style.transform = "translate(-50%, -50%)"; } setTimeout(() => { document.addEventListener("mousedown", onOutside, true); document.addEventListener("keydown", onEscape); }, 0); }