diff --git a/InteractiveHtmlBom/web/ibom.css b/InteractiveHtmlBom/web/ibom.css index a601c3f..823f52f 100644 --- a/InteractiveHtmlBom/web/ibom.css +++ b/InteractiveHtmlBom/web/ibom.css @@ -885,4 +885,16 @@ a { ::-moz-focus-inner { padding: 0; +} + +.copy-link-button { + font-size: 12px; + border: none; + background: transparent; + cursor: pointer; + vertical-align: middle; +} + +.copy-link-button:hover { + opacity: 0.7; } \ No newline at end of file diff --git a/InteractiveHtmlBom/web/ibom.js b/InteractiveHtmlBom/web/ibom.js index d628d98..4e5156b 100644 --- a/InteractiveHtmlBom/web/ibom.js +++ b/InteractiveHtmlBom/web/ibom.js @@ -671,6 +671,25 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { netname = bomentry; td = document.createElement("TD"); td.innerHTML = highlightFilter(netname ? netname : "<no net>"); + + // Add copy button for net names in netlist mode + if (settings.bommode === "netlist") { + var copyButton = document.createElement("button"); + copyButton.className = "copy-link-button"; + copyButton.title = "Copy deep-link URL"; + copyButton.innerHTML = "🔗"; + copyButton.style.cssText = "margin-left: 5px; font-size: 10pt; border: none; background: transparent; cursor: pointer; height: 100%"; + (function(currentNetname) { + copyButton.onclick = function(e) { + e.stopPropagation(); + var url = new URL(window.location.href); + url.searchParams.set("net", "\"" + currentNetname + "\""); + copyToClipboard(url.toString()); + }; + })(netname); + td.appendChild(copyButton); + } + tr.appendChild(td); var color = settings.netColors[netname] || defaultNetColor; td = document.createElement("TD"); @@ -721,7 +740,31 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { } } else if (column === "References") { td = document.createElement("TD"); - td.innerHTML = highlightFilter(references.map(r => r[0]).join(", ")); + var refHtml = highlightFilter(references.map(r => r[0]).join(", ")); + td.innerHTML = refHtml; + + // Add copy button for component references in ungrouped mode + if (settings.bommode === "ungrouped") { + var copyButton = document.createElement("button"); + copyButton.className = "copy-link-button"; + copyButton.title = "Copy deep-link URL"; + copyButton.innerHTML = "🔗"; + copyButton.style.cssText = "margin-left: 5px; font-size: 10pt; border: none; background: transparent; cursor: pointer; height: 100%"; + var refName = references[0][0]; + (function(currentRefname) { + copyButton.onclick = function(e) { + e.stopPropagation(); + // Get the first reference to create URL (since we have multiple refs, we'll use the first one) + var url = new URL(window.location.href); + url.searchParams.set("ref", "\"" + currentRefname + "\""); + copyToClipboard(url.toString()); + + }; + })(refName); + td.appendChild(copyButton); + } + + tr.appendChild(td); } else if (column === "Quantity" && settings.bommode == "grouped") { // Quantity @@ -783,6 +826,26 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { }); } +function copyToClipboard(text) { + var textArea = document.createElement("textarea"); + textArea.className = "clipboard-temp"; + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + try { + var successful = document.execCommand('copy'); + if (!successful) { + // Fallback for browsers that don't support execCommand + navigator.clipboard.writeText(text).catch(function(err) { + console.error('Could not copy text: ', err); + }); + } + } catch (err) { + console.error('Could not copy text: ', err); + } + document.body.removeChild(textArea); +} + function highlightPreviousRow() { if (!currentHighlightedRowId) { highlightHandlers[highlightHandlers.length - 1].handler(); @@ -1194,6 +1257,47 @@ function updateCheckboxStats(checkbox) { td.lastChild.innerHTML = checked + "/" + total + " (" + Math.round(percent) + "%)"; } +function selectComponentByReference(ref) { + // Find the footprint with matching reference + var footprintIndex = -1; + for (var i = 0; i < pcbdata.footprints.length; i++) { + if (pcbdata.footprints[i].ref === ref) { + footprintIndex = i; + break; + } + } + + // If we found the component, select it + if (footprintIndex !== -1) { + // Use the existing footprintIndexToHandler to trigger selection + if (footprintIndex in footprintIndexToHandler) { + footprintIndexToHandler[footprintIndex](); + // Scroll to the selected row to center it on screen + if (currentHighlightedRowId) { + smoothScrollToRow(currentHighlightedRowId); + } + } else { + // If no handler exists, try to find the row manually + for (var i = 0; i < highlightHandlers.length; i++) { + var handlerInfo = highlightHandlers[i]; + if (handlerInfo.handler && handlerInfo.handler.refs) { + // Check if any of the references in this row match our target ref + for (var j = 0; j < handlerInfo.handler.refs.length; j++) { + if (handlerInfo.handler.refs[j][0] === ref) { + handlerInfo.handler(); + if (currentHighlightedRowId) { + smoothScrollToRow(currentHighlightedRowId); + } + break; + } + } + } + } + } + } + // If component not found, do nothing (preserve existing behavior) +} + function constrain(number, min, max) { return Math.min(Math.max(parseInt(number), min), max); } @@ -1320,6 +1424,29 @@ window.onload = function (e) { // Triggers render changeBomLayout(settings.bomlayout); + // Parse URL parameters for deep-linking + var urlParams = new URLSearchParams(window.location.search); + var refParam = urlParams.get('ref') || urlParams.get('component'); + if (refParam) { + // Change layout to ungrouped + changeBomMode('ungrouped'); + // Extract the value from the "" or the '' string + refParam = refParam.replace(/^["']|["']$/g, ""); + // Try to find and select the component + selectComponentByReference(refParam); + } + + // Handle net parameter for deep-linking to nets + var netParam = urlParams.get('net'); + if (netParam && "nets" in pcbdata) { + // Change layout to netlist + changeBomMode('netlist'); + // Extract the value from the "" or the '' string + netParam = netParam.replace(/^["']|["']$/g, ""); + // Try to find and select the net + netClicked(netParam); + } + // Users may leave fullscreen without touching the checkbox. Uncheck. document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement)