Files
Aislo/A00_Common/spreadsheet/spreadsheet_extras.ts
T
eomsangdonandClaude Opus 5.5 87417b3a31 perf(spreadsheet): D 크기 줄임 — sub2 · sub8 병합 뒤 화면 예산 넘침 해소
- 함수 자동 완성 · 인자 도움말, 이름 상자 · 이름 관리, 서식 붓, 메모 편집(새 spreadsheet_memo.ts), 열 너비 맞춤을 처음 쓸 때 불러옴
- 화면 30,126 B → 28,102 B(예산 28,672) · 합 62.6 KB

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
2026-09-28 00:46:08 +09:00

222 lines
9.0 KiB
TypeScript

/* =============================================================================
* spreadsheet_extras.ts (주인 D)
* 2단계 부품 잇기(sub1 파일들의 「잇는 법」) — 찾기 패널 · 메모 풍선(마우스 올림) · 메모 편집(Shift+F2) ·
* 셀 서식 창(Ctrl+1) · 서식 붓(수식 입력줄 앞 단추 · 두 번 누르면 계속 · Esc 끔) · 골라 붙여넣기(Ctrl+Shift+V → 값만 · 서식만 · 수식만) ·
* 도구 모음을 누른 뒤 초점을 격자로 되돌림 · 데이터 단추 · 메뉴는 `spreadsheet_data_menu.ts`.
* 빨간 세모는 C 격자 · 우클릭 [메모 …] 는 E 메뉴 몫(브레인 배정).
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { watchClose } from "./spreadsheet_actions";
import { parseClipboard } from "./spreadsheet_clipboard";
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 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";
const TEXT = {
format: "셀 서식(Ctrl+1)",
painter: "서식 붓 — 한 번 누르면 한 번 · 두 번 누르면 Esc 까지 계속",
pasteTitle: "골라 붙여넣기",
modes: { 값: "값만", 서식: "서식만", 수식: "수식만" } as Record<PasteSpecialMode, string>,
};
export interface Extras {
keys: KeyHooks;
mouse: MouseHooks;
parts: PartHandle[];
}
export function attachExtras(
ctx: SpreadsheetContext,
ed: Editor,
toolbarRoot: HTMLElement | null,
): Extras {
const back = (): void => ed.focus();
/** 칸 오른쪽 자리(부품 뿌리 기준 px) — C editorSlot box 는 격자 뿌리 기준 */
const beside = (r: number, c: number): { x: number; y: number } => {
const { box } = ctx.grid.editorSlot(r, c);
const g = ctx.grid.root.getBoundingClientRect();
const o = ctx.root.getBoundingClientRect();
return { x: box.x + box.w + g.left - o.left + 4, y: box.y + g.top - o.top };
};
const put = (node: HTMLElement, r: number, c: number): void => {
const p = beside(r, c);
node.style.left = `${p.x}px`;
node.style.top = `${p.y}px`;
};
// ── 찾기(처음 열 때 불러옴 — 크기 예산) ─────────────────────────────────────
let find: FindPanelHandle | null = null;
async function openFind(replace: boolean): Promise<void> {
find ??= (await import("./spreadsheet_find_panel")).mountFindPanel(ctx, back);
find.open(replace);
}
// ── 메모 — 풍선 · 편집 ─────────────────────────────────────────────────
const comments = attachComments(ctx);
let hovered = "";
let memo: Memo | null = null; // 처음 쓸 때 불러옴 — 크기 예산
async function editMemo(): Promise<void> {
if (ctx.readOnly) return;
const { r, c } = ctx.selection.활성;
memo ??= (await import("./spreadsheet_memo")).createMemo(ctx, back, {
get: getComment,
set: setComment,
});
comments.hideBalloon();
memo.open(r, c, (node) => put(node, r, c));
}
// ── 서식 붓 ─────────────────────────────────────────────────────────────
let painter: FormatPainterHandle | null = null; // 처음 누를 때 불러옴 — 크기 예산
let sticky = false;
const brush = el("button", {
className: "ss-brush",
text: "붓",
attrs: { type: "button", title: TEXT.painter },
});
const showBrush = (): void => {
const on = !!painter?.active();
brush.classList.toggle("is-on", on);
ctx.root.classList.toggle("ss--painting", on);
};
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));
showBrush();
back();
});
// 셀 서식 창 — Ctrl+1 을 브라우저 · ORCA 가 가로챌 때 누를 자리
const fmt = el("button", {
className: "ss-brush",
text: "서식…",
attrs: { type: "button", title: TEXT.format },
});
/** 창이 닫히면(확인 · 취소) 초점을 격자로 */
async function openFormat(): Promise<void> {
if (ctx.readOnly) return;
(await import("./spreadsheet_format_dialog")).openFormatDialog(ctx); // 늦게 — 크기 예산
watchClose(back);
}
fmt.addEventListener("click", () => void openFormat());
if (!ctx.readOnly) ed.bar.prepend(fmt, brush);
// ── 데이터 단추 · 메뉴(정렬 · 필터 · 조건부 서식 · 유효성 · 이름 · 눈금선) ─────────
const data = mountDataMenu(ctx, back);
brush.after(...data.buttons);
// ── 골라 붙여넣기 ─────────────────────────────────────────────────────────
let armed = false;
const picker = el("div", { className: "ss-pick", attrs: { hidden: "", title: TEXT.pasteTitle } });
ctx.root.append(picker);
const onPaste = (e: ClipboardEvent): void => {
if (!armed || ed.editing()) return;
armed = false;
e.preventDefault();
e.stopPropagation(); // E 클립보드의 보통 붙여넣기를 막음
const at = { ...ctx.selection.활성 };
const data = e.clipboardData;
const block = parseClipboard(
data?.getData("text/html") ?? "",
data?.getData("text/plain") ?? "",
at,
);
if (!block) return;
// ponytail: 우리 복사면 원본을 같은 시트로 봄(ClipBlock 에 시트가 없음) — 값만 때 식 칸 계산값용
const source =
block.출처 === "aislo" && block.원점
? { 시트: ctx.selection.시트, ...block.원점 }
: undefined;
picker.replaceChildren(
...(Object.keys(TEXT.modes) as PasteSpecialMode[]).map((mode) => {
const b = el("button", {
text: TEXT.modes[mode],
attrs: { type: "button", "data-mode": mode },
});
b.addEventListener("click", () => {
picker.setAttribute("hidden", "");
// 늦게 — 크기 예산
void import("./spreadsheet_paste_special").then((m) => {
m.pasteSpecial(ctx, block, at, mode, source);
back();
});
});
return b;
}),
);
picker.removeAttribute("hidden");
put(picker, at.r, at.c);
(picker.firstElementChild as HTMLElement).focus();
};
picker.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
e.preventDefault();
picker.setAttribute("hidden", "");
back();
});
ctx.root.addEventListener("paste", onPaste, true);
// ── 도구 모음 뒤 초점 ───────────────────────────────────────────────────
const onToolbarClick = (e: Event): void => {
if ((e.target as HTMLElement).closest("button")) back();
};
toolbarRoot?.addEventListener("click", onToolbarClick);
toolbarRoot?.addEventListener("change", back);
const self: PartHandle = {
root: null,
refresh: () => showBrush(),
destroy() {
ctx.root.removeEventListener("paste", onPaste, true);
toolbarRoot?.removeEventListener("click", onToolbarClick);
toolbarRoot?.removeEventListener("change", back);
find?.destroy();
memo?.destroy();
picker.remove();
brush.remove();
fmt.remove();
},
};
return {
keys: {
find: (replace) => void openFind(replace),
pasteSpecial: () => (armed = true),
comment: () => void editMemo(),
formatDialog: () => void openFormat(),
...data.keys,
escape() {
painter?.clear();
sticky = false;
showBrush();
},
},
mouse: {
selected() {
if (!painter?.active()) return;
painter.paint(ctx.selection.범위[0], sticky);
showBrush();
},
hover(hit) {
const key = hit?.kind === "cell" ? `${hit.r},${hit.c}` : "";
if (key === hovered) return;
hovered = key;
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 자리 → 화면 자리로 고침
},
},
parts: [comments, data.part, self],
};
}