diff --git a/packages/11ty/_includes/components/head-tags/pagefind.js b/packages/11ty/_includes/components/head-tags/pagefind.js index d49c5780b..73a80611e 100644 --- a/packages/11ty/_includes/components/head-tags/pagefind.js +++ b/packages/11ty/_includes/components/head-tags/pagefind.js @@ -21,6 +21,13 @@ export default function (eleventyConfig) { { property: 'type', content: layout + }, + { + property: 'image', + content: '' + }, + { property: 'image_alt', + content: '' } ] diff --git a/packages/11ty/_includes/components/search.js b/packages/11ty/_includes/components/search.js index 7c2c5efab..959c4fda3 100644 --- a/packages/11ty/_includes/components/search.js +++ b/packages/11ty/_includes/components/search.js @@ -10,14 +10,6 @@ export default function (eleventyConfig) { const icon = eleventyConfig.getFilter('icon') return (params) => { return html` - - - + diff --git a/packages/11ty/_includes/web-components/search/README.md b/packages/11ty/_includes/web-components/search/README.md new file mode 100644 index 000000000..5d9321cc5 --- /dev/null +++ b/packages/11ty/_includes/web-components/search/README.md @@ -0,0 +1,29 @@ +# q-search-results-list + +## Overview + +The `q-search-results-list` component displays and manages search results using Pagefind search integration. + +## Usage + +```html + +``` + +### Attributes + +The component takes a `query` attribute with the search query string to search for in a Pagefind index. + +### Styling + +- `.search-list` - Results container +- `.search-result` - Individual result item +- `.search-subresults` - Sub-results list + +- `.result-title` - Result header area +- `.result-link` - Result links +- `.result-item` - Content container + - `.result-item-image` - Image container + - `.result-item-content` - Text content area + - `.result-meta` - Metadata paragraphs + - `.result-excerpt` - Excerpt text \ No newline at end of file diff --git a/packages/11ty/_includes/web-components/search/index.js b/packages/11ty/_includes/web-components/search/index.js new file mode 100644 index 000000000..4a749cd4e --- /dev/null +++ b/packages/11ty/_includes/web-components/search/index.js @@ -0,0 +1,176 @@ +import { LitElement, html } from 'lit' +import { unsafeHTML } from 'lit-html/directives/unsafe-html.js' +import { searchResultsListStyles } from './styles.js' + +/** + * @type {Object|null} Global reference to Pagefind search functionality + * Lazily loaded when search is first performed + */ +let PAGEFIND_GLOBAL + +/** + * @class SearchResultsList + * @extends LitElement + * @description A reactive Lit element for displaying and interacting with pagefind search results. + * Handles search queries, result fetching, and rendering of structured search results with metadata. + * + * @property {string} query - The current search query string + * @property {Array} results - Array of search result objects from Pagefind (internal state, not reflected as attribute) + */ +class SearchResultsList extends LitElement { + static properties = { + query: { type: String }, + results: { type: Array, attribute: false } + } + + static styles = [searchResultsListStyles] + + constructor () { + super() + this.results = [] + this.query = '' + } + + connectedCallback () { + super.connectedCallback() + } + + willUpdate (changedProperties) { + if (changedProperties.has('query')) { + this.updateResults(this.query) + } + } + + /** + * updateResults + * @description Performs search using Pagefind and updates the results array. + * @param {string} [query=this.query] - The search query string to execute + * @returns {Promise} Promise that resolves when results are updated + */ + async updateResults (query = this.query) { + if (!PAGEFIND_GLOBAL) { + PAGEFIND_GLOBAL = await import('../../../_search/pagefind.js') + } + const search = await PAGEFIND_GLOBAL.debouncedSearch(query) + if (!search) return + + const resultsData = search.results.map(async rawResult => rawResult.data()) + this.results = await Promise.all(resultsData) + } + + /** + * resultHeaderTemplate + * @description Renders the header section of a search result with title and link + * @param {Object} result - Search result object from Pagefind + * @returns {TemplateResult} Lit HTML template for the result header + */ + resultHeaderTemplate (result) { + return html` + + ` + } + + /** + * resultContentTemplate + * @description Renders the main content section of a search result including image, metadata, and excerpt + * @param {Object} result - Search result object from Pagefind + * @returns {TemplateResult} Lit HTML template for the result content + */ + resultContentTemplate (result) { + return html` +
+ ${this.resultImageTemplate(result)} +
+ ${this.resultMetaTemplate(result)} +

${unsafeHTML(result.excerpt)}

+
+
+ ` + } + + /** + * resultImageTemplate + * @description Renders the image section for a search result if an image is available + * @param {Object} result - Search result object from Pagefind + * @param {Object} result.meta - Metadata object + * @returns {TemplateResult|string} Lit HTML template for the image or empty string if no image + */ + resultImageTemplate (result) { + if (!result.meta.image) return '' + return html` +
+ ${result.meta.image_alt} +
+ ` + } + + /** + * subResultsTemplate + * @description Renders sub-results for a search result and filters out sub-results are repeated. + * @param {Object} [params={}] + * @param {Array} [params.sub_results] - Array of sub-result objects + * @param {Object} [params.meta] + * @returns {TemplateResult|string} Lit HTML template for sub-results or empty string if none + */ + subResultsTemplate ({ sub_results: subresults, meta } = {}) { + const filteredResults = (subresults).filter(subitem => subitem.title !== meta.title) + if (filteredResults.length === 0) return '' + return html` +
    + ${filteredResults.map(subitem => html` +
  1. + + ${subitem.title} + +

    ${unsafeHTML(subitem.excerpt)}

    +
  2. + `)} +
+ ` + } + + /** + * resultMetaTemplate + * @description Renders metadata information for a search result including page title, contributors, and credits + * @param {Object} result - Search result object from Pagefind + * @param {Object} result.meta - Metadata object + * @param {string} [result.meta.pageTitle] - Page title metadata + * @param {string} [result.meta.contributors] - Contributors metadata + * @param {string} [result.meta.credit] - Credit metadata + * @returns {TemplateResult} Lit HTML template for the result metadata + */ + resultMetaTemplate (result) { + return html` + ${result.meta.pageTitle ? html`

${result.meta.pageTitle}

` : ''} + ${result.meta.contributors ? html`

${result.meta.contributors}

` : ''} + ${result.meta.credit ? html`

${result.meta.credit}

` : ''} + ` + } + + /** + * resultTemplate + * @description Renders a complete search result item by combining header, content, and sub-results + * @param {Object} result - Search result object from Pagefind + * @returns {TemplateResult} Lit HTML template for the complete result item + */ + resultTemplate (result) { + return html` +
  • + ${this.resultHeaderTemplate(result)} + ${this.resultContentTemplate(result)} + ${this.subResultsTemplate(result)} +
  • ` + } + + render () { + return html`
      + ${this.results.map(result => this.resultTemplate(result))} +
    ` + } +} + +customElements.define('q-search-results-list', SearchResultsList) diff --git a/packages/11ty/_includes/web-components/search/styles.js b/packages/11ty/_includes/web-components/search/styles.js new file mode 100644 index 000000000..321f39057 --- /dev/null +++ b/packages/11ty/_includes/web-components/search/styles.js @@ -0,0 +1,87 @@ +import { css } from 'lit' + +export const searchResultsListStyles = css` + :host { + + } + + ol { + list-style: none; + padding: 0; + margin: 0; + } + + li { + font-family: var(--quire-primary-font, 'Noto Sans', sans-serif); + margin: 1.5rem 0 0 0; + } + + .result-link { + width: fit-content; + color: var(--accent-color, #CB3434); + border-bottom: 1px dotted var(--accent-color, #CB3434); + letter-spacing: 0px; + text-decoration: none; + &:hover { + border-bottom: 1px solid var(--accent-color-hover, #a02a2a); + } + & * { + color: var(--accent-color, #CB3434); + } + } + + .result-title { + font-size: 1.25rem; + line-height: 1.4; + font-family: var(--quire-headings-font, 'IBM Plex Sans Condensed', sans-serif); + text-transform: none; + margin-bottom: .25em; + } + + .result-meta { + font-style: italic; + } + + .result-item { + display: flex; + align-items: flex-start; + } + + .result-item-content { + flex: 1; + } + + .result-item-content p { + margin: 0 0 0.5rem 0; + } + + .result-excerpt { + margin: 0; + } + + .result-item-image { + margin-right: 1rem; + margin-top: 0.25rem; + } + + .result-item-image img { + object-fit: contain; + max-height: 6rem; + } + + .subresults-item { + margin: 1rem 0 0 0; + } + + .subresults-item .result-link { + display: block; + margin-bottom: 0.25rem; + } + + mark { + background-color:#ff0; + border-radius:2px; + padding:0 2px; + color:#000 + } + ` diff --git a/packages/11ty/_plugins/search/index.js b/packages/11ty/_plugins/search/index.js index 8333fa207..9e584ab0c 100644 --- a/packages/11ty/_plugins/search/index.js +++ b/packages/11ty/_plugins/search/index.js @@ -19,7 +19,7 @@ const SEARCH_INDEX_DIR = '_search' * */ export default function (eleventyConfig, collections, { - indexFigures = false, + indexFigures = true, excludeSelectors = [], searchIndexDir = SEARCH_INDEX_DIR } = {}) { diff --git a/packages/11ty/_plugins/search/search.js b/packages/11ty/_plugins/search/search.js index 0b56b4d99..db168b614 100644 --- a/packages/11ty/_plugins/search/search.js +++ b/packages/11ty/_plugins/search/search.js @@ -86,7 +86,7 @@ export default class SearchIndex { * @returns {Promise} */ async addFigureRecord ({ figureData, canonicalURL, title } = {}) { - const { id, caption, alt, src, thumbnail, label, credit, mediaType } = figureData + const { id, caption, alt, src, label, credit, mediaType } = figureData const markdownify = this.eleventyConfig.getFilter('markdownify') const removeHTML = this.eleventyConfig.getFilter('removeHTML') @@ -103,7 +103,7 @@ export default class SearchIndex { meta: { title: label, pageTitle: title, - image: thumbnail || this.assetSrc(src) || '', + image: this.assetSrc(src) || '', image_alt: alt || '', credit, type: mediaType diff --git a/packages/11ty/_plugins/shortcodes/figure.js b/packages/11ty/_plugins/shortcodes/figure.js index ed2e65ccb..b15242f95 100644 --- a/packages/11ty/_plugins/shortcodes/figure.js +++ b/packages/11ty/_plugins/shortcodes/figure.js @@ -66,7 +66,7 @@ export default function (eleventyConfig) { } return oneLine` -
    +
    ${await component({ ...figure, lazyLoading })}
    ` diff --git a/packages/11ty/content/_assets/javascript/application/index.js b/packages/11ty/content/_assets/javascript/application/index.js index d6ab22fb1..0170ad0aa 100644 --- a/packages/11ty/content/_assets/javascript/application/index.js +++ b/packages/11ty/content/_assets/javascript/application/index.js @@ -56,6 +56,7 @@ window.toggleSearch = () => { !searchControls.classList.contains('is-active') ) if (searchAriaStatus === 'true') { + searchInput.value = '' searchControls.setAttribute('aria-expanded', 'false') } else { searchInput.focus() @@ -69,48 +70,9 @@ window.toggleSearch = () => { */ window.search = async () => { const searchInput = document.getElementById('js-search-input') - const searchQuery = searchInput.value - if (window._searchResults) { - await window._searchResults - } - window._searchResults = updateSearchResults(searchQuery) -} - -/** - * updateSearchResults - * @description fetch and display search results from Pagefind - * @param {string} query The search query string - */ -async function updateSearchResults (query) { - const searchInstance = window.QUIRE_SEARCH - const searchResults = await searchInstance.search(query) - await displaySearchResults(searchResults) - window._searchResults = undefined -} - -/** - * displaySearchResults - * @description add search results items to the results container - * @param {object} pageFindResults The search results object from Pagefind - */ -async function displaySearchResults ({ results }) { - const resultsContainer = document.getElementById('js-search-results-list') - const resultsTemplate = document.getElementById('js-search-results-template') - resultsContainer.innerText = '' - - for (const rawResult of results) { - const result = await rawResult.data() - const clone = document.importNode(resultsTemplate.content, true) - const item = clone.querySelector('.js-search-results-item') - const title = clone.querySelector('.js-search-results-item-title') - const type = clone.querySelector('.js-search-results-item-type') - const length = clone.querySelector('.js-search-results-item-length') - item.href = result.url - title.textContent = result.meta.title - type.textContent = result.meta.type - length.textContent = result.word_count - resultsContainer.appendChild(clone) - } + const resultsList = document.getElementById('js-search-results-list') + if (!searchInput || !resultsList) return + resultsList.query = searchInput.value } function onHashLinkClick (event) {