Files
avcf/proc-browser.js
T
adrien b8e8ef9113 overhaul: modular architecture, tests, doc generation, warning system, and UX improvements
- Modular block files: split into blocks-algo, blocks-data, blocks-proc
- New modules: sections.js (labelled canvas areas), proc-browser.js (procedure tree modal), lib-loader.js (lazy library loading), templates.js (project templates), errno-data.js (errno table)
- Test infrastructure: browser test suite (28 tests) and Node pure-function tests (75 tests)
- Warning system: unified .block-warn for disconnected inputs, unconnected error hooks, and zero-exit-in-error-path
- Error path highlighting: red flow paths and block borders for error-only subgraphs
- Input hooks: hidden by default, shown during linking; outputs always at bottom
- Block editor modal: pencil icon, colour picker for accent, compact field labels
- Doc generation: standalone node script extracts JSDoc -> docs/API.md (124 declarations)
- Persistence: save/restore sections, errno selection, block colours
- Fixes: _segmentsIntersect bug, WCEnd crash from closest(this), drawpath guard on detached elements
2026-06-22 18:31:23 +02:00

361 lines
11 KiB
JavaScript

// ── Procedure Browser Modal ────────────────────────────────────────────────
//
// A modal popup that displays a searchable, collapsible tree of all available
// procedures (user-defined and library-provided). Replaces the old dropdown
// <select> approach for selecting procedures in WCProcCall blocks.
//
// Depends on: dom.js ($, $id), helpers.js (switchLibTab, getLibraryIndex)
/** @type {((qname:string|null)=>void)|null} */
let _procBrowserCallback = null;
/**
* Builds the tree data structure from all wc-procedure blocks and the library index.
* @returns {Array<{label:string, items:Array<{qname:string, naturalName:string, techName:string, readonly:boolean}>, userDefined:boolean}>}
*/
function _buildTreeData() {
const procs = document.querySelectorAll("wc-procedure");
const libIndex = getLibraryIndex();
const libMap = {};
for (const entry of libIndex) {
libMap[entry.id] = entry;
}
const userDefined = [];
const libGroups = {};
for (const proc of procs) {
const qname = proc.getQualifiedName();
if (!qname) continue;
const naturalName = proc.querySelector(".proc-natural-name").value || "";
const techName = proc.querySelector(".proc-tech-name").textContent || "";
const isReadonly = !!proc.dataset.readonly;
const item = { qname, naturalName, techName, readonly: isReadonly };
if (!isReadonly) {
userDefined.push(item);
} else {
const libId = proc.getLibrary();
if (!libGroups[libId]) {
const entry = libMap[libId];
libGroups[libId] = {
label: entry ? entry.name : libId,
items: []
};
}
libGroups[libId].items.push(item);
}
}
const groups = [];
if (userDefined.length > 0) {
userDefined.sort((a, b) => a.naturalName.localeCompare(b.naturalName) || a.qname.localeCompare(b.qname));
groups.push({ label: "User-defined", items: userDefined, userDefined: true });
}
const libIds = Object.keys(libGroups).sort((a, b) => {
const ga = libMap[a] ? libMap[a].group : "";
const gb = libMap[b] ? libMap[b].group : "";
if (ga !== gb) return ga.localeCompare(gb);
return (libMap[a] ? libMap[a].name : a).localeCompare(libMap[b] ? libMap[b].name : b);
});
for (const libId of libIds) {
libGroups[libId].items.sort((a, b) => a.techName.localeCompare(b.techName) || a.qname.localeCompare(b.qname));
groups.push({
label: libGroups[libId].label,
items: libGroups[libId].items,
userDefined: false
});
}
return groups;
}
/**
* Creates a collapsible group DOM element in the procedure browser tree.
* @param {{label:string, items:Array<any>, userDefined:boolean}} group
* @param {string} [selectedQname] QName to pre-select
* @returns {HTMLElement}
*/
function _createGroupElement(group, selectedQname) {
const groupDiv = document.createElement("div");
groupDiv.className = "proc-browser-tree-group";
const header = document.createElement("div");
header.className = "proc-browser-tree-group-header";
if (group.userDefined) {
header.classList.add("user-defined");
}
const arrow = document.createElement("span");
arrow.className = "proc-browser-tree-arrow";
arrow.textContent = group.userDefined ? "\u25BC" : "\u25B6";
header.appendChild(arrow);
const name = document.createElement("span");
name.className = "proc-browser-tree-group-name";
name.textContent = group.label;
header.appendChild(name);
const count = document.createElement("span");
count.className = "proc-browser-tree-group-count";
count.textContent = group.items.length.toString();
header.appendChild(count);
groupDiv.appendChild(header);
const itemsContainer = document.createElement("div");
itemsContainer.className = "proc-browser-tree-group-items";
if (!group.userDefined) {
itemsContainer.style.display = "none";
}
for (const item of group.items) {
const leaf = document.createElement("div");
leaf.className = "proc-browser-tree-leaf";
leaf.dataset.qname = item.qname;
if (item.qname === selectedQname) {
leaf.classList.add("selected");
}
const nameSpan = document.createElement("span");
nameSpan.className = "proc-browser-tree-leaf-name";
nameSpan.textContent = item.naturalName || item.techName || item.qname;
leaf.appendChild(nameSpan);
const qnameSpan = document.createElement("span");
qnameSpan.className = "proc-browser-tree-leaf-qname";
qnameSpan.textContent = item.qname;
leaf.appendChild(qnameSpan);
leaf.addEventListener("click", function () {
const sel = this.closest(".proc-browser-tree").querySelector(".proc-browser-tree-leaf.selected");
if (sel) sel.classList.remove("selected");
this.classList.add("selected");
});
leaf.addEventListener("dblclick", function () {
_confirmSelection(this.dataset.qname);
});
itemsContainer.appendChild(leaf);
}
header.addEventListener("click", function () {
const isCollapsed = arrow.textContent === "\u25B6";
arrow.textContent = isCollapsed ? "\u25BC" : "\u25B6";
itemsContainer.style.display = isCollapsed ? "" : "none";
});
groupDiv.appendChild(itemsContainer);
return groupDiv;
}
/**
* Filters the procedure tree by a search query, showing/hiding groups and items.
* @param {string} query
* @param {HTMLElement} overlay The procedure browser overlay element
*/
function _applySearchFilter(query, overlay) {
query = query.toLowerCase().trim();
const treeEl = overlay.querySelector(".proc-browser-tree");
const emptyEl = overlay.querySelector(".proc-browser-empty");
if (!treeEl) return;
if (!query) {
for (const groupEl of treeEl.querySelectorAll(".proc-browser-tree-group")) {
const header = groupEl.querySelector(".proc-browser-tree-group-header");
const items = groupEl.querySelector(".proc-browser-tree-group-items");
const arrow = groupEl.querySelector(".proc-browser-tree-arrow");
const isUserDefined = header.classList.contains("user-defined");
items.style.display = isUserDefined ? "" : "none";
arrow.textContent = isUserDefined ? "\u25BC" : "\u25B6";
for (const leaf of items.querySelectorAll(".proc-browser-tree-leaf")) {
leaf.style.display = "";
}
groupEl.style.display = "";
}
emptyEl.style.display = "none";
emptyEl.innerHTML = "";
return;
}
let hasVisible = false;
for (const groupEl of treeEl.querySelectorAll(".proc-browser-tree-group")) {
const items = groupEl.querySelector(".proc-browser-tree-group-items");
const arrow = groupEl.querySelector(".proc-browser-tree-arrow");
let groupHasMatch = false;
for (const leaf of items.querySelectorAll(".proc-browser-tree-leaf")) {
const nameEl = leaf.querySelector(".proc-browser-tree-leaf-name");
const qnameEl = leaf.querySelector(".proc-browser-tree-leaf-qname");
const name = nameEl ? nameEl.textContent.toLowerCase() : "";
const qname = qnameEl ? qnameEl.textContent.toLowerCase() : "";
if (name.includes(query) || qname.includes(query)) {
leaf.style.display = "";
groupHasMatch = true;
} else {
leaf.style.display = "none";
}
}
if (groupHasMatch) {
groupEl.style.display = "";
items.style.display = "";
arrow.textContent = "\u25BC";
hasVisible = true;
} else {
groupEl.style.display = "none";
}
}
if (hasVisible) {
emptyEl.style.display = "none";
} else {
emptyEl.style.display = "block";
emptyEl.innerHTML = "No procedures match '<strong>" + _escapeHtml(query) + "</strong>'.";
}
}
/**
* Escapes HTML special characters for safe innerHTML insertion.
* @param {string} str
* @returns {string}
*/
function _escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
/**
* Confirms a procedure selection, closes the browser, and invokes the callback.
* @param {string} qname Qualified procedure name
*/
function _confirmSelection(qname) {
const overlay = $("#proc-browser-overlay");
if (overlay) overlay.classList.remove("active");
if (_procBrowserCallback) {
_procBrowserCallback(qname);
_procBrowserCallback = null;
}
}
/** Cancels procedure selection, closes the browser, invokes callback with null. */
function _cancelBrowser() {
const overlay = $("#proc-browser-overlay");
if (overlay) overlay.classList.remove("active");
if (_procBrowserCallback) {
_procBrowserCallback(null);
_procBrowserCallback = null;
}
}
/**
* Opens the procedure browser modal, builds the tree, and wires up search/cancel/select.
* @param {string} selectedQname
* @param {(qname:string|null)=>void} callback
*/
function showProcedureBrowser(selectedQname, callback) {
_procBrowserCallback = callback;
const overlay = $("#proc-browser-overlay");
if (!overlay) return;
const groups = _buildTreeData();
const treeEl = overlay.querySelector(".proc-browser-tree");
const emptyEl = overlay.querySelector(".proc-browser-empty");
const searchInput = overlay.querySelector(".proc-browser-search");
treeEl.innerHTML = "";
searchInput.value = "";
if (groups.length === 0) {
emptyEl.style.display = "block";
emptyEl.innerHTML =
"No procedures available. " +
'<a href="#" class="proc-browser-goto-procedures">Go to the Procedures tab</a> ' +
"to define or load libraries.";
} else {
emptyEl.style.display = "none";
for (const group of groups) {
treeEl.appendChild(_createGroupElement(group, selectedQname));
}
}
_applySearchFilter("", overlay);
overlay.classList.add("active");
setTimeout(function () { searchInput.focus(); }, 100);
}
// Self-initialising IIFE — wires up the overlay event listeners at load time
(function () {
const overlay = $("#proc-browser-overlay");
if (!overlay) return;
overlay.addEventListener("click", function (e) {
if (e.target === overlay) _cancelBrowser();
});
const cancelBtn = overlay.querySelector(".proc-browser-cancel");
if (cancelBtn) {
cancelBtn.addEventListener("click", _cancelBrowser);
}
const selectBtn = overlay.querySelector(".proc-browser-select");
if (selectBtn) {
selectBtn.addEventListener("click", function () {
let selected = overlay.querySelector(".proc-browser-tree-leaf.selected");
if (!selected) {
for (const leaf of overlay.querySelectorAll(".proc-browser-tree-leaf")) {
if (leaf.offsetParent !== null) {
selected = leaf;
break;
}
}
}
if (selected) {
_confirmSelection(selected.dataset.qname);
}
});
}
const searchInput = overlay.querySelector(".proc-browser-search");
if (searchInput) {
searchInput.addEventListener("input", function () {
_applySearchFilter(this.value, overlay);
});
searchInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
const selected = overlay.querySelector(".proc-browser-tree-leaf.selected");
if (selected) {
_confirmSelection(selected.dataset.qname);
}
}
if (e.key === "Escape") {
_cancelBrowser();
}
});
}
const emptyEl = overlay.querySelector(".proc-browser-empty");
if (emptyEl) {
emptyEl.addEventListener("click", function (e) {
const link = e.target.closest(".proc-browser-goto-procedures");
if (link) {
e.preventDefault();
_cancelBrowser();
switchTab("procedures");
}
});
}
})();