diff --git a/InteractiveHtmlBom/web/ibom.css b/InteractiveHtmlBom/web/ibom.css index a601c3f..7092dac 100644 --- a/InteractiveHtmlBom/web/ibom.css +++ b/InteractiveHtmlBom/web/ibom.css @@ -19,6 +19,15 @@ --track-color-highlight: #D04040; --zone-color: #def5f1; --zone-color-highlight: #d0404080; + + --toast-bg: rgba(215,215,215,0.2); + --toast-border: #ffb629; + --toast-text: #222; + --toast-muted: #666; + --toast-header-border: rgba(0,0,0,0.12); + + --toast-primary: #42c642; + --toast-primary-hover: #30a130; } html, @@ -35,6 +44,11 @@ body { --pin1-outline-color-highlight: #ccff00; --track-color: #42524f; --zone-color: #42524f; + --toast-bg: rgba(215,215,215,0.20); + --toast-border: #ffb629; + --toast-text: #f3f4f6; + --toast-muted: #ffb629; + --toast-header-border: rgba(255,255,255,0.17); background-color: #252c30; color: #eee; } @@ -885,4 +899,180 @@ a { ::-moz-focus-inner { padding: 0; +} + +.copy-link-button { + font-size: 1em; + border: none; + height: 1.5em; + background: transparent; + background-color: transparent !important; + cursor: pointer; + vertical-align: middle; +} + +.copy-link-button:hover { + opacity: 0.3; +} + +.copy-status { + display: inline-block; + margin-left: 5px; + padding: 2px 6px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + + opacity: 0; + visibility: hidden; + transition: opacity 0.4s ease; +} + +.copy-status.visible { + opacity: 1; + visibility: visible; +} + +.reference-warning-toast { + position: fixed; + top: 16px; + right: 16px; + + width: 420px; + max-width: calc(100vw - 32px); + + border: 2px solid var(--toast-border); + border-left: 4px var(--toast-border); + + border-radius: 12px; + + box-shadow: + 0 10px 25px rgba(0, 0, 0, 0.12), + 0 2px 8px rgba(0, 0, 0, 0.06); + + z-index: 10000; + + overflow: hidden; + + animation: toast-slide-in 0.25s ease-out; + + backdrop-filter: blur(10px); + background: var(--toast-bg); +} + +.toast-header { + display: flex; + align-items: center; + gap: 10px; + + padding: 14px 16px; + + border-bottom: 2px solid var(--toast-header-border); +} + +.toast-icon { + font-size: 18px; +} + +.toast-title { + flex: 1; + font-weight: 600; + font-size: 15px; + color: var(--toast-border); +} + +.toast-close { + border: none; + background: transparent; + cursor: pointer; + + color: #f3f4f6; + font-size: 22px; + line-height: 1; + + padding: 0; +} + +.toast-close:hover { + color: #ffb629; +} + +.toast-body { + padding: 16px; + color: var(--toast-text); + line-height: 1.5; +} + +.toast-body p { + margin: 0; +} + +.toast-body p + p { + margin-top: 8px; +} + +.toast-note { + color: var(--toast-muted); + font-size: 14px; +} + +.toast-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + + padding: 0 16px 16px; +} + +.btn { + border: none; + border-radius: 8px; + + padding: 10px 16px; + + font-size: 14px; + font-weight: 500; + + cursor: pointer; + + width: auto; + height: auto; + + transition: + background-color 0.15s, + transform 0.1s; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + background: #42c642; + color: white; +} + +.btn-primary:hover { + background: #30A130; +} + +.btn-secondary { + background: #f3f4f6; + color: #111827; +} + +.btn-secondary:hover { + background: #e5e7eb; +} + +@keyframes toast-slide-in { + from { + opacity: 0; + transform: translateX(24px); + } + + to { + opacity: 1; + transform: translateX(0); + } } \ No newline at end of file diff --git a/InteractiveHtmlBom/web/ibom.js b/InteractiveHtmlBom/web/ibom.js index d628d98..79dc1f4 100644 --- a/InteractiveHtmlBom/web/ibom.js +++ b/InteractiveHtmlBom/web/ibom.js @@ -671,6 +671,13 @@ 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 = createCopyButton("net", netname); + td.appendChild(copyButton); + } + tr.appendChild(td); var color = settings.netColors[netname] || defaultNetColor; td = document.createElement("TD"); @@ -721,7 +728,16 @@ 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 = createCopyButton("ref", references[0][0]); + td.appendChild(copyButton); + } + + tr.appendChild(td); } else if (column === "Quantity" && settings.bommode == "grouped") { // Quantity @@ -783,6 +799,229 @@ function populateBomBody(placeholderColumn = null, placeHolderElements = null) { }); } +function createCopyButton(type, value) { + var copyButton = document.createElement("button"); + copyButton.className = "copy-link-button"; + copyButton.title = "Copy deep-link URL"; + copyButton.innerHTML = "🔗"; + + // status box for visual feedback + var statusBox = document.createElement("span"); + statusBox.className = "copy-status"; + statusBox.textContent = "Copied!"; + + // Cache footprint references for faster lookups (only build if needed) + var footprintRefCache = null; + + function buildUrl() { + var url = new URL(window.location.href); + + if (type === "net") { + url.searchParams.set("net", value); + } else if (type === "ref") { + // For reference links, also include the ID to make it more robust + url.searchParams.set("ref", value); + + // Try to find the corresponding ID for this reference + var id = null; + if (pcbdata && pcbdata.footprints) { + // Build cache if needed + if (!footprintRefCache) { + footprintRefCache = {}; + for (var i = 0; i < pcbdata.footprints.length; i++) { + footprintRefCache[pcbdata.footprints[i].ref] = i; + } + } + + // Use the cache to get ID quickly + if (footprintRefCache[value] !== undefined) { + id = footprintRefCache[value]; + } + } + if (id !== null) { + url.searchParams.set("id", id); + } + } + + return url.toString(); + } + + // Left-click -> copy + copyButton.addEventListener("click", function (e) { + e.stopPropagation(); + + var url = buildUrl(); + copyToClipboard(url); + + statusBox.classList.add("visible"); + + clearTimeout(statusBox.hideTimer); + + statusBox.hideTimer = setTimeout(function () { + statusBox.classList.remove("visible"); + }, 2000); + }); + + // Right-click -> open in new window + copyButton.addEventListener("mousedown", function (e) { + if (e.button === 1) { + e.preventDefault(); + e.stopPropagation(); + + window.open(buildUrl(), "_blank"); + } + }); + + // return with a wrapper + var wrapper = document.createElement("span"); + wrapper.appendChild(copyButton); + wrapper.appendChild(statusBox); + + return wrapper; +} + +function validateReferenceForId(ref, id) { + // Validate that the reference corresponds to the given ID in pcbdata + if (!pcbdata || !pcbdata.footprints || id >= pcbdata.footprints.length) { + return false; + } + + // Check if the footprint at the specified ID has the correct reference + return pcbdata.footprints[id].ref === ref; +} + +function showReferenceMismatchWarning(ref, id) { + // Ha már van ilyen figyelmeztetés, töröljük + document + .querySelectorAll(".reference-warning-toast") + .forEach(el => el.remove()); + + const toast = document.createElement("div"); + toast.className = "reference-warning-toast"; + + toast.innerHTML = ` +
+ ⚠️ + Reference mismatch + +
+ +
+

