Nets validator added.
This commit is contained in:
parent
d156954156
commit
df5607bfa8
3 changed files with 353 additions and 21 deletions
|
|
@ -381,7 +381,9 @@ class Inspector {
|
||||||
if (hit) {
|
if (hit) {
|
||||||
const res = await requestInput("Edit Node", "Node Name", hit.label, {
|
const res = await requestInput("Edit Node", "Node Name", hit.label, {
|
||||||
extraBtn: { label: 'Delete', value: '__DELETE__', class: 'danger' },
|
extraBtn: { label: 'Delete', value: '__DELETE__', class: 'danger' },
|
||||||
helpHtml: PIN_HELP_HTML
|
helpHtml: PIN_HELP_HTML,
|
||||||
|
validate: validateNetName,
|
||||||
|
validateArgs: this.activeNet ? [this.activeNet.id] : null
|
||||||
});
|
});
|
||||||
if (res === '__DELETE__') {
|
if (res === '__DELETE__') {
|
||||||
const idx = this.activeNet.nodes.indexOf(hit.origNode);
|
const idx = this.activeNet.nodes.indexOf(hit.origNode);
|
||||||
|
|
@ -914,12 +916,14 @@ class Inspector {
|
||||||
const nextIdx = this.activeNet ? this.activeNet.nodes.length + 1 : 1;
|
const nextIdx = this.activeNet ? this.activeNet.nodes.length + 1 : 1;
|
||||||
let defaultLabel = `P${nextIdx}`;
|
let defaultLabel = `P${nextIdx}`;
|
||||||
|
|
||||||
// NEW: Async Smart Suggestion
|
// Async Smart Suggestion
|
||||||
const smartLabel = await this.getSuggestedLabel(imgId, x, y);
|
const smartLabel = await this.getSuggestedLabel(imgId, x, y);
|
||||||
if (smartLabel) defaultLabel = smartLabel;
|
if (smartLabel) defaultLabel = smartLabel;
|
||||||
|
|
||||||
const label = await requestInput("Add Node", "Pad/Pin Name", defaultLabel, {
|
const label = await requestInput("Add Node", "Pad/Pin Name", defaultLabel, {
|
||||||
helpHtml: (typeof PIN_HELP_HTML !== 'undefined') ? PIN_HELP_HTML : null
|
helpHtml: (typeof PIN_HELP_HTML !== 'undefined') ? PIN_HELP_HTML : null,
|
||||||
|
validate: validateNetName,
|
||||||
|
validateArgs: this.activeNet ? [this.activeNet.id] : null
|
||||||
});
|
});
|
||||||
|
|
||||||
if(label) {
|
if(label) {
|
||||||
|
|
|
||||||
221
docs/nets.js
221
docs/nets.js
|
|
@ -1,15 +1,155 @@
|
||||||
/* nets.js - Netlist Management (v2) */
|
/* nets.js - Netlist Management (v3) */
|
||||||
|
|
||||||
class NetManager {
|
class NetManager {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.showOnlyProblematic = false; // Toggle state
|
||||||
}
|
}
|
||||||
|
|
||||||
async render() {
|
// Updates the global toolbar button state
|
||||||
|
updateTopBar(problemCount) {
|
||||||
|
const bar = document.querySelector('#view-nets .actions-bar');
|
||||||
|
if (!bar) return;
|
||||||
|
let fixBtn = document.getElementById('net-fix-btn');
|
||||||
|
if (!fixBtn) {
|
||||||
|
fixBtn = document.createElement('button');
|
||||||
|
fixBtn.id = 'net-fix-btn';
|
||||||
|
fixBtn.onclick = () => this.toggleFixMode();
|
||||||
|
bar.appendChild(fixBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problemCount === 0) {
|
||||||
|
fixBtn.className = 'secondary';
|
||||||
|
fixBtn.innerHTML = '✅ All Valid';
|
||||||
|
fixBtn.disabled = true;
|
||||||
|
fixBtn.style.opacity = '0.7';
|
||||||
|
this.showOnlyProblematic = false; // Auto-reset if everything is fixed
|
||||||
|
} else {
|
||||||
|
fixBtn.disabled = false;
|
||||||
|
fixBtn.style.opacity = '1';
|
||||||
|
if (this.showOnlyProblematic) {
|
||||||
|
fixBtn.className = 'primary'; // Active/Pressed state
|
||||||
|
fixBtn.innerHTML = `Showing ${problemCount} Issues (Click to Reset)`;
|
||||||
|
} else {
|
||||||
|
fixBtn.className = 'danger'; // Needs attention state
|
||||||
|
fixBtn.innerHTML = `⚠️ ${problemCount} Issues Found`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles the filter and checks for empty nets
|
||||||
|
async toggleFixMode() {
|
||||||
|
if (this.showOnlyProblematic) {
|
||||||
|
this.showOnlyProblematic = false;
|
||||||
|
this.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turning ON: Check for empty nets first
|
||||||
|
const allNets = await this.db.getNets();
|
||||||
|
const nets = allNets.filter(n => n.projectId === currentBomId);
|
||||||
|
const emptyNets = nets.filter(n => !n.nodes || n.nodes.length === 0);
|
||||||
|
|
||||||
|
if (emptyNets.length > 0) {
|
||||||
|
if (await confirmAction(`Found ${emptyNets.length} empty net(s).\n\nDelete them to clean up?`, "Delete Empty Nets")) {
|
||||||
|
for (const n of emptyNets) {
|
||||||
|
await this.db.deleteNet(n.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showOnlyProblematic = true;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict Resolution Logic
|
||||||
|
async resolveNet(netId) {
|
||||||
|
const allNets = await this.db.getNets();
|
||||||
|
const projectNets = allNets.filter(n => n.projectId === currentBomId);
|
||||||
|
const net = projectNets.find(n => n.id === netId);
|
||||||
|
if (!net) return;
|
||||||
|
|
||||||
|
// 1. Handle Empty Nets
|
||||||
|
if (!net.nodes || net.nodes.length === 0) {
|
||||||
|
if (await confirmAction(`This net is completely empty.\n\nDelete it?`, "Delete")) {
|
||||||
|
await this.db.deleteNet(net.id);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild mapping locally to find exact duplicates
|
||||||
|
const nodeMap = {};
|
||||||
|
projectNets.forEach(n => {
|
||||||
|
if(!n.nodes) return;
|
||||||
|
n.nodes.forEach(node => {
|
||||||
|
if (!nodeMap[node.label]) nodeMap[node.label] = [];
|
||||||
|
nodeMap[node.label].push(n);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Check for formatting errors first
|
||||||
|
const formatErrIdx = net.nodes.findIndex(n => !/^[A-Za-z0-9_-]+\.[1-9][0-9]*$/.test(n.label));
|
||||||
|
if (formatErrIdx > -1) {
|
||||||
|
const node = net.nodes[formatErrIdx];
|
||||||
|
alert(`Node "${node.label}" has an invalid format.\n\nPlease rename it using the "Ref.Pin" format (e.g., R1.1).`);
|
||||||
|
return this.editNode(net.id, formatErrIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check for duplicates and build explanation matrix
|
||||||
|
const conflicts =[];
|
||||||
|
const otherNetsMap = new Map(); // Maps id -> net object
|
||||||
|
|
||||||
|
net.nodes.forEach(node => {
|
||||||
|
if (nodeMap[node.label] && nodeMap[node.label].length > 1) {
|
||||||
|
const others = nodeMap[node.label].filter(n => n.id !== net.id);
|
||||||
|
others.forEach(o => {
|
||||||
|
conflicts.push(`• ${node.label} is also in ${o.name}`);
|
||||||
|
otherNetsMap.set(o.id, o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
const otherNets = Array.from(otherNetsMap.values());
|
||||||
|
// Pick the first conflicting net as the target for merging
|
||||||
|
const targetNet = otherNets[0];
|
||||||
|
|
||||||
|
let msg = `Conflict: Nodes in this net exist elsewhere:\n${conflicts.join('\n')}\n\n`;
|
||||||
|
msg += `Would you like to merge all nodes from ${net.name} into ${targetNet.name} and delete ${net.name}?\n\n`;
|
||||||
|
msg += `(Click "Cancel" to close this dialog and rename/fix the nodes manually)`;
|
||||||
|
|
||||||
|
// confirmAction already sets focus to the Cancel button by default
|
||||||
|
const doMerge = await confirmAction(msg, `Merge into ${targetNet.name}`);
|
||||||
|
|
||||||
|
if (doMerge) {
|
||||||
|
// SAFETY CHECK: Warn if merging a larger net into a smaller one
|
||||||
|
if (net.nodes.length > targetNet.nodes.length) {
|
||||||
|
const warnMsg = `WARNING: You are merging a larger net (${net.nodes.length} nodes) into a smaller net (${targetNet.nodes.length} nodes).\n\nAre you sure you want to completely merge ${net.name} into ${targetNet.name}?`;
|
||||||
|
const sure = await confirmAction(warnMsg, "Yes, Merge Anyway");
|
||||||
|
if (!sure) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform Merge (add all nodes, skipping exact label duplicates)
|
||||||
|
const targetLabels = new Set(targetNet.nodes.map(n => n.label));
|
||||||
|
for (const n of net.nodes) {
|
||||||
|
if (!targetLabels.has(n.label)) {
|
||||||
|
targetNet.nodes.push(n);
|
||||||
|
targetLabels.add(n.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.db.addNet(targetNet);
|
||||||
|
await this.db.deleteNet(net.id);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
const tbody = document.getElementById('nets-body');
|
const tbody = document.getElementById('nets-body');
|
||||||
if(!tbody) return;
|
if(!tbody) return;
|
||||||
|
|
||||||
// Safety check: If no board is loaded, clear the table
|
// Safety check
|
||||||
if (typeof currentBomId === 'undefined' || !currentBomId) {
|
if (typeof currentBomId === 'undefined' || !currentBomId) {
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
|
|
@ -18,10 +158,43 @@ async render() {
|
||||||
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center; color:#888;">Loading...</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center; color:#888;">Loading...</td></tr>';
|
||||||
|
|
||||||
const allNets = await this.db.getNets();
|
const allNets = await this.db.getNets();
|
||||||
|
|
||||||
// --- FIX: Filter by Current Board ID ---
|
|
||||||
const nets = allNets.filter(n => n.projectId === currentBomId);
|
const nets = allNets.filter(n => n.projectId === currentBomId);
|
||||||
|
|
||||||
|
// --- NEW: Pre-calculate errors ---
|
||||||
|
const nodeMap = {}; // label -> array of netIds
|
||||||
|
let totalProblems = 0;
|
||||||
|
|
||||||
|
nets.forEach(net => {
|
||||||
|
if (!net.nodes || net.nodes.length === 0) {
|
||||||
|
net._hasError = true;
|
||||||
|
net._isEmpty = true;
|
||||||
|
} else {
|
||||||
|
net.nodes.forEach(node => {
|
||||||
|
if (!nodeMap[node.label]) nodeMap[node.label] =[];
|
||||||
|
nodeMap[node.label].push(net.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nets.forEach(net => {
|
||||||
|
let netHasError = net._isEmpty || false;
|
||||||
|
if (net.nodes) {
|
||||||
|
net.nodes.forEach(node => {
|
||||||
|
node._isFormatInvalid = !/^[A-Za-z0-9_-]+\.[1-9][0-9]*$/.test(node.label);
|
||||||
|
node._isDuplicate = nodeMap[node.label] && nodeMap[node.label].length > 1;
|
||||||
|
if (node._isFormatInvalid || node._isDuplicate) {
|
||||||
|
node._hasError = true;
|
||||||
|
netHasError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
net._hasError = netHasError;
|
||||||
|
if (netHasError) totalProblems++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update toolbar toggle
|
||||||
|
this.updateTopBar(totalProblems);
|
||||||
|
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if(nets.length === 0) {
|
if(nets.length === 0) {
|
||||||
|
|
@ -29,28 +202,45 @@ async render() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nets.forEach(net => {
|
let renderedCount = 0;
|
||||||
const tr = document.createElement('tr');
|
|
||||||
|
|
||||||
|
nets.forEach(net => {
|
||||||
|
// Filter check
|
||||||
|
if (this.showOnlyProblematic && !net._hasError) return;
|
||||||
|
|
||||||
|
renderedCount++;
|
||||||
|
const tr = document.createElement('tr');
|
||||||
tr.style.height = 'auto';
|
tr.style.height = 'auto';
|
||||||
tr.style.minHeight = '2.2rem';
|
tr.style.minHeight = '2.2rem';
|
||||||
|
|
||||||
// Target Icon for Editing
|
|
||||||
const targetIcon = `<span style="cursor:pointer; margin-right:0.5rem;" onclick="netManager.editNet('${net.id}')" title="Edit Net on Board">🎯</span>`;
|
const targetIcon = `<span style="cursor:pointer; margin-right:0.5rem;" onclick="netManager.editNet('${net.id}')" title="Edit Net on Board">🎯</span>`;
|
||||||
|
|
||||||
|
// NEW: Net Level Fix Button
|
||||||
|
const fixBtn = net._hasError ? `<button class="danger sm-btn" style="padding:0 6px; margin-right:6px; font-size:0.75rem;" onclick="netManager.resolveNet('${net.id}')" title="Resolve Issues">⚠️ Fix</button>` : '';
|
||||||
|
|
||||||
let nodesHtml = '';
|
let nodesHtml = '';
|
||||||
net.nodes.forEach((n, idx) => {
|
net.nodes.forEach((n, idx) => {
|
||||||
nodesHtml += `<span class="net-chip" onclick="netManager.editNode('${net.id}', ${idx})" title="Edit Node">${n.label}</span>`;
|
let style = "class='net-chip'";
|
||||||
|
let warn = "";
|
||||||
|
// NEW: Node Level Warning Styles
|
||||||
|
if (n._hasError) {
|
||||||
|
style = "class='net-chip' style='border-color:#ef4444; background:#fef2f2; color:#b91c1c;'";
|
||||||
|
warn = n._isDuplicate ? " ⚠️(Dup)" : " ❌(Fmt)";
|
||||||
|
}
|
||||||
|
nodesHtml += `<span ${style} onclick="netManager.editNode('${net.id}', ${idx})" title="Edit Node">${n.label}${warn}</span>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td style="display:flex; align-items:center; vertical-align:top; height:auto;">
|
<td style="display:flex; align-items:center; vertical-align:top; height:auto; padding-top:6px">
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:4px; padding:4px 0;">
|
||||||
${targetIcon}
|
${targetIcon}
|
||||||
<input type="text" value="${net.name}" onchange="netManager.rename('${net.id}', this.value)" style="border:none; background:transparent; font-weight:bold; flex:1; min-width:0;">
|
<input type="text" value="${net.name}" onchange="netManager.rename('${net.id}', this.value)" style="border:none; background:transparent; font-weight:bold; flex:1; min-width:0;">
|
||||||
|
${fixBtn}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="white-space:normal; height:auto; overflow:visible;">
|
<td style="white-space:normal; height:auto; overflow:visible;">
|
||||||
<div style="display:flex; flex-wrap:wrap; gap:4px; padding:2px 0;">${nodesHtml}</div>
|
<div style="display:flex; flex-wrap:wrap; gap:4px; padding:4px 0;">${nodesHtml}</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="text-align:right; vertical-align:top; height:auto;">
|
<td style="text-align:right; vertical-align:top; height:auto;">
|
||||||
|
|
@ -59,6 +249,11 @@ async render() {
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Empty state if filter hides everything
|
||||||
|
if (renderedCount === 0 && this.showOnlyProblematic) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center; padding:2rem; color:#10b981;">No issues found! 🎉</td></tr>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit Net in Inspector
|
// Edit Net in Inspector
|
||||||
|
|
@ -80,7 +275,9 @@ async render() {
|
||||||
|
|
||||||
const res = await requestInput("Edit Node", "Node Name", node.label, {
|
const res = await requestInput("Edit Node", "Node Name", node.label, {
|
||||||
extraBtn: { label: 'Delete', value: '__DELETE__', class: 'danger' },
|
extraBtn: { label: 'Delete', value: '__DELETE__', class: 'danger' },
|
||||||
helpHtml: PIN_HELP_HTML
|
helpHtml: PIN_HELP_HTML,
|
||||||
|
validate: validateNetName,
|
||||||
|
validateArgs: [netId]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res === '__DELETE__') {
|
if (res === '__DELETE__') {
|
||||||
|
|
|
||||||
145
docs/studio.js
145
docs/studio.js
|
|
@ -1397,23 +1397,54 @@ function confirmAction(message, btnText = "Confirm") {
|
||||||
const msgEl = document.getElementById('confirm-msg');
|
const msgEl = document.getElementById('confirm-msg');
|
||||||
const okBtn = document.getElementById('confirm-btn-ok');
|
const okBtn = document.getElementById('confirm-btn-ok');
|
||||||
const cancelBtn = document.getElementById('confirm-btn-cancel');
|
const cancelBtn = document.getElementById('confirm-btn-cancel');
|
||||||
|
const closeBtn = modal.querySelector('.close-btn');
|
||||||
|
const modalContext = 'confirmation-modal';
|
||||||
|
|
||||||
msgEl.innerText = message;
|
msgEl.innerText = message;
|
||||||
okBtn.innerText = btnText;
|
okBtn.innerText = btnText;
|
||||||
|
|
||||||
// Cleanup old handlers by reassigning
|
let resultToResolve = false;
|
||||||
okBtn.onclick = () => {
|
|
||||||
|
// 1. Cleanup & Resolve
|
||||||
|
const close = () => {
|
||||||
|
window.removeEventListener('popstate', onPopState);
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
resolve(true);
|
resolve(resultToResolve);
|
||||||
};
|
};
|
||||||
|
|
||||||
cancelBtn.onclick = () => {
|
// 2. Handle History Changes (Back Button / Escape)
|
||||||
modal.style.display = 'none';
|
const onPopState = () => {
|
||||||
resolve(false);
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 3. Handle UI Actions (OK / Cancel)
|
||||||
|
const commit = (res) => {
|
||||||
|
resultToResolve = res;
|
||||||
|
if (history.state && history.state.context === modalContext) {
|
||||||
|
history.back(); // This triggers onPopState -> close()
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Setup DOM (Clone to cleanly remove old listeners)
|
||||||
|
const newOk = okBtn.cloneNode(true);
|
||||||
|
const newCancel = cancelBtn.cloneNode(true);
|
||||||
|
okBtn.replaceWith(newOk);
|
||||||
|
cancelBtn.replaceWith(newCancel);
|
||||||
|
|
||||||
|
newOk.onclick = (e) => { e.stopPropagation(); commit(true); };
|
||||||
|
newCancel.onclick = (e) => { e.stopPropagation(); commit(false); };
|
||||||
|
closeBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); commit(false); };
|
||||||
|
|
||||||
|
// 5. Open & Push State
|
||||||
|
if (!history.state || history.state.context !== modalContext) {
|
||||||
|
history.pushState({ context: modalContext }, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('popstate', onPopState);
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
cancelBtn.focus(); // Default focus on Cancel
|
newCancel.focus(); // Default focus on Cancel
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2454,6 +2485,21 @@ async function autoStitchNewImage(newId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW HELPER: Check Node Collision ---
|
||||||
|
async function checkNodeCollision(nodeLabel, excludeNetId = null) {
|
||||||
|
if (!nodeLabel || typeof currentBomId === 'undefined') return[];
|
||||||
|
const allNets = await db.getNets();
|
||||||
|
const projectNets = allNets.filter(n => n.projectId === currentBomId);
|
||||||
|
const collisions =[];
|
||||||
|
for (const net of projectNets) {
|
||||||
|
if (net.id === excludeNetId) continue;
|
||||||
|
if (net.nodes && net.nodes.some(n => n.label === nodeLabel)) {
|
||||||
|
collisions.push(net.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collisions;
|
||||||
|
}
|
||||||
|
|
||||||
const ImageGraph = {
|
const ImageGraph = {
|
||||||
// Helper: Invert 3x3 Matrix
|
// Helper: Invert 3x3 Matrix
|
||||||
invertH(m) {
|
invertH(m) {
|
||||||
|
|
@ -2721,6 +2767,7 @@ function requestInput(title, label, val, opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let resultToResolve = null;
|
let resultToResolve = null;
|
||||||
|
let isInputValid = true; // Updated by opts.validate; always true when no validator is set
|
||||||
|
|
||||||
// 1. Cleanup & Resolve
|
// 1. Cleanup & Resolve
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
|
@ -2738,6 +2785,18 @@ function requestInput(title, label, val, opts = {}) {
|
||||||
|
|
||||||
// 3. Handle UI Actions (OK / Cancel)
|
// 3. Handle UI Actions (OK / Cancel)
|
||||||
const commit = (v) => {
|
const commit = (v) => {
|
||||||
|
// Block submission when a validator is attached and the current value is invalid.
|
||||||
|
// Cancel (null) and extra-button sentinel values (e.g. '__DELETE__') always pass through.
|
||||||
|
if (v !== null && opts.validate && !isInputValid && v !== '__DELETE__') {
|
||||||
|
inp.style.borderColor = '#ef4444';
|
||||||
|
inp.style.backgroundColor = '#fef2f2';
|
||||||
|
setTimeout(() => {
|
||||||
|
inp.style.borderColor = '#cbd5e1';
|
||||||
|
inp.style.backgroundColor = '';
|
||||||
|
}, 400);
|
||||||
|
return; // Block submission
|
||||||
|
}
|
||||||
|
|
||||||
resultToResolve = v;
|
resultToResolve = v;
|
||||||
|
|
||||||
// SAFETY CHECK: Only go back if we are still in the modal state.
|
// SAFETY CHECK: Only go back if we are still in the modal state.
|
||||||
|
|
@ -2782,6 +2841,33 @@ function requestInput(title, label, val, opts = {}) {
|
||||||
// Let NavManager handle Escape -> history.back()
|
// Let NavManager handle Escape -> history.back()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generic live-validation hook.
|
||||||
|
// opts.validate: async (rawValue: string, ...args) => { sanitized?: string, isValid: boolean, feedbackHtml: string }
|
||||||
|
// The function owns all domain-specific logic (sanitization, format checks, collision lookups,
|
||||||
|
// etc.). requestInput only wires up the plumbing and gates OK on isValid.
|
||||||
|
if (opts.validate) {
|
||||||
|
const validateArgs = opts.validateArgs ? opts.validateArgs : [];
|
||||||
|
inp.addEventListener('input', async () => {
|
||||||
|
const result = await opts.validate(inp.value, ...validateArgs);
|
||||||
|
|
||||||
|
// Apply sanitized value if the validator changed it
|
||||||
|
if (result.sanitized !== undefined && inp.value !== result.sanitized) {
|
||||||
|
const pos = inp.selectionStart;
|
||||||
|
inp.value = result.sanitized;
|
||||||
|
inp.setSelectionRange(pos, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInputValid = result.isValid;
|
||||||
|
|
||||||
|
// Always show the help area while the user is typing so feedback is visible
|
||||||
|
helpToggle.style.display = 'block';
|
||||||
|
helpContent.style.display = 'block';
|
||||||
|
helpContent.innerHTML = result.feedbackHtml + (opts.helpHtml || '');
|
||||||
|
});
|
||||||
|
// Trigger once on open so the field is validated immediately
|
||||||
|
setTimeout(() => inp.dispatchEvent(new Event('input')), 50);
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Open & Push State
|
// 5. Open & Push State
|
||||||
// Use direct history.pushState to ensure it happens immediately and locally
|
// Use direct history.pushState to ensure it happens immediately and locally
|
||||||
// (Bypassing any potential NavManager checks/delays)
|
// (Bypassing any potential NavManager checks/delays)
|
||||||
|
|
@ -2796,6 +2882,51 @@ function requestInput(title, label, val, opts = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for net node names (for requestInput).
|
||||||
|
*/
|
||||||
|
async function validateNetName(raw, netId) {
|
||||||
|
// 1. Sanitize: strip spaces, enforce single dot with digits-only suffix
|
||||||
|
let v = raw.replace(/\s+/g, '');
|
||||||
|
const parts = v.split('.');
|
||||||
|
if (parts.length > 2) {
|
||||||
|
v = parts[0] + '.' + parts.slice(1).join('').replace(/[^0-9]/g, '');
|
||||||
|
} else if (parts.length === 2) {
|
||||||
|
v = parts[0] + '.' + parts[1].replace(/[^0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v) {
|
||||||
|
return { sanitized: v, isValid: false, feedbackHtml: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Format check
|
||||||
|
const validFormat = /^[A-Za-z0-9_-]+\.[1-9][0-9]*$/.test(v);
|
||||||
|
if (!validFormat) {
|
||||||
|
return {
|
||||||
|
sanitized: v,
|
||||||
|
isValid: false,
|
||||||
|
feedbackHtml: '<div style="color:#ef4444; font-weight:bold; margin-bottom:4px;">❌ Format must be Ref.Pin (e.g. R1.1)</div>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Collision check
|
||||||
|
const collisions = await checkNodeCollision(v, netId);
|
||||||
|
if (collisions.length > 0) {
|
||||||
|
return {
|
||||||
|
sanitized: v,
|
||||||
|
isValid: true, // Warn but still allow — user may be intentionally reassigning
|
||||||
|
feedbackHtml: `<div style="color:#d97706; font-weight:bold; margin-bottom:4px;">⚠️ Node already used in net(s): ${collisions.join(', ')}</div>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sanitized: v,
|
||||||
|
isValid: true,
|
||||||
|
feedbackHtml: '<div style="color:#10b981; font-weight:bold; margin-bottom:4px;">✅ Valid format</div>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- URL IMPORT LOGIC --- */
|
/* --- URL IMPORT LOGIC --- */
|
||||||
|
|
||||||
async function importDeviceFromURL() {
|
async function importDeviceFromURL() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue