Files
Aislo/A00_Common/spreadsheet/spreadsheet_view_menu.ts
T

198 lines
7.3 KiB
TypeScript

/* =============================================================================
* spreadsheet_view_menu.ts (2단계 · 주인 sub_laptop_1)
* 이동 대화상자(F5 · 주소 · 정의된 이름) · 틀 고정 메뉴(첫 행 · 첫 열 · 현재 칸 기준 · 풀기).
*
* 정의된 이름 목록은 `Sheet.이름정의`(3단계 · sub_laptop_4 계약 · 안 이어짐) 가 있으면
* 그대로 보여 줌 — 없으면 빈 목록(주소 입력만 됨). 이름으로 다른 시트를 못 가리키는
* 지금 계약대로 활성 시트 안에서만 찾음.
*
* 잇는 법(D · sub7) — F5 → `openGoToDialog(ctx)` 를 `spreadsheet_keys.ts` navKey 에 더함.
* 틀 고정 단추(E 도구 모음)는 눌렀을 때 `openFreezePanesMenu(ctx, button)` 를 부르면 그
* 단추 아래 작은 메뉴가 뜸.
* ========================================================================== */
import { createButton, createInputField, el } from "@ui/ui_template_elements";
import { parseRange, rangeToA1 } from "./spreadsheet_address";
import type { CellRange } from "./spreadsheet_types";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
const TEXT = {
goToTitle: "이동",
addressLabel: "참조",
namedRanges: "정의된 이름",
ok: "확인",
cancel: "취소",
notFound: "주소를 찾을 수 없음",
freezeFirstRow: "첫 행 고정",
freezeFirstColumn: "첫 열 고정",
freezeAtActive: "틀 고정(현재 칸 기준)",
unfreeze: "틀 고정 풀기",
};
// ═════════════════════════════════════════════════════════════════════════════
// 이동(F5)
// ═════════════════════════════════════════════════════════════════════════════
/** 범위(또는 칸) 글을 읽어 그 범위로 고름을 옮기고 보이게 스크롤함. */
export function goTo(ctx: SpreadsheetContext, target: CellRange): void {
const top = { r: target.r0, c: target.c0 };
ctx.select({ 시트: ctx.selection.시트, 범위: [target], 활성: top, 기준: top }, true);
}
export function openGoToDialog(ctx: SpreadsheetContext): void {
const overlay = el("div", { className: "ui-confirm" });
const panel = el("div", { className: "ui-confirm__panel" });
const close = (): void => overlay.remove();
overlay.addEventListener("click", (ev) => {
if (ev.target === overlay) close();
});
const errorMsg = el("p", { text: "" });
errorMsg.style.cssText = "color:var(--color-danger, red);font-size:0.8rem;min-height:1em;";
const addressField = createInputField({
label: TEXT.addressLabel,
value: rangeToA1(
ctx.selection.범위[0] ?? {
r0: ctx.selection.활성.r,
c0: ctx.selection.활성.c,
r1: ctx.selection.활성.r,
c1: ctx.selection.활성.c,
},
),
});
const goToAddress = (): void => {
const target = parseRange(addressField.input.value);
if (!target) {
errorMsg.textContent = TEXT.notFound;
return;
}
goTo(ctx, target);
close();
};
addressField.input.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") goToAddress();
});
const names = ctx.sheet().이름정의 ?? [];
const namesList = el("div", {
children: names.map((n) =>
createButton({
label: `${n.이름} (${rangeToA1(n.범위)})`,
variant: "ghost",
onClick: () => {
goTo(ctx, n.범위);
close();
},
}),
),
});
namesList.style.cssText =
"display:flex;flex-direction:column;gap:2px;max-height:160px;overflow:auto;";
panel.append(
el("h3", { text: TEXT.goToTitle }),
addressField.root,
errorMsg,
...(names.length > 0 ? [el("p", { text: TEXT.namedRanges }), namesList] : []),
el("div", {
className: "ui-confirm__actions",
children: [
createButton({ label: TEXT.cancel, variant: "ghost", onClick: close }),
createButton({ label: TEXT.ok, variant: "filled", onClick: goToAddress }),
],
}),
);
overlay.append(panel);
document.body.append(overlay);
addressField.input.focus();
addressField.input.select();
}
// ═════════════════════════════════════════════════════════════════════════════
// 틀 고정
// ═════════════════════════════════════════════════════════════════════════════
export function freezeFirstRow(ctx: SpreadsheetContext): void {
ctx.dispatch({
종류: "틀고정",
시트: ctx.selection.시트,
행: 1,
열: ctx.sheet().틀고정?.열 ?? 0,
});
}
export function freezeFirstColumn(ctx: SpreadsheetContext): void {
ctx.dispatch({
종류: "틀고정",
시트: ctx.selection.시트,
행: ctx.sheet().틀고정?.행 ?? 0,
열: 1,
});
}
/** 활성 칸의 왼위 자리를 기준으로 그 위 · 왼쪽을 고정(활성 칸이 A1 이면 사실상 풀기와 같음). */
export function freezeAtActiveCell(ctx: SpreadsheetContext): void {
const at = ctx.selection.활성;
ctx.dispatch({ 종류: "틀고정", 시트: ctx.selection.시트, 행: at.r, 열: at.c });
}
export function unfreezePanes(ctx: SpreadsheetContext): void {
ctx.dispatch({ 종류: "틀고정", 시트: ctx.selection.시트, 행: 0, 열: 0 });
}
/** 도구 모음 단추 아래(또는 화면 가운데, `anchor` 없으면) 뜨는 작은 메뉴 — 누르면 바로 적용하고 닫힘. */
export function openFreezePanesMenu(ctx: SpreadsheetContext, anchor?: HTMLElement): void {
const menu = el("div", { className: "ui-confirm__panel" });
menu.style.cssText =
"position:fixed;display:flex;flex-direction:column;gap:2px;min-width:180px;z-index:1000;padding:6px;";
const items: [string, () => void][] = [
[TEXT.freezeFirstRow, () => freezeFirstRow(ctx)],
[TEXT.freezeFirstColumn, () => freezeFirstColumn(ctx)],
[TEXT.freezeAtActive, () => freezeAtActiveCell(ctx)],
[TEXT.unfreeze, () => unfreezePanes(ctx)],
];
const close = (): void => {
menu.remove();
document.removeEventListener("mousedown", onOutside, true);
document.removeEventListener("keydown", onEscape);
};
const onOutside = (ev: MouseEvent): void => {
if (!menu.contains(ev.target as Node)) close();
};
const onEscape = (ev: KeyboardEvent): void => {
if (ev.key === "Escape") close();
};
menu.append(
...items.map(([label, action]) =>
createButton({
label,
variant: "ghost",
onClick: () => {
action();
close();
},
}),
),
);
document.body.append(menu);
if (anchor) {
const box = anchor.getBoundingClientRect();
menu.style.top = `${box.bottom + 4}px`;
menu.style.left = `${box.left}px`;
} else {
menu.style.top = "40%";
menu.style.left = "50%";
menu.style.transform = "translate(-50%, -50%)";
}
setTimeout(() => {
document.addEventListener("mousedown", onOutside, true);
document.addEventListener("keydown", onEscape);
}, 0);
}