/* ============================================================================= * spreadsheet_sort_dialog.ts (주인 A · 화면) * 정렬 대화 상자 · 빠른 정렬(오름 · 내림). 엔진은 `spreadsheet_sortfilter.ts`(명령 `정렬`). * 범위 = 고른 범위 · 칸 하나만 골랐으면 이어진 값 덩어리(엑셀 「현재 영역」) · 머리 행은 짐작해 켬. * * 잇는 법(D · sub7): * · 도구 모음 · 메뉴 「정렬…」 → `openSortDialog(ctx)` — 창을 `ctx.root` 에 띄우고 닫는 함수를 돌려줌. * · 「오름차순」 「내림차순」 단추 → `sortSelection(ctx, 내림)` — 활성 칸 열을 기준으로 바로 정렬. * · 막히면(병합 걸침 등) 토스트로 까닭을 보임 · 되돌리기는 `ctx.dispatch` 가 쌓음. * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { colName, rangeToA1 } from "./spreadsheet_address"; import { CommandError } from "./spreadsheet_commands"; import { currentRegion, displayText, sortCommand } from "./spreadsheet_sortfilter"; import type { SortKey, SortOptions } from "./spreadsheet_sortfilter"; import type { CellRange } from "./spreadsheet_types"; import type { SpreadsheetContext } from "./spreadsheet_view_types"; import "./spreadsheet_sortfilter.css"; const TEXT = { Title: ["정렬", "Sort"], Range: ["범위", "Range"], Header: ["머리 행 있음", "My data has headers"], Case: ["대소문자 구분", "Case sensitive"], Column: ["열", "Column"], Asc: ["오름차순", "Ascending"], Desc: ["내림차순", "Descending"], AddKey: ["기준 더하기", "Add level"], RemoveKey: ["빼기", "Remove"], Cancel: ["취소", "Cancel"], Ok: ["정렬", "Sort"], }; const t = (key: keyof typeof TEXT) => TEXT[key][currentLanguageIndex] ?? TEXT[key][0]; /** 정렬 범위 — 칸 하나면 현재 영역 */ function targetRange(ctx: SpreadsheetContext): CellRange { const g = ctx.selection.범위[0]; if (g.r0 === g.r1 && g.c0 === g.c1) return currentRegion(ctx.book, ctx.selection.시트, g.r0, g.c0); return g; } /** 머리 행 짐작 — 첫 행이 모두 글이고 둘째 행에 글 아닌 값이 있으면 머리 */ function guessHeader(ctx: SpreadsheetContext, rg: CellRange): boolean { if (rg.r0 === rg.r1) return false; const v = (r: number, c: number) => ctx.engine.value(ctx.selection.시트, r, c); let allText = true; let secondOther = false; for (let c = rg.c0; c <= rg.c1; c++) { const a = v(rg.r0, c); const b = v(rg.r0 + 1, c); if (a !== null && typeof a !== "string") allText = false; if (b !== null && typeof b !== "string") secondOther = true; } return allText && secondOther; } function run(ctx: SpreadsheetContext, rg: CellRange, opts: SortOptions): boolean { try { ctx.dispatch(sortCommand(ctx.book, ctx.engine, ctx.selection.시트, rg, opts)); return true; } catch (e) { if (!(e instanceof CommandError)) throw e; showToast(e.message, "error"); return false; } } /** 활성 칸 열 기준 바로 정렬 */ export function sortSelection(ctx: SpreadsheetContext, desc: boolean): void { if (ctx.readOnly) return; const rg = targetRange(ctx); const c = Math.min(Math.max(ctx.selection.활성.c, rg.c0), rg.c1); run(ctx, rg, { 머리: guessHeader(ctx, rg), 기준: [{ 열: c, 내림: desc }] }); } /** 정렬 대화 상자 — 닫는 함수를 돌려줌 */ export function openSortDialog(ctx: SpreadsheetContext): () => void { const rg = targetRange(ctx); const sheet = ctx.selection.시트; const keys: SortKey[] = [{ 열: Math.min(Math.max(ctx.selection.활성.c, rg.c0), rg.c1) }]; const header = el("input", { attrs: { type: "checkbox" } }); header.checked = guessHeader(ctx, rg); const caseBox = el("input", { attrs: { type: "checkbox" } }); const list = el("div", { className: "ss-sort__keys" }); const box = el("div", { className: "ss-sort__box", attrs: { role: "dialog", "aria-label": t("Title") }, }); const shade = el("div", { className: "ss-sort", children: [box] }); const columnLabel = (c: number) => { const head = header.checked ? displayText(ctx.book, ctx.engine, sheet, rg.r0, c) : ""; return head ? `${colName(c)}: ${head}` : `${t("Column")} ${colName(c)}`; }; function drawKeys() { list.replaceChildren( ...keys.map((k, i) => { const col = el("select", { attrs: { "aria-label": t("Column") } }); for (let c = rg.c0; c <= rg.c1; c++) col.append(el("option", { text: columnLabel(c), attrs: { value: String(c) } })); col.value = String(k.열); col.addEventListener("change", () => (k.열 = Number(col.value))); const dir = el("select", { children: [ el("option", { text: t("Asc"), attrs: { value: "asc" } }), el("option", { text: t("Desc"), attrs: { value: "desc" } }), ], }); dir.value = k.내림 ? "desc" : "asc"; dir.addEventListener("change", () => (k.내림 = dir.value === "desc")); const remove = createButton({ label: t("RemoveKey"), variant: "ghost", disabled: keys.length === 1, onClick: () => { keys.splice(i, 1); drawKeys(); }, }); return el("div", { className: "ss-sort__row", children: [col, dir, remove] }); }), ); } const close = () => { shade.remove(); document.removeEventListener("keydown", onKey, true); }; const ok = () => { if (run(ctx, rg, { 머리: header.checked, 기준: keys, 대소문자: caseBox.checked })) close(); }; const onKey = (ev: KeyboardEvent) => { if (ev.key === "Escape") close(); else if (ev.key === "Enter" && !(ev.target instanceof HTMLSelectElement)) ok(); else return; ev.preventDefault(); ev.stopPropagation(); }; header.addEventListener("change", drawKeys); box.append( el("div", { className: "ss-sort__title", text: t("Title") }), el("div", { className: "ss-sort__meta", text: `${t("Range")} ${rangeToA1(rg)}` }), el("label", { className: "ss-sort__meta", children: [header, t("Header")] }), el("label", { className: "ss-sort__meta", children: [caseBox, t("Case")] }), list, el("div", { className: "ss-sort__row", children: [ createButton({ label: t("AddKey"), variant: "ghost", onClick: () => { keys.push({ 열: rg.c0 }); drawKeys(); }, }), ], }), el("div", { className: "ss-sort__foot", children: [ createButton({ label: t("Cancel"), variant: "ghost", onClick: close }), createButton({ label: t("Ok"), onClick: ok }), ], }), ); shade.addEventListener("mousedown", (ev) => { if (ev.target === shade) close(); }); document.addEventListener("keydown", onKey, true); drawKeys(); ctx.root.append(shade); return close; }