Files
Aislo/ui_template/ui_template_pager.ts
T
eomsangdon 2e43011069 feat(709-1): 쪽 단추 부품 · 수평 구분선 부품 · 부품 모음 견본
- ui_template_pager.ts: M01 buildPager 이동 + 화면 높이 줄 수 셈(fitRows) · 첫 줄 유지(pageKeepFirst) · 화면 크기 알림(watchFit)
- ui_template_divider.ts + overlay.css .ui-divider: 수평 구분선
- M01 buildPager = 새 부품을 부르는 옛 이름(PickTable · M02 목록 그대로)
- 부품 모음 견본 두 줄 · 지킴 시험 test_709_pager_fit.py
2026-11-01 14:40:53 +09:00

98 lines
4.2 KiB
TypeScript

/* =============================================================================
* ui_template_pager.ts
* 부품 「쪽 단추」 `pager` — 이전 · 다음 + 「n / N 쪽 · 전체 줄」 한 줄 (M01 `buildPager` 를 옮김)
* 꼴 = 한 줄 도구줄(`ui-toolbar ui-pager`) · 글 → 이전 → 다음 · 쪽이 한 장뿐이어도 그대로 보임
* 크기 = 도구줄 단 없음(안 단추 = 보조 단추 기본 높이)
* 상태 = 첫 쪽 이전 막힘 · 끝 쪽 다음 막힘 (`disabled`)
* 쓰는 법 = createPager(total, size, page, (next) => …) — 표 **바로 아래**에 놓음
* 표 줄 수 = `fitRows(남은 높이)` 순수 셈 · `watchFit(표 칸, (줄 수) => …)` 가 화면 크기 바뀔 때 다시 알림
* 옛 이름 = M01 `buildPager`(`M01_MasterData_UI_Cells.ts`) 는 이 부품을 부르는 얇은 껍데기
* ========================================================================== */
import { createButton } from "./ui_template_button";
import { el } from "./ui_template_elements_base";
import { t as L } from "./ui_template_locale";
import "./ui_template_pager.css";
/** 표 한 줄 높이(px) · 머리 높이 — 실제 줄이 있으면 잰 값을 씀 */
export const ROW_H = 40;
/** 표 밑 쪽 단추 줄 + 여백(px) */
export const PAGER_ROOM = 56;
/** 표가 보일 수 있는 줄 수 아래 한도 · 위 한도 */
export const MIN_ROWS = 5;
export const MAX_ROWS = 200;
/** 전체 줄 · 쪽 크기 → 쪽 수(최소 1). */
export const pageCount = (total: number, size: number): number =>
Math.max(1, Math.ceil(total / Math.max(1, size)));
/** 남은 높이(표 머리 포함) → 줄 수 — 머리 한 줄 빼고 줄 높이로 나눔 · 한도 안으로. */
export function fitRows(room: number, rowH = ROW_H, headH = ROW_H): number {
const n = Math.floor((room - headH) / Math.max(1, rowH));
return Math.min(MAX_ROWS, Math.max(MIN_ROWS, Number.isFinite(n) ? n : MIN_ROWS));
}
/** 쪽 크기가 바뀌어도 보던 첫 줄이 같은 쪽에 남도록 새 쪽 번호. */
export const pageKeepFirst = (page: number, oldSize: number, newSize: number): number =>
Math.floor(((Math.max(1, page) - 1) * oldSize) / Math.max(1, newSize)) + 1;
/** 표 칸 위쪽 자리 ~ 화면 아래 사이로 들어가는 줄 수. 표 칸이 화면에 없으면 null. */
export function measureRows(grid: HTMLElement): number | null {
if (!grid.isConnected) return null;
const top = grid.getBoundingClientRect().top;
if (!grid.offsetParent && getComputedStyle(grid).position !== "fixed") return null; // 숨은 칸
const row = grid.querySelector<HTMLElement>("tbody tr");
const head = grid.querySelector<HTMLElement>("thead");
const rowH = row?.offsetHeight || ROW_H;
const headH = head?.offsetHeight || ROW_H;
return fitRows(window.innerHeight - top - PAGER_ROOM, rowH, headH);
}
/** 표 칸이 붙은 뒤 줄 수를 재서 알림 + 창 크기 바뀔 때마다 다시. 돌려받은 함수 = 해제. */
export function watchFit(grid: HTMLElement, onRows: (rows: number) => void): () => void {
let last = 0;
let frame = 0;
const check = (): void => {
frame = 0;
const n = measureRows(grid);
if (n !== null && n !== last) {
last = n;
onRows(n);
}
};
const ask = (): void => {
if (!frame) frame = requestAnimationFrame(check);
};
window.addEventListener("resize", ask);
ask();
return () => {
window.removeEventListener("resize", ask);
if (frame) cancelAnimationFrame(frame);
};
}
/** 쪽 나눔 띠 — 이전·다음 · 「n / N 쪽 · 전체 줄」. */
export function createPager(
total: number,
size: number,
page: number,
goto: (page: number) => void,
): HTMLElement {
const last = pageCount(total, size);
const prev = createButton({ label: L("M01_Prev"), variant: "ghost", disabled: page <= 1 });
const next = createButton({ label: L("M01_Next"), variant: "ghost", disabled: page >= last });
prev.addEventListener("click", () => goto(page - 1));
next.addEventListener("click", () => goto(page + 1));
return el("div", {
className: "ui-toolbar ui-pager",
children: [
el("span", {
className: "ui-pager__text",
text: `${page} / ${last} ${L("M01_Page")} · ${total.toLocaleString("ko-KR")} ${L("M01_Rows")}`,
}),
prev,
next,
],
});
}