feat(709-2): M01 표 · M02 목록 줄 수 = 화면 높이 · 쪽 단추 표 아래 · 한 장 표 화면 안 스크롤
- 쪽 있는 표(요소 · 표 목록 · 로직 목록 · 재료 고르기 모달 · M02 서고 · 새 항목 목록): 줄 수를 표 칸 위치 ~ 화면 아래로 셈(watchFit) · 창 크기 · 글꼴 늦게 실림에도 다시 셈 · 쪽 크기 바뀌어도 첫 줄 유지 - 쪽 단추 = 표 바로 아래 · 줄이 모자라면 표와 함께 위로 붙음 - 쪽 없는 한 장 표(소요량 · 계수): 높이를 화면 아래로 묶고 안에서 스크롤(watchHeight · 표 아래 주 높이 뺌) - UI_Style.css 의 max-height calc(100dvh - 300px) 제거 · m01-master__pager 규칙 제거(부품 ui-pager 로) - 낡은 점검 51-10(쪽 단추가 거름 줄 안에 있다고 봄) = 표 바로 아래 ui-pager 로 고침
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
import { createInputField, el } from "@ui/ui_template_elements";
|
import { createInputField, el } from "@ui/ui_template_elements";
|
||||||
import { aliasText, matchLogic } from "@ui/ui_template_logic_find";
|
import { aliasText, matchLogic } from "@ui/ui_template_logic_find";
|
||||||
import { buildPager } from "./M01_MasterData_UI_Cells";
|
import { buildPager } from "./M01_MasterData_UI_Cells";
|
||||||
|
import { pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||||
import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api";
|
import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api";
|
||||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||||
import "./M01_MasterData_UI_Head.css";
|
import "./M01_MasterData_UI_Head.css";
|
||||||
@@ -41,7 +42,7 @@ export interface ListHandle {
|
|||||||
|
|
||||||
export const logicId = (sub: string, key: string): string => `${sub}\n${key}`;
|
export const logicId = (sub: string, key: string): string => `${sub}\n${key}`;
|
||||||
|
|
||||||
const SIZE = 30;
|
const DEFAULT_SIZE = 30; // 붙기 전 어림 — 붙은 뒤 화면 높이로 다시 셈
|
||||||
const COLS = [
|
const COLS = [
|
||||||
"키",
|
"키",
|
||||||
"원문번호",
|
"원문번호",
|
||||||
@@ -64,6 +65,7 @@ export function buildList(
|
|||||||
let aliases = new Map<string, string>();
|
let aliases = new Map<string, string>();
|
||||||
const aliasOf = (x: ListItem): string => aliases.get(x.id) ?? x.alias;
|
const aliasOf = (x: ListItem): string => aliases.get(x.id) ?? x.alias;
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
let size = DEFAULT_SIZE;
|
||||||
const pick = { sub: "", detail: "" };
|
const pick = { sub: "", detail: "" };
|
||||||
const searchField = createInputField({ type: "search", placeholder: tx("List_Search") });
|
const searchField = createInputField({ type: "search", placeholder: tx("List_Search") });
|
||||||
const search = searchField.input;
|
const search = searchField.input;
|
||||||
@@ -84,9 +86,9 @@ export function buildList(
|
|||||||
별칭: aliasOf(x),
|
별칭: aliasOf(x),
|
||||||
}).ok,
|
}).ok,
|
||||||
);
|
);
|
||||||
page = Math.min(page, Math.max(1, Math.ceil(shown.length / SIZE)));
|
page = Math.min(page, Math.max(1, Math.ceil(shown.length / size)));
|
||||||
const rows = shown.slice((page - 1) * SIZE, page * SIZE).map(line);
|
const rows = shown.slice((page - 1) * size, page * size).map(line);
|
||||||
pager.replaceWith((pager = buildPager(shown.length, SIZE, page, go)));
|
pager.replaceWith((pager = buildPager(shown.length, size, page, go)));
|
||||||
grid.replaceChildren(
|
grid.replaceChildren(
|
||||||
el("table", {
|
el("table", {
|
||||||
className: "ui-table m01-master__grid",
|
className: "ui-table m01-master__grid",
|
||||||
@@ -144,18 +146,29 @@ export function buildList(
|
|||||||
};
|
};
|
||||||
|
|
||||||
search.addEventListener("input", () => go(1));
|
search.addEventListener("input", () => go(1));
|
||||||
let pager = buildPager(0, SIZE, 1, go);
|
let pager = buildPager(0, size, 1, go);
|
||||||
|
|
||||||
const root = el("div", {
|
const root = el("div", {
|
||||||
className: "m01-logic__listview",
|
className: "m01-logic__listview",
|
||||||
children: [
|
children: [
|
||||||
el("div", {
|
el("div", {
|
||||||
className: "m01-head__filters",
|
className: "m01-head__filters",
|
||||||
children: [searchField.root, pager],
|
children: [searchField.root],
|
||||||
}),
|
}),
|
||||||
grid,
|
grid,
|
||||||
|
pager,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
watchFit(
|
||||||
|
root,
|
||||||
|
() => grid,
|
||||||
|
(rows) => {
|
||||||
|
if (rows === size) return;
|
||||||
|
page = pageKeepFirst(page, size, rows);
|
||||||
|
size = rows;
|
||||||
|
draw();
|
||||||
|
},
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
root,
|
root,
|
||||||
setItems: (logics) => {
|
setItems: (logics) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||||
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
|
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
|
||||||
import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells";
|
import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells";
|
||||||
|
import { fitRows, PAGER_ROOM, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||||
import { openModal } from "@ui/ui_template_modal";
|
import { openModal } from "@ui/ui_template_modal";
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ const COLS: Record<PickTableKind, string[]> = {
|
|||||||
],
|
],
|
||||||
기계: ["원문번호", "구분", "상세구분", "이름", "규격", "값›가격"],
|
기계: ["원문번호", "구분", "상세구분", "이름", "규격", "값›가격"],
|
||||||
};
|
};
|
||||||
const SIZE = 30;
|
/** 모달 안 표 칸 + 쪽 단추가 쓸 높이 — 모달 최대 높이(80vh)에서 제목 · 찾기 줄을 뺌 */
|
||||||
|
const room = (): number => window.innerHeight * 0.8 - 130;
|
||||||
|
|
||||||
export interface PickTableOptions {
|
export interface PickTableOptions {
|
||||||
kind: PickTableKind;
|
kind: PickTableKind;
|
||||||
@@ -60,13 +62,14 @@ export function openPickTable(opt: PickTableOptions): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
let size = fitRows(room() - PAGER_ROOM); // 처음 어림 — 붙은 뒤 다시 재서 어긋나면 바로잡음
|
||||||
const tried = new Set([opt.seed]);
|
const tried = new Set([opt.seed]);
|
||||||
let seq = 0;
|
let seq = 0;
|
||||||
const load = async (fallback = false): Promise<void> => {
|
const load = async (fallback = false): Promise<void> => {
|
||||||
const mine = ++seq;
|
const mine = ++seq;
|
||||||
try {
|
try {
|
||||||
const q = search.value.trim();
|
const q = search.value.trim();
|
||||||
const got = await fetchRows(FILES[opt.kind], page, SIZE, q);
|
const got = await fetchRows(FILES[opt.kind], page, size, q);
|
||||||
if (mine !== seq) return; // 최신 응답만
|
if (mine !== seq) return; // 최신 응답만
|
||||||
if (!got.total && fallback) {
|
if (!got.total && fallback) {
|
||||||
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
|
const next = [...(q.includes(" ") ? [q.split(/\s+/)[0]] : []), ...(opt.alt ?? [])].find(
|
||||||
@@ -94,10 +97,6 @@ export function openPickTable(opt: PickTableOptions): void {
|
|||||||
return tr;
|
return tr;
|
||||||
});
|
});
|
||||||
host.replaceChildren(
|
host.replaceChildren(
|
||||||
buildPager(got.total, SIZE, got.page, (next) => {
|
|
||||||
page = next;
|
|
||||||
void load();
|
|
||||||
}),
|
|
||||||
el("div", {
|
el("div", {
|
||||||
className: "ui-table-wrap m01-master__grid-wrap",
|
className: "ui-table-wrap m01-master__grid-wrap",
|
||||||
children: [
|
children: [
|
||||||
@@ -112,12 +111,28 @@ export function openPickTable(opt: PickTableOptions): void {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
buildPager(got.total, size, got.page, (next) => {
|
||||||
|
page = next;
|
||||||
|
void load();
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
fit.ask();
|
||||||
host.querySelector(".is-picked")?.scrollIntoView({ block: "nearest" });
|
host.querySelector(".is-picked")?.scrollIntoView({ block: "nearest" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const fit = watchFit(
|
||||||
|
host,
|
||||||
|
() => host.querySelector<HTMLElement>(".m01-master__grid-wrap"),
|
||||||
|
(rows) => {
|
||||||
|
if (rows === size) return;
|
||||||
|
page = pageKeepFirst(page, size, rows);
|
||||||
|
size = rows;
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
{ cap: room, once: true },
|
||||||
|
);
|
||||||
let timer = 0;
|
let timer = 0;
|
||||||
search.addEventListener("input", () => {
|
search.addEventListener("input", () => {
|
||||||
window.clearTimeout(timer);
|
window.clearTimeout(timer);
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ import {
|
|||||||
withCell,
|
withCell,
|
||||||
} from "./M01_MasterData_UI_Cells";
|
} from "./M01_MasterData_UI_Cells";
|
||||||
import { createEmpty } from "@ui/ui_template_empty";
|
import { createEmpty } from "@ui/ui_template_empty";
|
||||||
|
import { guessRows, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||||
|
|
||||||
const SIZE = 50;
|
/** 표 칸 위에 오는 도구줄(단추 줄 + 간격) 높이 — 첫 줄 수 어림에만 씀 */
|
||||||
|
const BAR_H = 52;
|
||||||
const MARKET = "재료_자재품목.json";
|
const MARKET = "재료_자재품목.json";
|
||||||
const LINKED = "재료_품셈재료.json";
|
const LINKED = "재료_품셈재료.json";
|
||||||
const JOB = ["인력.json"];
|
const JOB = ["인력.json"];
|
||||||
@@ -94,6 +96,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
|
|||||||
const { sub, detail, state } = pick;
|
const { sub, detail, state } = pick;
|
||||||
const file = pick.file.file;
|
const file = pick.file.file;
|
||||||
let page = pick.view?.page ?? 1;
|
let page = pick.view?.page ?? 1;
|
||||||
|
let size = guessRows(host, BAR_H); // 화면 높이로 셈 — 그린 뒤 다시 재서 어긋나면 바로잡음
|
||||||
let data: RowsPage | null = null;
|
let data: RowsPage | null = null;
|
||||||
let unlinked = false;
|
let unlinked = false;
|
||||||
|
|
||||||
@@ -210,7 +213,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
|
|||||||
host.replaceChildren(
|
host.replaceChildren(
|
||||||
el("div", {
|
el("div", {
|
||||||
className: "ui-toolbar m01-master__bar",
|
className: "ui-toolbar m01-master__bar",
|
||||||
children: [add, ...only, buildPager(d.total, SIZE, d.page, go)],
|
children: [add, ...only],
|
||||||
}),
|
}),
|
||||||
el("div", {
|
el("div", {
|
||||||
className: "ui-table-wrap m01-master__grid-wrap",
|
className: "ui-table-wrap m01-master__grid-wrap",
|
||||||
@@ -221,8 +224,10 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
buildPager(d.total, size, d.page, go),
|
||||||
...empty,
|
...empty,
|
||||||
);
|
);
|
||||||
|
fit.ask();
|
||||||
};
|
};
|
||||||
|
|
||||||
const go = (next: number): void => {
|
const go = (next: number): void => {
|
||||||
@@ -232,7 +237,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
|
|||||||
|
|
||||||
const load = async (): Promise<void> => {
|
const load = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
data = await fetchRows(file, page, SIZE, q, unlinked, sub, detail, state);
|
data = await fetchRows(file, page, size, q, unlinked, sub, detail, state);
|
||||||
for (const [key, name] of Object.entries(data.refs ?? {})) names.set(key, name);
|
for (const [key, name] of Object.entries(data.refs ?? {})) names.set(key, name);
|
||||||
saveView(pick.group, { page: data.page });
|
saveView(pick.group, { page: data.page });
|
||||||
paint();
|
paint();
|
||||||
@@ -241,9 +246,22 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fit = watchFit(
|
||||||
|
host,
|
||||||
|
() => host.querySelector<HTMLElement>(".m01-master__grid-wrap"),
|
||||||
|
(rows) => {
|
||||||
|
if (rows === size) return;
|
||||||
|
page = pageKeepFirst(data?.page ?? page, size, rows);
|
||||||
|
size = rows;
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
);
|
||||||
const off = onDraftChange(paint);
|
const off = onDraftChange(paint);
|
||||||
void load();
|
void load();
|
||||||
return off;
|
return () => {
|
||||||
|
fit.stop();
|
||||||
|
off();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
|
function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ html.is-bm-route .m01-master__headbox {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 겉 = 표 부품(ui-table-wrap · ui-table) · 여기는 높이 · 폭 · 머리 고정 · 칸 상태만 */
|
/* 겉 = 표 부품(ui-table-wrap · ui-table) · 여기는 높이 · 폭 · 머리 고정 · 칸 상태만
|
||||||
.m01-master__grid-wrap {
|
쪽 있는 표 = 높이 제한 없음(줄 수를 화면 높이로 셈 · 쪽 단추 부품 watchFit) · 쪽 없는 한 장 표(--sheet) = 화면 아래까지 안에서 스크롤 */
|
||||||
max-height: calc(100dvh - 300px);
|
.m01-master__grid-wrap--sheet {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +130,9 @@ table.m01-master__grid {
|
|||||||
padding: 2px var(--spacing-4);
|
padding: 2px var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.m01-master__pager {
|
/* 쪽 단추(부품 ui-pager) — 간격은 부품이 줌(표 아래 한 칸) · 이미 칸 사이 간격이 있는 몸통은 겹치지 않게 뺌 */
|
||||||
display: flex;
|
.m01-master__body > .ui-table-wrap + .ui-pager {
|
||||||
align-items: center;
|
margin-top: 0;
|
||||||
gap: var(--spacing-12);
|
|
||||||
margin-left: auto;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
font-size: var(--text-body-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.m01-master__table-row {
|
.m01-master__table-row {
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ import {
|
|||||||
toNumberish,
|
toNumberish,
|
||||||
toRange,
|
toRange,
|
||||||
} from "./M01_MasterData_UI_Cells";
|
} from "./M01_MasterData_UI_Cells";
|
||||||
|
import { guessRows, pageKeepFirst, watchFit, watchHeight } from "@ui/ui_template_pager";
|
||||||
|
|
||||||
const LIST_SIZE = 30;
|
/** 표 칸 위에 오는 거름 줄 높이 — 첫 줄 수 어림에만 씀 */
|
||||||
|
const FILTER_H = 52;
|
||||||
const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "비고"] as const;
|
const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "비고"] as const;
|
||||||
/** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */
|
/** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */
|
||||||
const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"];
|
const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"];
|
||||||
@@ -45,16 +47,22 @@ export function renderTables(
|
|||||||
): () => void {
|
): () => void {
|
||||||
let stop = (): void => {};
|
let stop = (): void => {};
|
||||||
let usage = pick.view?.usage ?? "";
|
let usage = pick.view?.usage ?? "";
|
||||||
|
let size = guessRows(host, FILTER_H); // 화면 높이로 셈 — 그린 뒤 다시 재서 어긋나면 바로잡음
|
||||||
|
let at = 0; // 목록을 보고 있는 쪽(0 = 표 하나를 보는 중)
|
||||||
const list = (page = pick.view?.page ?? 1): void => {
|
const list = (page = pick.view?.page ?? 1): void => {
|
||||||
stop();
|
stop();
|
||||||
stop = (): void => {};
|
stop = (): void => {};
|
||||||
|
at = page;
|
||||||
void showList(
|
void showList(
|
||||||
host,
|
host,
|
||||||
pick,
|
pick,
|
||||||
q,
|
q,
|
||||||
usage,
|
usage,
|
||||||
page,
|
page,
|
||||||
|
size,
|
||||||
|
list,
|
||||||
(file, key) => {
|
(file, key) => {
|
||||||
|
at = 0;
|
||||||
stop = openTable(host, file, key, () => list(page), openLogic);
|
stop = openTable(host, file, key, () => list(page), openLogic);
|
||||||
},
|
},
|
||||||
(next) => {
|
(next) => {
|
||||||
@@ -62,11 +70,22 @@ export function renderTables(
|
|||||||
saveView(pick.group, { usage, page: 1 });
|
saveView(pick.group, { usage, page: 1 });
|
||||||
list(1);
|
list(1);
|
||||||
},
|
},
|
||||||
);
|
).then(() => fit.ask());
|
||||||
};
|
};
|
||||||
|
const fit = watchFit(
|
||||||
|
host,
|
||||||
|
() => host.querySelector<HTMLElement>(".m01-master__tables"),
|
||||||
|
(rows) => {
|
||||||
|
if (!at || rows === size) return;
|
||||||
|
const page = pageKeepFirst(at, size, rows);
|
||||||
|
size = rows;
|
||||||
|
list(page);
|
||||||
|
},
|
||||||
|
);
|
||||||
list();
|
list();
|
||||||
const off = onDraftChange(() => host.querySelector(".m01-master__tables") && markChanged(host));
|
const off = onDraftChange(() => host.querySelector(".m01-master__tables") && markChanged(host));
|
||||||
return () => {
|
return () => {
|
||||||
|
fit.stop();
|
||||||
stop();
|
stop();
|
||||||
off();
|
off();
|
||||||
};
|
};
|
||||||
@@ -86,12 +105,14 @@ async function showList(
|
|||||||
q: string,
|
q: string,
|
||||||
usage: string,
|
usage: string,
|
||||||
page: number,
|
page: number,
|
||||||
|
size: number,
|
||||||
|
goPage: (page: number) => void,
|
||||||
open: (file: string, key: string) => void,
|
open: (file: string, key: string) => void,
|
||||||
onUsageChange: (usage: string) => void,
|
onUsageChange: (usage: string) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let got: { total: number; tables: TableHead[] };
|
let got: { total: number; tables: TableHead[] };
|
||||||
try {
|
try {
|
||||||
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, LIST_SIZE, usage);
|
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, size, usage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
||||||
return;
|
return;
|
||||||
@@ -121,12 +142,6 @@ async function showList(
|
|||||||
usageField.select.addEventListener("change", () => onUsageChange(usageField.select.value));
|
usageField.select.addEventListener("change", () => onUsageChange(usageField.select.value));
|
||||||
host.replaceChildren(
|
host.replaceChildren(
|
||||||
el("div", { className: "m01-master__filter", children: [usageField.root] }),
|
el("div", { className: "m01-master__filter", children: [usageField.root] }),
|
||||||
buildPager(
|
|
||||||
got.total,
|
|
||||||
LIST_SIZE,
|
|
||||||
page,
|
|
||||||
(p) => void showList(host, pick, q, usage, p, open, onUsageChange),
|
|
||||||
),
|
|
||||||
el("div", {
|
el("div", {
|
||||||
className: "ui-table-wrap m01-master__grid-wrap m01-master__tables",
|
className: "ui-table-wrap m01-master__grid-wrap m01-master__tables",
|
||||||
children: [
|
children: [
|
||||||
@@ -141,6 +156,7 @@ async function showList(
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
buildPager(got.total, size, page, goPage),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +169,7 @@ function openTable(
|
|||||||
openLogic: (key: string) => void,
|
openLogic: (key: string) => void,
|
||||||
): () => void {
|
): () => void {
|
||||||
let off = (): void => {};
|
let off = (): void => {};
|
||||||
|
let unfit = (): void => {}; // 쪽 없는 한 장 표 — 화면 아래까지 높이 묶음
|
||||||
void (async () => {
|
void (async () => {
|
||||||
let version: string;
|
let version: string;
|
||||||
let table: Row & { 줄: Row[] };
|
let table: Row & { 줄: Row[] };
|
||||||
@@ -211,6 +228,7 @@ function openTable(
|
|||||||
const backButton = createButton({ label: L("M01_BackToTables"), variant: "ghost" });
|
const backButton = createButton({ label: L("M01_BackToTables"), variant: "ghost" });
|
||||||
backButton.addEventListener("click", () => {
|
backButton.addEventListener("click", () => {
|
||||||
off();
|
off();
|
||||||
|
unfit();
|
||||||
back();
|
back();
|
||||||
});
|
});
|
||||||
const notes = ((table.주 ?? []) as string[]).map((n) =>
|
const notes = ((table.주 ?? []) as string[]).map((n) =>
|
||||||
@@ -224,6 +242,24 @@ function openTable(
|
|||||||
btn.addEventListener("click", () => openLogic(k));
|
btn.addEventListener("click", () => openLogic(k));
|
||||||
return i === 0 ? btn : el("span", { text: ", ", children: [btn] });
|
return i === 0 ? btn : el("span", { text: ", ", children: [btn] });
|
||||||
});
|
});
|
||||||
|
const sheet = el("div", {
|
||||||
|
className: "ui-table-wrap m01-master__grid-wrap m01-master__grid-wrap--sheet",
|
||||||
|
children: [
|
||||||
|
el("table", {
|
||||||
|
className: "ui-table m01-master__grid",
|
||||||
|
children: [
|
||||||
|
el("thead", {
|
||||||
|
children: [
|
||||||
|
el("tr", {
|
||||||
|
children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
body,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
host.replaceChildren(
|
host.replaceChildren(
|
||||||
el("div", {
|
el("div", {
|
||||||
className: "ui-toolbar m01-master__bar",
|
className: "ui-toolbar m01-master__bar",
|
||||||
@@ -246,29 +282,17 @@ function openTable(
|
|||||||
add,
|
add,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
el("div", {
|
sheet,
|
||||||
className: "ui-table-wrap m01-master__grid-wrap",
|
|
||||||
children: [
|
|
||||||
el("table", {
|
|
||||||
className: "ui-table m01-master__grid",
|
|
||||||
children: [
|
|
||||||
el("thead", {
|
|
||||||
children: [
|
|
||||||
el("tr", {
|
|
||||||
children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
body,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
...notes,
|
...notes,
|
||||||
);
|
);
|
||||||
|
unfit();
|
||||||
|
unfit = watchHeight(sheet);
|
||||||
};
|
};
|
||||||
off = onDraftChange(paint);
|
off = onDraftChange(paint);
|
||||||
paint();
|
paint();
|
||||||
})();
|
})();
|
||||||
return () => off();
|
return () => {
|
||||||
|
off();
|
||||||
|
unfit();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||||
import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells";
|
import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells";
|
||||||
|
import { PAGER_ROOM, pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||||
import { dateText } from "../common_util/common_util_time";
|
import { dateText } from "../common_util/common_util_time";
|
||||||
import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
|
import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
|
||||||
import "../M01_MasterData/M01_MasterData_UI_Style.css";
|
import "../M01_MasterData/M01_MasterData_UI_Style.css";
|
||||||
@@ -160,6 +161,7 @@ export interface ItemListHandle {
|
|||||||
export function buildItemList(): ItemListHandle {
|
export function buildItemList(): ItemListHandle {
|
||||||
let draw = (): void => {};
|
let draw = (): void => {};
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
let size = SIZE; // 붙은 뒤 화면 높이로 다시 셈
|
||||||
const searchField = createInputField({ type: "search", placeholder: TEXT.search });
|
const searchField = createInputField({ type: "search", placeholder: TEXT.search });
|
||||||
const search = searchField.input;
|
const search = searchField.input;
|
||||||
const grid = el("div", { className: "ui-table-wrap m01-master__grid-wrap" });
|
const grid = el("div", { className: "ui-table-wrap m01-master__grid-wrap" });
|
||||||
@@ -176,9 +178,9 @@ export function buildItemList(): ItemListHandle {
|
|||||||
): void => {
|
): void => {
|
||||||
page = 1;
|
page = 1;
|
||||||
draw = (): void => {
|
draw = (): void => {
|
||||||
const got = pageOf(rows, cols, search.value, page);
|
const got = pageOf(rows, cols, search.value, page, size);
|
||||||
page = got.page;
|
page = got.page;
|
||||||
const next = buildPager(got.total, SIZE, page, (p) => {
|
const next = buildPager(got.total, size, page, (p) => {
|
||||||
page = p;
|
page = p;
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
@@ -230,8 +232,23 @@ export function buildItemList(): ItemListHandle {
|
|||||||
});
|
});
|
||||||
const root = el("div", {
|
const root = el("div", {
|
||||||
className: "m02-itemlist",
|
className: "m02-itemlist",
|
||||||
children: [buildHeadFilters([searchField.root, pager]), grid],
|
children: [buildHeadFilters([searchField.root]), grid, pager],
|
||||||
});
|
});
|
||||||
|
// 모달 안 목록 — 모달이 줄 수에 따라 움직이니 화면 위치 대신 높이 한도로 셈
|
||||||
|
watchFit(
|
||||||
|
root,
|
||||||
|
() => grid,
|
||||||
|
(rows) => {
|
||||||
|
if (rows === size) return;
|
||||||
|
page = pageKeepFirst(page, size, rows);
|
||||||
|
size = rows;
|
||||||
|
draw();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cap: () => Math.min(window.innerHeight * 0.6, window.innerHeight - 300) + PAGER_ROOM,
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
// 처음 = 빈 표 자리(줄이 오면 `show`)
|
// 처음 = 빈 표 자리(줄이 오면 `show`)
|
||||||
note(TEXT.loading);
|
note(TEXT.loading);
|
||||||
return { root, show, message: (text = TEXT.loading) => note(text) };
|
return { root, show, message: (text = TEXT.loading) => note(text) };
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { t as L } from "@ui/ui_template_locale";
|
import { t as L } from "@ui/ui_template_locale";
|
||||||
import { openModal } from "@ui/ui_template_modal";
|
import { openModal } from "@ui/ui_template_modal";
|
||||||
import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells";
|
import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells";
|
||||||
|
import { pageKeepFirst, watchFit } from "@ui/ui_template_pager";
|
||||||
import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
|
import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
|
||||||
import "../M01_MasterData/M01_MasterData_UI_Style.css";
|
import "../M01_MasterData/M01_MasterData_UI_Style.css";
|
||||||
import {
|
import {
|
||||||
@@ -103,6 +104,7 @@ export function mountLibraryList(
|
|||||||
let rows: Row[] = [];
|
let rows: Row[] = [];
|
||||||
let picked: string | null = null;
|
let picked: string | null = null;
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
let size = SIZE; // 붙은 뒤 화면 높이로 다시 셈
|
||||||
const searchField = createInputField({ type: "search", placeholder: tc("Search") });
|
const searchField = createInputField({ type: "search", placeholder: tc("Search") });
|
||||||
const body = el("tbody");
|
const body = el("tbody");
|
||||||
let pager = buildPager(0, SIZE, 1, () => {});
|
let pager = buildPager(0, SIZE, 1, () => {});
|
||||||
@@ -116,8 +118,8 @@ export function mountLibraryList(
|
|||||||
const draw = (): void => {
|
const draw = (): void => {
|
||||||
const q = searchField.input.value.trim().toLowerCase();
|
const q = searchField.input.value.trim().toLowerCase();
|
||||||
const shown = rows.filter((r) => !q || libraryLabel(r).toLowerCase().includes(q));
|
const shown = rows.filter((r) => !q || libraryLabel(r).toLowerCase().includes(q));
|
||||||
page = Math.min(page, Math.max(1, Math.ceil(shown.length / SIZE)));
|
page = Math.min(page, Math.max(1, Math.ceil(shown.length / size)));
|
||||||
const next = buildPager(shown.length, SIZE, page, (p) => {
|
const next = buildPager(shown.length, size, page, (p) => {
|
||||||
page = p;
|
page = p;
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
@@ -125,7 +127,7 @@ export function mountLibraryList(
|
|||||||
pager = next;
|
pager = next;
|
||||||
body.replaceChildren(
|
body.replaceChildren(
|
||||||
...(shown.length
|
...(shown.length
|
||||||
? shown.slice((page - 1) * SIZE, page * SIZE).map((row) => {
|
? shown.slice((page - 1) * size, page * size).map((row) => {
|
||||||
const radio = el("input", {
|
const radio = el("input", {
|
||||||
attrs: { type: "radio", name: `m02-lib-${opt.kind}`, "aria-label": row.이름 },
|
attrs: { type: "radio", name: `m02-lib-${opt.kind}`, "aria-label": row.이름 },
|
||||||
}) as HTMLInputElement;
|
}) as HTMLInputElement;
|
||||||
@@ -252,25 +254,34 @@ export function mountLibraryList(
|
|||||||
"Lib_Base",
|
"Lib_Base",
|
||||||
"Lib_Date",
|
"Lib_Date",
|
||||||
] as const;
|
] as const;
|
||||||
host.replaceChildren(
|
const gridBox = el("div", {
|
||||||
buildHeadFilters([searchField.root, pager]),
|
className: "ui-table-wrap m01-master__grid-wrap",
|
||||||
el("div", {
|
attrs: { "data-lib-list": opt.kind },
|
||||||
className: "ui-table-wrap m01-master__grid-wrap",
|
children: [
|
||||||
attrs: { "data-lib-list": opt.kind },
|
el("table", {
|
||||||
children: [
|
className: "ui-table m01-master__grid",
|
||||||
el("table", {
|
children: [
|
||||||
className: "ui-table m01-master__grid",
|
el("thead", {
|
||||||
children: [
|
children: [
|
||||||
el("thead", {
|
el("tr", { children: heads.map((h) => el("th", { text: h ? tc(h) : "" })) }),
|
||||||
children: [
|
],
|
||||||
el("tr", { children: heads.map((h) => el("th", { text: h ? tc(h) : "" })) }),
|
}),
|
||||||
],
|
body,
|
||||||
}),
|
],
|
||||||
body,
|
}),
|
||||||
],
|
],
|
||||||
}),
|
});
|
||||||
],
|
host.replaceChildren(buildHeadFilters([searchField.root]), gridBox, pager);
|
||||||
}),
|
watchFit(
|
||||||
|
host,
|
||||||
|
() => gridBox,
|
||||||
|
(rows) => {
|
||||||
|
if (rows === size) return;
|
||||||
|
page = pageKeepFirst(page, size, rows);
|
||||||
|
size = rows;
|
||||||
|
draw();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
);
|
);
|
||||||
if (opt.canEdit)
|
if (opt.canEdit)
|
||||||
tools.replaceChildren(
|
tools.replaceChildren(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가
|
|||||||
def _pure() -> str:
|
def _pure() -> str:
|
||||||
text = PAGER.read_text(encoding="utf-8")
|
text = PAGER.read_text(encoding="utf-8")
|
||||||
start = text.index("export const ROW_H")
|
start = text.index("export const ROW_H")
|
||||||
end = text.index("/** 표 칸 위쪽")
|
end = text.index("/* ── 아래부터 화면 재기")
|
||||||
return text[start:end]
|
return text[start:end]
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ def test_쪽_수():
|
|||||||
|
|
||||||
|
|
||||||
def test_화면_높이_줄_수():
|
def test_화면_높이_줄_수():
|
||||||
# 머리 40 + 줄 40 · 표 칸 위 200 · 쪽 단추 자리 56 → 1080: (824-40)/40=19 · 768: (512-40)/40=11
|
# 머리 40 + 줄 40 · 표 칸 위 200 · 쪽 단추 자리 64 → 1080: (816-40)/40=19 · 768: (504-40)/40=11
|
||||||
got = _run(
|
got = _run(
|
||||||
"console.log(JSON.stringify(["
|
"console.log(JSON.stringify(["
|
||||||
"fitRows(1080-200-PAGER_ROOM), fitRows(768-200-PAGER_ROOM), "
|
"fitRows(1080-200-PAGER_ROOM), fitRows(768-200-PAGER_ROOM), "
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 쓰기 없음 — 서고 목록 · 문서 읽기는 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림)
|
* 쓰기 없음 — 서고 목록 · 문서 읽기는 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림)
|
||||||
* 기대 수치:
|
* 기대 수치:
|
||||||
* - table.wrap = "m01-master__grid-wrap" · table.table = "m01-master__grid" · table.row = "m01-master__table-row"
|
* - table.wrap = "m01-master__grid-wrap" · table.table = "m01-master__grid" · table.row = "m01-master__table-row"
|
||||||
* - table.filters = true(찾기 칸 + 쪽 `m01-master__pager`) · table.sameCell = true(M01 표 칸과 padding · 글자 · 밑줄 같음)
|
* - table.filters = true(찾기 칸 + 표 바로 아래 쪽 단추 `ui-pager`) · table.sameCell = true(M01 표 칸과 padding · 글자 · 밑줄 같음)
|
||||||
* - table.search = 1(「값 없음」 치면 한 줄)
|
* - table.search = 1(「값 없음」 치면 한 줄)
|
||||||
* - buttons.variants = ["filled", "ghost", "danger"](M01 [신규 로직] · [로직 복사/생성] · [로직 지우기] 와 같음) · buttons.redDelete = true
|
* - buttons.variants = ["filled", "ghost", "danger"](M01 [신규 로직] · [로직 복사/생성] · [로직 지우기] 와 같음) · buttons.redDelete = true
|
||||||
* - precombo.add = "로직 추가" · picker.label = "로직 고르기" · picker.kinds ⊂ {단가승격, 일위조합} · picker.logicRows = 0
|
* - precombo.add = "로직 추가" · picker.label = "로직 고르기" · picker.kinds ⊂ {단가승격, 일위조합} · picker.logicRows = 0
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
row: wrap.querySelector("[data-lib-key]").className,
|
row: wrap.querySelector("[data-lib-key]").className,
|
||||||
filters:
|
filters:
|
||||||
!!wrap.parentElement.querySelector(".m01-head__filters input[type=search]") &&
|
!!wrap.parentElement.querySelector(".m01-head__filters input[type=search]") &&
|
||||||
!!wrap.parentElement.querySelector(".m01-head__filters .m01-master__pager"),
|
wrap.nextElementSibling?.classList.contains("ui-pager") === true,
|
||||||
sameCell: box(td) === box(ref.querySelector("td")),
|
sameCell: box(td) === box(ref.querySelector("td")),
|
||||||
cell: box(td),
|
cell: box(td),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/* 쪽 단추(pager) — 표 바로 아래 한 줄 · 글은 보조 색 */
|
/* 쪽 단추(pager) — 표 바로 아래 한 줄(표와 간격 한 칸) · 글은 보조 색 */
|
||||||
.ui-pager {
|
.ui-pager {
|
||||||
gap: var(--spacing-12);
|
gap: var(--spacing-12);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: var(--text-body-sm);
|
font-size: var(--text-body-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-table-wrap + .ui-pager {
|
||||||
|
margin-top: var(--spacing-12);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
* 크기 = 도구줄 단 없음(안 단추 = 보조 단추 기본 높이)
|
* 크기 = 도구줄 단 없음(안 단추 = 보조 단추 기본 높이)
|
||||||
* 상태 = 첫 쪽 이전 막힘 · 끝 쪽 다음 막힘 (`disabled`)
|
* 상태 = 첫 쪽 이전 막힘 · 끝 쪽 다음 막힘 (`disabled`)
|
||||||
* 쓰는 법 = createPager(total, size, page, (next) => …) — 표 **바로 아래**에 놓음
|
* 쓰는 법 = createPager(total, size, page, (next) => …) — 표 **바로 아래**에 놓음
|
||||||
* 표 줄 수 = `fitRows(남은 높이)` 순수 셈 · `watchFit(표 칸, (줄 수) => …)` 가 화면 크기 바뀔 때 다시 알림
|
* 표 줄 수 = `fitRows(남은 높이)` 순수 셈 · `watchFit(바깥 칸, 표 칸 꺼내기, (줄 수) => …)` 가 그린 뒤 · 화면 크기 바뀔 때 알림
|
||||||
|
* 한 장 표(쪽 없음) = `watchHeight(표 칸)` 화면 아래까지 높이 묶고 안에서 스크롤
|
||||||
* 옛 이름 = M01 `buildPager`(`M01_MasterData_UI_Cells.ts`) 는 이 부품을 부르는 얇은 껍데기
|
* 옛 이름 = M01 `buildPager`(`M01_MasterData_UI_Cells.ts`) 는 이 부품을 부르는 얇은 껍데기
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
@@ -16,8 +17,8 @@ import "./ui_template_pager.css";
|
|||||||
|
|
||||||
/** 표 한 줄 높이(px) · 머리 높이 — 실제 줄이 있으면 잰 값을 씀 */
|
/** 표 한 줄 높이(px) · 머리 높이 — 실제 줄이 있으면 잰 값을 씀 */
|
||||||
export const ROW_H = 40;
|
export const ROW_H = 40;
|
||||||
/** 표 밑 쪽 단추 줄 + 여백(px) */
|
/** 표 밑 쪽 단추 줄(32) + 간격(12) + 판 아래 여백(16) + 여유(4) */
|
||||||
export const PAGER_ROOM = 56;
|
export const PAGER_ROOM = 64;
|
||||||
/** 표가 보일 수 있는 줄 수 아래 한도 · 위 한도 */
|
/** 표가 보일 수 있는 줄 수 아래 한도 · 위 한도 */
|
||||||
export const MIN_ROWS = 5;
|
export const MIN_ROWS = 5;
|
||||||
export const MAX_ROWS = 200;
|
export const MAX_ROWS = 200;
|
||||||
@@ -36,39 +37,98 @@ export function fitRows(room: number, rowH = ROW_H, headH = ROW_H): number {
|
|||||||
export const pageKeepFirst = (page: number, oldSize: number, newSize: number): number =>
|
export const pageKeepFirst = (page: number, oldSize: number, newSize: number): number =>
|
||||||
Math.floor(((Math.max(1, page) - 1) * oldSize) / Math.max(1, newSize)) + 1;
|
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;
|
export interface FitOptions {
|
||||||
const top = grid.getBoundingClientRect().top;
|
/** 화면 위치 대신 쓸 높이 한도(px) — 모달처럼 위치가 줄 수에 따라 움직이는 곳 */
|
||||||
if (!grid.offsetParent && getComputedStyle(grid).position !== "fixed") return null; // 숨은 칸
|
cap?: () => number;
|
||||||
const row = grid.querySelector<HTMLElement>("tbody tr");
|
/** 사라지면(떼어지면) 알림을 그침 — 열 때마다 새로 만드는 목록용 */
|
||||||
const head = grid.querySelector<HTMLElement>("thead");
|
once?: boolean;
|
||||||
const rowH = row?.offsetHeight || ROW_H;
|
|
||||||
const headH = head?.offsetHeight || ROW_H;
|
|
||||||
return fitRows(window.innerHeight - top - PAGER_ROOM, rowH, headH);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 표 칸이 붙은 뒤 줄 수를 재서 알림 + 창 크기 바뀔 때마다 다시. 돌려받은 함수 = 해제. */
|
/** 표 칸 위쪽 자리 ~ 화면 아래 사이로 들어가는 줄 수. 표 칸이 안 보이면 null. */
|
||||||
export function watchFit(grid: HTMLElement, onRows: (rows: number) => void): () => void {
|
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 last = 0;
|
||||||
let frame = 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 => {
|
const check = (): void => {
|
||||||
frame = 0;
|
frame = 0;
|
||||||
const n = measureRows(grid);
|
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) {
|
if (n !== null && n !== last) {
|
||||||
last = n;
|
last = n;
|
||||||
onRows(n);
|
onRows(n);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const ask = (): void => {
|
function ask(): void {
|
||||||
if (!frame) frame = requestAnimationFrame(check);
|
if (!frame) frame = requestAnimationFrame(check);
|
||||||
};
|
// 위 칸(거름 · 찾기)이 늦게 자리 잡아 표 위치만 밀리면 크기 변화가 안 잡힘 — 한 번 더 잼
|
||||||
|
window.clearTimeout(late);
|
||||||
|
late = window.setTimeout(check, 400);
|
||||||
|
}
|
||||||
window.addEventListener("resize", ask);
|
window.addEventListener("resize", ask);
|
||||||
|
const watcher = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(ask);
|
||||||
|
watcher?.observe(root); // 숨었다 다시 보일 때 · 위 칸이 늘고 줄 때
|
||||||
ask();
|
ask();
|
||||||
return () => {
|
return { ask, stop };
|
||||||
window.removeEventListener("resize", ask);
|
}
|
||||||
if (frame) cancelAnimationFrame(frame);
|
|
||||||
|
/** 첫 그림 전에 쓸 줄 수 어림 — `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 쪽 · 전체 줄」. */
|
/** 쪽 나눔 띠 — 이전·다음 · 「n / N 쪽 · 전체 줄」. */
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ const pickDemo = (s: KitState): HTMLElement | null => {
|
|||||||
if (s === "disabled") [one.root, many.root].forEach((b) => (b.disabled = true));
|
if (s === "disabled") [one.root, many.root].forEach((b) => (b.disabled = true));
|
||||||
return row(one.root, many.root);
|
return row(one.root, many.root);
|
||||||
};
|
};
|
||||||
/** 709-1 견본 — 쪽 단추(가운데 쪽 · 한 장뿐이라 둘 다 막힘) · 수평 구분선(소제목 사이) */
|
/** 709-1 견본 — 쪽 단추(가운데 쪽 · 한 장뿐이라 이전 · 다음 다 막힘) · 수평 구분선(소제목 사이) */
|
||||||
const pagerDemo = (s: KitState): HTMLElement | null =>
|
const pagerDemo = (s: KitState): HTMLElement | null =>
|
||||||
s === "default" ? createPager(1234, 30, 2, noop) : s === "disabled" ? createPager(12, 30, 1, noop) : null;
|
s === "default" ? row(createPager(1234, 30, 2, noop), createPager(12, 30, 1, noop)) : null;
|
||||||
const dividerDemo = (s: KitState): HTMLElement | null => {
|
const dividerDemo = (s: KitState): HTMLElement | null => {
|
||||||
if (s !== "default") return null;
|
if (s !== "default") return null;
|
||||||
const box = document.createElement("div");
|
const box = document.createElement("div");
|
||||||
|
|||||||
Reference in New Issue
Block a user