mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
This fixes issue #701. For the search field this was caused by algolia/autocomplete lib, which do not support multiple autocomplete fields on a single page. If initailly loaded on the homepage, which features a second autocomplete, this one "steals" the input listening, and the one in the navbar do not close anymore when clicking outside. Custom code which triggers the closing of the autocomplete manually when clicking outside, was added as a workaround.
200 lines
No EOL
8.5 KiB
JavaScript
200 lines
No EOL
8.5 KiB
JavaScript
/*
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
*
|
|
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { Controller } from "@hotwired/stimulus";
|
|
import { autocomplete } from '@algolia/autocomplete-js';
|
|
//import "@algolia/autocomplete-theme-classic/dist/theme.css";
|
|
import "../../css/components/autocomplete_bootstrap_theme.css";
|
|
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
|
|
import {marked} from "marked";
|
|
|
|
import {
|
|
trans,
|
|
SEARCH_PLACEHOLDER,
|
|
SEARCH_SUBMIT,
|
|
STATISTICS_PARTS
|
|
} from '../../translator';
|
|
|
|
|
|
/**
|
|
* This controller is responsible for the search fields in the navbar and the homepage.
|
|
* It uses the Algolia Autocomplete library to provide a fast and responsive search.
|
|
*/
|
|
export default class extends Controller {
|
|
|
|
static targets = ["input"];
|
|
|
|
_autocomplete;
|
|
|
|
// Highlight the search query in the results
|
|
_highlight = (text, query) => {
|
|
if (!text) return text;
|
|
if (!query) return text;
|
|
|
|
const HIGHLIGHT_PRE_TAG = '__aa-highlight__'
|
|
const HIGHLIGHT_POST_TAG = '__/aa-highlight__'
|
|
|
|
const escaped = query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
const regex = new RegExp(escaped, 'gi');
|
|
|
|
return text.replace(regex, (match) => `${HIGHLIGHT_PRE_TAG}${match}${HIGHLIGHT_POST_TAG}`);
|
|
}
|
|
|
|
initialize() {
|
|
// The endpoint for searching parts
|
|
const base_url = this.element.dataset.autocomplete;
|
|
// The URL template for the part detail pages
|
|
const part_detail_uri_template = this.element.dataset.detailUrl;
|
|
|
|
//The URL of the placeholder picture
|
|
const placeholder_image = this.element.dataset.placeholderImage;
|
|
|
|
//If the element is in navbar mode, or not
|
|
const navbar_mode = this.element.dataset.navbarMode === "true";
|
|
|
|
const that = this;
|
|
|
|
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
|
|
key: 'RECENT_SEARCH',
|
|
limit: 5,
|
|
});
|
|
|
|
this._autocomplete = autocomplete({
|
|
container: this.element,
|
|
//Place the panel in the navbar, if the element is in navbar mode
|
|
panelContainer: navbar_mode ? document.getElementById("navbar-search-form") : document.body,
|
|
panelPlacement: this.element.dataset.panelPlacement,
|
|
plugins: [recentSearchesPlugin],
|
|
openOnFocus: true,
|
|
placeholder: trans(SEARCH_PLACEHOLDER),
|
|
translations: {
|
|
submitButtonTitle: trans(SEARCH_SUBMIT)
|
|
},
|
|
|
|
// Use a navigator compatible with turbo:
|
|
navigator: {
|
|
navigate({ itemUrl }) {
|
|
window.Turbo.visit(itemUrl, { action: "advance" });
|
|
},
|
|
navigateNewTab({ itemUrl }) {
|
|
const windowReference = window.open(itemUrl, '_blank', 'noopener');
|
|
|
|
if (windowReference) {
|
|
windowReference.focus();
|
|
}
|
|
},
|
|
navigateNewWindow({ itemUrl }) {
|
|
window.open(itemUrl, '_blank', 'noopener');
|
|
},
|
|
},
|
|
|
|
// If the form is submitted, forward the term to the form
|
|
onSubmit({state, event, ...setters}) {
|
|
//Put the current text into each target input field
|
|
const input = that.inputTarget;
|
|
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
//Do not submit the form, if the input is empty
|
|
if (state.query === "") {
|
|
return;
|
|
}
|
|
|
|
input.value = state.query;
|
|
input.form.requestSubmit();
|
|
},
|
|
|
|
|
|
getSources({ query }) {
|
|
return [
|
|
// The parts source
|
|
{
|
|
sourceId: 'parts',
|
|
getItems() {
|
|
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
|
|
|
const data = fetch(url)
|
|
.then((response) => response.json())
|
|
;
|
|
|
|
//Iterate over all fields besides the id and highlight them
|
|
const fields = ["name", "description", "category", "footprint"];
|
|
|
|
data.then((items) => {
|
|
items.forEach((item) => {
|
|
for (const field of fields) {
|
|
item[field] = that._highlight(item[field], query);
|
|
}
|
|
});
|
|
});
|
|
|
|
return data;
|
|
},
|
|
getItemUrl({ item }) {
|
|
return part_detail_uri_template.replace('__ID__', item.id);
|
|
},
|
|
templates: {
|
|
header({ html }) {
|
|
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PARTS)}</span>
|
|
<div class="aa-SourceHeaderLine" />`;
|
|
},
|
|
item({item, components, html}) {
|
|
const details_url = part_detail_uri_template.replace('__ID__', item.id);
|
|
|
|
return html`
|
|
<a class="aa-ItemLink" href="${details_url}">
|
|
<div class="aa-ItemContent">
|
|
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
|
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
|
</div>
|
|
<div class="aa-ItemContentBody">
|
|
<div class="aa-ItemContentTitle">
|
|
<b>
|
|
${components.Highlight({hit: item, attribute: 'name'})}
|
|
</b>
|
|
</div>
|
|
<div class="aa-ItemContentDescription">
|
|
${components.Highlight({hit: item, attribute: 'description'})}
|
|
${item.category ? html`<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span>${components.Highlight({hit: item, attribute: 'category'})}</p>` : ""}
|
|
${item.footprint ? html`<p class="m-0"><span class="fa-solid fa-microchip fa-fw"></span>${components.Highlight({hit: item, attribute: 'footprint'})}</p>` : ""}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
`;
|
|
},
|
|
},
|
|
},
|
|
];
|
|
},
|
|
});
|
|
|
|
//Try to find the input field and register a defocus handler. This is necessarry, as by default the autocomplete
|
|
//lib has problems when multiple inputs are present on the page. (see https://github.com/algolia/autocomplete/issues/1216)
|
|
const inputs = this.element.getElementsByClassName('aa-Input');
|
|
for (const input of inputs) {
|
|
input.addEventListener('blur', () => {
|
|
this._autocomplete.setIsOpen(false);
|
|
});
|
|
}
|
|
|
|
}
|
|
} |