- 도구 모음: 글꼴 · 크기 · 굵게 · 기울임 · 밑줄 · 취소선 · 글자색 · 채움색 · 가로/세로 정렬 · 줄 바꿈 · 병합 · 테두리 · 숫자 형식 · 되돌리기 - 시트 탭: 더하기 · 이름 바꾸기 · 지우기 · 복사 · 끌어 옮기기 - 우클릭 메뉴: 칸 · 행 머리 · 열 머리별 명령(공용 `ui_template_context_menu` 재사용) - 클립보드: 안 → 안 서식 · 병합 · 수식 상대 이동 왕복 · 밖 → 안(엑셀 값 + 「수식 표시」 · 구글 R1C1) · 안 → 밖(TSV + 인라인 서식 HTML) - E 화면 시험 틀(A · B 몫 가짜 대역) · TSV 파싱 Node 시험 추가
165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
/* =============================================================================
|
|
* 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<HTMLElement>(`[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();
|
|
},
|
|
};
|
|
}
|