/* ============================================================================= * spreadsheet_tabs.ts (주인 E) * 시트 탭 — 고르기(`ctx.showSheet`) · 더하기 · 이름 바꾸기(두 번 누르기) · 지우기 · * 끌어 옮기기 · 복사. 우클릭 메뉴는 `ui_template_context_menu.ts` 재사용. * 이름 바꾸기 · 지우기 · 옮기기는 명령(`시트이름` · `시트지우기` · `시트옮기기` · `시트더하기`). * ========================================================================== */ import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu"; import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { emptySheet } from "./spreadsheet_commands"; import { st } from "./spreadsheet_text"; import type { Sheet, Workbook } from "./spreadsheet_types"; import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; import "./spreadsheet_toolbar.css"; /** 없는 `s숫자` 하나 고름. */ function nextSheetId(book: Workbook): string { const used = new Set(book.시트.map((s) => s.id)); let n = 1; while (used.has(`s${n}`)) n++; return `s${n}`; } /** 「시트2」 · 「시트2 (2)」 처럼 안 겹치는 이름. */ function nextSheetName(book: Workbook, base: string): string { const used = new Set(book.시트.map((s) => s.이름)); if (!used.has(base)) return base; let n = 2; while (used.has(`${base} (${n})`)) n++; return `${base} (${n})`; } export function mountTabs(ctx: SpreadsheetContext): PartHandle { const menu = createMapContextMenu("ss"); const root = el("div", { className: "ss-tabs", children: [menu.element] }); let dragging: string | null = null; function tabEl(id: string): HTMLElement | null { return root.querySelector(`[data-sheet-id="${CSS.escape(id)}"]`); } function render(): void { root.replaceChildren(menu.element, ...ctx.book.시트.map(renderTab), addButton()); } function renderTab(sheet: Sheet): HTMLElement { const tab = el("div", { className: `ss-tabs__tab${sheet.id === ctx.book.활성 ? " is-active" : ""}`, text: sheet.이름, attrs: { "data-sheet-id": sheet.id }, }); tab.draggable = true; tab.addEventListener("click", () => ctx.showSheet(sheet.id)); tab.addEventListener("dblclick", () => beginRename(sheet)); tab.addEventListener("contextmenu", (ev) => { ev.preventDefault(); const box = root.getBoundingClientRect(); openMenu(sheet, ev.clientX - box.left, ev.clientY - box.top); }); tab.addEventListener("dragstart", (ev) => { dragging = sheet.id; ev.dataTransfer?.setData("text/plain", sheet.id); }); tab.addEventListener("dragover", (ev) => { if (!dragging || dragging === sheet.id) return; ev.preventDefault(); tab.classList.add("is-dragover"); }); tab.addEventListener("dragleave", () => tab.classList.remove("is-dragover")); tab.addEventListener("drop", (ev) => { ev.preventDefault(); tab.classList.remove("is-dragover"); if (!dragging || dragging === sheet.id) return; const 자리 = ctx.book.시트.findIndex((s) => s.id === sheet.id); ctx.dispatch({ 종류: "시트옮기기", 시트: dragging, 자리 }); dragging = null; }); return tab; } function beginRename(sheet: Sheet): void { const tab = tabEl(sheet.id); if (!tab) return; const input = el("input", { attrs: { value: sheet.이름 } }) as HTMLInputElement; tab.replaceChildren(input); input.focus(); input.select(); const commit = (): void => { const name = input.value.trim(); if (name && name !== sheet.이름) ctx.dispatch({ 종류: "시트이름", 시트: sheet.id, 이름: name }); else render(); }; input.addEventListener("keydown", (ev) => { if (ev.key === "Enter") input.blur(); if (ev.key === "Escape") { input.removeEventListener("blur", commit); render(); } }); input.addEventListener("blur", commit); } function openMenu(sheet: Sheet, x: number, y: number): void { const items: MapContextMenuItem[] = [ [st("SheetRename"), () => beginRename(sheet)], [ st("SheetDuplicate"), () => { const copy: Sheet = { ...structuredClone(sheet), id: nextSheetId(ctx.book), 이름: nextSheetName(ctx.book, sheet.이름), }; const 자리 = ctx.book.시트.findIndex((s) => s.id === sheet.id) + 1; ctx.dispatch({ 종류: "시트더하기", 시트: copy, 자리 }); }, ], [ st("SheetDelete"), async () => { if (ctx.book.시트.length <= 1) { showToast(st("SheetDeleteLastError"), "error"); return; } if (await showConfirmDialog(st("SheetDeleteConfirm"))) { ctx.dispatch({ 종류: "시트지우기", 시트: sheet.id }); } }, ], ]; menu.open(x, y, items); } function addButton(): HTMLElement { const btn = el("button", { className: "ss-tabs__add", text: "+", attrs: { type: "button", title: st("SheetAdd") }, }); btn.addEventListener("click", () => { const name = nextSheetName(ctx.book, "Sheet"); const sheet = emptySheet(nextSheetId(ctx.book), name); ctx.dispatch({ 종류: "시트더하기", 시트: sheet, 자리: ctx.book.시트.length }); }); return btn; } render(); return { root, refresh: render, destroy() { menu.close(); root.remove(); }, }; }