- 쪽 있는 표(요소 · 표 목록 · 로직 목록 · 재료 고르기 모달 · M02 서고 · 새 항목 목록): 줄 수를 표 칸 위치 ~ 화면 아래로 셈(watchFit) · 창 크기 · 글꼴 늦게 실림에도 다시 셈 · 쪽 크기 바뀌어도 첫 줄 유지 - 쪽 단추 = 표 바로 아래 · 줄이 모자라면 표와 함께 위로 붙음 - 쪽 없는 한 장 표(소요량 · 계수): 높이를 화면 아래로 묶고 안에서 스크롤(watchHeight · 표 아래 주 높이 뺌) - UI_Style.css 의 max-height calc(100dvh - 300px) 제거 · m01-master__pager 규칙 제거(부품 ui-pager 로) - 낡은 점검 51-10(쪽 단추가 거름 줄 안에 있다고 봄) = 표 바로 아래 ui-pager 로 고침
158 lines
6.9 KiB
TypeScript
158 lines
6.9 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_pager.ts
|
|
* 부품 「쪽 단추」 `pager` — 이전 · 다음 + 「n / N 쪽 · 전체 줄」 한 줄 (M01 `buildPager` 를 옮김)
|
|
* 꼴 = 한 줄 도구줄(`ui-toolbar ui-pager`) · 글 → 이전 → 다음 · 쪽이 한 장뿐이어도 그대로 보임
|
|
* 크기 = 도구줄 단 없음(안 단추 = 보조 단추 기본 높이)
|
|
* 상태 = 첫 쪽 이전 막힘 · 끝 쪽 다음 막힘 (`disabled`)
|
|
* 쓰는 법 = createPager(total, size, page, (next) => …) — 표 **바로 아래**에 놓음
|
|
* 표 줄 수 = `fitRows(남은 높이)` 순수 셈 · `watchFit(바깥 칸, 표 칸 꺼내기, (줄 수) => …)` 가 그린 뒤 · 화면 크기 바뀔 때 알림
|
|
* 한 장 표(쪽 없음) = `watchHeight(표 칸)` 화면 아래까지 높이 묶고 안에서 스크롤
|
|
* 옛 이름 = 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;
|
|
/** 표 밑 쪽 단추 줄(32) + 간격(12) + 판 아래 여백(16) + 여유(4) */
|
|
export const PAGER_ROOM = 64;
|
|
/** 표가 보일 수 있는 줄 수 아래 한도 · 위 한도 */
|
|
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;
|
|
|
|
/* ── 아래부터 화면 재기(순수 셈 아님) ── */
|
|
|
|
export interface FitOptions {
|
|
/** 화면 위치 대신 쓸 높이 한도(px) — 모달처럼 위치가 줄 수에 따라 움직이는 곳 */
|
|
cap?: () => number;
|
|
/** 사라지면(떼어지면) 알림을 그침 — 열 때마다 새로 만드는 목록용 */
|
|
once?: boolean;
|
|
}
|
|
|
|
/** 표 칸 위쪽 자리 ~ 화면 아래 사이로 들어가는 줄 수. 표 칸이 안 보이면 null. */
|
|
function measureRows(grid: HTMLElement, opt: FitOptions): number | null {
|
|
if (!grid.isConnected || !grid.offsetParent) return null;
|
|
const body = grid.querySelector<HTMLTableSectionElement>("tbody");
|
|
const count = body?.rows.length ?? 0;
|
|
const head = grid.querySelector<HTMLElement>("thead");
|
|
const room = opt.cap ? opt.cap() : window.innerHeight - grid.getBoundingClientRect().top;
|
|
// 줄 높이 = 그린 줄들의 평균(작은 글 · 두 줄 칸이 섞여도 맞음) · 줄 없으면 40
|
|
const rowH = count ? body!.offsetHeight / count : ROW_H;
|
|
return fitRows(room - PAGER_ROOM, rowH, head?.offsetHeight || ROW_H);
|
|
}
|
|
|
|
export interface Fit {
|
|
/** 그린 뒤 다시 재라고 알림 */
|
|
ask: () => void;
|
|
stop: () => void;
|
|
}
|
|
|
|
/** 표가 그려진 뒤 · 창 크기 바뀔 때 줄 수를 재서 **바뀌었을 때만** 알림. `root` = 늘 붙어 있는 바깥 칸. */
|
|
export function watchFit(
|
|
root: HTMLElement,
|
|
getGrid: () => HTMLElement | null,
|
|
onRows: (rows: number) => void,
|
|
opt: FitOptions = {},
|
|
): Fit {
|
|
let last = 0;
|
|
let frame = 0;
|
|
let seen = false;
|
|
let late = 0;
|
|
const stop = (): void => {
|
|
window.clearTimeout(late);
|
|
window.removeEventListener("resize", ask);
|
|
watcher?.disconnect();
|
|
if (frame) cancelAnimationFrame(frame);
|
|
};
|
|
const check = (): void => {
|
|
frame = 0;
|
|
if (opt.once) {
|
|
if (root.isConnected) seen = true;
|
|
else if (seen) return stop();
|
|
}
|
|
const grid = getGrid();
|
|
if (grid) watcher?.observe(grid); // 글꼴이 늦게 실려 줄 높이가 바뀌어도 다시 잼
|
|
const n = grid ? measureRows(grid, opt) : null;
|
|
if (n !== null && n !== last) {
|
|
last = n;
|
|
onRows(n);
|
|
}
|
|
};
|
|
function ask(): void {
|
|
if (!frame) frame = requestAnimationFrame(check);
|
|
// 위 칸(거름 · 찾기)이 늦게 자리 잡아 표 위치만 밀리면 크기 변화가 안 잡힘 — 한 번 더 잼
|
|
window.clearTimeout(late);
|
|
late = window.setTimeout(check, 400);
|
|
}
|
|
window.addEventListener("resize", ask);
|
|
const watcher = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(ask);
|
|
watcher?.observe(root); // 숨었다 다시 보일 때 · 위 칸이 늘고 줄 때
|
|
ask();
|
|
return { ask, stop };
|
|
}
|
|
|
|
/** 첫 그림 전에 쓸 줄 수 어림 — `above` = 표 칸 위에 올 줄(px). */
|
|
export function guessRows(host: HTMLElement, above = 0, cap?: () => number): number {
|
|
const room = cap ? cap() : window.innerHeight - host.getBoundingClientRect().top - above;
|
|
return fitRows(room - PAGER_ROOM);
|
|
}
|
|
|
|
/** 쪽 없는 한 장 표 — 표 칸 위 ~ 화면 아래로 높이를 묶고 안에서 스크롤. 돌려받은 함수 = 해제. */
|
|
export function watchHeight(grid: HTMLElement, pad = 16): () => void {
|
|
const set = (): void => {
|
|
if (!grid.isConnected || !grid.offsetParent) return;
|
|
// 표 아래에 붙은 칸(주 · 안내)이 있으면 그 높이 + 칸 사이 간격(12)만큼 비워 둠
|
|
let below = 0;
|
|
for (let n = grid.nextElementSibling; n; n = n.nextElementSibling)
|
|
below += (n as HTMLElement).offsetHeight + 12;
|
|
const room = window.innerHeight - grid.getBoundingClientRect().top - pad - below;
|
|
grid.style.maxHeight = `${Math.max(120, Math.floor(room))}px`;
|
|
};
|
|
const frame = (): number => requestAnimationFrame(set);
|
|
window.addEventListener("resize", set);
|
|
frame();
|
|
return () => window.removeEventListener("resize", set);
|
|
}
|
|
|
|
/** 쪽 나눔 띠 — 이전·다음 · 「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,
|
|
],
|
|
});
|
|
}
|