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)