diff --git a/doc/_extensions/zephyr/domain/static/css/board-catalog.css b/doc/_extensions/zephyr/domain/static/css/board-catalog.css index e37c78537d137..8a13b3b183ebd 100644 --- a/doc/_extensions/zephyr/domain/static/css/board-catalog.css +++ b/doc/_extensions/zephyr/domain/static/css/board-catalog.css @@ -106,6 +106,10 @@ margin-right: 8px; } +#compatibles-tags .tag { + font-family: var(--monospace-font-family); +} + .tag:hover { background-color: #0056b3; } diff --git a/doc/_extensions/zephyr/domain/static/js/board-catalog.js b/doc/_extensions/zephyr/domain/static/js/board-catalog.js index 3d433d237e220..8a987b71d5c8f 100644 --- a/doc/_extensions/zephyr/domain/static/js/board-catalog.js +++ b/doc/_extensions/zephyr/domain/static/js/board-catalog.js @@ -41,15 +41,34 @@ function populateFormFromURL() { const features = hashParams.get("features").split(","); setTimeout(() => { features.forEach(feature => { - const tagContainer = document.getElementById('tag-container'); - const tagInput = document.getElementById('tag-input'); + const tagContainer = document.getElementById('hwcaps-tags'); + const tagInput = document.getElementById('hwcaps-input'); const tagElement = document.createElement('span'); tagElement.classList.add('tag'); tagElement.textContent = feature; tagElement.onclick = () => { - const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent); - selectedTags.splice(selectedTags.indexOf(feature), 1); + tagElement.remove(); + filterBoards(); + }; + tagContainer.insertBefore(tagElement, tagInput); + }); + filterBoards(); + }, 0); + } + + // Restore compatibles from URL + if (hashParams.has("compatibles")) { + const compatibles = hashParams.get("compatibles").split("|"); + setTimeout(() => { + compatibles.forEach(compatible => { + const tagContainer = document.getElementById('compatibles-tags'); + const tagInput = document.getElementById('compatibles-input'); + + const tagElement = document.createElement('span'); + tagElement.classList.add('tag'); + tagElement.textContent = compatible; + tagElement.onclick = () => { tagElement.remove(); filterBoards(); }; @@ -83,8 +102,12 @@ function updateURL() { }); // Add supported features to URL - const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent); - selectedTags.length ? hashParams.set("features", selectedTags.join(",")) : hashParams.delete("features"); + const selectedHWTags = [...document.querySelectorAll('#hwcaps-tags .tag')].map(tag => tag.textContent); + selectedHWTags.length ? hashParams.set("features", selectedHWTags.join(",")) : hashParams.delete("features"); + + // Add compatibles to URL + const selectedCompatibles = [...document.querySelectorAll('#compatibles-tags .tag')].map(tag => tag.textContent); + selectedCompatibles.length ? hashParams.set("compatibles", selectedCompatibles.join("|")) : hashParams.delete("compatibles"); window.history.replaceState({}, "", `#${hashParams.toString()}`); } @@ -126,8 +149,8 @@ function fillSocSocSelect(families, series = undefined, selectOnFill = false) { function setupHWCapabilitiesField() { let selectedTags = []; - const tagContainer = document.getElementById('tag-container'); - const tagInput = document.getElementById('tag-input'); + const tagContainer = document.getElementById('hwcaps-tags'); + const tagInput = document.getElementById('hwcaps-input'); const datalist = document.getElementById('tag-list'); const tagCounts = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => { @@ -198,6 +221,80 @@ function setupHWCapabilitiesField() { updateDatalist(); } +function setupCompatiblesField() { + let selectedCompatibles = []; + + const tagContainer = document.getElementById('compatibles-tags'); + const tagInput = document.getElementById('compatibles-input'); + const datalist = document.getElementById('compatibles-list'); + + // Collect all unique compatibles from boards + const allCompatibles = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => { + (board.getAttribute('data-compatibles') || '').split(' ').forEach(compat => { + if (compat && !acc.includes(compat)) { + acc.push(compat); + } + }); + return acc; + }, []); + + allCompatibles.sort(); + + function addCompatible(compatible) { + if (selectedCompatibles.includes(compatible) || compatible === "") return; + selectedCompatibles.push(compatible); + + const tagElement = document.createElement('span'); + tagElement.classList.add('tag'); + tagElement.textContent = compatible; + tagElement.onclick = () => removeCompatible(compatible); + tagContainer.insertBefore(tagElement, tagInput); + + tagInput.value = ''; + updateDatalist(); + } + + function removeCompatible(compatible) { + selectedCompatibles = selectedCompatibles.filter(c => c !== compatible); + document.querySelectorAll('.tag').forEach(el => { + if (el.textContent === compatible && el.parentElement === tagContainer) { + el.remove(); + } + }); + updateDatalist(); + } + + function updateDatalist() { + datalist.innerHTML = ''; + const filteredCompatibles = allCompatibles.filter(c => !selectedCompatibles.includes(c)); + + filteredCompatibles.forEach(compatible => { + const option = document.createElement('option'); + option.value = compatible; + datalist.appendChild(option); + }); + + filterBoards(); + } + + tagInput.addEventListener('input', () => { + if (allCompatibles.includes(tagInput.value)) { + addCompatible(tagInput.value); + } + }); + + tagInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && tagInput.value) { + addCompatible(tagInput.value); + e.preventDefault(); + } else if (e.key === 'Backspace' && tagInput.value === '' && selectedCompatibles.length > 0) { + removeCompatible(selectedCompatibles[selectedCompatibles.length - 1]); + } + }); + + updateDatalist(); +} + document.addEventListener("DOMContentLoaded", function () { const form = document.querySelector(".filter-form"); @@ -218,6 +315,7 @@ document.addEventListener("DOMContentLoaded", function () { populateFormFromURL(); setupHWCapabilitiesField(); + setupCompatiblesField(); socFamilySelect = document.getElementById("family"); socFamilySelect.addEventListener("change", () => { @@ -272,8 +370,12 @@ function resetForm() { document.getElementById("show-shields").checked = true; // Clear supported features - document.querySelectorAll('.tag').forEach(tag => tag.remove()); - document.getElementById('tag-input').value = ''; + document.querySelectorAll('#hwcaps-tags .tag').forEach(tag => tag.remove()); + document.getElementById('hwcaps-input').value = ''; + + // Clear compatibles + document.querySelectorAll('#compatibles-tags .tag').forEach(tag => tag.remove()); + document.getElementById('compatibles-input').value = ''; filterBoards(); } @@ -289,6 +391,16 @@ function updateBoardCount() { + ` ${visibleShields.length} of ${shields.length} shields`; } +function wildcardMatch(pattern, str) { + // Convert wildcard pattern to regex + // Escape special regex characters except * + const regexPattern = pattern + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(str); +} + function filterBoards() { const nameInput = document.getElementById("name").value.toLowerCase(); const archSelect = document.getElementById("arch").value; @@ -297,10 +409,14 @@ function filterBoards() { const showBoards = document.getElementById("show-boards").checked; const showShields = document.getElementById("show-shields").checked; - const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent); + // Get selected hardware capability tags + const selectedHWTags = [...document.querySelectorAll('#hwcaps-tags .tag')].map(tag => tag.textContent); + + // Get selected compatible tags + const selectedCompatibles = [...document.querySelectorAll('#compatibles-tags .tag')].map(tag => tag.textContent); const resetFiltersBtn = document.getElementById("reset-filters"); - if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length || selectedTags.length || !showBoards || !showShields) { + if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length || selectedHWTags.length || selectedCompatibles.length || !showBoards || !showShields) { resetFiltersBtn.classList.remove("btn-disabled"); } else { resetFiltersBtn.classList.add("btn-disabled"); @@ -314,6 +430,7 @@ function filterBoards() { const boardVendor = board.getAttribute("data-vendor") || ""; const boardSocs = (board.getAttribute("data-socs") || "").split(" ").filter(Boolean); const boardSupportedFeatures = (board.getAttribute("data-supported-features") || "").split(" ").filter(Boolean); + const boardCompatibles = (board.getAttribute("data-compatibles") || "").split(" ").filter(Boolean); const isShield = board.classList.contains("shield"); let matches = true; @@ -323,12 +440,19 @@ function filterBoards() { if ((isShield && !showShields) || (!isShield && !showBoards)) { matches = false; } else { + // Check if board matches all selected compatibles (with wildcard support) + const compatiblesMatch = selectedCompatibles.length === 0 || + selectedCompatibles.every((pattern) => + boardCompatibles.some((compatible) => wildcardMatch(pattern, compatible)) + ); + matches = !(nameInput && !boardName.includes(nameInput)) && !(archSelect && !boardArchs.includes(archSelect)) && !(vendorSelect && boardVendor !== vendorSelect) && (selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))) && - (selectedTags.length === 0 || selectedTags.every((tag) => boardSupportedFeatures.includes(tag))); + (selectedHWTags.length === 0 || selectedHWTags.every((tag) => boardSupportedFeatures.includes(tag))) && + compatiblesMatch; } board.classList.toggle("hidden", !matches); diff --git a/doc/_extensions/zephyr/domain/templates/board-card.html b/doc/_extensions/zephyr/domain/templates/board-card.html index 0e4808b85a74f..961048e2e9285 100644 --- a/doc/_extensions/zephyr/domain/templates/board-card.html +++ b/doc/_extensions/zephyr/domain/templates/board-card.html @@ -25,7 +25,19 @@ {%- endfor -%} {%- endfor -%} {{- feature_types|join(' ') -}} - " tabindex="0"> + " + data-compatibles=" + {%- set all_compatibles = [] -%} + {%- for target_compatibles in board.compatibles.values() -%} + {%- for compatible in target_compatibles -%} + {%- if compatible not in all_compatibles -%} + {%- set _ = all_compatibles.append(compatible) -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{- all_compatibles|join(' ') -}} + " + tabindex="0">