diff --git a/A00_Common/spreadsheet/spreadsheet_data_menu.ts b/A00_Common/spreadsheet/spreadsheet_data_menu.ts index 3dbdc1d9..3339afae 100644 --- a/A00_Common/spreadsheet/spreadsheet_data_menu.ts +++ b/A00_Common/spreadsheet/spreadsheet_data_menu.ts @@ -17,7 +17,6 @@ import { autoSumRange } from "./spreadsheet_autosum"; import { isError } from "./spreadsheet_eval"; import type { FilterHandle } from "./spreadsheet_filter"; import { createCondFmtPanel } from "./spreadsheet_condfmt"; -import { createNameManagerPanel } from "./spreadsheet_names"; import { createSheetExtraPanel } from "./spreadsheet_sheet_extra"; import { mountActions } from "./spreadsheet_actions"; import type { ErrorTipHandle } from "./spreadsheet_errcheck"; @@ -174,7 +173,14 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe [TEXT.filterItem, () => (toggleFilter(), back())], [TEXT.condfmt, () => void openPanel(TEXT.condfmt, async () => createCondFmtPanel)], [TEXT.validation, () => void openPanel(TEXT.validation, async () => createValidationPanel)], - [TEXT.names, () => void openPanel(TEXT.names, async () => createNameManagerPanel)], + [ + TEXT.names, + () => + void openPanel( + TEXT.names, + async () => (await import("./spreadsheet_names")).createNameManagerPanel, + ), + ], [TEXT.sheets, () => void openPanel(TEXT.sheets, async () => createSheetExtraPanel)], [TEXT.grid, () => (toggleGridlines(), back())], ...act.items, diff --git a/A00_Common/spreadsheet/spreadsheet_editor.ts b/A00_Common/spreadsheet/spreadsheet_editor.ts index 3ede2592..86d1a14e 100644 --- a/A00_Common/spreadsheet/spreadsheet_editor.ts +++ b/A00_Common/spreadsheet/spreadsheet_editor.ts @@ -11,8 +11,7 @@ import { el, showToast } from "@ui/ui_template_elements"; import { moveFormula } from "./spreadsheet_refshift"; import { colName, parseA1, parseRange, rangeToA1, toA1 } from "./spreadsheet_address"; -import { funcHelp, suggestFunctions, type FuncHelp } from "./spreadsheet_func_help"; -import { defineName, resolveName } from "./spreadsheet_names"; +import type { FuncHelp } from "./spreadsheet_func_help"; import { parseFormula, tokenize } from "./spreadsheet_parser"; import { cellsOf, move, selectCell } from "./spreadsheet_selection"; import { checkValue, findValidation } from "./spreadsheet_validation"; @@ -284,13 +283,15 @@ export function createEditor(ctx: SpreadsheetContext): Editor { let sugg: FuncHelp[] = []; let pick = 0; let typed = ""; + let fh: typeof import("./spreadsheet_func_help") | null = null; // 식을 처음 칠 때 불러옴 — 크기 예산 function showHint(): void { const node = src(); const h = st ? formulaHint(node.value.slice(0, node.selectionEnd)) : null; - sugg = h && "prefix" in h ? suggestFunctions(h.prefix).slice(0, 8) : []; + if (h && !fh) return void import("./spreadsheet_func_help").then((m) => ((fh = m), showHint())); + sugg = h && "prefix" in h ? fh!.suggestFunctions(h.prefix).slice(0, 8) : []; typed = h && "prefix" in h ? h.prefix : ""; - const help = h && "func" in h ? funcHelp(h.func) : null; + const help = h && "func" in h ? fh!.funcHelp(h.func) : null; if (!sugg.length && !help) return void hint.setAttribute("hidden", ""); pick = Math.min(pick, Math.max(0, sugg.length - 1)); hint.replaceChildren( @@ -489,7 +490,8 @@ export function createEditor(ctx: SpreadsheetContext): Editor { }); /** 이름 상자(sub4) — 있는 이름이면 그 범위로 · 없으면 지금 고름에 이름을 붙임(엑셀) */ - function nameBox(name: string): void { + async function nameBox(name: string): Promise { + const { defineName, resolveName } = await import("./spreadsheet_names"); // 늦게 — 크기 예산 const hit = resolveName(ctx.book, ctx.selection.시트, name); if (hit) { if (hit.시트 !== ctx.selection.시트) ctx.showSheet(hit.시트); @@ -514,7 +516,7 @@ export function createEditor(ctx: SpreadsheetContext): Editor { const body = value.slice(bang + 1).replace(/\$/g, ""); const one = parseA1(body); const g = parseRange(body); - if (!g) return nameBox(value.trim()); + if (!g) return void nameBox(value.trim()); if (one) return ctx.select(selectCell(sheet, one.r, one.c), true); const at = { r: g.r0, c: g.c0 }; ctx.select({ 시트: sheet.id, 범위: [g], 활성: at, 기준: { ...at } }, true); diff --git a/A00_Common/spreadsheet/spreadsheet_extras.ts b/A00_Common/spreadsheet/spreadsheet_extras.ts index 13be5d3c..ccf2db0c 100644 --- a/A00_Common/spreadsheet/spreadsheet_extras.ts +++ b/A00_Common/spreadsheet/spreadsheet_extras.ts @@ -13,8 +13,9 @@ import { attachComments, getComment, setComment } from "./spreadsheet_comments"; import { mountDataMenu } from "./spreadsheet_data_menu"; import type { Editor } from "./spreadsheet_editor"; import type { FindPanelHandle } from "./spreadsheet_find_panel"; -import { createFormatPainter } from "./spreadsheet_format_painter"; +import type { FormatPainterHandle } from "./spreadsheet_format_painter"; import type { KeyHooks } from "./spreadsheet_keys"; +import type { Memo } from "./spreadsheet_memo"; import type { MouseHooks } from "./spreadsheet_mouse"; import type { PasteSpecialMode } from "./spreadsheet_paste_special"; import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; @@ -24,7 +25,6 @@ const TEXT = { painter: "서식 붓 — 한 번 누르면 한 번 · 두 번 누르면 Esc 까지 계속", pasteTitle: "골라 붙여넣기", modes: { 값: "값만", 서식: "서식만", 수식: "수식만" } as Record, - memo: "메모 — Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소", }; export interface Extras { @@ -62,40 +62,20 @@ export function attachExtras( // ── 메모 — 풍선 · 편집 ───────────────────────────────────────────────── const comments = attachComments(ctx); let hovered = ""; - const memo = el("textarea", { className: "ss-memo", attrs: { hidden: "", title: TEXT.memo } }); - ctx.root.append(memo); - let memoAt: { r: number; c: number } | null = null; - const closeMemo = (save: boolean): void => { - if (!memoAt) return; - const at = memoAt; - memoAt = null; - memo.setAttribute("hidden", ""); - if (save && memo.value !== (getComment(ctx.sheet(), at.r, at.c) ?? "")) - setComment(ctx, at.r, at.c, memo.value.trim()); - back(); - }; - memo.addEventListener("keydown", (e) => { - if (e.key === "Escape" || (e.key === "Enter" && e.ctrlKey)) { - e.preventDefault(); - e.stopPropagation(); - closeMemo(e.key === "Enter"); - } - }); - memo.addEventListener("blur", () => closeMemo(true)); - - function editMemo(): void { + let memo: Memo | null = null; // 처음 쓸 때 불러옴 — 크기 예산 + async function editMemo(): Promise { if (ctx.readOnly) return; const { r, c } = ctx.selection.활성; - memoAt = { r, c }; + memo ??= (await import("./spreadsheet_memo")).createMemo(ctx, back, { + get: getComment, + set: setComment, + }); comments.hideBalloon(); - memo.value = getComment(ctx.sheet(), r, c) ?? ""; - memo.removeAttribute("hidden"); - put(memo, r, c); - memo.focus(); + memo.open(r, c, (node) => put(node, r, c)); } // ── 서식 붓 ───────────────────────────────────────────────────────────── - const painter = createFormatPainter(ctx); + let painter: FormatPainterHandle | null = null; // 처음 누를 때 불러옴 — 크기 예산 let sticky = false; const brush = el("button", { className: "ss-brush", @@ -103,10 +83,12 @@ export function attachExtras( attrs: { type: "button", title: TEXT.painter }, }); const showBrush = (): void => { - brush.classList.toggle("is-on", painter.active()); - ctx.root.classList.toggle("ss--painting", painter.active()); + const on = !!painter?.active(); + brush.classList.toggle("is-on", on); + ctx.root.classList.toggle("ss--painting", on); }; - brush.addEventListener("click", (e) => { + brush.addEventListener("click", async (e) => { + painter ??= (await import("./spreadsheet_format_painter")).createFormatPainter(ctx); if (e.detail === 2) sticky = true; else if (painter.active()) (painter.clear(), (sticky = false)); else (painter.pick(), (sticky = false)); @@ -198,7 +180,7 @@ export function attachExtras( toolbarRoot?.removeEventListener("click", onToolbarClick); toolbarRoot?.removeEventListener("change", back); find?.destroy(); - memo.remove(); + memo?.destroy(); picker.remove(); brush.remove(); fmt.remove(); @@ -209,18 +191,18 @@ export function attachExtras( keys: { find: (replace) => void openFind(replace), pasteSpecial: () => (armed = true), - comment: editMemo, + comment: () => void editMemo(), formatDialog: () => void openFormat(), ...data.keys, escape() { - painter.clear(); + painter?.clear(); sticky = false; showBrush(); }, }, mouse: { selected() { - if (!painter.active()) return; + if (!painter?.active()) return; painter.paint(ctx.selection.범위[0], sticky); showBrush(); }, @@ -228,7 +210,7 @@ export function attachExtras( const key = hit?.kind === "cell" ? `${hit.r},${hit.c}` : ""; if (key === hovered) return; hovered = key; - if (!hit || !key || memoAt || !getComment(ctx.sheet(), hit.r, hit.c)) + if (!hit || !key || memo?.busy() || !getComment(ctx.sheet(), hit.r, hit.c)) return comments.hideBalloon(); comments.showBalloon(hit.r, hit.c); put(comments.root!, hit.r, hit.c); // showBalloon 의 cellBox 자리 → 화면 자리로 고침 diff --git a/A00_Common/spreadsheet/spreadsheet_memo.ts b/A00_Common/spreadsheet/spreadsheet_memo.ts new file mode 100644 index 00000000..baa05cca --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_memo.ts @@ -0,0 +1,59 @@ +/* ============================================================================= + * spreadsheet_memo.ts (주인 D) + * 메모 편집(Shift+F2) — 칸 옆 글상자 · Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소. + * 처음 쓸 때 `spreadsheet_extras.ts` 가 불러옴(크기 예산). + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import type { getComment, setComment } from "./spreadsheet_comments"; +import type { SpreadsheetContext } from "./spreadsheet_view_types"; + +const TITLE = "메모 — Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소"; + +export interface Memo { + /** (r, c) 메모를 열고 `place` 로 칸 옆에 둠 */ + open(r: number, c: number, place: (node: HTMLElement) => void): void; + /** 편집 중인가 — 풍선을 띄우지 않음 */ + busy(): boolean; + destroy(): void; +} + +/** 메모 읽기 · 쓰기는 부른 쪽이 넘김 — 여기서 끌어오면 메모 조각이 따로 쪼개져 크기가 늘어남 */ +export function createMemo( + ctx: SpreadsheetContext, + back: () => void, + io: { get: typeof getComment; set: typeof setComment }, +): Memo { + const memo = el("textarea", { className: "ss-memo", attrs: { hidden: "", title: TITLE } }); + ctx.root.append(memo); + let memoAt: { r: number; c: number } | null = null; + const close = (save: boolean): void => { + if (!memoAt) return; + const at = memoAt; + memoAt = null; + memo.setAttribute("hidden", ""); + if (save && memo.value !== (io.get(ctx.sheet(), at.r, at.c) ?? "")) + io.set(ctx, at.r, at.c, memo.value.trim()); + back(); + }; + memo.addEventListener("keydown", (e) => { + if (e.key === "Escape" || (e.key === "Enter" && e.ctrlKey)) { + e.preventDefault(); + e.stopPropagation(); + close(e.key === "Enter"); + } + }); + memo.addEventListener("blur", () => close(true)); + + return { + open(r, c, place) { + memoAt = { r, c }; + memo.value = io.get(ctx.sheet(), r, c) ?? ""; + memo.removeAttribute("hidden"); + place(memo); + memo.focus(); + }, + busy: () => !!memoAt, + destroy: () => memo.remove(), + }; +} diff --git a/A00_Common/spreadsheet/spreadsheet_mouse.ts b/A00_Common/spreadsheet/spreadsheet_mouse.ts index 4d5c03b8..e35ecda2 100644 --- a/A00_Common/spreadsheet/spreadsheet_mouse.ts +++ b/A00_Common/spreadsheet/spreadsheet_mouse.ts @@ -6,7 +6,6 @@ * 머리 끝(폭 · 높이 끌기)은 C 몫 — 여기서 건드리지 않음. * ========================================================================== */ -import { autofitColumn } from "./spreadsheet_autofit"; import { fillCells } from "./spreadsheet_fill"; import { hasValue, @@ -242,7 +241,10 @@ export function attachMouse( // 고른 열 전체의 경계면 고른 열 모두(엑셀) const g = ctx.selection.범위[0]; const whole = g.r0 === 0 && g.r1 === LAST_R && inRange(g, 0, hit.c); - for (let c = whole ? g.c0 : hit.c; c <= (whole ? g.c1 : hit.c); c++) autofitColumn(ctx, c); + void import("./spreadsheet_autofit").then((m) => { + for (let c = whole ? g.c0 : hit.c; c <= (whole ? g.c1 : hit.c); c++) + m.autofitColumn(ctx, c); + }); // 늦게 — 크기 예산 return; } if (hit.kind === "cell" && !ed.editing()) ed.begin();