198 lines
7.3 KiB
TypeScript
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);
|
|
}
|