/* ============================================================================= * spreadsheet_menu.ts (주인 E) * 우클릭 메뉴 — 칸 · 행 머리 · 열 머리마다: 잘라내기 · 복사 · (붙여넣기 = 「Ctrl+V 를 쓸 것」 안내) · * 행열 넣기 · 지우기 · 숨기기 · 보이기 · 내용 지우기 · 서식 지우기 · 병합 · 풀기. * `ui_template_context_menu.ts` 재사용 — 우클릭 밖 자리는 그냥 눌러 선택부터 옮김. * * 이음(D `spreadsheet.ts` 잇는 자리) — `attachMenu(ctx)` 한 번 부름(반환 `root` = 메뉴 껍데기 · * 안 붙여도 됨 · `ctx.root` 안에 스스로 넣음). `ctx.grid.hitTest` 가 준비된 뒤(C 격자 붙은 뒤) 불러야 함. * ========================================================================== */ import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu"; import { showToast } from "@ui/ui_template_elements"; import { rangeToA1 } from "./spreadsheet_address"; import { st } from "./spreadsheet_text"; import { MAX_COLS, MAX_ROWS, type CellRange } from "./spreadsheet_types"; import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; import "./spreadsheet_toolbar.css"; function within(range: CellRange, r: number, c: number): boolean { return r >= range.r0 && r <= range.r1 && c >= range.c0 && c <= range.c1; } /** `ctx.root` 의 contextmenu 를 받음(root null) */ export function attachMenu(ctx: SpreadsheetContext): PartHandle { if (ctx.readOnly) return { root: null, refresh() {}, destroy() {} }; const menu = createMapContextMenu("ss"); ctx.root.append(menu.element); function isMerged(range: CellRange): boolean { return (ctx.sheet().병합 ?? []).includes(rangeToA1(range)); } function cutCopy(kind: "cut" | "copy"): void { document.execCommand(kind); } function cellMenuItems(range: CellRange): MapContextMenuItem[] { return [ [st("MenuCut"), () => cutCopy("cut")], [st("MenuCopy"), () => cutCopy("copy")], [st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")], [ st("MenuClearContent"), () => ctx.dispatch({ 종류: "내용지움", 시트: ctx.selection.시트, 범위: [range] }), ], [ st("MenuClearFormat"), () => ctx.dispatch({ 종류: "서식지움", 시트: ctx.selection.시트, 범위: [range] }), ], [ isMerged(range) ? st("MenuUnmerge") : st("MenuMerge"), () => ctx.dispatch( isMerged(range) ? { 종류: "병합풀기", 시트: ctx.selection.시트, 범위: range } : { 종류: "병합", 시트: ctx.selection.시트, 범위: range }, ), ], ]; } function rowMenuItems(rows: number[]): MapContextMenuItem[] { const at = Math.min(...rows); const 수 = rows.length; return [ [st("MenuCopy"), () => cutCopy("copy")], [ st("MenuInsertRow"), () => ctx.dispatch({ 종류: "행넣기", 시트: ctx.selection.시트, at, 수 }), ], [ st("MenuDeleteRow"), () => ctx.dispatch({ 종류: "행지우기", 시트: ctx.selection.시트, at, 수 }), ], [ st("MenuHide"), () => ctx.dispatch({ 종류: "숨김", 시트: ctx.selection.시트, 축: "행", 번호: rows, 숨김: true, }), ], [ st("MenuUnhide"), () => ctx.dispatch({ 종류: "숨김", 시트: ctx.selection.시트, 축: "행", 번호: rows, 숨김: false, }), ], ]; } function colMenuItems(cols: number[]): MapContextMenuItem[] { const at = Math.min(...cols); const 수 = cols.length; return [ [st("MenuCopy"), () => cutCopy("copy")], [ st("MenuInsertCol"), () => ctx.dispatch({ 종류: "열넣기", 시트: ctx.selection.시트, at, 수 }), ], [ st("MenuDeleteCol"), () => ctx.dispatch({ 종류: "열지우기", 시트: ctx.selection.시트, at, 수 }), ], [ st("MenuHide"), () => ctx.dispatch({ 종류: "숨김", 시트: ctx.selection.시트, 축: "열", 번호: cols, 숨김: true, }), ], [ st("MenuUnhide"), () => ctx.dispatch({ 종류: "숨김", 시트: ctx.selection.시트, 축: "열", 번호: cols, 숨김: false, }), ], ]; } function range(r0: number, c0: number, r1: number, c1: number): CellRange { return { r0, c0, r1, c1 }; } function onContextMenu(ev: MouseEvent): void { const hit = ctx.grid.hitTest(ev.clientX, ev.clientY); if (!hit) return; ev.preventDefault(); const box = ctx.root.getBoundingClientRect(); const x = ev.clientX - box.left; const y = ev.clientY - box.top; if (hit.kind === "rowHead") { const covered = ctx.selection.범위.find( (rg) => rg.c0 === 0 && rg.c1 >= MAX_COLS - 1 && within(rg, hit.r, 0), ); const rows = covered ? Array.from({ length: covered.r1 - covered.r0 + 1 }, (_, i) => covered.r0 + i) : [hit.r]; if (!covered) ctx.select({ 시트: ctx.selection.시트, 범위: [range(hit.r, 0, hit.r, MAX_COLS - 1)], 활성: { r: hit.r, c: 0 }, 기준: { r: hit.r, c: 0 }, }); menu.open(x, y, rowMenuItems(rows)); return; } if (hit.kind === "colHead") { const covered = ctx.selection.범위.find( (rg) => rg.r0 === 0 && rg.r1 >= MAX_ROWS - 1 && within(rg, 0, hit.c), ); const cols = covered ? Array.from({ length: covered.c1 - covered.c0 + 1 }, (_, i) => covered.c0 + i) : [hit.c]; if (!covered) ctx.select({ 시트: ctx.selection.시트, 범위: [range(0, hit.c, MAX_ROWS - 1, hit.c)], 활성: { r: 0, c: hit.c }, 기준: { r: 0, c: hit.c }, }); menu.open(x, y, colMenuItems(cols)); return; } if (hit.kind === "cell") { const covered = ctx.selection.범위.some((rg) => within(rg, hit.r, hit.c)); if (!covered) { ctx.select({ 시트: ctx.selection.시트, 범위: [range(hit.r, hit.c, hit.r, hit.c)], 활성: { r: hit.r, c: hit.c }, 기준: { r: hit.r, c: hit.c }, }); } const active = ctx.selection.범위[0] ?? range(hit.r, hit.c, hit.r, hit.c); menu.open(x, y, cellMenuItems(active)); } } ctx.root.addEventListener("contextmenu", onContextMenu); return { root: menu.element, refresh() {}, destroy() { ctx.root.removeEventListener("contextmenu", onContextMenu); menu.close(); menu.element.remove(); }, }; }