This commit is contained in:
web-devinition.de 2026-02-25 10:22:35 +01:00 committed by GitHub
commit a556fbdffa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
158 changed files with 25953 additions and 307 deletions

View file

@ -0,0 +1,70 @@
import {Controller} from "@hotwired/stimulus";
import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
import {marked} from "marked";
export default class extends Controller {
_tomSelect;
connect() {
let settings = {
allowEmptyOption: true,
plugins: ['dropdown_input', 'clear_button'],
searchField: ["name", "description", "category", "footprint"],
valueField: "id",
labelField: "name",
preload: "focus",
render: {
item: (data, escape) => {
return '<span>' + (data.image ? "<img style='height: 1.5rem; margin-right: 5px;' ' src='" + data.image + "'/>" : "") + escape(data.name) + '</span>';
},
option: (data, escape) => {
if(data.text) {
return '<span>' + escape(data.text) + '</span>';
}
let tmp = '<div class="row m-0">' +
"<div class='col-2 p-0 d-flex align-items-center' style='max-width: 80px;'>" +
(data.image ? "<img class='typeahead-image' src='" + data.image + "'/>" : "") +
"</div>" +
"<div class='col-10'>" +
'<h6 class="m-0">' + escape(data.name) + '</h6>' +
(data.description ? '<p class="m-0">' + marked.parseInline(data.description) + '</p>' : "") +
(data.category ? '<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span> ' + escape(data.category) : "");
return tmp + '</p>' +
'</div></div>';
}
}
};
if (this.element.dataset.autocomplete) {
const base_url = this.element.dataset.autocomplete;
settings.valueField = "id";
settings.load = (query, callback) => {
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
fetch(url)
.then(response => response.json())
.then(json => {callback(json);})
.catch(() => {
callback()
});
};
this._tomSelect = new TomSelect(this.element, settings);
//this._tomSelect.clearOptions();
}
}
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
}

View file

@ -54,10 +54,19 @@ export default class extends Controller {
}
initialize() {
// The endpoint for searching parts
// The endpoint for searching parts or assemblies
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 template for the assembly detail pages
const assembly_detail_uri_template = this.element.dataset.assemblyDetailUrl;
// The URL template for the project detail pages
const project_detail_uri_template = this.element.dataset.projectDetailUrl;
const hasAssemblyDetailUrl =
typeof assembly_detail_uri_template === "string" && assembly_detail_uri_template.length > 0;
const hasProjectDetailUrl =
typeof project_detail_uri_template === "string" && project_detail_uri_template.length > 0;
//The URL of the placeholder picture
const placeholder_image = this.element.dataset.placeholderImage;
@ -72,6 +81,43 @@ export default class extends Controller {
limit: 5,
});
// Cache the last query to avoid fetching the same endpoint twice (parts source + assemblies source)
let lastQuery = null;
let lastFetchPromise = null;
const fetchMixedItems = (query) => {
if (query === lastQuery && lastFetchPromise) {
return lastFetchPromise;
}
lastQuery = query;
const urlString = base_url.replace('__QUERY__', encodeURIComponent(query));
const url = new URL(urlString, window.location.href);
if (hasAssemblyDetailUrl || hasProjectDetailUrl) {
url.searchParams.set('multidatasources', '1');
}
lastFetchPromise = fetch(url.toString())
.then((response) => response.json())
.then((items) => {
//Iterate over all fields besides the id and highlight them (if present)
const fields = ["name", "description", "category", "footprint"];
items.forEach((item) => {
for (const field of fields) {
if (item[field] !== undefined && item[field] !== null) {
item[field] = that._highlight(item[field], query);
}
}
});
return items;
});
return lastFetchPromise;
};
this._autocomplete = autocomplete({
container: this.element,
//Place the panel in the navbar, if the element is in navbar mode
@ -102,7 +148,7 @@ export default class extends Controller {
},
// If the form is submitted, forward the term to the form
onSubmit({state, event, ...setters}) {
onSubmit({ state, event, ...setters }) {
//Put the current text into each target input field
const input = that.inputTarget;
@ -119,31 +165,15 @@ export default class extends Controller {
input.form.requestSubmit();
},
getSources({ query }) {
return [
// The parts source
const sources = [
// Parts source (filtered from mixed endpoint results)
{
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;
return fetchMixedItems(query).then((items) =>
items.filter((item) => item.type !== "assembly")
);
},
getItemUrl({ item }) {
return part_detail_uri_template.replace('__ID__', item.id);
@ -151,36 +181,130 @@ export default class extends Controller {
templates: {
header({ html }) {
return html`<span class="aa-SourceHeaderTitle">${trans("part.labelp")}</span>
<div class="aa-SourceHeaderLine" />`;
<div class="aa-SourceHeaderLine" />`;
},
item({item, components, html}) {
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>
<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-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 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>
</div>
</a>
`;
</a>
`;
},
},
},
];
if (hasAssemblyDetailUrl) {
sources.push(
// Assemblies source (filtered from the same mixed endpoint results)
{
sourceId: 'assemblies',
getItems() {
return fetchMixedItems(query).then((items) =>
items.filter((item) => item.type === "assembly")
);
},
getItemUrl({ item }) {
return assembly_detail_uri_template.replace('__ID__', item.id);
},
templates: {
header({ html }) {
return html`<span class="aa-SourceHeaderTitle">${trans("assembly.labelp")}</span>
<div class="aa-SourceHeaderLine" />`;
},
item({ item, components, html }) {
const details_url = assembly_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'})}
</div>
</div>
</div>
</a>
`;
},
},
}
);
}
if (hasProjectDetailUrl) {
sources.push(
// Projects source (filtered from the same mixed endpoint results)
{
sourceId: 'projects',
getItems() {
return fetchMixedItems(query).then((items) =>
items.filter((item) => item.type === "project")
);
},
getItemUrl({ item }) {
return project_detail_uri_template.replace('__ID__', item.id);
},
templates: {
header({ html }) {
return html`<span class="aa-SourceHeaderTitle">${trans("project.labelp")}</span>
<div class="aa-SourceHeaderLine" />`;
},
item({ item, components, html }) {
const details_url = project_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'})}
</div>
</div>
</div>
</a>
`;
},
},
}
);
}
return sources;
},
});
@ -192,6 +316,5 @@ export default class extends Controller {
this._autocomplete.setIsOpen(false);
});
}
}
}

View file

@ -0,0 +1,62 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = {
classes: Array
};
connect() {
this.displayCheckbox = this.element.querySelector("#display");
this.displaySelect = this.element.querySelector("select#display");
if (this.displayCheckbox) {
this.toggleContainers(this.displayCheckbox.checked);
this.displayCheckbox.addEventListener("change", (event) => {
this.toggleContainers(event.target.checked);
});
}
if (this.displaySelect) {
this.toggleContainers(this.hasDisplaySelectValue());
this.displaySelect.addEventListener("change", () => {
this.toggleContainers(this.hasDisplaySelectValue());
});
}
}
/**
* Check whether a value was selected in the selectbox
* @returns {boolean} True when a value has not been selected that is not empty
*/
hasDisplaySelectValue() {
return this.displaySelect && this.displaySelect.value !== "";
}
/**
* Hides specified containers if the state is active (checkbox checked or select with value).
*
* @param {boolean} isActive - True when the checkbox is activated or the selectbox has a value.
*/
toggleContainers(isActive) {
if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) {
return;
}
this.classesValue.forEach((cssClass) => {
const elements = document.querySelectorAll(`.${cssClass}`);
if (!elements.length) {
return;
}
elements.forEach((element) => {
element.style.display = isActive ? "none" : "";
});
});
}
}