/* ============================================================================= * ui_template_map_tools.ts * 지도 화면 제어 도구줄(568-1) — 평면 지도 위 아이콘 단추 한 벌 · B05 평면 · D03 평면이 같이 씀. * 꼴 = 유리 도구줄(`createToolbar` glass · 32) 한 줄 · 묶음 사이 나눔선 · 단추는 모두 아이콘(마우스 올리면 이름) * [?] 안내 판 = 도구줄 아래 2열 글판(기본 접힘 · 클릭은 지도로 통과) * 크기 = 단 32 하나(지도 위 도구) * 좁을 때 = 줄바꿈 없이 가로 밀기(터치 · 휠) · 넘친 쪽 끝만 흐림(`is-more-start` · `is-more-end`) * 상태 = 켬끔 단추는 `aria-pressed`(눌림 꼴) · 막힘 `disabled` · [?] 는 `aria-expanded` * 쓰는 법 = createMapTools({ label: "평면 도구", groups: [["undo", "redo"], ["rotate-ccw", "rotate-cw", "measure"]], * hint: { lines: ["휠 = 확대"] } }) → { root, bar, hint, button(act) } * 페이지 문구가 다르면 `mapToolButton("measure", { title })` 를 묶음에 그대로 끼움 · 고르기 · 칩도 요소로 끼움 * 누름 동작은 페이지가 붙임(`onClick` 또는 `[data-act]` 로 찾아 붙임) · 자리는 페이지 CSS(root) * 한 행 두 영역 = `createMapToolRow(겹 줄 root, 도구줄 root)`(634-1) · 막힘 글 = `mapToolTip(단추, 까닭)` * 대신하는 옛 변종 = B05 `b05-route__view-controls` 글자 유리 단추 줄 · `b05-routeedit__hint` 안내 판 · D03 `tool()` 지역 함수 * ========================================================================== */ import { createIconButton } from "./ui_template_button"; import type { IconName } from "./ui_template_icons"; import { createToolbar } from "./ui_template_toolbar"; import "./ui_template_map_tools.css"; /** 단추 목록 — 차례 = 도구줄 차례 · toggle = 켬끔 단추(aria-pressed) */ export const MAP_TOOLS = { undo: { icon: "undo", title: "되돌리기" }, redo: { icon: "redo", title: "다시하기" }, view3d: { icon: "cube", title: "3D" }, "rotate-ccw": { icon: "rotateCcw", title: "좌 회전" }, "rotate-cw": { icon: "rotateCw", title: "우 회전" }, "contour-minor": { icon: "contours", title: "등고선 1m", toggle: true }, stations: { icon: "tallymarks", title: "측점", toggle: true }, slopes: { icon: "percentage", title: "길이·기울기", toggle: true }, footprint: { icon: "cutFill", title: "절성토선", toggle: true }, measure: { icon: "ruler", title: "길이측정", toggle: true }, "station-snap": { icon: "magnet", title: "스냅", toggle: true }, "expected-draw": { icon: "pencil", title: "예정노선 변경", toggle: true }, "expected-original": { icon: "lineDashed", title: "예정노선 원본", toggle: true }, } satisfies Record; export type MapToolAct = keyof typeof MAP_TOOLS; export interface MapToolButtonOptions { /** 마우스 글 — 없으면 목록 이름 */ title?: string; /** 켬끔 단추 처음 값(기본 false) */ pressed?: boolean; disabled?: boolean; onClick?: (ev: MouseEvent) => void; } /** 아이콘 단추 하나(`data-act` = act) */ export function mapToolButton(act: MapToolAct, opts: MapToolButtonOptions = {}): HTMLButtonElement { const tool: { icon: IconName; title: string; toggle?: boolean } = MAP_TOOLS[act]; const button = createIconButton({ icon: tool.icon, title: opts.title ?? tool.title, size: 32, pressed: tool.toggle ? (opts.pressed ?? false) : undefined, disabled: opts.disabled, onClick: opts.onClick, }); button.dataset.act = act; button.dataset.tip = opts.title ?? tool.title; return button; } /** 마우스 글(요구 546) — 평소 제목만 · 막힘이면 「제목 — 까닭」 · 제목 = `data-tip`(없으면 지금 글). */ export function mapToolTip(el: HTMLElement, why?: string | null): void { el.dataset.tip ??= el.title; const text = why ? `${el.dataset.tip} — ${why}` : el.dataset.tip; el.title = text; el.setAttribute("aria-label", text); } export interface MapToolsOptions { /** 묶음들 — 사이에 나눔선 · act 이름 또는 페이지가 만든 요소 */ groups: (MapToolAct | HTMLElement)[][]; /** 보조기기 이름 */ label: string; /** [?] 안내 판(맨 앞 묶음) — 줄마다 한 칸 · 2열 */ hint?: { lines: string[]; open?: boolean; onToggle?: (open: boolean) => void }; } export interface MapTools { root: HTMLElement; bar: HTMLElement; hint: HTMLElement | null; button(act: MapToolAct): HTMLButtonElement | null; } /** 넘침 표시 + 휠을 가로 밀기로 — 도구줄이 넘칠 때만 */ function watchOverflow(bar: HTMLElement): void { const mark = (): void => { const max = bar.scrollWidth - bar.clientWidth; bar.classList.toggle("is-more-start", bar.scrollLeft > 1); bar.classList.toggle("is-more-end", max - bar.scrollLeft > 1); }; bar.addEventListener("scroll", mark, { passive: true }); bar.addEventListener( "wheel", (ev) => { if (bar.scrollWidth <= bar.clientWidth || Math.abs(ev.deltaX) > Math.abs(ev.deltaY)) return; ev.preventDefault(); bar.scrollLeft += ev.deltaY; }, { passive: false }, ); if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(mark); observer.observe(bar); for (const child of Array.from(bar.children)) observer.observe(child); } export function createMapTools(opts: MapToolsOptions): MapTools { const root = document.createElement("div"); root.className = "ui-map-tools"; const groups = opts.groups .filter((group) => group.length) .map((group) => group.map((item) => (typeof item === "string" ? mapToolButton(item) : item))); let hint: HTMLElement | null = null; if (opts.hint) { const { lines, onToggle } = opts.hint; const toggle = createIconButton({ icon: "help", title: "도움말", size: 32 }); toggle.dataset.act = "hint-toggle"; const panel = document.createElement("div"); panel.className = "ui-map-tools__hint"; for (const line of lines) panel.append(Object.assign(document.createElement("span"), { textContent: line })); const setOpen = (open: boolean): void => { panel.classList.toggle("is-open", open); toggle.setAttribute("aria-expanded", String(open)); }; setOpen(opts.hint.open ?? false); toggle.addEventListener("click", () => { const open = !panel.classList.contains("is-open"); setOpen(open); onToggle?.(open); }); groups.unshift([toggle]); hint = panel; } const bar = createToolbar({ glass: true, size: 32, label: opts.label, items: groups.flatMap((group, i) => (i ? ["divider" as const, ...group] : group)), }); root.append(bar); if (hint) root.append(hint); watchOverflow(bar); return { root, bar, hint, button: (act) => root.querySelector(`[data-act="${act}"]`), }; } /** 한 행 두 영역(634-1 · 요구 546) — 왼쪽(지도 겹) · 오른쪽(도구) · 영역마다 자기 가로 밀기 · 모자라면 오른쪽 먼저 밀림 · * [?] 안내 판은 행 아래로 옮김 · 자리(위 · 왼쪽 · 오른쪽)는 페이지 CSS(root). */ export function createMapToolRow(left: HTMLElement, right: HTMLElement): HTMLElement { const root = document.createElement("div"); root.className = "ui-map-row"; const line = document.createElement("div"); line.className = "ui-map-row__line"; line.append(left, right); const hints = [left, right].flatMap((area) => Array.from(area.querySelectorAll(":scope > .ui-map-tools__hint")), ); root.append(line, ...hints); return root; }