- 숨긴 시트(Sheet.숨김)는 탭 목록에서 뺌 - 탭 우클릭에 [숨기기] · 숨긴 시트가 있으면 [숨긴 시트 보이기] 하위 목록(이름 눌러 바로 보임) - Sheet.탭색 은 탭 밑줄 색으로 - ORCA 실 엔진 확인 — 숨기기 → 탭 사라짐 → 하위 목록에서 보이기 → 다시 나타남 · 탭색 지정 → 밑줄 반영
191 lines
6.7 KiB
TypeScript
191 lines
6.7 KiB
TypeScript
/* =============================================================================
|
|
* 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<HTMLElement>(`[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();
|
|
},
|
|
};
|
|
}
|