(() => { // src/ui/panel.ui.js var root = window; var panelNamespace = root.Panel ?? (root.Panel = {}); var page = panelNamespace.ui ?? {}; panelNamespace.ui = page; var core = panelNamespace.core; var Panel = core?.Panel; if (!Panel) throw new Error("加载 panel.ui.js 前必须先加载 Panel.core.Panel"); var panelUtil = core?.util ?? Panel.util; var panelConfig = panelNamespace.config ?? null; var panelSelects = panelNamespace.selects ?? null; var panelActivationCode = panelNamespace.activationCode ?? ""; var localStorageKey = panelNamespace.storageKey ?? "panel:data"; var noop = () => { }; var TestActionIds = Object.freeze(["test1", "test2", "test3", "test4", "test5", "test6", "test7", "test8", "test9"]); var getTestApi = () => panelNamespace.test ?? null; var resolveActionRunner = (action) => { if (!action || action === "invokeAction") return null; const panel = page.panel; if (panel && typeof panel[action] === "function") return () => panel.invokeAction(action); const handler = page[action]; if (typeof handler === "function" && handler !== page.invokeAction) return () => handler(); const testApi = getTestApi(); if (testApi?.has?.(action) === true && typeof testApi.run === "function") return () => testApi.run(action); return null; }; (() => { "use strict"; const applyConfig = () => { if (panelConfig) { if (panelConfig.panel && panelConfig.panel.config) { page.panel.config = panelUtil.mergeDeep(page.panel.config, panelConfig.panel.config); } if (panelConfig.ui) { page.ui = panelUtil.mergeDeep(page.ui, panelConfig.ui); } } }; const bindOnce = (element, type, handler) => element.addEventListener(type, handler); const unbindOnce = (element, type, handler) => element.removeEventListener(type, handler); const bindListOnce = (elements, type, handler) => { if (!elements?.length) return; for (let i = 0; i < elements.length; i++) bindOnce(elements[i], type, handler); }; const unbindList = (elements, type, handler) => { if (!elements?.length) return; for (let i = 0; i < elements.length; i++) unbindOnce(elements[i], type, handler); }; const closest = (target, selector) => target?.closest(selector) ?? null; const getData = (el, key) => el?.dataset?.[key] ?? null; const ensureHandler = (handlers, key, factory) => { if (!handlers[key]) handlers[key] = factory(); return handlers[key]; }; const bindPanelHandlers = (panel, handlers, entries) => { if (!panel) return; for (let i = 0; i < entries.length; i++) { const [key, type, factory] = entries[i]; bindOnce(panel, type, ensureHandler(handlers, key, factory)); } }; const unbindPanelHandlers = (panel, handlers, entries) => { if (!panel) return; for (let i = 0; i < entries.length; i++) { const [key, type] = entries[i]; const handler = handlers[key]; if (handler) unbindOnce(panel, type, handler); } }; const scheduleRafTask = (tasks, key, handler) => { if (!tasks || tasks[key]) return; tasks[key] = requestAnimationFrame(() => { tasks[key] = 0; handler(); }); }; const cancelRafTask = (tasks, key) => { if (!tasks || !tasks[key]) return; cancelAnimationFrame(tasks[key]); tasks[key] = 0; }; const cancelAllRafTasks = (tasks) => { if (!tasks) return; for (const key in tasks) cancelRafTask(tasks, key); }; const queryAll = (selector, root2 = document) => Array.from(root2.querySelectorAll(selector)); const toastTypes = /* @__PURE__ */ new Set(["info", "success", "warning", "error"]); const shapeFormConfigs = Panel.uiShapeConfigs; const shapeFormMap = /* @__PURE__ */ new Map(); for (let i = 0; i < shapeFormConfigs.length; i++) { const config = shapeFormConfigs[i]; shapeFormMap.set(config.type, config); } const formActionSchemas = Panel.formActionSchemas || {}; const renameConfigs = { layer: { stateKey: "layerState", listKey: "layerList", dataAttr: "layer-id", dataKey: "layerId", itemSelector: ".layer-item", nameSelector: ".layer-name", inputSelector: ".layer-input", getEntry: (id) => page.panel.getLayer(id), rename: (id, name) => page.panel.renameLayer(id, name, { history: true }), renderList: () => page.renderLayerList() }, group: { stateKey: "groupState", listKey: "groupList", dataAttr: "group-id", dataKey: "groupId", itemSelector: ".group-item", nameSelector: ".group-name", inputSelector: ".group-input", getEntry: (id) => page.panel.getGroup(id), rename: (id, name) => page.panel.renameGroup(id, name, { history: true }), renderList: () => page.renderGroupList() } }; const getRenameConfig = (type) => renameConfigs[type]; const panelEventEntries = [["click", "Click"], ["dblclick", "DblClick"], ["keydown", "Keydown"], ["focusout", "FocusOut"]]; const buildPanelHandlerEntries = (type) => { const entries = new Array(panelEventEntries.length); for (let i = 0; i < panelEventEntries.length; i++) { const [eventType, suffix] = panelEventEntries[i]; entries[i] = [ `${type}Panel${suffix}`, eventType, () => (event) => page.handleSettingsPanelEvent(type, eventType, event) ]; } return entries; }; const layerPanelHandlerEntries = buildPanelHandlerEntries("layer"); const groupPanelHandlerEntries = buildPanelHandlerEntries("group"); const formEventEntries = Object.freeze([ ["input", "handleFormInput"], ["change", "handleFormChange"], ["click", "handleFormClick"] ]); const nodePositionNumberKeys = ["x", "y", "theta", "allowedDeviationXY", "allowedDeviationTheta"]; const controlPointNumberKeys = ["x", "y", "weight"]; const runCommandAction = (id) => { const run = resolveActionRunner(id); if (!run) return false; run(); return true; }; const getFormEventTargets = () => { const targets = []; const container = document.querySelector(".property"); if (container) targets.push(container); const settingsPanel = document.getElementById("settingsPanel"); if (settingsPanel && settingsPanel !== container) targets.push(settingsPanel); return targets; }; const toggleFormEventTargets = (targets, method) => { if (!targets?.length) return; for (let i = 0; i < targets.length; i++) { const target = targets[i]; if (!target) continue; for (let j = 0; j < formEventEntries.length; j++) { const [type, key] = formEventEntries[j]; target[method](type, page[key]); } } }; const dispatchPanelAction = (event, config) => { const actionEl = closest(event.target, config.actionSelector); if (!actionEl) return; const action = getData(actionEl, config.actionKey); const handler = config.handlers[action]; if (!handler) return; const item = closest(actionEl, config.itemSelector); const payload = { action, item }; payload[config.idKey] = getData(item, config.idKey); if (handler(payload) === true) config.refresh(); }; const startRenameFromAction = (type, item, targetId) => { if (!targetId) return false; const targetItem = item || page.getRenameListItem(type, targetId); if (!targetItem) return false; page.startRename(type, targetId, targetItem); return false; }; const createRenameAction = (type, getActiveId) => (payload) => { const targetId = payload?.[`${type}Id`] || getActiveId(); return startRenameFromAction(type, payload?.item, targetId); }; const moveLayerAction = ({ action }) => { const activeId = page.panel.getActiveLayerId(); if (!activeId) return false; page.panel.moveLayer(activeId, action, { history: true }); return true; }; const withLayerId = (layerId, handler) => { if (!layerId) return false; handler(layerId); return true; }; const removeGroupAction = ({ groupId }) => { const targetId = groupId || page.panel.getActiveGroupId(); if (!targetId) return false; return page.panel.removeGroupById(targetId, { history: true }); }; const layerActionHandlers = Object.freeze({ add: () => { const layer = page.panel.addLayer(void 0, { history: true }); if (layer) page.panel.setActiveLayer(layer.id); return true; }, moveSelected: () => { const activeId = page.panel.getActiveLayerId(); if (!activeId) { page.notify("未找到当前图层", "warning"); return false; } const moved = page.panel.moveActiveShapesToLayer(activeId, { history: true }); if (!moved) { page.notify("未选中可移动对象", "warning"); } return true; }, rename: createRenameAction("layer", () => page.panel.getActiveLayerId()), delete: () => { const activeId = page.panel.getActiveLayerId(); if (!activeId) return false; const layers = page.panel.getLayers(); const activeIndex = layers.findIndex((layer) => layer.id === activeId); let nextActiveId = null; if (activeIndex > 0) { nextActiveId = layers[activeIndex - 1]?.id ?? null; } else if (activeIndex === 0 && layers.length > 1) { nextActiveId = layers[1]?.id ?? null; } const removed = page.panel.removeLayer(activeId, { history: true }); if (!removed) { page.notify("至少保留一个图层", "warning"); } if (removed && nextActiveId) { page.panel.setActiveLayer(nextActiveId); } return true; }, up: moveLayerAction, down: moveLayerAction, select: ({ layerId }) => withLayerId(layerId, (id) => page.panel.setActiveLayer(id)), visibility: ({ layerId }) => withLayerId(layerId, (id) => page.panel.toggleLayerVisibility(id, { history: true })), lock: ({ layerId }) => withLayerId(layerId, (id) => page.panel.toggleLayerLock(id, { history: true })) }); const groupActionHandlers = Object.freeze({ group: () => { page.panel.setGroup(); return true; }, ungroup: removeGroupAction, delete: removeGroupAction, rename: createRenameAction("group", () => page.panel.getActiveGroupId()), select: ({ groupId }) => { if (!groupId) return false; const shapes = page.panel.getGroupShapes(groupId); if (!Array.isArray(shapes) || shapes.length === 0) { page.panel.setActiveGroupId(groupId); page.panel.requestRender(); return true; } page.panel.setActiveShapes(shapes); return true; } }); const layerPanelDispatchConfig = Object.freeze({ actionSelector: "[data-layer-action]", actionKey: "layerAction", itemSelector: ".layer-item", idKey: "layerId", handlers: layerActionHandlers, refresh: () => page.refreshLayerList() }); const groupPanelDispatchConfig = Object.freeze({ actionSelector: "[data-group-action]", actionKey: "groupAction", itemSelector: ".group-item", idKey: "groupId", handlers: groupActionHandlers, refresh: () => page.refreshGroupList() }); const panelDispatchConfigs = Object.freeze({ layer: layerPanelDispatchConfig, group: groupPanelDispatchConfig }); const settingsPanelEventHandlers = Object.freeze({ dblclick: "handleRenameDblClick", keydown: "handleRenameKeydown", focusout: "handleRenameFocusOut" }); Object.assign(page, { canvas: {}, panel: { config: { license: { activationCode: panelActivationCode }, map: { base: { size: { w: 1e4, h: 1e4 } } }, mode: "Edit", info: { enable: true } } }, ui: { statusInterval: 1e3, statusTimer: null, pageReady: null, visibilityHandler: null, shapeBindingsReady: false, toolFilter: { group: "all" }, toolFilterState: { text: null, group: null }, els: {}, formActionMap: /* @__PURE__ */ new Map(), formActionSchemaMap: /* @__PURE__ */ new Map(), formEventsBound: false, formEventTargets: [], multiSelectBound: false, rafTasks: {}, formInputQueue: /* @__PURE__ */ new Map(), formInputList: [], formInputsCache: /* @__PURE__ */ new Map(), resizeObserver: null, defaultTheme: null, initialized: false, bound: false, handlers: {}, dialogListeners: [], statusCache: { mapMeta: null, toggles: {} }, searchState: { keyword: "", matches: [], index: 0 }, toastHost: null, settingsPanelOpen: false, layerState: { editingId: null }, groupState: { editingId: null } }, init: () => { if (page.ui.initialized) return; page.ui.initialized = true; page.canvas = document.getElementById("canvas"); if (page.ui.defaultTheme) { page.applyUiTheme(page.ui.defaultTheme); } page.panel = new Panel(page.canvas, page.panel.config).init(); page.bind(); const panelTheme = page.panel.getTheme(); const preferredTheme = page.ui.defaultTheme || panelTheme; page.applyUiTheme(preferredTheme); page.panel.setTheme(preferredTheme, { force: true }); page.setMapRectForm(page.panel); }, start: () => { applyConfig(); page.init(); const handlers = page.ui.handlers ?? (page.ui.handlers = {}); if (!handlers.beforeUnload) { handlers.beforeUnload = () => page.destroy(); window.addEventListener("beforeunload", handlers.beforeUnload); } }, bind: () => { if (page.ui.bound) return; page.ui.bound = true; const handlers = page.ui.handlers ?? (page.ui.handlers = {}); const onResize = ensureHandler(handlers, "resize", () => () => page.queueResize()); window.addEventListener("resize", onResize); const searchButton = document.getElementById("buttonSearch"); if (searchButton) { page.ui.els.searchButton = searchButton; const onSearchClick = ensureHandler(handlers, "searchClick", () => () => page.setSearch()); bindOnce(searchButton, "click", onSearchClick); } const searchInput = document.getElementById("textSearch"); if (searchInput) { page.ui.els.searchInput = searchInput; const onSearchKeydown = ensureHandler(handlers, "searchKeydown", () => (event) => { if (event.key === "Enter") { event.preventDefault(); page.setSearch(); } }); bindOnce(searchInput, "keydown", onSearchKeydown); } page.bindCommandButtons(); page.bindFormEvents(); const panelActions = { panelThemeAction: (theme, preset, panel) => page.handleThemeChange(theme, preset, panel), panelActiveAction: (panel) => page.setMapRectForm(panel), panelPointerDownAction: (event) => page.setPointerDownAction(event), panelOpenMapAction: () => page.refreshLayerAndGroupList(), panelSaveMapAction: () => page.updateStatusBar(), panelAlertAction: (message, options) => page.alert(message, options), panelConfirmAction: (message, options) => page.confirm(message, options) }; Object.assign(page.panel, panelActions); page.initShapeBindings(); page.setMapRectPanel(); page.setSettingsPanel(); page.initPropertyTabs(); page.initFormDefaults(); page.initFormAccessibility(); page.initDialogs(); page.initUI(); page.initResizeObserver(); page.syncTestButtons(); }, bindCommandButtons: () => { const handlers = page.ui.handlers; const onCommandClick = ensureHandler(handlers, "commandClick", () => (event) => { const button = closest(event.target, ".command"); if (!button) return; page.setButtonActive(button); runCommandAction(button.id); }); if (!handlers.commandClickBound) { document.addEventListener("click", onCommandClick); handlers.commandClickBound = true; } }, queueResize: () => { scheduleRafTask(page.ui.rafTasks, "resize", () => { page.panel.setResize(); }); }, initResizeObserver: () => { if (page.ui.resizeObserver) return; const target = page.canvas.parentNode; page.ui.resizeObserver = new ResizeObserver(() => page.queueResize()); page.ui.resizeObserver.observe(target); }, initShapeBindings: () => { if (page.ui.shapeBindingsReady) return; page.ui.shapeBindingsReady = true; page.ui.formActionMap.clear(); page.ui.formActionSchemaMap.clear(); for (let i = 0; i < shapeFormConfigs.length; i++) { const config = shapeFormConfigs[i]; page.panel[config.activeAction] = (shape) => page.setFormByType(config.type, shape); page.bindPanelInputsBySchema(config.tabId, config.type, config.panelHandler); } }, cacheUI: () => { page.ui.toolFilterState = { text: null, group: null }; const toolGroups = []; const toolGroupElements = queryAll(".toolbar .tool-group"); for (let i = 0; i < toolGroupElements.length; i++) { const group = toolGroupElements[i]; const title = group.dataset.title || ""; const buttons = []; const groupButtons = queryAll("button", group); for (let j = 0; j < groupButtons.length; j++) { const button = groupButtons[j]; buttons.push({ el: button, search: `${button.textContent} ${button.id || ""} ${title}`.toLowerCase() }); } toolGroups.push({ el: group, key: group.dataset.group || "all", title, buttons }); } const quickActions = []; const actionButtons = queryAll("[data-action]"); for (let i = 0; i < actionButtons.length; i++) { const button = actionButtons[i]; if (!closest(button, "dialog")) quickActions.push(button); } const testButtons = []; for (let i = 0; i < TestActionIds.length; i++) { const button = document.getElementById(TestActionIds[i]); if (button) testButtons.push(button); } page.ui.els = { appContainer: document.querySelector(".app-container"), toolbarPanel: document.querySelector(".toolbar.panel"), propertyPanel: document.querySelector(".property.panel"), toolSearch: document.getElementById("toolSearch"), toolClear: document.getElementById("clearToolSearch"), toolEmpty: document.getElementById("toolEmpty"), toolGroups, toolChips: queryAll(".toolbar-filters .chip"), switchButtons: queryAll(".command.switch"), quickActions, toggleButtons: queryAll("[data-toggle]"), searchClear: document.getElementById("buttonSearchClear"), searchInput: document.getElementById("textSearch"), themeSelect: document.querySelector('[data-theme-select="ui"]') || document.querySelector('[name="visual.theme"]'), mapMeta: document.getElementById("mapMeta"), propertyTitle: document.getElementById("titleProperty"), propertyTabs: queryAll(".property.panel .tabProperty"), layerPanel: document.getElementById("layerPanel"), layerList: document.getElementById("layerList"), groupPanel: document.getElementById("groupPanel"), groupList: document.getElementById("groupList"), settingsPanel: document.getElementById("settingsPanel"), settingsToggleButton: document.getElementById("toggleSettingsPanel") || document.querySelector('[data-action="toggleSettingsPanel"]'), testToolbar: document.getElementById("testToolbar"), testButtons }; }, syncTestButtons: () => { const els = page.ui.els; const buttons = els.testButtons ?? []; let visible = 0; for (let i = 0; i < buttons.length; i++) { const button = buttons[i]; if (!button) continue; const available = Boolean(resolveActionRunner(button.id)); button.hidden = !available; button.disabled = !available; button.classList.toggle("is-hidden", !available); if (available) visible += 1; } const toolbar = els.testToolbar; if (toolbar) { const hidden = visible === 0; toolbar.hidden = hidden; toolbar.classList.toggle("is-hidden", hidden); } }, initUI: () => { page.cacheUI(); page.syncTestButtons(); page.initRenderSelects(); page.bindQuickActions(); page.initSettingsPanel(); page.initDockablePanels(); page.bindThemeSelect(); page.bindSearchClear(); page.bindToolFilter(); page.updateStatusBar(); page.startStatusTicker(); }, initDockablePanels: () => { const { toolbarPanel, propertyPanel } = page.ui.els; if (!toolbarPanel && !propertyPanel) return; const panels = [toolbarPanel, propertyPanel]; for (let i = 0; i < panels.length; i++) { const panel = panels[i]; if (!panel) continue; const toggle = panel.querySelector('[data-panel-action="collapse"]'); if (toggle && !toggle.dataset.panelCollapseBound) { toggle.dataset.panelCollapseBound = "true"; bindOnce(toggle, "click", () => page.togglePanelCollapse(panel)); } } page.updatePanelDockWidths(); }, updatePanelDockWidths: () => { const { appContainer, toolbarPanel, propertyPanel } = page.ui.els; if (!appContainer) return; const collapseWidth = 52; if (toolbarPanel) { if (toolbarPanel.classList.contains("is-collapsed")) { appContainer.style.setProperty("--toolbar-width", `${collapseWidth}px`); } else { appContainer.style.removeProperty("--toolbar-width"); } } if (propertyPanel) { if (propertyPanel.classList.contains("is-collapsed")) { appContainer.style.setProperty("--property-width", `${collapseWidth}px`); } else { appContainer.style.removeProperty("--property-width"); } } }, togglePanelCollapse: (panel, force) => { if (!panel) return; const next = typeof force === "boolean" ? force : !panel.classList.contains("is-collapsed"); panel.classList.toggle("is-collapsed", next); const toggle = panel.querySelector('[data-panel-action="collapse"]'); if (toggle) { toggle.setAttribute("aria-pressed", next ? "true" : "false"); toggle.textContent = next ? "展开" : "收缩"; } page.updatePanelDockWidths(); }, bindQuickActions: () => { const handlers = page.ui.handlers; const onQuickActionClick = ensureHandler(handlers, "quickActionClick", () => (event) => { const target = event.currentTarget; page.invokeAction(target.dataset.action); }); bindListOnce(page.ui.els.quickActions, "click", onQuickActionClick); }, initSettingsPanel: () => { const { settingsPanel, layerPanel, groupPanel } = page.ui.els; if (!settingsPanel) return; const handlers = page.ui.handlers; if (layerPanel) { bindPanelHandlers(layerPanel, handlers, layerPanelHandlerEntries); } if (groupPanel) { bindPanelHandlers(groupPanel, handlers, groupPanelHandlerEntries); } page.setSettingsPanelOpen(!settingsPanel.hidden); }, initRenderSelects: () => { page.bindMultiSelectToggles(); if (page.panel) page.setSettingsForm(page.panel); }, bindMultiSelectToggles: () => { if (page.ui.multiSelectBound) return; page.ui.multiSelectBound = true; const handler = (event) => { const select = event.currentTarget; const option = closest(event.target, "option"); if (!select || !option) return; event.preventDefault(); option.selected = !option.selected; select.dispatchEvent(new Event("change", { bubbles: true })); }; const selects = queryAll("select[multiple]"); bindListOnce(selects, "mousedown", handler); }, getRenameListItem: (type, id) => { if (!id) return null; const config = getRenameConfig(type); if (!config) return null; const list = page.ui.els[config.listKey]; return list ? list.querySelector(`[data-${config.dataAttr}="${id}"]`) : null; }, startRename: (type, id, item) => { const config = getRenameConfig(type); if (!config || !id || !item) return; const entry = config.getEntry(id); if (!entry) return; const state = page.ui[config.stateKey]; if (state.editingId && state.editingId !== id) { page.cancelRename(type); } const input = item.querySelector(config.inputSelector); if (!input) return; state.editingId = id; item.classList.add("is-editing"); input.value = entry.name || entry.id || ""; input.focus(); input.select(); }, commitRename: (type, id, input) => { const config = getRenameConfig(type); if (!config) return; const state = page.ui[config.stateKey]; if (!id || state.editingId !== id) return; const raw = input ? input.value : ""; const trimmed = raw.trim(); state.editingId = null; const item = page.getRenameListItem(type, id); if (!trimmed) { if (item) item.classList.remove("is-editing"); else config.renderList(); return; } config.rename(id, trimmed); if (item) { const name = item.querySelector(config.nameSelector); const inputEl = item.querySelector(config.inputSelector); if (name) name.textContent = trimmed; if (inputEl) inputEl.value = trimmed; item.classList.remove("is-editing"); } else { config.renderList(); } page.updateStatusBar(); }, cancelRename: (type) => { const config = getRenameConfig(type); if (!config) return; const state = page.ui[config.stateKey]; const editingId = state.editingId; if (!editingId) return; state.editingId = null; const item = page.getRenameListItem(type, editingId); if (item) item.classList.remove("is-editing"); else config.renderList(); }, handleRenameDblClick: (type, event) => { const config = getRenameConfig(type); if (!config) return; const nameEl = closest(event.target, config.nameSelector); if (!nameEl) return; const item = closest(nameEl, config.itemSelector); if (!item) return; const targetId = getData(item, config.dataKey); if (!targetId) return; page.startRename(type, targetId, item); }, handleRenameKeydown: (type, event) => { const config = getRenameConfig(type); if (!config) return; const input = closest(event.target, config.inputSelector); if (!input) return; const item = closest(input, config.itemSelector); if (!item) return; const targetId = getData(item, config.dataKey); if (!targetId) return; if (event.key === "Enter") { event.preventDefault(); page.commitRename(type, targetId, input); } else if (event.key === "Escape") { event.preventDefault(); page.cancelRename(type); } }, handleRenameFocusOut: (type, event) => { const config = getRenameConfig(type); if (!config) return; const input = closest(event.target, config.inputSelector); if (!input) return; const item = closest(input, config.itemSelector); if (!item) return; const targetId = getData(item, config.dataKey); if (!targetId) return; const state = page.ui[config.stateKey]; if (state.editingId === targetId) { page.commitRename(type, targetId, input); } }, getSettingsPanelBody: () => { const panel = page.ui.els.settingsPanel; if (!panel) return null; const body = panel.querySelector(".panel-body"); if (!body) return null; return body.querySelector(".tabProperty") || body; }, withSettingsScroll: (action) => { if (typeof action !== "function") return; const body = page.getSettingsPanelBody(); const prevScrollTop = body ? body.scrollTop : null; action(); if (!body || prevScrollTop === null) return; const maxTop = Math.max(0, body.scrollHeight - body.clientHeight); body.scrollTop = Math.min(prevScrollTop, maxTop); }, setSettingsPanelOpen: (open) => { const { settingsPanel, settingsToggleButton } = page.ui.els; if (!settingsPanel) return; const shouldOpen = Boolean(open); settingsPanel.hidden = !shouldOpen; page.ui.settingsPanelOpen = shouldOpen; if (settingsToggleButton) { settingsToggleButton.classList.toggle("active", shouldOpen); settingsToggleButton.setAttribute("aria-pressed", shouldOpen ? "true" : "false"); } if (shouldOpen) { page.setSettingsForm(page.panel); page.refreshLayerAndGroupList(); } }, toggleSettingsPanel: () => { page.setSettingsPanelOpen(!page.ui.settingsPanelOpen); }, setDeleteImage: () => page.panel.setDeleteImage({ force: true, history: true }), setReset: () => page.panel.setReset({ force: true, history: true }), handleSettingsPanelEvent: (type, eventType, event) => { const config = panelDispatchConfigs[type]; if (!config) return; if (eventType === "click") { dispatchPanelAction(event, config); return; } const handler = settingsPanelEventHandlers[eventType]; if (handler) page[handler](type, event); }, createLayerItem: (layer, counts, activeId) => { const item = document.createElement("div"); item.className = `layer-item${layer.id === activeId ? " is-active" : ""}`; item.dataset.layerId = layer.id; const visibility = document.createElement("button"); visibility.type = "button"; visibility.className = "action layer-toggle"; visibility.dataset.layerAction = "visibility"; visibility.setAttribute("aria-pressed", layer.visible ? "true" : "false"); visibility.title = layer.visible ? "隐藏图层" : "显示图层"; visibility.textContent = layer.visible ? "隐" : "显"; const lock = document.createElement("button"); lock.type = "button"; lock.className = "action layer-toggle"; lock.dataset.layerAction = "lock"; lock.setAttribute("aria-pressed", layer.locked ? "true" : "false"); lock.title = layer.locked ? "解锁图层" : "锁定图层"; lock.textContent = layer.locked ? "解" : "锁"; const name = document.createElement("button"); name.type = "button"; name.className = "layer-name"; name.dataset.layerAction = "select"; name.textContent = layer.name || layer.id; const count = document.createElement("span"); count.className = "layer-count"; count.textContent = String(counts.get(layer.id) ?? 0); const input = document.createElement("input"); input.type = "text"; input.className = "layer-input"; input.setAttribute("aria-label", "图层名称"); input.value = layer.name || layer.id || ""; item.append(visibility, lock, name, count, input); return item; }, buildSettingsCountMaps: () => { const layer = /* @__PURE__ */ new Map(); const group = /* @__PURE__ */ new Map(); const shapes = page.panel.allShapes; for (let i = 0; i < shapes.length; i++) { const shape = shapes[i]; const layerId = page.panel.getShapeLayerId(shape); layer.set(layerId, (layer.get(layerId) ?? 0) + 1); const groupId = shape.groupId; if (groupId) group.set(groupId, (group.get(groupId) ?? 0) + 1); } return { layer, group }; }, buildShapeCountMap: (type) => { const counts = /* @__PURE__ */ new Map(); const shapes = page.panel.allShapes; const isLayer = type === "layer"; for (let i = 0; i < shapes.length; i++) { const shape = shapes[i]; const key = isLayer ? page.panel.getShapeLayerId(shape) : shape.groupId; if (!isLayer && !key) continue; counts.set(key, (counts.get(key) ?? 0) + 1); } return counts; }, renderSettingsByType: (type, options = {}) => { const isLayer = type === "layer"; const list = isLayer ? page.ui.els.layerList : page.ui.els.groupList; if (!list) return; const entries = isLayer ? page.panel.getLayers() : page.panel.getGroups(); const activeId = isLayer ? page.panel.getActiveLayerId() : page.panel.getActiveGroupId(); const counts = options.counts ?? page.buildShapeCountMap(type); const state = isLayer ? page.ui.layerState : page.ui.groupState; const createItem = isLayer ? (layer) => page.createLayerItem(layer, counts, activeId) : (group) => page.createGroupItem(group, counts, activeId); page.renderSettingsList(list, state, entries, createItem, options); }, renderLayerList: (options = {}) => page.renderSettingsByType("layer", options), refreshSettingsList: (renderList) => { page.withSettingsScroll(() => renderList({ preserveScroll: true })); page.updateStatusBar(); }, refreshSettingsByType: (type) => page.refreshSettingsList((options) => page.renderSettingsByType(type, options)), refreshLayerList: () => page.refreshSettingsByType("layer"), renderSettingsList: (list, state, entries, createItem, options = {}) => { if (!list) return; state.editingId = null; const preserveScroll = options.preserveScroll === true; const prevScrollTop = preserveScroll ? Number.isFinite(options.scrollTop) ? options.scrollTop : list.scrollTop : 0; const fragment = document.createDocumentFragment(); for (let i = 0; i < entries.length; i++) { fragment.appendChild(createItem(entries[i])); } list.replaceChildren(fragment); if (!preserveScroll) return; const maxTop = Math.max(0, list.scrollHeight - list.clientHeight); list.scrollTop = Math.min(prevScrollTop, maxTop); }, createGroupItem: (group, counts, activeId) => { const item = document.createElement("div"); item.className = `group-item${group.id === activeId ? " is-active" : ""}`; item.dataset.groupId = group.id; const name = document.createElement("button"); name.type = "button"; name.className = "group-name"; name.dataset.groupAction = "select"; name.textContent = group.name || group.id; const count = document.createElement("span"); count.className = "group-count"; count.textContent = String(counts.get(group.id) ?? 0); const input = document.createElement("input"); input.type = "text"; input.className = "group-input"; input.setAttribute("aria-label", "组合名称"); input.value = group.name || group.id || ""; item.append(name, count, input); return item; }, renderGroupList: (options = {}) => page.renderSettingsByType("group", options), refreshGroupList: () => page.refreshSettingsByType("group"), refreshLayerAndGroupList: () => { const counts = page.buildSettingsCountMaps(); page.withSettingsScroll(() => { page.renderSettingsByType("layer", { preserveScroll: true, counts: counts.layer }); page.renderSettingsByType("group", { preserveScroll: true, counts: counts.group }); }); page.updateStatusBar(); }, invokeAction: (action, options = {}) => { const run = resolveActionRunner(action); if (!run) return false; run(); if (options.refresh === false) return true; if (page.ui.settingsPanelOpen) { page.refreshLayerAndGroupList(); } else { page.updateStatusBar(); } return true; }, bindSearchClear: () => { const clearButton = page.ui.els.searchClear; const handlers = page.ui.handlers; const onSearchClearClick = ensureHandler(handlers, "searchClearClick", () => () => page.clearSearch()); bindOnce(clearButton, "click", onSearchClearClick); }, bindThemeSelect: () => { const themeSelect = page.ui.els.themeSelect; const handlers = page.ui.handlers; const onThemeSelectChange = ensureHandler(handlers, "themeSelectChange", () => (event) => { page.setSettingParam(event.currentTarget); page.updateStatusBar(); }); bindOnce(themeSelect, "change", onThemeSelectChange); }, updateToolChipState: () => { const chips = page.ui.els.toolChips; for (let i = 0; i < chips.length; i++) { const chip = chips[i]; chip.setAttribute("aria-pressed", chip.classList.contains("active") ? "true" : "false"); } }, getToolFilterContext: () => { const { toolSearch, toolEmpty, toolGroups } = page.ui.els; if (toolSearch && toolGroups?.length) return { toolSearch, toolEmpty, toolGroups }; if (toolEmpty) toolEmpty.style.display = "none"; return null; }, scheduleToolFilter: () => { if (!page.getToolFilterContext()) return; scheduleRafTask(page.ui.rafTasks, "toolFilter", () => page.applyToolFilter()); }, queueFormInput: (input) => { const queue = page.ui.formInputQueue ?? (page.ui.formInputQueue = /* @__PURE__ */ new Map()); queue.set(input, true); scheduleRafTask(page.ui.rafTasks, "formInput", () => page.flushFormInputs()); }, flushFormInputs: () => { const queue = page.ui.formInputQueue; if (!queue || queue.size === 0) return; const inputs = page.ui.formInputList ?? (page.ui.formInputList = []); inputs.length = 0; inputs.push(...queue.keys()); queue.clear(); page.panel.withBatch(() => { for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; if (input?.isConnected) page.dispatchFormInput(input); } inputs.length = 0; }); }, scheduleHistory: () => { if (page.panel.history?.enable === false) return; scheduleRafTask(page.ui.rafTasks, "history", () => page.panel.setHistory()); }, bindToolFilter: () => { const context = page.getToolFilterContext(); if (!context) return; const { toolSearch } = context; const { toolClear, toolChips } = page.ui.els; const handlers = page.ui.handlers; const onToolSearchInput = ensureHandler(handlers, "toolSearchInput", () => () => page.scheduleToolFilter()); const onToolClearClick = ensureHandler(handlers, "toolClearClick", () => () => { toolSearch.value = ""; page.scheduleToolFilter(); toolSearch.focus(); }); const onToolChipClick = ensureHandler(handlers, "toolChipClick", () => (event) => { const chip = event.currentTarget; for (let i = 0; i < toolChips.length; i++) toolChips[i].classList.remove("active"); chip.classList.add("active"); page.ui.toolFilter.group = chip.dataset.toolFilter || "all"; page.updateToolChipState(); page.scheduleToolFilter(); }); bindOnce(toolSearch, "input", onToolSearchInput); if (toolClear) bindOnce(toolClear, "click", onToolClearClick); bindListOnce(toolChips, "click", onToolChipClick); page.updateToolChipState(); page.applyToolFilter(); }, applyToolFilter: () => { const context = page.getToolFilterContext(); if (!context) return; const { toolSearch, toolEmpty, toolGroups } = context; const text = toolSearch.value.trim().toLowerCase(); const activeGroup = page.ui.toolFilter.group; const filterState = page.ui.toolFilterState; if (filterState.text === text && filterState.group === activeGroup) return; filterState.text = text; filterState.group = activeGroup; let visibleGroups = 0; for (let i = 0; i < toolGroups.length; i++) { const group = toolGroups[i]; if (activeGroup !== "all" && group.key !== activeGroup) { group.el.classList.add("is-hidden"); continue; } let groupVisible = false; const buttons = group.buttons; for (let j = 0; j < buttons.length; j++) { const button = buttons[j]; const matched = !text || button.search.includes(text); button.el.classList.toggle("is-hidden", !matched); if (matched) groupVisible = true; } group.el.classList.toggle("is-hidden", !groupVisible); if (groupVisible) visibleGroups += 1; } if (toolEmpty) { toolEmpty.style.display = visibleGroups === 0 ? "block" : "none"; } }, startStatusTicker: () => { if (!page.ui.visibilityHandler) { page.ui.visibilityHandler = () => { if (document.hidden) { page.stopStatusTicker(); } else { page.startStatusTicker(); } }; document.addEventListener("visibilitychange", page.ui.visibilityHandler); } if (page.ui.statusTimer) return; page.updateStatusBar(); page.ui.statusTimer = window.setInterval(() => page.updateStatusBar(), page.ui.statusInterval); }, stopStatusTicker: () => { if (!page.ui.statusTimer) return; window.clearInterval(page.ui.statusTimer); page.ui.statusTimer = null; }, destroy: () => { page.stopStatusTicker(); getTestApi()?.stop?.(); cancelAllRafTasks(page.ui.rafTasks); page.ui.rafTasks = {}; if (page.ui.formInputQueue) { page.ui.formInputQueue.clear(); } if (Array.isArray(page.ui.formInputList)) { page.ui.formInputList.length = 0; } if (page.ui.resizeObserver) { page.ui.resizeObserver.disconnect(); page.ui.resizeObserver = null; } if (page.ui.visibilityHandler) { document.removeEventListener("visibilitychange", page.ui.visibilityHandler); page.ui.visibilityHandler = null; } const handlers = page.ui.handlers; if (handlers.resize) { window.removeEventListener("resize", handlers.resize); } if (handlers.commandClickBound && handlers.commandClick) { document.removeEventListener("click", handlers.commandClick); handlers.commandClickBound = false; } if (handlers.beforeUnload) { window.removeEventListener("beforeunload", handlers.beforeUnload); delete handlers.beforeUnload; } const els = page.ui.els; if (handlers.quickActionClick) { unbindList(els.quickActions, "click", handlers.quickActionClick); } const cleanupBindings = [ [els.searchClear, "click", handlers.searchClearClick], [els.themeSelect, "change", handlers.themeSelectChange], [els.toolSearch, "input", handlers.toolSearchInput], [els.toolClear, "click", handlers.toolClearClick], [els.searchButton, "click", handlers.searchClick], [els.searchInput, "keydown", handlers.searchKeydown] ]; for (let i = 0; i < cleanupBindings.length; i++) { const [element, type, handler] = cleanupBindings[i]; unbindOnce(element, type, handler); } if (handlers.toolChipClick) { unbindList(els.toolChips, "click", handlers.toolChipClick); } unbindPanelHandlers(els.layerPanel, handlers, layerPanelHandlerEntries); unbindPanelHandlers(els.groupPanel, handlers, groupPanelHandlerEntries); if (page.ui.formEventsBound) toggleFormEventTargets(page.ui.formEventTargets, "removeEventListener"); page.ui.formEventTargets = []; page.ui.formEventsBound = false; const dialogListeners = page.ui.dialogListeners; if (Array.isArray(dialogListeners) && dialogListeners.length) { for (let i = 0; i < dialogListeners.length; i++) { const entry = dialogListeners[i]; if (!entry) continue; const element = entry.element; if (element) element.removeEventListener(entry.type, entry.handler, entry.options); } dialogListeners.length = 0; } if (page.dialogs) { page.dialogs = null; } page.ui.shapeBindingsReady = false; page.ui.formActionMap.clear(); page.ui.formActionSchemaMap.clear(); if (page.ui.toastHost) { page.ui.toastHost.remove(); page.ui.toastHost = null; } page.ui.multiSelectBound = false; page.ui.els = {}; page.panel.dispose(); page.ui.bound = false; page.ui.initialized = false; }, updateMapMeta: () => { const { mapMeta } = page.ui.els; if (!mapMeta) return; const size = page.panel.map.base.size; const text = `尺寸 ${size.w} x ${size.h}`; if (page.ui.statusCache.mapMeta === text) return; page.ui.statusCache.mapMeta = text; mapMeta.textContent = text; }, updateToggleButtons: () => { const { toggleButtons } = page.ui.els; if (toggleButtons.length === 0) return; const cache = page.ui.statusCache.toggles ?? (page.ui.statusCache.toggles = {}); const states = { grid: page.panel.grid.enable, info: page.panel.info.enable, area: page.panel.area.enable, zoom: page.panel.zoom.enable, carRoute: page.panel.carEdge.enable && page.panel.carNode.enable }; for (let i = 0; i < toggleButtons.length; i++) { const button = toggleButtons[i]; const key = button.dataset.toggle; const isActive = Boolean(states[key]); if (key && cache[key] === isActive) continue; button.classList.toggle("active", isActive); button.setAttribute("aria-pressed", isActive ? "true" : "false"); if (key) cache[key] = isActive; } }, updateStatusBar: () => { page.updateToggleButtons(); page.updateMapMeta(); }, ensureToastHost: () => { if (page.ui.toastHost && page.ui.toastHost.isConnected) { return page.ui.toastHost; } let host = document.getElementById("toastStack"); if (!host) { host = document.createElement("div"); host.id = "toastStack"; host.className = "toast-stack"; host.setAttribute("aria-live", "polite"); host.setAttribute("aria-relevant", "additions"); document.body.appendChild(host); } page.ui.toastHost = host; return host; }, notify: (message, type = "info", options = {}) => { const host = page.ensureToastHost(); const toastType = toastTypes.has(type) ? type : "info"; const toast = document.createElement("div"); toast.className = `toast toast-${toastType}`; toast.setAttribute("role", toastType === "error" ? "alert" : "status"); toast.setAttribute("aria-live", toastType === "error" ? "assertive" : "polite"); const indicator = document.createElement("span"); indicator.className = "toast-indicator"; const text = document.createElement("div"); text.className = "toast-text"; text.textContent = String(message); const close = document.createElement("button"); close.type = "button"; close.className = "toast-close"; close.setAttribute("aria-label", "关闭"); close.textContent = "×"; toast.append(indicator, text, close); host.appendChild(toast); requestAnimationFrame(() => toast.classList.add("show")); const duration = Number.isFinite(options && options.duration) ? options.duration : toastType === "error" ? 6e3 : 3200; let timer = null; const remove = () => { if (toast.dataset.state === "leaving") return; toast.dataset.state = "leaving"; toast.classList.remove("show"); window.setTimeout(() => toast.remove(), 200); }; close.addEventListener("click", remove); if (duration > 0) { timer = window.setTimeout(remove, duration); toast.addEventListener("pointerenter", () => { if (timer) { window.clearTimeout(timer); timer = null; } }); toast.addEventListener("pointerleave", () => { if (!timer) { timer = window.setTimeout(remove, duration); } }); } }, ensureConfirmDialog: () => { if (page.ui.confirmDialog && page.ui.confirmDialog.dialog) { return page.ui.confirmDialog; } const dialog = document.getElementById("dialogConfirm") || null; if (!dialog) return null; const messageEl = dialog.querySelector(".dialog-message") || dialog.querySelector('[data-role="message"]'); const okButton = dialog.querySelector('[name="ok"]'); const cancelButton = dialog.querySelector('[name="cancel"]'); const state = { dialog, messageEl, okButton, cancelButton, resolving: false }; page.ui.confirmDialog = state; return state; }, alert: (message, options = {}) => { const type = options.type || "warning"; page.notify(message, type, options); return true; }, confirm: (message, options = {}) => { return new Promise((resolve) => { const fallback = () => { page.notify(message, options.type || "warning", options); resolve(false); }; const state = page.ensureConfirmDialog(); if (!state || typeof state.dialog.showModal !== "function") { fallback(); return; } if (state.resolving) { state.dialog.close(); } state.resolving = true; if (state.messageEl) { state.messageEl.textContent = String(message); } const okText = options.okText || "确定"; const cancelText = options.cancelText || "取消"; if (state.okButton) state.okButton.textContent = okText; if (state.cancelButton) state.cancelButton.textContent = cancelText; const onOk = () => { cleanup(); if (state.dialog.open) state.dialog.close("ok"); resolve(true); }; const onCancel = () => { cleanup(); if (state.dialog.open) state.dialog.close("cancel"); resolve(false); }; const onClose = () => { cleanup(); resolve(false); }; const cleanup = () => { state.resolving = false; if (state.okButton) state.okButton.removeEventListener("click", onOk); if (state.cancelButton) state.cancelButton.removeEventListener("click", onCancel); state.dialog.removeEventListener("close", onClose); }; if (state.okButton) state.okButton.addEventListener("click", onOk, { once: true }); if (state.cancelButton) state.cancelButton.addEventListener("click", onCancel, { once: true }); state.dialog.addEventListener("close", onClose, { once: true }); state.dialog.showModal(); page.focusDialog(state.dialog); }); }, requireSelection: (state, message = "请先选择一条记录") => { if (!state || state.selected < 0 || state.selected >= state.list.length) { page.notify(message, "warning"); return null; } return state.list[state.selected]; }, setMonitorButtonState: (isRunning) => { const button = page.ui.els.monitorButton || document.getElementById("buttonMonitor"); if (!button) return; button.textContent = isRunning ? "停止监控" : "启动监控"; button.setAttribute("aria-pressed", isRunning ? "true" : "false"); }, getTableTarget: (target) => typeof target === "string" ? document.getElementById(target) : target, syncTable: (target, rows, columns, buildRow) => { const tbody = page.getTableTarget(target); if (!tbody) return null; const list = Array.isArray(rows) ? rows : rows ? Object.values(rows) : []; if (!list.length) { tbody.replaceChildren(); return tbody; } const fragment = document.createDocumentFragment(); for (let i = 0; i < list.length; i++) { const item = list[i]; const row = document.createElement("tr"); if (typeof buildRow === "function") { buildRow(row, item, i); } else if (columns?.length) { for (let j = 0; j < columns.length; j++) { row.appendChild(page.createCell(item?.[columns[j]])); } } if (row.childNodes.length) fragment.appendChild(row); } tbody.replaceChildren(fragment); return tbody; }, clearTable: (target) => { page.syncTable(target); }, renderTable: (target, rows, columns) => { page.syncTable(target, rows, columns); }, applyThemeOverride: () => { const theme = page.panel.getTheme(); page.panel.setTheme(theme, { force: true, isPreserveStyle: false }); page.updateStatusBar(); }, setPointerDownAction: (event) => { if (event.button === 2) { const setSelect = document.getElementById("setSelect"); if (setSelect) page.setButtonActive(setSelect); page.panel.setSelect(); } }, getInputValue: (input) => { if (input.tagName === "SELECT" && input.multiple) { const options = input.selectedOptions; const values = new Array(options.length); for (let i = 0; i < options.length; i++) { values[i] = options[i].value; } return values; } if (input.type === "number") { const raw = input.valueAsNumber; if (!Number.isFinite(raw)) return null; let value = raw; const min = input.min === "" ? NaN : Number(input.min); const max = input.max === "" ? NaN : Number(input.max); if (Number.isFinite(min)) value = Math.max(value, min); if (Number.isFinite(max)) value = Math.min(value, max); return value; } else if (input.type === "checkbox") { return input.checked; } else { return input.value; } }, setInputValue: (input, value) => { if (input.tagName === "SELECT" && input.multiple) { const valueSet = /* @__PURE__ */ new Set(); if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { valueSet.add(String(value[i])); } } const options = input.options; for (let i = 0; i < options.length; i++) { const option = options[i]; option.selected = valueSet.has(option.value); } return; } if (input.type === "number") { input.value = Number.isFinite(value) ? value : ""; } else if (input.type === "checkbox") { input.checked = Boolean(value); } else { if (Array.isArray(value)) { const listMode = input.dataset && input.dataset.list; input.value = listMode === "lines" ? value.join("\n") : value.join(","); } else { input.value = value ?? ""; } } }, updateCountBadge: (input, value) => { if (!input || !input.dataset) return; const targetId = input.dataset.countTarget; if (!targetId) return; const target = document.getElementById(targetId); if (!target) return; const count = Array.isArray(value) ? value.length : 0; target.textContent = `${count} 项`; }, applyUiTheme: (theme) => { const root2 = document.documentElement; if (root2.dataset.theme === theme) return theme; root2.dataset.theme = theme; return theme; }, getCachedElements: (key, selector) => { const cached = page.ui.els[key]; if (Array.isArray(cached)) return cached; const next = queryAll(selector); page.ui.els[key] = next; return next; }, handleThemeChange: (theme, preset, panel) => { const applied = page.applyUiTheme(theme); const uiSelect = page.ui.els.themeSelect; if (uiSelect) page.setInputValue(uiSelect, applied); }, setButtonActive: (button) => { if (!button || !button.classList.contains("switch")) return; const buttons = page.getCachedElements("switchButtons", ".command.switch"); for (let i = 0; i < buttons.length; i++) buttons[i].classList.remove("active"); button.classList.add("active"); }, setFormActive: (titleText, tabId) => { const title = page.ui.els.propertyTitle || document.getElementById("titleProperty"); if (title) title.textContent = titleText; const tabs = page.getCachedElements("propertyTabs", ".property.panel .tabProperty"); for (let i = 0; i < tabs.length; i++) tabs[i].hidden = true; const tab = document.getElementById(tabId); if (!tab) return; tab.hidden = false; page.setPropertyTab(tab, 0); }, getFormInputs: (tabId) => { if (!tabId) return []; const cache = page.ui.formInputsCache ?? (page.ui.formInputsCache = /* @__PURE__ */ new Map()); if (cache.has(tabId)) return cache.get(tabId); const list = queryAll(`#${tabId} .bind`); cache.set(tabId, list); return list; }, cloneData: (value) => { return structuredClone(value); }, ensureList: (value) => Array.isArray(value) ? value : [], getRuntimeSelectList: (key) => { return panelSelects?.[key] ?? []; }, buildOptionEntries: (items, options) => { const config = options || {}; const valueKey = config.valueKey || "value"; const textKey = config.textKey || "text"; const fallback = Array.isArray(config.fallback) ? config.fallback : []; const seen = /* @__PURE__ */ new Set(); const entries = []; const appendItem = (item) => { let value; let text; if (typeof item === "string" || typeof item === "number") { value = String(item); text = String(item); } else if (item && typeof item === "object") { value = item[valueKey] ?? item.value ?? item.id ?? item.code ?? ""; text = item[textKey] ?? item.text ?? item.name ?? value; } else { return; } if (value === "" && text === "") return; const key = String(value); if (seen.has(key)) return; seen.add(key); entries.push({ value: value ?? "", text: text ?? "" }); }; if (Array.isArray(items)) { for (let i = 0; i < items.length; i++) appendItem(items[i]); } if (fallback.length) { for (let i = 0; i < fallback.length; i++) appendItem(fallback[i]); } return entries; }, createOption: (value, text) => { const option = document.createElement("option"); option.value = value ?? ""; option.textContent = text ?? ""; return option; }, syncOptionList: (target, items, options, keepValue = false, placeholder = null) => { if (!target) return null; const config = options || {}; const entries = page.buildOptionEntries(items, config); const current = keepValue ? target.value : ""; const currentText = String(current); const fragment = document.createDocumentFragment(); if (placeholder !== null) fragment.appendChild(page.createOption("", placeholder)); let hasCurrent = !current; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; fragment.appendChild(page.createOption(entry.value, entry.text)); if (!hasCurrent && String(entry.value) === currentText) hasCurrent = true; } if (!hasCurrent) fragment.appendChild(page.createOption(current, current)); target.replaceChildren(fragment); if (current) target.value = current; return target; }, fillSelectOptions: (select, items, options) => { const config = options || {}; return page.syncOptionList(select, items, config, true, config.placeholder ?? ""); }, fillDatalistOptions: (datalist, items, options) => { return page.syncOptionList(datalist, items, options); }, fillSelectOrDatalist: (input, items, options) => { if (!input) return null; if (input.tagName === "SELECT") { return page.fillSelectOptions(input, items, options); } const listId = input.getAttribute("list"); if (!listId) return null; const datalist = document.getElementById(listId); return page.fillDatalistOptions(datalist, items, options); }, initActionFormSources: () => { const state = page.dialogs.actionForm; if (state.sourcesReady) return; state.sourcesReady = true; const form = page.getDialogForm(state); const actionSelect = form.querySelector('[name="actionType"]'); const blockingSelect = form.querySelector('[name="blockingType"]'); const actionList = page.getRuntimeSelectList("actionTypes"); const blockingList = page.getRuntimeSelectList("blockingTypes"); page.fillSelectOrDatalist(actionSelect, actionList, { valueKey: "code", textKey: "name", placeholder: "请选择", fallback: [] }); page.fillSelectOrDatalist(blockingSelect, blockingList, { valueKey: "code", textKey: "name", placeholder: "请选择", fallback: [] }); page.initActionParamKeySources(); }, initActionParamKeySources: () => { const state = page.dialogs.actionParameterForm; if (state.keysReady) return; state.keysReady = true; const form = page.getDialogForm(state); const keyInput = form.querySelector('[name="key"]'); const keyList = page.getRuntimeSelectList("actionParamKeys"); page.fillSelectOrDatalist(keyInput, keyList, { fallback: [] }); }, createId: () => page.panel.util.getGuid(), formatNumber: (value) => { const num = Number(value); return Number.isFinite(num) ? String(num) : ""; }, createCell: (value) => { const cell = document.createElement("td"); cell.textContent = value ?? ""; return cell; }, getField: (scope, name) => scope?.querySelector(`[name="${name}"]`), getFieldText: (scope, name, trim = false) => { const value = page.getField(scope, name)?.value ?? ""; return trim ? value.trim() : value; }, setFieldText: (scope, name, value = "") => { const input = page.getField(scope, name); if (input) input.value = value ?? ""; }, getFieldNumber: (scope, name, fallback = 0) => { const value = page.getField(scope, name)?.valueAsNumber; return Number.isFinite(value) ? value : fallback; }, setFieldNumber: (scope, name, value, fallback = 0) => { const input = page.getField(scope, name); if (input) input.value = Number.isFinite(value) ? value : fallback; }, bindModalOkCancel: (dialog, onOk) => { const okButton = page.getField(dialog, "ok"); const cancelButton = page.getField(dialog, "cancel"); if (okButton) okButton.onclick = onOk; if (cancelButton) cancelButton.onclick = () => dialog.close(); }, createDialogState: (id, state = {}) => ({ dialog: document.getElementById(id), ...state }), createListDialogState: (id, render, state = {}) => ({ dialog: document.getElementById(id), list: [], selected: -1, onSave: noop, onClose: noop, render, ...state }), getDialogRender: (state, render) => { if (typeof render === "function") return render; if (typeof state?.render === "function") return state.render; return noop; }, getDialogPart: (state, key, selector) => { if (!state?.dialog) return null; const parts = state.parts ?? (state.parts = {}); if (key in parts) return parts[key]; const part = state.dialog.querySelector(selector); parts[key] = part; return part; }, getDialogForm: (state) => page.getDialogPart(state, "form", "form"), getDialogTableBody: (state) => page.getDialogPart(state, "tbody", "tbody"), getDialogEmptyState: (state) => page.getDialogPart(state, "empty", ".dialog-empty"), bindDialogButton: (dialog, selector, handler) => { const button = dialog?.querySelector(selector); if (!button || typeof handler !== "function") return; page.bindDialogListener(button, "click", handler); }, renderDialogTable: (state, buildRow) => { const tbody = page.getDialogTableBody(state); const empty = page.getDialogEmptyState(state); const list = page.ensureList(state.list); state.list = list; if (state.selected >= list.length) state.selected = -1; page.syncTable(tbody, list, null, (row, item, index) => { row.dataset.index = index; const isSelected = index === state.selected; row.classList.toggle("is-selected", isSelected); row.setAttribute("aria-selected", isSelected ? "true" : "false"); row.tabIndex = isSelected ? 0 : -1; buildRow(row, item); }); if (empty) empty.style.display = list.length ? "none" : "block"; }, bindDialogRowSelection: (state, render = state?.render) => { if (!state || !state.dialog) return; const tbody = page.getDialogTableBody(state); if (!tbody) return; const rerender = page.getDialogRender(state, render); page.bindDialogListener(tbody, "click", (event) => { const row = closest(event.target, "tr"); if (!row) return; state.selected = Number(row.dataset.index); rerender(); }); }, openListDialog: (state, list, onSave, onClose, render = state?.render) => { if (!state || !state.dialog) return; const rerender = page.getDialogRender(state, render); state.list = page.ensureList(page.cloneData(list)); state.selected = -1; state.onSave = onSave || noop; if ("onClose" in state) state.onClose = onClose || noop; state.render = rerender; rerender(); page.openDialog(state.dialog); }, deleteSelectedDialogItem: (state, render = state?.render, message = "确定删除所选项吗?") => { if (!page.requireSelection(state)) return; const rerender = page.getDialogRender(state, render); page.confirm(message).then((ok) => { if (!ok) return; state.list.splice(state.selected, 1); state.selected = -1; rerender(); }); }, saveDialogList: (dialog, state, closeValue = true) => { state.onSave(page.cloneData(state.list)); dialog.close(); if ("onClose" in state) state.onClose(closeValue); }, closeDialogList: (dialog, state, closeValue = false) => { dialog.close(); if ("onClose" in state) state.onClose(closeValue); }, openListFormDialog: (dialog, openForm, mode, item, onSave, onRestore) => { dialog.close(); const onClose = () => { onRestore(); page.openDialog(dialog); }; openForm(...mode == null ? [item, onSave, onClose] : [mode, item, onSave, onClose]); }, renderKeyValueDialogList: (state) => { page.renderDialogTable(state, (row, item) => { row.append( page.createCell(item.key || ""), page.createCell(item.value || "") ); }); }, editDialogListItem: (dialog, state, openForm, mode, onSave, onRestore) => { const item = page.requireSelection(state); if (!item) return; page.openListFormDialog(dialog, openForm, mode, page.cloneData(item), (updated) => { onSave(updated, state); }, onRestore); }, bindListDialog: (state, options = {}) => { if (!state?.dialog) return; const dialog = state.dialog; const render = page.getDialogRender(state, options.render); state.render = render; page.bindDialogRowSelection(state, render); if (typeof options.add === "function") page.bindDialogButton(dialog, '[data-action="add"]', options.add); if (typeof options.edit === "function") page.bindDialogButton(dialog, '[data-action="edit"]', options.edit); if (typeof options.delete === "function") page.bindDialogButton(dialog, '[data-action="delete"]', options.delete); page.bindDialogButton(dialog, '[name="ok"]', options.ok || (() => page.saveDialogList(dialog, state, options.okCloseValue ?? true))); page.bindDialogButton(dialog, '[name="cancel"]', options.cancel || (() => page.closeDialogList(dialog, state, options.cancelCloseValue ?? false))); if (options.navigation !== false) page.bindDialogNavigation(state, render); }, bindKeyValueListDialog: (state, openForm, render = state?.render) => { if (!state?.dialog) return; const dialog = state.dialog; const rerender = page.getDialogRender(state, render); page.bindListDialog(state, { render: rerender, add: () => { page.openListFormDialog( dialog, openForm, "add", { key: "", value: "" }, (item) => { for (let i = 0; i < state.list.length; i++) { if (state.list[i].key !== item.key) continue; page.notify("添加项重复", "warning"); return; } state.list.push(item); }, rerender ); }, edit: () => { page.editDialogListItem( dialog, state, openForm, "edit", (updated) => { state.list[state.selected] = updated; }, rerender ); }, delete: () => { page.deleteSelectedDialogItem(state, rerender); } }); }, captureFocus: () => { const active = document.activeElement; if (active && active !== document.body) { page.ui.lastActiveElement = active; } }, restoreFocus: () => { const lastActive = page.ui.lastActiveElement; if (lastActive) lastActive.focus(); page.ui.lastActiveElement = null; }, focusDialog: (dialog) => { if (!dialog) return; const focusTarget = dialog.querySelector("[autofocus]") || dialog.querySelector("input, select, textarea, button, [tabindex]"); if (focusTarget) { focusTarget.focus(); return; } dialog.focus(); }, openDialog: (dialog) => { if (!dialog || typeof dialog.showModal !== "function") return; page.captureFocus(); dialog.showModal(); page.focusDialog(dialog); }, bindDialogListener: (element, type, handler, options) => { if (!element || typeof element.addEventListener !== "function" || typeof handler !== "function") return; element.addEventListener(type, handler, options); page.ui.dialogListeners.push({ element, type, handler, options }); }, bindDialogNavigation: (state, render = state?.render) => { const dialog = state.dialog; const rerender = page.getDialogRender(state, render); page.bindDialogListener(dialog, "keydown", (event) => { const key = event.key; const tag = event.target.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; if (key === "Enter") { const editButton = dialog.querySelector('[data-action="edit"]'); if (editButton && state.selected >= 0) { event.preventDefault(); editButton.click(); } return; } if (key === "Delete" || key === "Backspace") { const deleteButton = dialog.querySelector('[data-action="delete"]'); if (deleteButton && state.selected >= 0) { event.preventDefault(); deleteButton.click(); } return; } if (key !== "ArrowDown" && key !== "ArrowUp" && key !== "Home" && key !== "End") return; const list = page.ensureList(state.list); if (!list.length) return; event.preventDefault(); let next = state.selected; if (!Number.isFinite(next) || next < 0) next = 0; if (key === "ArrowDown") next = Math.min(list.length - 1, next + 1); if (key === "ArrowUp") next = Math.max(0, next - 1); if (key === "Home") next = 0; if (key === "End") next = list.length - 1; if (next === state.selected) return; state.selected = next; rerender(); const row = dialog.querySelector(`tbody tr[data-index="${next}"]`); if (row) row.focus(); }); }, initPropertyTabs: () => { const panels = document.querySelectorAll(".tabProperty"); for (let i = 0; i < panels.length; i++) { const panel = panels[i]; const forms = panel.querySelectorAll(":scope > form"); if (forms.length <= 1) continue; const header = document.createElement("div"); header.className = "property-tab-header"; for (let index = 0; index < forms.length; index++) { const form = forms[index]; const legend = form.querySelector("legend"); const label = legend ? legend.textContent.trim() : `Tab ${index + 1}`; const button = document.createElement("button"); button.type = "button"; button.className = "property-tab-button"; button.textContent = label; button.addEventListener("click", () => page.setPropertyTab(panel, index)); header.appendChild(button); } panel.insertBefore(header, panel.firstChild); page.setPropertyTab(panel, 0); } }, initFormDefaults: () => { const alphaInputs = document.querySelectorAll('input[name$="globalAlpha"]'); for (let i = 0; i < alphaInputs.length; i++) { const input = alphaInputs[i]; if (!input.step || input.step === "any") { input.step = "0.01"; } } const numberInputs = document.querySelectorAll('input[type="number"]'); for (let i = 0; i < numberInputs.length; i++) { const input = numberInputs[i]; if (!input.inputMode) { input.inputMode = "decimal"; } } }, initFormAccessibility: () => { const labels = document.querySelectorAll("label[for]"); for (let i = 0; i < labels.length; i++) { const label = labels[i]; const key = label.getAttribute("for"); if (!key) continue; const scope = closest(label, "div") || label.parentElement; let field = scope ? scope.querySelector(`[name="${key}"]`) : document.querySelector(`[name="${key}"]`); if (!field && scope) { const candidates = scope.querySelectorAll("input, select, textarea, button"); if (candidates.length === 1) { field = candidates[0]; } } if (!field) continue; if (field.id) { label.setAttribute("for", field.id); } else { const tab = closest(label, ".tabProperty"); const prefix = tab && tab.id ? `${tab.id}-` : ""; const baseId = `${prefix}${key}`.replace(/\s+/g, "-"); let id = baseId; let counter = 1; while (document.getElementById(id)) { id = `${baseId}-${counter}`; counter += 1; } field.id = id; label.setAttribute("for", id); } } }, setPropertyTab: (panel, index) => { if (!panel) return; const forms = panel.querySelectorAll(":scope > form"); const buttons = panel.querySelectorAll(":scope > .property-tab-header .property-tab-button"); for (let i = 0; i < forms.length; i++) { forms[i].classList.toggle("active", i === index); } for (let i = 0; i < buttons.length; i++) { buttons[i].classList.toggle("active", i === index); } }, getFormActionKey: (shapeType, name) => `${shapeType || "settings"}:${name || ""}`, registerFormAction: (shapeType, name, action) => { if (!name || typeof action !== "function") return; page.ui.formActionMap.set(page.getFormActionKey(shapeType, name), action); }, getBindInput: (target) => { if (!target) return null; const input = target.classList && target.classList.contains("bind") ? target : closest(target, ".bind"); if (!input) return null; const host = closest(input, ".property") || closest(input, "#settingsPanel"); return host ? input : null; }, dispatchFormInput: (input) => { if (input.dataset.setting === "true") { page.setSettingParam(input); return; } const shapeType = input.dataset.shapeType; if (shapeType) { page.setInputParam(input, shapeType); } }, handleFormInput: (event) => { const input = page.getBindInput(event.target); if (!input) return; if (input.tagName === "SELECT") return; page.queueFormInput(input); }, handleFormChange: (event) => { const input = page.getBindInput(event.target); if (!input) return; page.scheduleHistory(); if (input.tagName === "SELECT") { page.dispatchFormInput(input); } }, handleFormClick: (event) => { const input = page.getBindInput(event.target); if (!input) return; const action = page.ui.formActionMap.get(page.getFormActionKey(input.dataset.shapeType, input.name)); if (action) { event.preventDefault(); action(input.dataset.shapeType); } }, bindFormEvents: () => { if (page.ui.formEventsBound) return; const targets = getFormEventTargets(); if (!targets.length) return; toggleFormEventTargets(targets, "addEventListener"); page.ui.formEventTargets = targets; page.ui.formEventsBound = true; }, bindPanelAction: (input, shapeType, actionMap) => { input.dataset.shapeType = shapeType; const action = actionMap && actionMap[input.name]; if (action) { page.registerFormAction(shapeType, input.name, action); } }, setInputParam: (input, shapeType) => { const value = page.getInputValue(input); if (input.type === "number" && value === null) return; if (shapeType === "MapRect") { page.panel.setPanelValue(input.name, value); } else { page.panel.setActiveShapeValue(input.name, value, shapeType); } }, setSettingParam: (input) => { const value = page.getInputValue(input); if (input.type === "number" && value === null) return; if (input.name === "visual.theme") { page.panel.setTheme(value, { force: true }); return; } page.panel.setPanelValue(input.name, value); if (input.name === "visual.render.cull") { const isOff = value === false || value === "off" || value === "false"; if (isOff) { page.panel.setPanelValue("visual.render.cullThreshold", 0); page.panel.setPanelValue("visual.render.cullPadding", 0); const thresholdField = page.getSettingsField("visual.render.cullThreshold"); const paddingField = page.getSettingsField("visual.render.cullPadding"); if (thresholdField) page.setInputValue(thresholdField, 0); if (paddingField) page.setInputValue(paddingField, 0); } } const isVisual = input.name && input.name.startsWith("visual."); const isRender = input.name && input.name.startsWith("visual.render."); if (isVisual && !isRender) { page.panel.applyVisualSettings(); page.updateSettingsState(); return; } if (isVisual || input.name && input.name.startsWith("history.")) { page.updateSettingsState(); } }, setInputForm: (input, shape, key) => { const value = page.panel.getDeepValue(shape, key); page.setInputValue(input, value); page.updateCountBadge(input, value); }, setForm: (titleText, tabId, shape) => { page.setFormActive(titleText, tabId); const inputs = page.getFormInputs(tabId); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; page.setInputForm(input, shape, input.name); } }, setFormByType: (shapeType, shape) => { const config = shapeFormMap.get(shapeType); if (!config) return; page.setForm(config.title, config.tabId, shape); }, setSettingsForm: (panel) => { if (!panel) return; const inputs = page.getFormInputs("tabSettings"); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; page.setInputForm(input, panel, input.name); } page.updateSettingsState(); }, setMapRectForm: (panel) => { page.setForm(":地图 [ 矩形 ]", "tabMapRect", panel); }, bindFormActionsBySchema: (input, shapeType, schemaName) => { const actionMap = page.getFormSchemaActions(schemaName); page.bindPanelAction(input, shapeType, actionMap); }, bindPanelInputsBySchema: (tabId, shapeType, schemaName) => { page.bindPanelInputs(tabId, (input) => page.bindFormActionsBySchema(input, shapeType, schemaName), shapeType); }, getFormSchemaActions: (schemaName) => { const cache = page.ui.formActionSchemaMap ?? (page.ui.formActionSchemaMap = /* @__PURE__ */ new Map()); if (cache.has(schemaName)) return cache.get(schemaName); const schema = formActionSchemas[schemaName]; const actionMap = {}; if (schema && typeof schema === "object") { for (const name in schema) { if (!Object.prototype.hasOwnProperty.call(schema, name)) continue; const handler = page[schema[name]]; if (typeof handler === "function") actionMap[name] = handler; } } cache.set(schemaName, actionMap); return actionMap; }, setSettingsEvent: (input) => { input.dataset.setting = "true"; }, bindPanelInputs: (tabId, handler, shapeType) => { const inputs = page.getFormInputs(tabId); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; if (shapeType) input.dataset.shapeType = shapeType; handler(input, shapeType); } }, setMapRectPanel: () => { page.bindPanelInputsBySchema("tabMapRect", "MapRect", "setMapEvent"); }, setSettingsPanel: () => { page.bindPanelInputs("tabSettings", page.setSettingsEvent); }, getSettingsField: (name) => { return document.querySelector(`#tabSettings [name="${name}"]`); }, updateSettingsState: () => { const guides = page.panel.visual?.guides ?? {}; const setDisabled = (name, disabled) => { const field = page.getSettingsField(name); if (field) field.disabled = Boolean(disabled); }; setDisabled("visual.guides.groupBounds", !guides.enable); setDisabled("visual.guides.align", !guides.enable); const snapDisabled = !guides.snapEnable; setDisabled("visual.guides.snapGrid", snapDisabled); setDisabled("visual.guides.snapMapBounds", snapDisabled); setDisabled("visual.guides.snapDistribution", snapDisabled); setDisabled("visual.guides.snapTolerance", snapDisabled); const render = page.panel.visual?.render ?? {}; const mode = typeof render.mode === "string" ? render.mode.toLowerCase() : ""; const isAuto = mode === "auto"; setDisabled("visual.render.boost", !isAuto); const cullMode = render.cull; const cullDisabled = cullMode === false || cullMode === "off" || cullMode === "false"; setDisabled("visual.render.cullThreshold", cullDisabled); setDisabled("visual.render.cullPadding", cullDisabled); const dynamicDisabled = render.dynamic === false; setDisabled("visual.render.segmented", dynamicDisabled); setDisabled("visual.render.segmentMax", dynamicDisabled || render.segmented === false); setDisabled("visual.render.dynamicKinds", dynamicDisabled); setDisabled("visual.render.dynamicTypes", dynamicDisabled); const historyEnabled = page.panel.history?.enable !== false; setDisabled("history.max", !historyEnabled); setDisabled("history.fastPatch", !historyEnabled); }, clearSearch: () => { const input = page.ui.els.searchInput; input.value = ""; input.focus(); const state = page.ui.searchState; state.keyword = ""; state.matches = []; state.index = 0; }, setSearch: () => { const input = page.ui.els.searchInput; const search = input.value.trim(); if (!search) { page.notify("请输入编码名称", "warning"); input.focus(); return; } const keyword = search.toLowerCase(); const matches = []; const shapes = page.panel.allShapes; for (const shape of shapes) { if (!page.panel.isPlane(shape)) continue; const code = String(shape.code && shape.code.text ? shape.code.text : "").toLowerCase(); const name = String(shape.name && shape.name.text ? shape.name.text : "").toLowerCase(); if (code.includes(keyword) || name.includes(keyword)) { matches.push(shape); } } if (matches.length === 0) { page.notify("未搜索到对象", "warning"); const state2 = page.ui.searchState; state2.keyword = keyword; state2.matches = []; state2.index = 0; return; } const state = page.ui.searchState; let nextIndex = 0; if (state.keyword === keyword && Array.isArray(state.matches) && state.matches.length) { nextIndex = (state.index + 1) % matches.length; } state.keyword = keyword; state.matches = matches; state.index = nextIndex; const focusShape = matches[nextIndex]; page.panel.setActiveShapes(matches); page.panel.setView(focusShape); page.notify(`找到 ${matches.length} 个对象,定位第${nextIndex + 1} 个`, "info", { duration: 2200 }); }, setOpenDatabase: () => { try { const data = localStorage.getItem(localStorageKey); if (!data) { page.notify("本地存储中未找到地图数据。", "warning"); return false; } const parsed = JSON.parse(data); if (!parsed || typeof parsed !== "object") throw new Error("本地存储数据无效"); page.panel.setData(parsed); page.panel.setScalingAdapt(); page.updateStatusBar(); page.notify("已从本地存储加载地图数据。", "success"); return true; } catch { page.notify("从本地存储加载地图数据失败。", "error"); return false; } }, setSaveDatabase: () => { try { const data = JSON.stringify(page.panel.getData({ clone: false })); localStorage.setItem(localStorageKey, data); page.notify("已将地图数据保存到本地存储。", "success"); return true; } catch { page.notify("保存地图数据到本地存储失败。", "error"); return false; } }, initDialogs: () => { if (page.dialogs) return; page.dialogs = { actions: page.createListDialogState("dialogActions", page.renderActionsList), actionForm: page.createDialogState("dialogActionForm", { item: null, onSave: noop, onClose: noop, parameters: [] }), actionParameters: page.createListDialogState("dialogActionParameters", page.renderActionParametersList), actionParameterForm: page.createDialogState("dialogActionParameterForm", { item: null, onSave: noop, onClose: noop, isEdit: false }), extends: page.createListDialogState("dialogExtends", page.renderExtendsList), extendForm: page.createDialogState("dialogExtendForm", { item: null, onSave: noop, onClose: noop, isEdit: false }), controlPoints: page.createListDialogState("dialogControlPoints", page.renderControlPointsList), controlPointForm: page.createDialogState("dialogControlPointForm", { item: null, onSave: noop, onClose: noop }), nodeIds: page.createListDialogState("dialogNodeIds", page.renderNodeIdsList) }; const dialogs = document.querySelectorAll("dialog"); for (let i = 0; i < dialogs.length; i++) { const dialog = dialogs[i]; page.bindDialogListener(dialog, "cancel", (event) => { event.preventDefault(); const cancelButton = dialog.querySelector('[name="cancel"]'); if (cancelButton) { cancelButton.click(); return; } dialog.close(); }); page.bindDialogListener(dialog, "close", () => page.restoreFocus()); } const actionsState = page.dialogs.actions; const actionsDialog = actionsState.dialog; if (actionsDialog) { page.bindListDialog(actionsState, { add: () => { page.openListFormDialog( actionsDialog, page.openActionForm, "add", { actionId: page.createId(), actionParameters: [] }, (item) => { actionsState.list.push(item); }, actionsState.render ); }, edit: () => { page.editDialogListItem( actionsDialog, actionsState, page.openActionForm, "edit", (updated) => { actionsState.list[actionsState.selected] = updated; }, actionsState.render ); }, delete: () => { page.deleteSelectedDialogItem(actionsState); } }); } const actionFormDialog = page.dialogs.actionForm.dialog; if (actionFormDialog) { page.initActionFormSources(); const actionFormState = page.dialogs.actionForm; const form = page.getDialogForm(actionFormState); page.bindDialogListener(page.getField(actionFormDialog, "ok"), "click", () => { if (!form.reportValidity()) return; const state = actionFormState; const item = state.item || {}; item.actionType = page.getFieldText(form, "actionType", true); item.actionDescription = page.getFieldText(form, "actionDescription"); item.blockingType = page.getFieldText(form, "blockingType", true); item.sortNumber = page.getFieldNumber(form, "sortNumber", 0); item.actionParameters = page.cloneData(state.parameters); if (!item.actionId) item.actionId = page.createId(); state.onSave(item); actionFormDialog.close(); state.onClose(true); }); page.bindDialogListener(page.getField(actionFormDialog, "cancel"), "click", () => { const state = actionFormState; actionFormDialog.close(); state.onClose(false); }); page.bindDialogListener(page.getField(actionFormDialog, "actionParameters"), "click", () => { const state = actionFormState; page.captureActionForm(); actionFormDialog.close(); page.openActionParametersDialog(state.parameters, (updated) => { state.parameters = updated; }, () => { state.item.actionParameters = state.parameters; page.openActionForm(state.mode, state.item, state.onSave, state.onClose); }); }); } const actionParamsDialog = page.dialogs.actionParameters.dialog; if (actionParamsDialog) { page.bindKeyValueListDialog(page.dialogs.actionParameters, page.openActionParameterForm); } const actionParamFormDialog = page.dialogs.actionParameterForm.dialog; if (actionParamFormDialog) { page.initActionParamKeySources(); page.bindKeyValueForm("actionParameterForm"); } const extendsDialog = page.dialogs.extends.dialog; if (extendsDialog) { page.bindKeyValueListDialog(page.dialogs.extends, page.openExtendForm); } const extendFormDialog = page.dialogs.extendForm.dialog; if (extendFormDialog) { page.bindKeyValueForm("extendForm"); } const controlPointsState = page.dialogs.controlPoints; const controlPointsDialog = controlPointsState.dialog; if (controlPointsDialog) { page.bindListDialog(controlPointsState, { edit: () => { page.editDialogListItem( controlPointsDialog, controlPointsState, page.openControlPointForm, null, (updated) => { controlPointsState.list[controlPointsState.selected] = updated; }, controlPointsState.render ); } }); } const controlPointFormDialog = page.dialogs.controlPointForm.dialog; if (controlPointFormDialog) { const controlPointFormState = page.dialogs.controlPointForm; const form = page.getDialogForm(controlPointFormState); page.bindDialogListener(page.getField(controlPointFormDialog, "ok"), "click", () => { if (!form.reportValidity()) return; const state = controlPointFormState; const item = state.item || {}; item.weight = page.getFieldNumber(form, "weight", 0); state.onSave(item); controlPointFormDialog.close(); state.onClose(true); }); page.bindDialogListener(page.getField(controlPointFormDialog, "cancel"), "click", () => { const state = controlPointFormState; controlPointFormDialog.close(); state.onClose(false); }); } const nodeIdsDialog = page.dialogs.nodeIds.dialog; if (nodeIdsDialog) { page.bindDialogButton(nodeIdsDialog, '[name="ok"]', () => nodeIdsDialog.close()); page.bindDialogButton(nodeIdsDialog, '[name="cancel"]', () => nodeIdsDialog.close()); } }, openActionsDialog: (list, onSave) => { page.openListDialog(page.dialogs.actions, list, onSave); }, renderActionsList: () => { const state = page.dialogs.actions; page.renderDialogTable(state, (row, item) => { row.append( page.createCell(item.actionType || ""), page.createCell(item.blockingType || ""), page.createCell(page.formatNumber(item.sortNumber)) ); }); }, openActionForm: (mode, item, onSave, onClose) => { const state = page.dialogs.actionForm; state.mode = mode; state.item = page.cloneData(item) || {}; state.onSave = onSave || noop; state.onClose = onClose || noop; state.parameters = page.ensureList(state.item.actionParameters); const form = page.getDialogForm(state); page.setFieldText(form, "actionType", state.item.actionType); page.setFieldText(form, "actionDescription", state.item.actionDescription); page.setFieldText(form, "blockingType", state.item.blockingType); page.setFieldNumber(form, "sortNumber", Number(state.item.sortNumber), 0); page.setFieldText(form, "actionId", state.item.actionId); page.updateActionParameterCount(); page.openDialog(state.dialog); }, updateActionParameterCount: () => { const state = page.dialogs.actionForm; const count = page.ensureList(state.parameters).length; const countEl = page.getDialogPart(state, "actionParametersCount", "#actionParametersCount"); if (countEl) countEl.textContent = `${count} 项`; }, captureActionForm: () => { const state = page.dialogs.actionForm; if (!state.item) return; const form = page.getDialogForm(state); state.item.actionType = page.getFieldText(form, "actionType", true); state.item.actionDescription = page.getFieldText(form, "actionDescription"); state.item.blockingType = page.getFieldText(form, "blockingType", true); state.item.sortNumber = page.getFieldNumber(form, "sortNumber", 0); state.item.actionId = page.getFieldText(form, "actionId"); }, openActionParametersDialog: (list, onSave, onClose) => { page.openListDialog(page.dialogs.actionParameters, list, onSave, onClose); }, renderActionParametersList: () => { page.renderKeyValueDialogList(page.dialogs.actionParameters); }, openKeyValueForm: (stateKey, mode, item, onSave, onClose) => { const state = page.dialogs[stateKey]; if (!state || !state.dialog) return; state.isEdit = mode === "edit"; state.item = page.cloneData(item) || {}; state.onSave = onSave || noop; state.onClose = onClose || noop; const form = page.getDialogForm(state); if (!form) return; const keyInput = form.querySelector('[name="key"]'); const valueInput = form.querySelector('[name="value"]'); keyInput.value = state.item.key || ""; keyInput.disabled = state.isEdit; valueInput.value = state.item.value || ""; page.openDialog(state.dialog); }, bindKeyValueForm: (stateKey) => { const state = page.dialogs[stateKey]; if (!state || !state.dialog) return; const dialog = state.dialog; const form = page.getDialogForm(state); if (!form) return; page.bindDialogListener(page.getField(dialog, "ok"), "click", () => { if (!form.reportValidity()) return; const item = state.item || {}; item.key = form.querySelector('[name="key"]').value.trim(); item.value = form.querySelector('[name="value"]').value; state.onSave(item); dialog.close(); state.onClose(true); }); page.bindDialogListener(page.getField(dialog, "cancel"), "click", () => { dialog.close(); state.onClose(false); }); }, openActionParameterForm: (mode, item, onSave, onClose) => { page.openKeyValueForm("actionParameterForm", mode, item, onSave, onClose); }, openExtendsDialog: (list, onSave) => { page.openListDialog(page.dialogs.extends, list, onSave); }, renderExtendsList: () => { page.renderKeyValueDialogList(page.dialogs.extends); }, openExtendForm: (mode, item, onSave, onClose) => { page.openKeyValueForm("extendForm", mode, item, onSave, onClose); }, openControlPointsDialog: (list, onSave, onClose) => { page.openListDialog(page.dialogs.controlPoints, list, onSave, onClose); }, renderControlPointsList: () => { const state = page.dialogs.controlPoints; page.renderDialogTable(state, (row, item) => { row.append( page.createCell(page.formatNumber(item.x)), page.createCell(page.formatNumber(item.y)), page.createCell(page.formatNumber(item.weight)) ); }); }, openNodeIdsDialog: (list) => { page.openListDialog(page.dialogs.nodeIds, list); }, renderNodeIdsList: () => { const state = page.dialogs.nodeIds; if (!state || !state.dialog) return; page.renderDialogTable(state, (row, item) => { const index = Number(row.dataset.index) + 1; row.append( page.createCell(String(index)), page.createCell(item == null ? "" : String(item)) ); }); }, updateNodeIdsCount: (shapeType, list) => { const config = shapeFormMap.get(shapeType); if (!config) return; const input = document.querySelector(`#${config.tabId} [name="data.nodeIds"]`); if (!input) return; page.updateCountBadge(input, list); }, openControlPointForm: (item, onSave, onClose) => { const state = page.dialogs.controlPointForm; state.item = page.cloneData(item) || {}; state.onSave = onSave || noop; state.onClose = onClose || noop; const form = page.getDialogForm(state); for (let i = 0; i < controlPointNumberKeys.length; i++) { const key = controlPointNumberKeys[i]; page.setFieldNumber(form, key, Number(state.item[key]), 0); } page.openDialog(state.dialog); }, setNodeIdsList: (shapeType) => { const shape = page.panel.getActiveShapeLast(false); if (!shape) return; if (shapeType && shapeType.startsWith("Zone")) { page.panel.setZoneNodes(shape); } const nodeIds = page.cloneData(page.panel.getDeepValue(shape, "data.nodeIds")) || []; page.updateNodeIdsCount(shapeType, nodeIds); page.openNodeIdsDialog(nodeIds); }, setNodePosition: (shapeType) => { const shape = page.panel.getActiveShapeLast(false); if (!shape) return; const data = page.cloneData(page.panel.getDeepValue(shape, "data.nodePosition")) || {}; const dialog = document.getElementById("dialogNodePosition"); if (!dialog) return; page.bindModalOkCancel(dialog, () => { for (let i = 0; i < nodePositionNumberKeys.length; i++) { const key = nodePositionNumberKeys[i]; data[key] = page.getFieldNumber(dialog, key, 0); } data.mapId = page.getFieldText(dialog, "mapId"); data.mapDescription = page.getFieldText(dialog, "mapDescription"); page.panel.setActiveShapeValue("data.nodePosition", data, shapeType); page.scheduleHistory(); dialog.close(); }); for (let i = 0; i < nodePositionNumberKeys.length; i++) { const key = nodePositionNumberKeys[i]; page.setFieldNumber(dialog, key, data[key], 0); } page.setFieldText(dialog, "mapId", data.mapId); page.setFieldText(dialog, "mapDescription", data.mapDescription); page.openDialog(dialog); }, setTrajectory: (shapeType) => { const shape = page.panel.getActiveShapeLast(false); if (!shape) return; const data = page.cloneData(page.panel.getDeepValue(shape, "data.trajectory")) || {}; data.controlPoints = page.ensureList(data.controlPoints); const dialog = document.getElementById("dialogTrajectory"); if (!dialog) return; page.bindModalOkCancel(dialog, () => { data.degree = page.getFieldNumber(dialog, "degree", 0); const knotInput = page.getFieldText(dialog, "knotVector"); const knotParts = knotInput.split(/[\s,]+/); const knots = []; for (let i = 0; i < knotParts.length; i++) { const parsed = Number(knotParts[i]); if (Number.isFinite(parsed)) knots.push(parsed); } data.knotVector = knots.length ? knots : [0]; page.panel.setActiveShapeValue("data.trajectory", data, shapeType); page.scheduleHistory(); dialog.close(); }); const controlPointsButton = dialog.querySelector('[name="controlPoints"]'); if (controlPointsButton) { controlPointsButton.onclick = () => { dialog.close(); page.openControlPointsDialog(data.controlPoints, (updated) => { data.controlPoints = updated; }, () => { page.setFieldNumber(dialog, "degree", data.degree, 0); page.setFieldText(dialog, "knotVector", Array.isArray(data.knotVector) ? data.knotVector.join(",") : data.knotVector); page.openDialog(dialog); }); }; } page.setFieldNumber(dialog, "degree", data.degree, 0); page.setFieldText(dialog, "knotVector", Array.isArray(data.knotVector) ? data.knotVector.join(",") : data.knotVector); page.openDialog(dialog); }, setActions: (shapeType) => { const shape = page.panel.getActiveShapeLast(false); if (!shape) return; const actions = page.cloneData(page.panel.getDeepValue(shape, "data.actions")) || []; page.openActionsDialog(actions, (updated) => { page.panel.setActiveShapeValue("data.actions", updated, shapeType); page.scheduleHistory(); }); }, setExtends: (shapeType) => { if (shapeType === "MapRect") { const extendsList2 = page.cloneData(page.panel.map.extends) || []; page.openExtendsDialog(extendsList2, (updated) => { page.panel.setPanelValue("map.extends", updated); page.scheduleHistory(); }); return; } const shape = page.panel.getActiveShapeLast(false); if (!shape) return; const extendsList = page.cloneData(page.panel.getDeepValue(shape, "extends")) || []; page.openExtendsDialog(extendsList, (updated) => { page.panel.setActiveShapeValue("extends", updated, shapeType); page.scheduleHistory(); }); }, applyImageSource: (shapeType, src, options = {}) => { const isMap = shapeType === "MapRect"; if (isMap) { if (options.clear) { page.panel.setDeleteImage({ force: true }); } else { page.panel.withBatch(() => { page.panel.setPanelValue("map.image.enable", true); if (options.dataUrl) page.panel.setPanelValue("map.image._dataUrl", options.dataUrl); page.panel.setPanelValue("map.image._sharedObjectUrl", false); page.panel.setPanelValue("map.image.src", src); }); } } else if (options.clear) { page.panel.setActiveShapeAssign({ image: { src: "", image: new Image(), _src: "", _resolvedSrc: "", _dataUrl: "", _objectUrl: "", _sharedObjectUrl: false } }, shapeType); } else { const image = { enable: true, src, _sharedObjectUrl: false }; if (options.dataUrl) image._dataUrl = options.dataUrl; page.panel.setActiveShapeAssign({ image }, shapeType); } const tabId = isMap ? "tabMapRect" : shapeFormMap.get(shapeType)?.tabId; if (tabId) { const srcName = isMap ? "map.image.src" : "image.src"; const srcInput = document.querySelector(`#${tabId} [name="${srcName}"]`); if (srcInput) page.setInputValue(srcInput, src || ""); if (!options.clear) { const enableName = isMap ? "map.image.enable" : "image.enable"; const enableInput = document.querySelector(`#${tabId} [name="${enableName}"]`); if (enableInput) page.setInputValue(enableInput, true); } } page.scheduleHistory(); }, setImagePick: (shapeType) => { panelUtil.update(["jpg", "jpeg", "png", "webp", "gif", "svg"]).then((file) => { const objectUrl = URL.createObjectURL(file); return panelUtil.readImage(file).then((dataUrl) => { page.applyImageSource(shapeType, objectUrl, { dataUrl }); }).catch(() => { panelUtil.revokeObjectUrl(objectUrl); }); }).catch(() => { }); }, setImageClear: (shapeType) => { page.applyImageSource(shapeType, "", { clear: true }); } }); panelNamespace.ui = page; page.start(); if (typeof page.ui.pageReady === "function") { page.ui.pageReady(page); } })(); })();