/* ============================================================================= * spreadsheet_actions_run.ts (주인 D) * `spreadsheet_actions.ts` 단축키 · 메뉴가 부르는 몸통 — 처음 누를 때 늦게 불러옴(크기 예산). * ========================================================================== */ import { toA1 } from "./spreadsheet_address"; import { watchClose, type Act } from "./spreadsheet_actions"; import { cellStyleAt } from "./spreadsheet_cellstyle"; import { LAST_C, LAST_R } from "./spreadsheet_selection"; import type { CellRange, CellStyle, Command } from "./spreadsheet_types"; import type { SpreadsheetContext } from "./spreadsheet_view_types"; /** 엑셀 날짜 일련번호 0 = 1899-12-30 */ const EPOCH = Date.UTC(1899, 11, 30); export function run(ctx: SpreadsheetContext, back: () => void, act: Act): void { const insert = () => import("./spreadsheet_insert_dialog"); const view = () => import("./spreadsheet_view_menu"); const tools = () => import("./spreadsheet_data_tools"); /** 늦게 불러 창을 열고 닫히면 격자로 */ const dialog = (load: () => Promise, open: (m: M) => void) => void load().then((m) => (open(m), watchClose(back))); const freezeBy = (pick: (m: Awaited>) => void) => void view().then((m) => (pick(m), back())); const range = (): CellRange => ctx.selection.범위[0]; const fullRows = (g: CellRange) => g.c0 === 0 && g.c1 === LAST_C; const fullCols = (g: CellRange) => g.r0 === 0 && g.r1 === LAST_R; const span = (a: number, b: number) => Array.from({ length: b - a + 1 }, (_, i) => a + i); const id = () => ctx.selection.시트; /** 칸 넣기 · 지우기 — 행 · 열 통째 고름이면 엑셀처럼 창 없이 바로 */ function cells(add: boolean): void { const g = range(); if (fullRows(g) || fullCols(g)) { const rows = fullRows(g); ctx.dispatch({ 종류: rows ? (add ? "행넣기" : "행지우기") : add ? "열넣기" : "열지우기", 시트: id(), at: rows ? g.r0 : g.c0, 수: rows ? g.r1 - g.r0 + 1 : g.c1 - g.c0 + 1, }); } else dialog(insert, (m) => (add ? m.openInsertCellsDialog(ctx) : m.openDeleteCellsDialog(ctx))); } function hide(rows: boolean, 숨김: boolean): void { const g = range(); const 번호 = rows ? span(g.r0, g.r1) : span(g.c0, g.c1); ctx.dispatch({ 종류: "숨김", 시트: id(), 축: rows ? "행" : "열", 번호, 숨김 }); } function group(on: boolean): void { const g = range(); const cols = fullCols(g) && !fullRows(g); const 번호 = cols ? span(g.c0, g.c1) : span(g.r0, g.r1); const 묶음 = on ? { 층: 1 } : null; ctx.dispatch({ 종류: "묶음설정", 시트: id(), 축: cols ? "열" : "행", 번호, 묶음 }); } function flip(key: keyof CellStyle): void { const { r, c } = ctx.selection.활성; const on = !!cellStyleAt(ctx.book, ctx.sheet(), r, c)[key]; ctx.dispatch({ 종류: "서식", 시트: id(), 범위: ctx.selection.범위, 바꿀: { [key]: !on } }); } /** 바깥 테두리(가는 선) — 가장자리 칸마다 지금 테두리에 변을 더함 */ function outline(): void { const g = range(); const n = (g.r1 - g.r0 + 1) * (g.c1 - g.c0 + 1); // ponytail: 가장자리 칸만 셈 · 행 · 열 통째(백만 칸)는 안 함 — 쓴 범위로 자르면 풀림 if (fullRows(g) || fullCols(g) || n > 1e5) return; const cmds: Command[] = []; for (let r = g.r0; r <= g.r1; r++) for (let c = g.c0; c <= g.c1; c++) { const sides = [ r === g.r0 && "위", r === g.r1 && "아래", c === g.c0 && "왼", c === g.c1 && "오른", ].filter(Boolean) as ("위" | "아래" | "왼" | "오른")[]; if (!sides.length) continue; const 테두리 = { ...cellStyleAt(ctx.book, ctx.sheet(), r, c).테두리 }; for (const s of sides) 테두리[s] = { 선: "thin" }; cmds.push({ 종류: "서식", 시트: id(), 범위: [{ r0: r, c0: c, r1: r, c1: c }], 바꿀: { 테두리 }, }); } ctx.dispatch({ 종류: "묶음", 명령: cmds }); } function today(): void { const d = new Date(); const 값 = Math.round((Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) - EPOCH) / 864e5); const { r, c } = ctx.selection.활성; const key = toA1(r, c); const 서식 = ctx.sheet().칸[key]?.서식; ctx.dispatch({ 종류: "묶음", 명령: [ { 종류: "칸", 시트: id(), 칸: { [key]: 서식 === undefined ? { 값 } : { 값, 서식 } } }, { 종류: "서식", 시트: id(), 범위: [{ r0: r, c0: c, r1: r, c1: c }], 바꿀: { 형식: "yyyy-mm-dd" }, }, ], }); } /** 다음 · 이전 보이는 시트 */ function sheetStep(d: number): void { const list = ctx.book.시트.filter((s) => !s.숨김); const i = list.findIndex((s) => s.id === id()); const to = list[i + d]; if (to) ctx.showSheet(to.id); } const goTo = () => dialog(view, (m) => m.openGoToDialog(ctx)); function clearBorders(): void { ctx.dispatch({ 종류: "서식", 시트: id(), 범위: ctx.selection.범위, 바꿀: { 테두리: null } }); } if (act.startsWith("flip:")) return flip(act.slice(5) as keyof CellStyle); switch (act) { case "goTo": return goTo(); case "insert": case "remove": return cells(act === "insert"); case "hideRows": case "hideCols": case "showRows": case "showCols": return hide(act.endsWith("Rows"), act.startsWith("hide")); case "group": case "ungroup": return group(act === "group"); case "outline": return outline(); case "clearBorders": return clearBorders(); case "today": return today(); case "next": case "prev": return sheetStep(act === "next" ? 1 : -1); case "newSheet": return ctx.root.querySelector(".ss-tabs__add")?.click(); case "freezeRow": return freezeBy((m) => m.freezeFirstRow(ctx)); case "freezeCol": return freezeBy((m) => m.freezeFirstColumn(ctx)); case "freezeAt": return freezeBy((m) => m.freezeAtActiveCell(ctx)); case "unfreeze": return freezeBy((m) => m.unfreezePanes(ctx)); case "split": return dialog(tools, (m) => m.openTextToColumnsDialog(ctx)); case "dedupe": return dialog(tools, (m) => m.openRemoveDuplicatesDialog(ctx)); case "series": return dialog(tools, (m) => m.openFillSeriesDialog(ctx)); } }