210 lines
6.4 KiB
Twig
210 lines
6.4 KiB
Twig
{#
|
||
Meilisearch Frontend Search
|
||
Contao 5 – Frontend Module Template
|
||
#}
|
||
|
||
<!-- indexer::stop -->
|
||
<div
|
||
id="topsearch"
|
||
class="meilisearch-search"
|
||
data-limit="{{ meiliLimit }}"
|
||
>
|
||
<div class="headersearch">
|
||
<form id="search-form" onsubmit="return false;">
|
||
<div class="formbody">
|
||
<div class="widget widget-text">
|
||
<label for="search_input" class="invisible">
|
||
{{ 'Suchen'|trans }}
|
||
</label>
|
||
|
||
<div class="search-field">
|
||
<input
|
||
type="search"
|
||
name="keywords"
|
||
id="search_input"
|
||
class="text"
|
||
placeholder="Suchbegriff eingeben…"
|
||
autocomplete="off"
|
||
>
|
||
|
||
<button
|
||
type="button"
|
||
class="clear-button is-hidden"
|
||
aria-label="Suche löschen"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<div id="search-results"></div>
|
||
|
||
<template id="search-result-template">
|
||
<div class="search-item">
|
||
<div class="siteimage">
|
||
<img src="" alt="" loading="lazy">
|
||
</div>
|
||
|
||
<div class="teaser">
|
||
<div class="title"></div>
|
||
<div class="extract"></div>
|
||
<div class="pfad"></div>
|
||
</div>
|
||
|
||
<a class="masterurl" href="#" title=""></a>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import MeiliSearch from 'https://cdn.jsdelivr.net/npm/meilisearch@latest/dist/bundles/meilisearch.esm.js';
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
|
||
const wrapper = document.querySelector('.meilisearch-search');
|
||
if (!wrapper) return;
|
||
|
||
const input = wrapper.querySelector('#search_input');
|
||
const clear = wrapper.querySelector('.clear-button');
|
||
const results = wrapper.querySelector('#search-results');
|
||
const template = wrapper.querySelector('#search-result-template');
|
||
|
||
if (!input || !clear || !results || !template) {
|
||
console.warn('[Meilisearch] Required elements not found');
|
||
return;
|
||
}
|
||
|
||
const limit = parseInt(wrapper.dataset.limit, 10) || 50;
|
||
|
||
const client = new MeiliSearch({
|
||
host: '{{ meiliHost }}',
|
||
apiKey: '{{ meiliSearchKey }}'
|
||
});
|
||
|
||
const index = client.index('{{ meiliIndex }}');
|
||
|
||
let abortController = null;
|
||
|
||
// ----------------------------
|
||
// Clear button
|
||
// ----------------------------
|
||
clear.addEventListener('click', () => {
|
||
input.value = '';
|
||
results.innerHTML = '';
|
||
clear.classList.add('is-hidden');
|
||
input.focus();
|
||
});
|
||
|
||
// ----------------------------
|
||
// Input handling
|
||
// ----------------------------
|
||
input.addEventListener('input', async () => {
|
||
|
||
const query = input.value.trim();
|
||
|
||
clear.classList.toggle('is-hidden', query.length === 0);
|
||
|
||
if (query.length < 2) {
|
||
results.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
abortController?.abort();
|
||
abortController = new AbortController();
|
||
|
||
try {
|
||
const response = await index.search(query, {
|
||
limit,
|
||
|
||
attributesToRetrieve: [
|
||
'title',
|
||
'url',
|
||
'text',
|
||
'poster',
|
||
'priority'
|
||
],
|
||
|
||
attributesToHighlight: ['text'],
|
||
attributesToCrop: ['text'],
|
||
cropLength: 50,
|
||
cropMarker: '…',
|
||
|
||
sort: ['startDate:asc', 'priority:desc']
|
||
});
|
||
|
||
renderResults(response.hits);
|
||
|
||
} catch (e) {
|
||
if (e.name !== 'AbortError') {
|
||
console.error('[Meilisearch]', e);
|
||
}
|
||
}
|
||
});
|
||
|
||
// ----------------------------
|
||
// Render results via <template>
|
||
// ----------------------------
|
||
function renderResults(hits) {
|
||
|
||
results.innerHTML = '';
|
||
|
||
if (!hits || !hits.length) {
|
||
return;
|
||
}
|
||
|
||
for (const hit of hits) {
|
||
|
||
const node = template.content.cloneNode(true);
|
||
const item = node.firstElementChild;
|
||
|
||
// TYPE → CSS-Klasse
|
||
if (hit.type) {
|
||
item.classList.add(
|
||
String(hit.type)
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9_-]/g, '')
|
||
);
|
||
}
|
||
|
||
const image = item.querySelector('.siteimage');
|
||
const img = image.querySelector('img');
|
||
const title = item.querySelector('.title');
|
||
const extract = item.querySelector('.extract');
|
||
const path = item.querySelector('.pfad');
|
||
const link = item.querySelector('.masterurl');
|
||
|
||
title.textContent = hit.title || '';
|
||
link.href = hit.url || '#';
|
||
link.title = hit.title || '';
|
||
|
||
if (hit._formatted?.text) {
|
||
extract.innerHTML = hit._formatted.text;
|
||
} else {
|
||
extract.textContent = '';
|
||
}
|
||
|
||
if (hit.url) {
|
||
path.textContent = hit.url.replace(/^https?:\/\//, '');
|
||
}
|
||
|
||
const img = image.querySelector('img');
|
||
|
||
if (hit.poster) {
|
||
img.src = hit.poster;
|
||
img.alt = hit.title || '';
|
||
image.classList.remove('is-empty');
|
||
} else {
|
||
img.removeAttribute('src');
|
||
img.alt = '';
|
||
image.classList.add('is-empty');
|
||
}
|
||
|
||
results.appendChild(node);
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
<!-- indexer::continue --> |