/* ============================================================================= * 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 { parseClipboard } from "./spreadsheet_clipboard"; import { attachComments, getComment, setComment } from "./spreadsheet_comments"; import { mountDataMenu } from "./spreadsheet_data_menu"; import type { Editor } from "./spreadsheet_editor"; import { mountFindPanel } from "./spreadsheet_find_panel"; import { openFormatDialog } from "./spreadsheet_format_dialog"; import { createFormatPainter } from "./spreadsheet_format_painter"; import type { KeyHooks } from "./spreadsheet_keys"; import type { MouseHooks } from "./spreadsheet_mouse"; import { pasteSpecial, 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, memo: "메모 — Ctrl+Enter 또는 밖을 누르면 적용 · Esc 취소", }; 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`; }; // ── 찾기 ─────────────────────────────────────────────────────────────── const find = mountFindPanel(ctx, back); // ── 메모 — 풍선 · 편집 ───────────────────────────────────────────────── 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 { if (ctx.readOnly) return; const { r, c } = ctx.selection.활성; memoAt = { r, c }; comments.hideBalloon(); memo.value = getComment(ctx.sheet(), r, c) ?? ""; memo.removeAttribute("hidden"); put(memo, r, c); memo.focus(); } // ── 서식 붓 ───────────────────────────────────────────────────────────── const painter = createFormatPainter(ctx); let sticky = false; const brush = el("button", { className: "ss-brush", text: "붓", attrs: { type: "button", title: TEXT.painter }, }); const showBrush = (): void => { brush.classList.toggle("is-on", painter.active()); ctx.root.classList.toggle("ss--painting", painter.active()); }; brush.addEventListener("click", (e) => { 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 }, }); /** 창이 닫히면(확인 · 취소) 초점을 격자로 */ function openFormat(): void { if (ctx.readOnly) return; openFormatDialog(ctx); const watch = new MutationObserver(() => { if (document.querySelector(".ss-fmtdlg")) return; watch.disconnect(); back(); }); watch.observe(document.body, { childList: true }); } fmt.addEventListener("click", 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", ""); 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); memo.remove(); picker.remove(); brush.remove(); fmt.remove(); }, }; return { keys: { find: (replace) => find.open(replace), pasteSpecial: () => (armed = true), comment: editMemo, formatDialog: 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 || memoAt || !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: [find, comments, data.part, self], }; }