diff --git a/blocks/header/categories.js b/blocks/header/categories.js new file mode 100644 index 00000000..77e3ec67 --- /dev/null +++ b/blocks/header/categories.js @@ -0,0 +1,40 @@ +export function slugify(label) { + return String(label) + .toLowerCase() + .replace(/&/g, ' ') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function pathFromHref(href) { + try { + return new URL(href, 'https://tools.aem.live/').pathname; + } catch { + return href; + } +} + +export function parseCategories(html) { + if (!html) return []; + const doc = new window.DOMParser().parseFromString(html, 'text/html'); + const topUl = doc.querySelector('ul'); + if (!topUl) return []; + + const categories = []; + [...topUl.children].forEach((li) => { + const sub = li.querySelector(':scope > ul'); + if (!sub) return; + const labelText = [...li.childNodes] + .filter((n) => n.nodeType === 3 || (n.nodeType === 1 && n.tagName !== 'UL')) + .map((n) => n.textContent) + .join(' ') + .trim(); + if (!labelText) return; + const tools = [...sub.querySelectorAll('a[href]')].map((a) => ({ + url: pathFromHref(a.getAttribute('href')), + label: a.textContent.trim(), + })); + categories.push({ slug: slugify(labelText), label: labelText, tools }); + }); + return categories; +} diff --git a/blocks/header/combobox-filter.js b/blocks/header/combobox-filter.js new file mode 100644 index 00000000..c91882a1 --- /dev/null +++ b/blocks/header/combobox-filter.js @@ -0,0 +1,12 @@ +/** + * Filters a list of string items by a query, matching case-insensitively on substring. + * @param {string} query - The search query. Empty or whitespace-only returns a copy of all items. + * @param {string[]} items - The candidate items. + * @returns {string[]} A new array of the matching items. Never mutates the input. + */ +// eslint-disable-next-line import/prefer-default-export +export function filterItems(query, items) { + const normalized = (query || '').trim().toLowerCase(); + if (!normalized) return [...items]; + return items.filter((item) => String(item).toLowerCase().includes(normalized)); +} diff --git a/blocks/header/combobox.js b/blocks/header/combobox.js new file mode 100644 index 00000000..01255ef2 --- /dev/null +++ b/blocks/header/combobox.js @@ -0,0 +1,302 @@ +import { filterItems } from './combobox-filter.js'; + +export const CHEVRON_SVG = ''; + +/** + * Creates a hand-rolled, Spectrum-styled combobox (filterable single-select with free text). + * Follows the WAI-ARIA combobox pattern with aria-autocomplete="list". + * @param {Object} options + * @param {string} options.id - Base id for the input; the listbox gets `${id}-listbox`. + * @param {string} options.label - Accessible label for the input. + * @param {string} options.placeholder - Input placeholder text. + * @param {boolean} [options.disabled=false] - Whether the combobox starts disabled. + * @param {boolean} [options.labelVisible=false] - Render `label` as a visible `