2653 lines
110 KiB
JavaScript
2653 lines
110 KiB
JavaScript
(() => {
|
||
// 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);
|
||
}
|
||
})();
|
||
})();
|