Major upgrate to Inspector/Netlist workflow: the tool now intelligently suggests node names if components defined, also added collapsible cheat sheet for pins numbering
This commit is contained in:
parent
46d538e55f
commit
8266a8e81f
4 changed files with 378 additions and 8 deletions
|
|
@ -13,6 +13,7 @@ class Inspector {
|
|||
this.activeNet = null;
|
||||
this.masterId = null;
|
||||
this.netNodeCache = {}; // Cache for calculated node positions
|
||||
this.bomCache = {}; // Cache for projected BOM coordinates
|
||||
|
||||
// Cache for image dimensions to avoid async bitmap creation on every render
|
||||
this.resolutionCache = {};
|
||||
|
|
@ -47,6 +48,8 @@ class Inspector {
|
|||
}
|
||||
|
||||
async _performInit() {
|
||||
this.bomCache = {}; // Clear cache on re-init
|
||||
this.backImagesCache = null; // Clear back-side cache
|
||||
this.sidebarList.innerHTML = '';
|
||||
|
||||
const newNetBtn = document.querySelector('button[onclick="inspector.startNewNet()"]');
|
||||
|
|
@ -377,7 +380,8 @@ class Inspector {
|
|||
|
||||
if (hit) {
|
||||
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
|
||||
});
|
||||
if (res === '__DELETE__') {
|
||||
const idx = this.activeNet.nodes.indexOf(hit.origNode);
|
||||
|
|
@ -419,19 +423,26 @@ class Inspector {
|
|||
async updateNetNodeCache() {
|
||||
this.netNodeCache = {};
|
||||
if (!this.activeNet || !this.activeNet.nodes) return;
|
||||
|
||||
// Initialize arrays for currently visible layers
|
||||
for (const vid of this.visibleIds) this.netNodeCache[vid] = [];
|
||||
|
||||
for (const node of this.activeNet.nodes) {
|
||||
if (this.visibleIds.has(node.imgId)) {
|
||||
// 1. Direct Nodes (Source)
|
||||
// Safety Check: Ensure the cache array exists before pushing
|
||||
if (this.netNodeCache[node.imgId]) {
|
||||
this.netNodeCache[node.imgId].push({
|
||||
x: node.x, y: node.y, label: node.label,
|
||||
color: '#2563eb', isSource: true, origNode: node
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Inferred Nodes (Projected)
|
||||
const paths = await ImageGraph.solvePaths(node.imgId, this.cv, this.db);
|
||||
for (const p of paths) {
|
||||
if (this.visibleIds.has(p.id)) {
|
||||
// CHANGE: Check this.netNodeCache[p.id] instead of this.visibleIds.has(p.id)
|
||||
// This prevents the crash if netNodeCache was reset by a concurrent call
|
||||
if (this.netNodeCache[p.id]) {
|
||||
const proj = this.cv.projectPoint(node.x, node.y, p.H);
|
||||
if (proj) {
|
||||
this.netNodeCache[p.id].push({
|
||||
|
|
@ -578,6 +589,322 @@ class Inspector {
|
|||
}
|
||||
}
|
||||
|
||||
// --- SMART NAMING LOGIC ---
|
||||
|
||||
async calculateGlobalRotation() {
|
||||
// Default to 0 if data missing
|
||||
if (typeof currentBomId === 'undefined' || typeof bomData === 'undefined') return 0;
|
||||
|
||||
const allNets = await this.db.getNets();
|
||||
const projectNets = allNets.filter(n => n.projectId === currentBomId);
|
||||
|
||||
let totalAngle = 0;
|
||||
let count = 0;
|
||||
|
||||
// We only trust 2-pin passive components for orientation
|
||||
// (Resistors, Caps, Inductors, Diodes).
|
||||
// ICs are complex, Transistors have triangles.
|
||||
const SAFE_PREFIXES = ['R', 'C', 'L', 'D', 'VD'];
|
||||
|
||||
projectNets.forEach(net => {
|
||||
net.nodes.forEach(node => {
|
||||
// Find the component this node belongs to
|
||||
// Node label format "R1.1" -> Ref "R1"
|
||||
const parts = node.label.split('.');
|
||||
if (parts.length !== 2) return;
|
||||
|
||||
const ref = parts[0];
|
||||
|
||||
// Check prefix
|
||||
const prefix = ref.match(/^[A-Z]+/);
|
||||
if (!prefix || !SAFE_PREFIXES.includes(prefix[0])) return;
|
||||
|
||||
const comp = bomData.find(c => c.label === ref);
|
||||
|
||||
// Critical: We must use coordinates from the SAME image to calculate angle
|
||||
if (comp && comp.imgId === node.imgId && comp.x !== undefined) {
|
||||
const dx = node.x - comp.x;
|
||||
const dy = node.y - comp.y;
|
||||
|
||||
// Calculate raw angle in degrees
|
||||
let deg = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
|
||||
// Normalize to deviation from nearest 90-degree axis (-45 to +45)
|
||||
// Examples:
|
||||
// 5 deg -> 5
|
||||
// 85 deg -> -5 (relative to 90)
|
||||
// 175 deg -> -5 (relative to 180)
|
||||
while (deg <= -45) deg += 90;
|
||||
while (deg > 45) deg -= 90;
|
||||
|
||||
// Filter outliers (e.g. diagonal placement)
|
||||
// User requested up to 15 degrees, we allow 20 for safety
|
||||
if (Math.abs(deg) < 20) {
|
||||
totalAngle += deg;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (count === 0) return 0;
|
||||
|
||||
// Return average rotation in Radians
|
||||
const avgDeg = totalAngle / count;
|
||||
return avgDeg * (Math.PI / 180);
|
||||
}
|
||||
|
||||
async detectBackImages() {
|
||||
if (this.backImagesCache) return this.backImagesCache;
|
||||
if (typeof currentBomId === 'undefined' || typeof bomImages === 'undefined') return new Set();
|
||||
|
||||
const overlaps = await this.db._tx('overlappedImages', 'readonly', s => s.getAll());
|
||||
|
||||
// 1. Build Adjacency Graph (Partitioning)
|
||||
const polarity = {}; // 1 vs -1
|
||||
const adj = {};
|
||||
|
||||
overlaps.forEach(ov => {
|
||||
if(!adj[ov.fromImageId]) adj[ov.fromImageId] = [];
|
||||
if(!adj[ov.toImageId]) adj[ov.toImageId] = [];
|
||||
|
||||
// Determinant < 0 implies reflection (Flip)
|
||||
const h = ov.homography;
|
||||
const det = (h[0] * h[4]) - (h[1] * h[3]);
|
||||
const isFlip = det < 0;
|
||||
|
||||
adj[ov.fromImageId].push({ target: ov.toImageId, isFlip });
|
||||
adj[ov.toImageId].push({ target: ov.fromImageId, isFlip });
|
||||
});
|
||||
|
||||
// BFS to propagate polarity
|
||||
const visited = new Set();
|
||||
const queue = [];
|
||||
|
||||
const startImg = bomImages[0];
|
||||
if (!startImg) return new Set();
|
||||
|
||||
polarity[startImg.id] = 1;
|
||||
queue.push(startImg.id);
|
||||
visited.add(startImg.id);
|
||||
|
||||
while(queue.length > 0) {
|
||||
const curr = queue.shift();
|
||||
const curPol = polarity[curr];
|
||||
|
||||
if (adj[curr]) {
|
||||
adj[curr].forEach(edge => {
|
||||
if (!visited.has(edge.target)) {
|
||||
visited.add(edge.target);
|
||||
polarity[edge.target] = edge.isFlip ? -curPol : curPol;
|
||||
queue.push(edge.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const groupA = new Set(Object.keys(polarity).filter(k => polarity[k] === 1));
|
||||
const groupB = new Set(Object.keys(polarity).filter(k => polarity[k] === -1));
|
||||
|
||||
// 2. Heuristic 2: Check existing Resistor Nets (Reliable)
|
||||
// Now checks both Pin 1 and Pin 2
|
||||
const rot = await this.calculateGlobalRotation();
|
||||
const cosR = Math.cos(-rot);
|
||||
const sinR = Math.sin(-rot);
|
||||
|
||||
let scoreA = 0; // Positive = A is Top, Negative = A is Back
|
||||
|
||||
const allNets = await this.db.getNets();
|
||||
const projectNets = allNets.filter(n => n.projectId === currentBomId);
|
||||
|
||||
for (const net of projectNets) {
|
||||
for (const node of net.nodes) {
|
||||
// Match R*.1 OR R*.2
|
||||
const match = node.label.match(/^(R\d+)\.([12])$/);
|
||||
if (!match) continue;
|
||||
|
||||
const ref = match[1];
|
||||
const pinSuffix = match[2]; // '1' or '2'
|
||||
|
||||
const comp = bomData.find(c => c.label === ref);
|
||||
|
||||
// Ensure node and component are on the same image
|
||||
if (comp && comp.imgId === node.imgId && comp.x !== undefined) {
|
||||
const dx = node.x - comp.x;
|
||||
const dy = node.y - comp.y;
|
||||
|
||||
// Rotate to align with horizontal axis
|
||||
const rDx = dx * cosR - dy * sinR;
|
||||
|
||||
// Rule for TOP side:
|
||||
// Pin 1 is Left (<0).
|
||||
// Pin 2 is Right (>0).
|
||||
const isTopBehavior = (pinSuffix === '1') ? (rDx < 0) : (rDx > 0);
|
||||
|
||||
if (groupA.has(node.imgId)) scoreA += (isTopBehavior ? 1 : -1);
|
||||
else if (groupB.has(node.imgId)) scoreA += (isTopBehavior ? -1 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scoreA !== 0) {
|
||||
this.backImagesCache = scoreA > 0 ? groupB : groupA;
|
||||
return this.backImagesCache;
|
||||
}
|
||||
|
||||
// 3. Heuristic 1: Count (Fallback)
|
||||
if (groupB.size === 0) this.backImagesCache = new Set();
|
||||
else if (groupA.size === 0) this.backImagesCache = groupA;
|
||||
else this.backImagesCache = (groupA.size <= groupB.size) ? groupA : groupB;
|
||||
|
||||
return this.backImagesCache;
|
||||
}
|
||||
|
||||
async getProjectedComponents(targetImgId) {
|
||||
if (this.bomCache[targetImgId]) return this.bomCache[targetImgId];
|
||||
|
||||
const projected = [];
|
||||
|
||||
// 1. Calculate paths FROM the target TO everything else
|
||||
// We want to answer: "Where is Image X relative to ME (Target)?"
|
||||
// This runs Dijkstra once (One-to-Many) instead of Many-to-One
|
||||
let pathMap = {};
|
||||
|
||||
if (typeof ImageGraph !== 'undefined') {
|
||||
// solvePaths returns H for: Target -> Remote
|
||||
const paths = await ImageGraph.solvePaths(targetImgId, this.cv, this.db);
|
||||
|
||||
paths.forEach(p => {
|
||||
// To render a Remote component on Target, we need: Remote -> Target
|
||||
// So we invert the matrix: inv(Target -> Remote)
|
||||
const invH = ImageGraph.invertH(p.H);
|
||||
if (invH) pathMap[p.id] = invH;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Iterate all components and project them
|
||||
if (typeof bomData !== 'undefined') {
|
||||
bomData.forEach(c => {
|
||||
if (!c.imgId) return;
|
||||
|
||||
// Case A: Component is on the current image (Direct)
|
||||
if (c.imgId === targetImgId) {
|
||||
if (c.x !== undefined && c.y !== undefined) {
|
||||
projected.push({ ...c, projX: c.x, projY: c.y });
|
||||
}
|
||||
}
|
||||
// Case B: Component is on a connected image (Inferred)
|
||||
else if (pathMap[c.imgId]) {
|
||||
if (c.x !== undefined && c.y !== undefined) {
|
||||
const H = pathMap[c.imgId];
|
||||
const pt = this.cv.projectPoint(c.x, c.y, H);
|
||||
|
||||
// Basic sanity bounds to prevent projecting into infinity
|
||||
// (can happen with near-singular matrices or extreme perspective)
|
||||
if (pt && Math.abs(pt.x) < 50000 && Math.abs(pt.y) < 50000) {
|
||||
projected.push({ ...c, projX: pt.x, projY: pt.y });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.bomCache[targetImgId] = projected;
|
||||
return projected;
|
||||
}
|
||||
|
||||
async checkGlobalLabelUsage(label) {
|
||||
// dependency: currentBomId is global from studio.js
|
||||
if (!label || typeof currentBomId === 'undefined') return false;
|
||||
|
||||
// 1. Fetch ALL nets (Async)
|
||||
// Optimization: In a huge app we'd index this, but filtering memory is fast enough for <10k nets
|
||||
const allNets = await this.db.getNets();
|
||||
|
||||
// 2. Filter for current board
|
||||
const projectNets = allNets.filter(n => n.projectId === currentBomId);
|
||||
|
||||
// 3. Scan for label collision
|
||||
for (const net of projectNets) {
|
||||
if (net.nodes && net.nodes.some(n => n.label === label)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getSuggestedLabel(imgId, x, y) {
|
||||
const HIT_RADIUS = 150;
|
||||
|
||||
// 1. Get Data
|
||||
const components = await this.getProjectedComponents(imgId);
|
||||
const rotation = await this.calculateGlobalRotation();
|
||||
const backImages = await this.detectBackImages();
|
||||
const isBack = backImages.has(imgId);
|
||||
|
||||
const cosR = Math.cos(-rotation);
|
||||
const sinR = Math.sin(-rotation);
|
||||
|
||||
let bestComp = null;
|
||||
let minScore = Infinity;
|
||||
|
||||
// 2. Weighted Scoring Loop
|
||||
components.forEach(c => {
|
||||
const dx = x - c.projX;
|
||||
const dy = y - c.projY;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
if (dist < HIT_RADIUS) {
|
||||
// Rotate vector
|
||||
const rDx = dx * cosR - dy * sinR;
|
||||
const rDy = dx * sinR + dy * cosR;
|
||||
|
||||
let angleDeg = Math.abs(Math.atan2(rDy, rDx) * (180 / Math.PI));
|
||||
angleDeg = angleDeg % 90;
|
||||
let deviation = Math.min(angleDeg, 90 - angleDeg);
|
||||
|
||||
const score = dist * (1 + (deviation * 0.1));
|
||||
|
||||
if (score < minScore) {
|
||||
minScore = score;
|
||||
bestComp = c;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!bestComp) return null;
|
||||
|
||||
// 3. Pin Logic
|
||||
const bDx = x - bestComp.projX;
|
||||
const bDy = y - bestComp.projY;
|
||||
|
||||
const rotDx = bDx * cosR - bDy * sinR;
|
||||
const rotDy = bDx * sinR + bDy * cosR;
|
||||
|
||||
// Logic Branch:
|
||||
// Top Side: Left/Top is 1 => (rotDx + rotDy) < 0
|
||||
// Back Side: Right/Top is 1 => (rotDx - rotDy) > 0
|
||||
let isPin1;
|
||||
if (isBack) {
|
||||
// Back: Favor Right (x>0) and Top (y<0).
|
||||
// x - y => pos - neg = pos.
|
||||
isPin1 = (rotDx - rotDy) > 0;
|
||||
} else {
|
||||
// Top: Favor Left (x<0) and Top (y<0).
|
||||
// x + y => neg + neg = neg.
|
||||
isPin1 = (rotDx + rotDy) < 0;
|
||||
}
|
||||
|
||||
const primaryPin = isPin1 ? '1' : '2';
|
||||
const secondaryPin = isPin1 ? '2' : '1';
|
||||
|
||||
const labelPrimary = `${bestComp.label}.${primaryPin}`;
|
||||
const labelSecondary = `${bestComp.label}.${secondaryPin}`;
|
||||
|
||||
const primaryTaken = await this.checkGlobalLabelUsage(labelPrimary);
|
||||
return primaryTaken ? labelSecondary : labelPrimary;
|
||||
}
|
||||
|
||||
startNewNet() {
|
||||
this.activeNet = { id: uuid(), name: "New Net", nodes: [], isNew: true };
|
||||
this.updateNetUI();
|
||||
|
|
@ -585,8 +912,16 @@ class Inspector {
|
|||
|
||||
async handleAddNode(imgId, x, y) {
|
||||
const nextIdx = this.activeNet ? this.activeNet.nodes.length + 1 : 1;
|
||||
const defaultLabel = `P${nextIdx}`;
|
||||
const label = await requestInput("Add Node", "Pad/Pin Name", defaultLabel);
|
||||
let defaultLabel = `P${nextIdx}`;
|
||||
|
||||
// NEW: Async Smart Suggestion
|
||||
const smartLabel = await this.getSuggestedLabel(imgId, x, y);
|
||||
if (smartLabel) defaultLabel = smartLabel;
|
||||
|
||||
const label = await requestInput("Add Node", "Pad/Pin Name", defaultLabel, {
|
||||
helpHtml: (typeof PIN_HELP_HTML !== 'undefined') ? PIN_HELP_HTML : null
|
||||
});
|
||||
|
||||
if(label) {
|
||||
if(!this.activeNet) this.startNewNet();
|
||||
this.activeNet.nodes.push({ id: uuid(), imgId: imgId, x: Math.round(x), y: Math.round(y), label: label });
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ async render() {
|
|||
const node = net.nodes[nodeIdx];
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
if (res === '__DELETE__') {
|
||||
|
|
|
|||
|
|
@ -1070,15 +1070,21 @@
|
|||
<!-- GENERIC INPUT MODAL -->
|
||||
<div style="display:none; z-index:1150;" id="generic-input-modal" class="modal-overlay">
|
||||
<div class="modal-content" style="height:auto; max-width:300px; background:#fff; border:1px solid #cbd5e1;">
|
||||
<div class="modal-header">
|
||||
<h3 style="margin:0" id="gim-title">Input</h3>
|
||||
<div class="modal-header" style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<h3 style="margin:0" id="gim-title">Input</h3>
|
||||
<button id="gim-help-toggle" class="secondary sm-btn" style="display:none; padding:0 6px; font-size:0.75rem; height:20px; line-height:1;">?</button>
|
||||
</div>
|
||||
<button class="close-btn" onclick="document.getElementById('generic-input-modal').style.display='none'">×</button>
|
||||
</div>
|
||||
<div style="padding:1.5rem; display:flex; flex-direction:column; gap:1rem;">
|
||||
<div>
|
||||
<label id="gim-label" style="font-size:0.8rem; font-weight:700; color:#64748b; text-transform:uppercase; margin-bottom:0.2rem; display:block;">Value</label>
|
||||
<input type="text" id="gim-input" style="width:100%; height:auto; padding:0.5rem; border:1px solid #cbd5e1; border-radius:0.3rem; font-size:1rem; box-sizing:border-box;">
|
||||
<!-- NEW HELP CONTAINER -->
|
||||
<div id="gim-help-content" style="display:none; margin-top:0.5rem; padding:0.5rem; background:#f0f9ff; border:1px solid #bae6fd; border-radius:4px; font-size:0.8rem; color:#0c4a6e;"></div>
|
||||
</div>
|
||||
<!-- (Footer remains the same) -->
|
||||
<div style="display:flex; justify-content:space-between; gap:10px;" id="gim-footer">
|
||||
<button id="gim-extra-btn" class="danger" style="display:none; padding:0.6rem 1rem; margin-right:auto;"></button>
|
||||
<button id="gim-cancel-btn" class="secondary" style="padding:0.6rem 1rem;">Cancel</button>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ const DB_NAME = 'PcbReTrace'; const DB_VER = 1;
|
|||
const ICONS = { RESISTOR: `<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="6" width="16" height="12" rx="3" fill="#bae6fd" stroke="#0ea5e9" stroke-width="1"/><rect x="7" y="6" width="2" height="12" fill="#ef4444"/><rect x="11" y="6" width="2" height="12" fill="#000000"/><rect x="15" y="6" width="2" height="12" fill="#ef4444"/><line x1="1" y1="12" x2="4" y2="12" stroke="#94a3b8" stroke-width="2"/><line x1="20" y1="12" x2="23" y2="12" stroke="#94a3b8" stroke-width="2"/></svg>`, INDUCTOR: `<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="5" width="16" height="14" rx="4" fill="#bbf7d0" stroke="#22c55e" stroke-width="1"/><rect x="7" y="5" width="2" height="14" fill="#cbd5e1"/><rect x="11" y="5" width="2" height="14" fill="#ef4444"/><rect x="15" y="5" width="2" height="14" fill="#ef4444"/><line x1="1" y1="12" x2="4" y2="12" stroke="#94a3b8" stroke-width="2"/><line x1="20" y1="12" x2="23" y2="12" stroke="#94a3b8" stroke-width="2"/></svg>`, COIL: `<svg viewBox="0 0 24 24" fill="none" stroke="#ea580c" stroke-width="2" stroke-linecap="round"><line x1="1" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="23" y2="12"/><path d="M5 12 C5 4 9 4 9 12"/><path d="M9 12 C9 19 10 19 10 12" stroke-opacity="0.5"/><path d="M10 12 C10 4 14 4 14 12"/><path d="M14 12 C14 19 15 19 15 12" stroke-opacity="0.5"/><path d="M15 12 C15 4 19 4 19 12"/></svg>` };
|
||||
const TOOL_REGISTRY = { 'R': [{url:'resistor.html',icon:ICONS.RESISTOR,title:'Resistor'}], 'L': [{url:'inductor.html',icon:ICONS.INDUCTOR,title:'Inductor'},{url:'coil.html',icon:ICONS.COIL,title:'Coil'}] };
|
||||
const MAX_DIRECT_TOOLS = 3;
|
||||
const PIN_HELP_HTML = `
|
||||
<strong style="display:block; margin-bottom:4px;">Pin # Conventions:</strong>
|
||||
<ul style="margin:0; padding-left:1.2rem; display:flex; flex-direction:column; gap:2px;">
|
||||
<li><b>Res/Cap/Ind:</b> Left/Top = 1</li>
|
||||
<li><b>Diode:</b> Stripe (K) = 1</li>
|
||||
<li><b>LED:</b> Cathode (K) = 1</li>
|
||||
<li><b>Tantalum:</b> Stripe (+) = 1 ⚠️</li>
|
||||
<li><b>Electrolytic:</b> Stripe (-) = 2 ⚠️</li>
|
||||
<li><b>IC:</b> Dot/Notch = 1 (CCW)</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
// Global State
|
||||
let db=null, deviceList=[], currentDeviceId=null, bomList=[], currentBomId=null, bomData=[], bomImages=[], currentImgId=null, sortMode='none', editingIndex=-1, mapState={scale:1,x:0,y:0,isDragging:false,startX:0,startY:0};
|
||||
|
|
@ -2396,6 +2407,8 @@ const NavManager = {
|
|||
function requestInput(title, label, val, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById('generic-input-modal');
|
||||
const helpToggle = document.getElementById('gim-help-toggle');
|
||||
const helpContent = document.getElementById('gim-help-content');
|
||||
const inp = document.getElementById('gim-input');
|
||||
const extraBtn = document.getElementById('gim-extra-btn');
|
||||
const modalContext = 'generic-input-modal';
|
||||
|
|
@ -2404,6 +2417,21 @@ function requestInput(title, label, val, opts = {}) {
|
|||
document.getElementById('gim-label').innerText = label;
|
||||
inp.value = val || '';
|
||||
|
||||
// --- Handle Help Text ---
|
||||
helpContent.style.display = 'none'; // Reset to hidden
|
||||
if (opts.helpHtml) {
|
||||
helpToggle.style.display = 'block';
|
||||
helpContent.innerHTML = opts.helpHtml;
|
||||
helpToggle.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const isHidden = helpContent.style.display === 'none';
|
||||
helpContent.style.display = isHidden ? 'block' : 'none';
|
||||
if(isHidden) inp.focus(); // Keep focus
|
||||
};
|
||||
} else {
|
||||
helpToggle.style.display = 'none';
|
||||
}
|
||||
|
||||
let resultToResolve = null;
|
||||
|
||||
// 1. Cleanup & Resolve
|
||||
|
|
|
|||
Loading…
Reference in a new issue