From 7e2a2451ad0e5b10d7e13e676790c84ec8a2481b Mon Sep 17 00:00:00 2001 From: fkakatie Date: Fri, 10 Jul 2026 13:56:58 -0600 Subject: [PATCH 1/2] feat: move shared widget utils into scripts --- scripts/scripts.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/scripts/scripts.js b/scripts/scripts.js index 163f8ba..0c17dd8 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -11,6 +11,7 @@ import { loadCSS, buildBlock, getMetadata, + createOptimizedPicture, } from './aem.js'; /** @@ -293,6 +294,84 @@ export function normalizeIndexImageUrl(src) { } } +/** + * Fetches the locale-specific query index. + * @param {string} lang - Language key (e.g. en) + * @returns {Promise>} + */ +export async function loadIndex(lang) { + const base = (window.hlx && window.hlx.codeBasePath) || ''; + const resp = await fetch(`${base}/${lang}/query-index.json`); + if (!resp.ok) return []; + const json = await resp.json(); + return Array.isArray(json.data) ? json.data : []; +} + +/** + * Whether a query-index item path is exactly one segment below a parent path. + * @param {string} itemPath - Path from query-index.json + * @param {string} parentPath - Path to check against + * @returns {boolean} + */ +export function isDirectChild(itemPath, parentPath) { + const normalized = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath; + if (!itemPath.startsWith(`${normalized}/`)) return false; + const rest = itemPath.slice(normalized.length + 1); + return rest.length > 0 && !rest.includes('/'); +} + +/** + * Builds a cards-block-compatible row (media + body cells) from a query-index item. + * @param {Object} item - Normalized query-index item + * @param {boolean} [showDescription=true] - Whether to include the description paragraph + * @returns {HTMLElement} + */ +export function buildCardRow(item, showDescription = true) { + const row = document.createElement('div'); + + const image = normalizeIndexImageUrl(item.image); + if (image) { + const mediaCell = document.createElement('div'); + const picture = createOptimizedPicture(image, item.title || '', false, [{ width: '750' }]); + mediaCell.append(picture); + row.append(mediaCell); + } + + const bodyCell = document.createElement('div'); + if (item.title) { + const heading = document.createElement('h3'); + const link = document.createElement('a'); + link.href = item.path; + link.textContent = item.title; + heading.append(link); + bodyCell.append(heading); + } + if (showDescription && item.description) { + const p = document.createElement('p'); + p.textContent = item.description; + bodyCell.append(p); + } + row.append(bodyCell); + + return row; +} + +/** + * Hydrates all `[data-copy]` elements within a container from a widget copy object. + * @param {HTMLElement} container - Root element to search within + * @param {Object} copy - Widget copy for the current language + */ +export function hydrateCopy(container, copy) { + container.querySelectorAll('[data-copy]').forEach((el) => { + const value = copy[el.dataset.copy]; + if (!value) return; + const target = el.dataset.copyTarget; + if (target) { + target.split(',').forEach((attr) => el.setAttribute(attr.trim(), value)); + } else el.textContent = value; + }); +} + /** * Sets target and rel on links whose hostname differs from the current page. * @param {Element} container - The element to search for links within From 3d3232531ab818ae81719c637b86907b7e274c07 Mon Sep 17 00:00:00 2001 From: fkakatie Date: Fri, 10 Jul 2026 13:57:19 -0600 Subject: [PATCH 2/2] feat: use exported widget utils from scripts --- widgets/card-list/card-list.js | 51 ++---------------------- widgets/plp/plp.js | 45 ++------------------- widgets/press-releases/press-releases.js | 18 +-------- widgets/search/search.js | 20 ++-------- 4 files changed, 11 insertions(+), 123 deletions(-) diff --git a/widgets/card-list/card-list.js b/widgets/card-list/card-list.js index 4ed9eab..31d96dc 100644 --- a/widgets/card-list/card-list.js +++ b/widgets/card-list/card-list.js @@ -1,7 +1,7 @@ +import { decorateBlock, loadBlock } from '../../scripts/aem.js'; import { - createOptimizedPicture, decorateBlock, loadBlock, -} from '../../scripts/aem.js'; -import { normalizeIndexImageUrl, getLocale } from '../../scripts/scripts.js'; + getLocale, loadIndex, isDirectChild, buildCardRow, +} from '../../scripts/scripts.js'; function resolveRoot(rootParam, lang) { const pagePath = window.location.pathname; @@ -15,51 +15,6 @@ function resolveRoot(rootParam, lang) { return pagePath; } -function isDirectChild(itemPath, parentPath) { - const normalized = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath; - if (!itemPath.startsWith(`${normalized}/`)) return false; - const rest = itemPath.slice(normalized.length + 1); - return rest.length > 0 && !rest.includes('/'); -} - -async function loadIndex(lang) { - const base = window.hlx?.codeBasePath || ''; - const resp = await fetch(`${base}/${lang}/query-index.json`); - if (!resp.ok) return []; - const json = await resp.json(); - return Array.isArray(json.data) ? json.data : []; -} - -function buildCardRow(item, showDescription) { - const row = document.createElement('div'); - - const image = normalizeIndexImageUrl(item.image); - if (image) { - const mediaCell = document.createElement('div'); - const picture = createOptimizedPicture(image, item.title || '', false, [{ width: '750' }]); - mediaCell.append(picture); - row.append(mediaCell); - } - - const bodyCell = document.createElement('div'); - if (item.title) { - const heading = document.createElement('h3'); - const link = document.createElement('a'); - link.href = item.path; - link.textContent = item.title; - heading.append(link); - bodyCell.append(heading); - } - if (showDescription && item.description) { - const p = document.createElement('p'); - p.textContent = item.description; - bodyCell.append(p); - } - row.append(bodyCell); - - return row; -} - export default async function decorate(widget) { const lang = getLocale(); const rootParam = widget.dataset.root; diff --git a/widgets/plp/plp.js b/widgets/plp/plp.js index edf29f3..7d3caf7 100644 --- a/widgets/plp/plp.js +++ b/widgets/plp/plp.js @@ -1,7 +1,7 @@ +import { decorateBlock, loadBlock } from '../../scripts/aem.js'; import { - createOptimizedPicture, decorateBlock, loadBlock, -} from '../../scripts/aem.js'; -import { normalizeIndexImageUrl, getLocale, loadCopy } from '../../scripts/scripts.js'; + getLocale, loadCopy, loadIndex, isDirectChild, buildCardRow, +} from '../../scripts/scripts.js'; const FACETS = [ { key: 'region', copyKey: 'region' }, @@ -12,21 +12,6 @@ const FACETS = [ { key: 'traction-system', copyKey: 'tractionSystem' }, ]; -function isDirectChild(itemPath, parentPath) { - const normalized = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath; - if (!itemPath.startsWith(`${normalized}/`)) return false; - const rest = itemPath.slice(normalized.length + 1); - return rest.length > 0 && !rest.includes('/'); -} - -async function loadIndex(lang) { - const base = window.hlx?.codeBasePath || ''; - const resp = await fetch(`${base}/${lang}/query-index.json`); - if (!resp.ok) return []; - const json = await resp.json(); - return Array.isArray(json.data) ? json.data : []; -} - function parseValues(raw) { return (raw || '').split(',').map((v) => v.trim()).filter(Boolean); } @@ -54,28 +39,6 @@ function matchesFilters(item, selected) { }); } -function buildCardRow(item) { - const row = document.createElement('div'); - const image = normalizeIndexImageUrl(item.image); - if (image) { - const mediaCell = document.createElement('div'); - const picture = createOptimizedPicture(image, item.title || '', false, [{ width: '750' }]); - mediaCell.append(picture); - row.append(mediaCell); - } - const bodyCell = document.createElement('div'); - if (item.title) { - const heading = document.createElement('h3'); - const link = document.createElement('a'); - link.href = item.path; - link.textContent = item.title; - heading.append(link); - bodyCell.append(heading); - } - row.append(bodyCell); - return row; -} - function buildFiltersPanel(panel, counts, selected, onchange, copy) { panel.innerHTML = ''; FACETS.forEach(({ key, copyKey }) => { @@ -127,7 +90,7 @@ function buildFiltersPanel(panel, counts, selected, onchange, copy) { async function renderCards(container, items) { const cardsBlock = document.createElement('div'); cardsBlock.classList.add('cards'); - items.forEach((item) => cardsBlock.append(buildCardRow(item))); + items.forEach((item) => cardsBlock.append(buildCardRow(item, false))); const wrapper = document.createElement('div'); wrapper.append(cardsBlock); container.replaceChildren(wrapper); diff --git a/widgets/press-releases/press-releases.js b/widgets/press-releases/press-releases.js index 7feeec3..107b8a8 100644 --- a/widgets/press-releases/press-releases.js +++ b/widgets/press-releases/press-releases.js @@ -1,4 +1,4 @@ -import { getLocale, loadCopy } from '../../scripts/scripts.js'; +import { getLocale, loadCopy, hydrateCopy } from '../../scripts/scripts.js'; const DEFAULT_PAGE_SIZE = 12; @@ -75,22 +75,6 @@ function formatReleaseDate(dateStr, locale) { }).format(date); } -/** - * Hydrate all [data-copy] elements from widget copy. - * @param {HTMLElement} container - .press-releases root element - * @param {Object} copy - Widget copy for the current language - */ -function hydrateCopy(container, copy) { - container.querySelectorAll('[data-copy]').forEach((el) => { - const value = copy[el.dataset.copy]; - if (!value) return; - const target = el.dataset.copyTarget; - if (target) { - target.split(',').forEach((attr) => el.setAttribute(attr.trim(), value)); - } else el.textContent = value; - }); -} - /** * Create a press release preview card. * @param {Object} item - Normalized release diff --git a/widgets/search/search.js b/widgets/search/search.js index bf69538..1ddf0f1 100644 --- a/widgets/search/search.js +++ b/widgets/search/search.js @@ -1,5 +1,7 @@ import { createOptimizedPicture, loadCSS } from '../../scripts/aem.js'; -import { normalizeIndexImageUrl, getLocale, loadCopy } from '../../scripts/scripts.js'; +import { + normalizeIndexImageUrl, getLocale, loadCopy, hydrateCopy, +} from '../../scripts/scripts.js'; /** * Normalize a single item from the query index. @@ -397,22 +399,6 @@ async function searchItems(searchTerm, limit) { return limit ? results.slice(0, limit) : results; } -/** - * Hydrate all [data-copy] elements from widget copy. - * @param {HTMLElement} container - .search root element - * @param {Object} copy - Widget copy for the current language - */ -function hydrateCopy(container, copy) { - container.querySelectorAll('[data-copy]').forEach((el) => { - const value = copy[el.dataset.copy]; - if (!value) return; - const target = el.dataset.copyTarget; - if (target) { - target.split(',').forEach((attr) => el.setAttribute(attr.trim(), value)); - } else el.textContent = value; - }); -} - const ITEMS_PER_PAGE = 16; const SUGGESTIONS_DEBOUNCE_MS = 150;