Files
PTL/1.0.0/ref/Map/dist/js/panel.ui.js
T
2026-07-14 21:29:27 +08:00

2653 lines
110 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
// 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);
}
})();
})();