Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 105 additions & 14 deletions src/instruction-search.njk
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ permalink: search/index.html

<input id="search" placeholder="Search mnemonic, e.g. addi" style="padding:8px; width: 100%; max-width: 480px;" />

{# Creates box divide between checkboxes and rest of page #}
<fieldset id="extension-filters-fieldset" class="filters">
<legend>Filter by extension</legend>
<div id="extension-filters" class="filter-options"></div>
</fieldset>

<ul id="list">
{% for inst in instructions %}
<li data-name="{{ inst.name }} {{ inst.longName | lower }}">
<li
data-search="{{ (inst.name ~ ' ' ~ inst.longName) | lower }}"
data-mnemonic="{{ inst.name | lower }}"
data-extension="{{ inst.extension }}"
data-extension-slug="{{ inst.extensionSlug }}"
>
<a href="/instructions/{{ inst.name }}/">{{ inst.name }}</a>
</li>
{% endfor %}
Expand All @@ -19,25 +30,101 @@ permalink: search/index.html
<script>
const input = document.getElementById('search');
const list = document.getElementById('list');
const filtersFieldset = document.getElementById('extension-filters-fieldset');
{# the blank area where we’ll insert checkboxes #}
const filtersContainer = document.getElementById('extension-filters');
const items = Array.from(list.querySelectorAll('li'));
input.addEventListener('input', () => {
const activeExtensions = new Set();

const extensionMap = new Map();
for (const li of items) {
const slug = li.dataset.extensionSlug;
const extension = li.dataset.extension;
if (slug && extension && !extensionMap.has(slug)) {
extensionMap.set(slug, { extension, slug });
}
}

const extensions = Array.from(extensionMap.values()).sort((a, b) => a.extension.localeCompare(b.extension));
if (extensions.length === 0 && filtersFieldset) {
filtersFieldset.style.display = 'none';
} else {
for (const { extension, slug } of extensions) {
const id = `ext-${slug}`;
const label = document.createElement('label');
label.className = 'filter-option';

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = extension;
checkbox.id = id;
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
activeExtensions.add(extension);
} else {
activeExtensions.delete(extension);
}
applyFilters();
});

const text = document.createElement('span');
text.textContent = extension;

label.appendChild(checkbox);
label.appendChild(text);
filtersContainer.appendChild(label);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, it looks like the strategy here is to populate the list of extension in the client, rather than when generating the HTML. Rather like the situation where we were generating SVG in the browser before, I think this could be a lot simpler if we simply generate the HTML for the checkboxes up front. This would avoid all the createElement and stuff, and it could be much easier to read.

I strongly suggest that we leave that change to a future PR as well! In the interest of getting this one merged, so the next step will be clear.

}
}

function applyFilters() {
const q = input.value.trim().toLowerCase();
const filtersActive = activeExtensions.size > 0;
for (const li of items) {
const txt = li.getAttribute('data-name');
li.style.display = (!q || txt.includes(q)) ? '' : 'none';

{# Check if the item matches the text search
- If the search box is empty, match everything
- Otherwise, match if the search string includes the query #}
const matchesQuery = !q || (li.dataset.search || '').includes(q);

{# Check if the item matches an active extension filter
- If no filters are active, match everything
- Otherwise, show only if its extension is selected #}
const matchesExtension = !filtersActive || activeExtensions.has(li.dataset.extension);

{# Show or hide this <li> depending on whether both match conditions are true #}
li.style.display = (matchesQuery && matchesExtension) ? '' : 'none';
}
// If exact mnemonic match, navigate on Enter
});
}

input.addEventListener('input', applyFilters);

input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const q = input.value.trim().toLowerCase();
if (!q) return;
const exact = items.find(li => li.querySelector('a').textContent.toLowerCase() === q);
if (exact) {
location.href = exact.querySelector('a').getAttribute('href');
}
{# Only act if Enter is pressed #}
if (e.key !== 'Enter') return;

const q = input.value.trim().toLowerCase();

{# Do nothing if the box is empty #}
if (!q) return;

const filtersActive = activeExtensions.size > 0;

{# Look for an exact mnemonic match among all instructions
- If filters are active, also ensure it belongs to one of the selected extensions #}
const exact = items.find(li => {
if (li.dataset.mnemonic !== q) return false;
if (!filtersActive) return true;
return activeExtensions.has(li.dataset.extension);
});

{# If an exact match is found, redirect to that instruction's detail page #}
if (exact) {
const link = exact.querySelector('a').getAttribute('href');
location.href = link;
}
});

applyFilters();
</script>

<style>
Expand All @@ -46,5 +133,9 @@ ul { list-style: none; padding: 0; }
li { margin: 6px 0; }
a { text-decoration: none; color: #0366d6; }
a:hover { text-decoration: underline; }
#extension-filters-fieldset { margin: 1rem 0; padding: 0.75rem 1rem; border: 1px solid #d0d7de; border-radius: 6px; }
#extension-filters-fieldset legend { font-weight: 600; font-size: 0.95rem; padding: 0 6px; }
.filter-options { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; margin-top: 0.5rem; align-items: center; }
.filter-option { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.95rem; white-space: nowrap; }
.filter-option input { width: 16px; height: 16px; }
</style>