Files
Aislo/A00_Common/spreadsheet/spreadsheet_menu_items.ts
T

219 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<boolean> {
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<boolean> {
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<void> {
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);
}