/* ============================================================================= * spreadsheet_menu_items.ts (주인 E · 81-3) * 우클릭 메뉴에 덧붙이는 구글 시트 항목 — 기능은 전부 이미 있는 것만(분석_정함.md 81-3 · 조사_A 3 표(a)). * `spreadsheet_menu.ts` 가 메뉴를 열 때 한 번만 `import()`(크기 예산 — 안 열면 안 받음). * 안 만듦: 하이퍼링크 · 댓글 · 범위/시트 보호 · 다른 파일로 복사. * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { dialogKeys } from "./spreadsheet_actions"; import { pasteBlock, parseClipboard } from "./spreadsheet_clipboard"; import { pasteSpecial } from "./spreadsheet_paste_special"; import { openInsertCellsDialog, openDeleteCellsDialog } from "./spreadsheet_insert_dialog"; import { autofitColumn } from "./spreadsheet_autofit"; import { sortSelection } from "./spreadsheet_sort_dialog"; import { toggleFilter } from "./spreadsheet_filter"; import { freezeFirstRow, freezeFirstColumn, freezeAtActiveCell, unfreezePanes, } from "./spreadsheet_view_menu"; import { createCondFmtPanel } from "./spreadsheet_condfmt"; import { createValidationPanel } from "./spreadsheet_validation"; import { st } from "./spreadsheet_text"; import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; /** 보안 문맥(localhost · https)에서만 시스템 클립보드를 읽음 — http(Tailscale 주소)는 null * (부른 쪽이 Ctrl+V 안내로 되돌림, 지금처럼). */ async function readSystemClipboard(): Promise<{ html: string; text: string } | null> { if (!window.isSecureContext || !navigator.clipboard?.read) return null; try { const items = await navigator.clipboard.read(); let html = ""; let text = ""; for (const item of items) { if (item.types.includes("text/html")) html = await (await item.getType("text/html")).text(); if (item.types.includes("text/plain")) text = await (await item.getType("text/plain")).text(); } return { html, text }; } catch { return null; // 권한 거부 · 빈 클립보드 등 } } /** 메뉴 [붙여넣기] — 됐으면 true(부른 쪽은 더 안 함) · 안 됐으면 false(Ctrl+V 안내로 되돌림). */ export async function pasteFromMenu(ctx: SpreadsheetContext): Promise { const data = await readSystemClipboard(); if (!data) return false; const at = { ...ctx.selection.활성 }; const block = parseClipboard(data.html, data.text, at); if (!block) { showToast(st("ClipboardPasteFailed"), "error"); return true; } pasteBlock(ctx, block, at); return true; } /** 메뉴 [선택하여 붙여넣기 — 값만 · 서식만] — 결과는 위와 같은 뜻. */ export async function pasteSpecialFromMenu( ctx: SpreadsheetContext, mode: "값" | "서식", ): Promise { const data = await readSystemClipboard(); if (!data) return false; const at = { ...ctx.selection.활성 }; const block = parseClipboard(data.html, data.text, at); if (!block) { showToast(st("ClipboardPasteFailed"), "error"); return true; } const source = block.출처 === "aislo" && block.원점 ? { 시트: ctx.selection.시트, ...block.원점 } : undefined; pasteSpecial(ctx, block, at, mode, source); return true; } // ── 행/열 크기(숫자 창) ────────────────────────────────────────────────────── function sizeDialog(title: string, initial: number, unit: string, onOk: (n: number) => void): void { const input = el("input", { attrs: { type: "number", min: "4", step: "1", value: String(Math.round(initial)) }, }) as HTMLInputElement; input.style.cssText = "width:80px;margin-right:6px;"; 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 commit = (): void => { const n = Number(input.value); if (Number.isFinite(n) && n > 0) onOk(n); close(); }; panel.append( el("h3", { text: title }), el("div", { children: [input, el("span", { text: unit })] }), el("div", { className: "ui-confirm__actions", children: [ createButton({ label: "취소", variant: "ghost", onClick: close }), createButton({ label: "확인", variant: "filled", onClick: commit }), ], }), ); overlay.append(panel); document.body.append(overlay); dialogKeys(overlay, close, commit); input.focus(); input.select(); } export function openRowSizeDialog(ctx: SpreadsheetContext, rows: number[]): void { if (ctx.readOnly) return; sizeDialog(st("MenuRowSize"), 15, "pt", (pt) => ctx.dispatch({ 종류: "행높이", 시트: ctx.sheet().id, 행: rows, 높이: pt }), ); } export function openColSizeDialog(ctx: SpreadsheetContext, cols: number[]): void { if (ctx.readOnly) return; sizeDialog( st("MenuColSize"), 64, "px", (px) => ctx.dispatch({ 종류: "열폭", 시트: ctx.sheet().id, 열: cols, 폭: px / 7 }), // 대략 7px/문자(헤더 쪽과 같은 어림) ); } export function autofitCols(ctx: SpreadsheetContext, cols: number[]): void { for (const c of cols) autofitColumn(ctx, c); } export function sortAsc(ctx: SpreadsheetContext): void { sortSelection(ctx, false); } export function sortDesc(ctx: SpreadsheetContext): void { sortSelection(ctx, true); } export function filterToggle(ctx: SpreadsheetContext): void { toggleFilter(ctx); } export { freezeFirstRow, freezeFirstColumn, freezeAtActiveCell, unfreezePanes }; export function groupAxis( ctx: SpreadsheetContext, axis: "행" | "열", 번호: number[], on: boolean, ): void { ctx.dispatch({ 종류: "묶음설정", 시트: ctx.sheet().id, 축: axis, 번호, 묶음: on ? { 층: 1 } : null, }); } // ── 조건부 서식 · 데이터 확인 · 범위 이름 — 작은 창에 패널을 그대로 띄움 ──────────── let panel: PartHandle | null = null; let host: HTMLElement | null = null; function closePanelHost(): void { panel?.destroy(); panel = null; host?.remove(); host = null; } function openPanelHost( ctx: SpreadsheetContext, title: string, make: (c: SpreadsheetContext) => PartHandle, ): void { closePanelHost(); const shut = el("button", { text: "×", attrs: { type: "button", title: "닫기" } }); shut.style.cssText = "margin-left:auto;"; const headRow = el("div", { children: [el("strong", { text: title }), shut], }); headRow.style.cssText = "display:flex;align-items:center;gap:8px;margin-bottom:8px;"; const overlay = el("div", { className: "ui-confirm" }); const panelEl = el("div", { className: "ui-confirm__panel" }); overlay.addEventListener("click", (ev) => { if (ev.target === overlay) closePanelHost(); }); shut.addEventListener("click", () => closePanelHost()); panelEl.append(headRow); panel = make(ctx); if (panel.root) panelEl.append(panel.root); overlay.append(panelEl); document.body.append(overlay); host = overlay; dialogKeys(overlay, closePanelHost); } export function openCondFmt(ctx: SpreadsheetContext): void { openPanelHost(ctx, st("MenuCondFmt"), createCondFmtPanel); } export function openValidation(ctx: SpreadsheetContext): void { openPanelHost(ctx, st("MenuValidation"), createValidationPanel); } export async function openNames(ctx: SpreadsheetContext): Promise { const m = await import("./spreadsheet_names"); openPanelHost(ctx, st("MenuNames"), m.createNameManagerPanel); } export function insertCells(ctx: SpreadsheetContext): void { openInsertCellsDialog(ctx); } export function deleteCells(ctx: SpreadsheetContext): void { openDeleteCellsDialog(ctx); }