/* ============================================================================= * spreadsheet_tabs.ts (주인 E) * 시트 탭 — 고르기(`ctx.showSheet`) · 더하기 · 이름 바꾸기(두 번 누르기) · 지우기 · 숨기기 · * 끌어 옮기기 · 복사. 우클릭 메뉴는 `ui_template_context_menu.ts` 재사용. * 이름 바꾸기 · 지우기 · 옮기기는 명령(`시트이름` · `시트지우기` · `시트옮기기` · `시트더하기`). * 숨김 · 탭 색은 `spreadsheet_sheet_extra.ts`(sub4)의 `hideSheet` · `unhideSheet` 를 부름 — * `Sheet.숨김` 인 시트는 탭을 안 그림 · 숨긴 시트 목록은 우클릭 [숨긴 시트 보이기] 하위 메뉴 · * `Sheet.탭색` 은 탭 밑줄 색. * * 이음(D `spreadsheet.ts` 잇는 자리) — `mountTabs(ctx)` 한 번 부르고 `root` 를 탭 자리(격자 아래)에 * 붙임. `ctx.showSheet` 로 활성 시트가 바뀌면(다른 길로도) `refresh()` 를 불러 다시 그림. * ========================================================================== */ 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 { hideSheet, unhideSheet } from "./spreadsheet_sheet_extra"; 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.시트.filter((s) => !s.숨김).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; if (sheet.탭색) tab.style.borderBottom = `3px solid ${sheet.탭색}`; 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 }); } }, ], [ st("SheetHide"), () => { const err = hideSheet(ctx, sheet.id); if (err) showToast(err, "error"); }, ], ]; const hidden = ctx.book.시트.filter((s) => s.숨김); if (hidden.length) { items.push({ label: st("SheetUnhideMenu"), children: hidden.map((s): [string, () => void] => [s.이름, () => unhideSheet(ctx, s.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(); }, }; }