+ Reference ${escapeHtml(ref)} does not match the + expected reference for ID ${id}. +

+ +

+ This link may be stale. Choose which value to use. +

+
+ +
+ + + +
+ `; + + toast.querySelector(".toast-close").addEventListener("click", () => { + toast.remove(); + }); + + toast.querySelector(".btn-primary").addEventListener("click", () => { + toast.remove(); + selectComponentByReference(ref); + }); + + toast.querySelector(".btn-secondary").addEventListener("click", () => { + toast.remove(); + selectComponentById(id); + }); + + document.body.appendChild(toast); +} + +function escapeHtml(str) { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +function selectComponentByReference(ref) { + // Find the footprint with matching reference + var footprintIndex = -1; + if (pcbdata && pcbdata.footprints) { + 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 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); + // Fallback to navigator.clipboard if execCommand fails + try { + navigator.clipboard.writeText(text).catch(function(err) { + console.error('Could not copy text with clipboard API: ', err); + }); + } catch (clipboardErr) { + console.error('Clipboard API also failed: ', clipboardErr); + } + } + document.body.removeChild(textArea); +} + function highlightPreviousRow() { if (!currentHighlightedRowId) { highlightHandlers[highlightHandlers.length - 1].handler(); @@ -1194,6 +1433,14 @@ function updateCheckboxStats(checkbox) { td.lastChild.innerHTML = checked + "/" + total + " (" + Math.round(percent) + "%)"; } +function selectComponentById(id) { + // Find the footprint with matching ID + if (pcbdata && pcbdata.footprints && id < pcbdata.footprints.length) { + var ref = pcbdata.footprints[id].ref; + selectComponentByReference(ref); + } +} + function constrain(number, min, max) { return Math.min(Math.max(parseInt(number), min), max); } @@ -1320,6 +1567,51 @@ 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'); + var idParam = urlParams.get('id'); + + if (refParam) { + // Change layout to ungrouped + changeBomMode('ungrouped'); + // Extract the value from the "" or the '' string + refParam = refParam.replace(/^["']|["']$/g, ""); + + // If we have both ref and id parameters, validate them together + if (idParam !== null) { + var id = parseInt(idParam); + if (!isNaN(id)) { + if (validateReferenceForId(refParam, id)) { + // Both parameters are valid, select the component + selectComponentByReference(refParam); + } else { + // Parameters don't match, show warning + showReferenceMismatchWarning(refParam, id); + // Fallback to selecting by reference + selectComponentByReference(refParam); + } + } else { + // Invalid ID, fallback to selecting by reference only + selectComponentByReference(refParam); + } + } else { + // Only ref parameter is provided, select by reference + 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)