- 계약 더함: Command 「정렬」(차례를 박음 → 다시 하기가 같음) · 「필터」(걸러짐 행을 박음) · AutoFilter · Sheet.필터 · RowInfo.걸러짐 · 파일 목록 - spreadsheet_sortfilter.ts(A 엔진) — 수 < 글 < 참거짓 < 오류 · 빈 칸 늘 맨 뒤 · 같은 값 원래 차례 · 대소문자 옵션 · 현재 영역 · 정렬 옮김 = 범위 열만 · 식은 상대 참조만 복사처럼 · 범위 밖 식 그대로 · 서식 · 메모 따라감 · 걸러진 행 제자리 · 병합 걸치면 막음 · 자동 필터 엔진(값 목록 · 걸러짐 셈 · 적용 · 행열 넣기 · 지우기 때 필터 따라감) · 되돌림 짝 = 시트통째 - spreadsheet_sort_dialog.ts — 정렬 대화 상자(범위 · 머리 행 짐작 · 기준 더하기 · 오름 · 내림) · 빠른 정렬 sortSelection - spreadsheet_commands_sheet.ts — 시트 명령을 떼어 냄(commands 700 줄 안쪽) - 시험 test_spreadsheet_sortfilter.py 엑셀 규칙표 4 묶음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
181 lines
6.9 KiB
TypeScript
181 lines
6.9 KiB
TypeScript
/* =============================================================================
|
|
* 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;
|
|
}
|