Files
PTL/1.0.0/ref/Map/dist/js/panel.ui.js
T

2653 lines
110 KiB
JavaScript
Raw Normal View History

2026-07-14 21:29:27 +08:00
(() => {
// 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);
}
})();
})();