feat(spreadsheet): 우클릭 메뉴에 구글 시트 항목 덧붙임(81-3)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
/* =============================================================================
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user