- 도구줄 「연도 마커」(공용 지도 도구줄 year-mark · 깃발 아이콘)를 선 위로 끌어 놓음 = 그 자리 구간을 둘로 — 구역만 나눔 · 해 그대로 - 끄는 동안 마름모 미리보기 · 선 끝 · 다른 마커 1 m 안 · 선 밖 = 경고 색 · 못 놓음 - 놓인 마커 끌기 = 경계 옮김 · 선 밖으로 끌어냄 · 오른쪽 누름 · Delete = 지움(앞 구간과 합침) · 같은 해 양쪽 = 속 빈 마름모 - 좌측 [연도 구분] · 「긋는 해」 걷음 · 새 선 = 사업연도 시작 해 · 새 갈래 = 붙은 자리 구간 해 - 속성 판 [갈래 시점 지정] = 다른 선에 닿은 끝을 시점(No.0)으로 갈래 · 꼬리에 닿은 머리 = 한 선 · 본선 = [방향 바꾸기] - 시험: 585-2 시험에 나눔 · 합침 · 갈래 해 · 갈래 시점 덧 · 530-3 갈래 해 · 553-4 속성 판 · 568-1 도구줄 목록 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
6.3 KiB
TypeScript
146 lines
6.3 KiB
TypeScript
/* =============================================================================
|
|
* 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)
|
|
* 대신하는 옛 변종 = 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: "되돌리기 (Ctrl+Z)" },
|
|
redo: { icon: "redo", title: "다시하기 (Ctrl+Y)" },
|
|
view3d: { icon: "cube", title: "3D 보기" },
|
|
"rotate-ccw": { icon: "rotateCcw", title: "반시계로 돌리기" },
|
|
"rotate-cw": { icon: "rotateCw", title: "시계로 돌리기" },
|
|
"contour-minor": { icon: "ripple", title: "1m 등고선", toggle: true },
|
|
stations: { icon: "tallymarks", title: "측점", toggle: true },
|
|
slopes: { icon: "percentage", title: "기울기", toggle: true },
|
|
footprint: { icon: "road", title: "절/성토선", toggle: true },
|
|
measure: { icon: "ruler", title: "거리 재기", toggle: true },
|
|
"station-snap": { icon: "magnet", title: "측점 스냅", toggle: true },
|
|
"year-mark": { icon: "flag", title: "연도 구역 마커 — 선 위로 끌어 놓기" },
|
|
} satisfies Record<string, { icon: IconName; title: string; toggle?: boolean }>;
|
|
|
|
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;
|
|
return button;
|
|
}
|
|
|
|
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<HTMLButtonElement>(`[data-act="${act}"]`),
|
|
};
|
|
}
|