Files
Aislo/A00_Common/spreadsheet/spreadsheet_insert_dialog.ts
T

241 lines
9.2 KiB
TypeScript

/* =============================================================================
* spreadsheet_insert_dialog.ts (2단계 · 주인 sub_laptop_1)
* 칸 삽입(Ctrl+Shift++) · 칸 삭제(Ctrl+-) 대화상자 — 엑셀처럼 네 갈래:
* 삽입 = 셀을 오른쪽으로 밀기 · 셀을 아래로 밀기 · 행 전체 · 열 전체
* 삭제 = 셀을 왼쪽으로 밀기 · 셀을 위로 밀기 · 행 전체 · 열 전체
* 「행 전체」·「열 전체」는 A 의 행넣기 · 행지우기 · 열넣기 · 열지우기 명령을 그대로 씀
* (참조 옮김까지 A 가 다 함). 「오른쪽 · 아래로 · 왼쪽 · 위로 밀기」는 0 계약에 전용 명령이
* 없어(부분 범위 이동 명령 없음) 「칸」 명령으로 손수 값 · 식 글 · 서식 번호를 옮김 —
* ⚠ 식의 상대 참조는 안 고쳐짐 · 행 · 열 단위 기본서식(`행`·`열` 레코드)도 안 옮김
* (부분 이동은 칸 레코드만). 온전한 참조 옮김이 필요하면 「행 전체」·「열 전체」를 쓰거나
* A 의 `spreadsheet_refshift.ts` 가 부분 범위까지 다루게 된 뒤 이 파일의 shift* 함수만 고치면 됨.
*
* 잇는 법(D · sub7) — Ctrl+Shift+= → `openInsertCellsDialog(ctx)` · Ctrl+- →
* `openDeleteCellsDialog(ctx)` 를 `spreadsheet_keys.ts` navKey 에 더함. 우클릭 메뉴
* [삽입]·[삭제] 도 같은 함수를 부르면 됨.
* ========================================================================== */
import { createButton, el } from "@ui/ui_template_elements";
import { parseA1, toA1 } from "./spreadsheet_address";
import type { Cell, CellRange, Sheet } from "./spreadsheet_types";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
const TEXT = {
insertTitle: "삽입",
deleteTitle: "삭제",
shiftRight: "셀을 오른쪽으로 밀기",
shiftDown: "셀을 아래로 밀기",
shiftLeft: "셀을 왼쪽으로 밀기",
shiftUp: "셀을 위로 밀기",
wholeRow: "행 전체",
wholeCol: "열 전체",
ok: "확인",
cancel: "취소",
};
function readRange(ctx: SpreadsheetContext): CellRange | null {
return ctx.selection.범위[0] ?? null;
}
/** 시트 `칸` 레코드 안에서 실제 값이 있는 가장 큰 행 · 열(없으면 -1). */
function sheetUsedBounds(sheet: Sheet): { maxR: number; maxC: number } {
let maxR = -1;
let maxC = -1;
for (const key of Object.keys(sheet.칸)) {
const at = parseA1(key);
if (!at) continue;
if (at.r > maxR) maxR = at.r;
if (at.c > maxC) maxC = at.c;
}
return { maxR, maxC };
}
// ═════════════════════════════════════════════════════════════════════════════
// 부분 범위 밀기 — 「칸」 명령으로 값 · 식 · 서식 번호만 옮김(위 머리 주석의 한계 참고)
// ═════════════════════════════════════════════════════════════════════════════
export function insertShiftRight(ctx: SpreadsheetContext, range: CellRange): void {
const sheet = ctx.sheet();
const width = range.c1 - range.c0 + 1;
const lastCol = Math.max(sheetUsedBounds(sheet).maxC + width, range.c1);
const cells: Record<string, Cell | null> = {};
for (let r = range.r0; r <= range.r1; r++) {
for (let c = lastCol; c >= range.c0; c--) {
const src = c - width;
cells[toA1(r, c)] = src >= range.c0 ? (sheet.칸[toA1(r, src)] ?? null) : null;
}
}
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
}
export function insertShiftDown(ctx: SpreadsheetContext, range: CellRange): void {
const sheet = ctx.sheet();
const height = range.r1 - range.r0 + 1;
const lastRow = Math.max(sheetUsedBounds(sheet).maxR + height, range.r1);
const cells: Record<string, Cell | null> = {};
for (let c = range.c0; c <= range.c1; c++) {
for (let r = lastRow; r >= range.r0; r--) {
const src = r - height;
cells[toA1(r, c)] = src >= range.r0 ? (sheet.칸[toA1(src, c)] ?? null) : null;
}
}
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
}
export function deleteShiftLeft(ctx: SpreadsheetContext, range: CellRange): void {
const sheet = ctx.sheet();
const width = range.c1 - range.c0 + 1;
const lastCol = Math.max(sheetUsedBounds(sheet).maxC, range.c1);
const cells: Record<string, Cell | null> = {};
for (let r = range.r0; r <= range.r1; r++) {
for (let c = range.c0; c <= lastCol; c++) {
const src = c + width;
cells[toA1(r, c)] = src <= lastCol ? (sheet.칸[toA1(r, src)] ?? null) : null;
}
}
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
}
export function deleteShiftUp(ctx: SpreadsheetContext, range: CellRange): void {
const sheet = ctx.sheet();
const height = range.r1 - range.r0 + 1;
const lastRow = Math.max(sheetUsedBounds(sheet).maxR, range.r1);
const cells: Record<string, Cell | null> = {};
for (let c = range.c0; c <= range.c1; c++) {
for (let r = range.r0; r <= lastRow; r++) {
const src = r + height;
cells[toA1(r, c)] = src <= lastRow ? (sheet.칸[toA1(src, c)] ?? null) : null;
}
}
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
}
// ═════════════════════════════════════════════════════════════════════════════
// 대화상자
// ═════════════════════════════════════════════════════════════════════════════
function radioGroup(
name: string,
options: [string, string][],
): { root: HTMLElement; value(): string } {
const inputs: HTMLInputElement[] = [];
const root = el("div", {
children: options.map(([value, label], i) => {
const input = el("input", {
attrs: { type: "radio", name, value, ...(i === 0 ? { checked: "true" } : {}) },
}) as HTMLInputElement;
inputs.push(input);
const wrap = el("label", { children: [input, el("span", { text: label })] });
wrap.style.cssText = "display:flex;align-items:center;gap:6px;margin-bottom:4px;";
return wrap;
}),
});
return { root, value: () => inputs.find((i) => i.checked)?.value ?? options[0][0] };
}
function modal(
title: string,
body: HTMLElement,
onOk: (choice: string) => void,
choice: () => string,
): 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();
});
panel.append(
el("h3", { text: title }),
body,
el("div", {
className: "ui-confirm__actions",
children: [
createButton({ label: TEXT.cancel, variant: "ghost", onClick: close }),
createButton({
label: TEXT.ok,
variant: "filled",
onClick: () => {
onOk(choice());
close();
},
}),
],
}),
);
overlay.append(panel);
document.body.append(overlay);
}
export function openInsertCellsDialog(ctx: SpreadsheetContext): void {
if (ctx.readOnly) return;
const range = readRange(ctx);
if (!range) return;
const group = radioGroup("insertMode", [
["right", TEXT.shiftRight],
["down", TEXT.shiftDown],
["row", TEXT.wholeRow],
["col", TEXT.wholeCol],
]);
modal(
TEXT.insertTitle,
group.root,
(choice) => {
if (choice === "right") insertShiftRight(ctx, range);
else if (choice === "down") insertShiftDown(ctx, range);
else if (choice === "row") {
ctx.dispatch({
종류: "행넣기",
시트: ctx.selection.시트,
at: range.r0,
수: range.r1 - range.r0 + 1,
});
} else {
ctx.dispatch({
종류: "열넣기",
시트: ctx.selection.시트,
at: range.c0,
수: range.c1 - range.c0 + 1,
});
}
},
group.value,
);
}
export function openDeleteCellsDialog(ctx: SpreadsheetContext): void {
if (ctx.readOnly) return;
const range = readRange(ctx);
if (!range) return;
const group = radioGroup("deleteMode", [
["left", TEXT.shiftLeft],
["up", TEXT.shiftUp],
["row", TEXT.wholeRow],
["col", TEXT.wholeCol],
]);
modal(
TEXT.deleteTitle,
group.root,
(choice) => {
if (choice === "left") deleteShiftLeft(ctx, range);
else if (choice === "up") deleteShiftUp(ctx, range);
else if (choice === "row") {
ctx.dispatch({
종류: "행지우기",
시트: ctx.selection.시트,
at: range.r0,
수: range.r1 - range.r0 + 1,
});
} else {
ctx.dispatch({
종류: "열지우기",
시트: ctx.selection.시트,
at: range.c0,
수: range.c1 - range.c0 + 1,
});
}
},
group.value,
);
}