import {UIElement} from "../UIElement"; import {InputElement} from "./InputElement"; import BaseUIElement from "../BaseUIElement"; import {Store, UIEventSource} from "../../Logic/UIEventSource"; import Translations from "../i18n/Translations"; import Locale from "../i18n/Locale"; import Combine from "../Base/Combine"; import {TextField} from "./TextField"; import Svg from "../../Svg"; import {VariableUiElement} from "../Base/VariableUIElement"; /** * A single 'pill' which can hide itself if the search criteria is not met */ class SelfHidingToggle extends UIElement implements InputElement { private readonly _shown: BaseUIElement; public readonly _selected: UIEventSource public readonly isShown: Store = new UIEventSource(true); public readonly forceSelected: UIEventSource private readonly _squared: boolean; public constructor( shown: string | BaseUIElement, mainTerm: Record, search: Store, options?: { searchTerms?: Record, selected?: UIEventSource, forceSelected?: UIEventSource, squared?: boolean } ) { super(); this._shown = Translations.W(shown); this._squared = options?.squared ?? false; const searchTerms: Record = {}; for (const lng in options?.searchTerms ?? []) { if (lng === "_context") { continue } searchTerms[lng] = options?.searchTerms[lng]?.map(SelfHidingToggle.clean) } for (const lng in mainTerm) { if (lng === "_context") { continue } const main = SelfHidingToggle.clean( mainTerm[lng]) searchTerms[lng] = [main].concat(searchTerms[lng] ?? []) } const selected = this._selected = options?.selected ?? new UIEventSource(false); const forceSelected = this.forceSelected = options?.forceSelected ?? new UIEventSource(false) this.isShown = search.map(s => { if (s === undefined || s.length === 0) { return true; } if (selected.data && !forceSelected.data) { return true } s = s?.trim()?.toLowerCase() if(searchTerms[Locale.language.data]?.some(t => t.indexOf(s) >= 0)){ return true } if(searchTerms["*"]?.some(t => t.indexOf(s) >= 0)){ return true } return false; }, [selected, Locale.language]) const self = this; this.isShown.addCallbackAndRun(shown => { if (shown) { self.RemoveClass("hidden") } else { self.SetClass("hidden") } }) } private static clean(s: string) : string{ return s?.trim()?.toLowerCase()?.replace(/[-]/, "") } GetValue(): UIEventSource { return this._selected } IsValid(t: boolean): boolean { return true; } protected InnerRender(): string | BaseUIElement { let el: BaseUIElement = this._shown; const selected = this._selected; selected.addCallbackAndRun(selected => { if (selected) { el.SetClass("border-4") el.RemoveClass("border") el.SetStyle("margin: 0") } else { el.SetStyle("margin: 3px") el.SetClass("border") el.RemoveClass("border-4") } }) const forcedSelection = this.forceSelected el.onClick(() => { if(forcedSelection.data){ selected.setData(true) }else{ selected.setData(!selected.data); } }) if(!this._squared){ el.SetClass("rounded-full") } return el.SetClass("border border-black p-1 px-4") } } /** * The searchable mappings selector is a selector which shows various pills from which one (or more) options can be chosen. * A searchfield can be used to filter the values */ export class SearchablePillsSelector extends Combine implements InputElement { private readonly selectedElements: UIEventSource; public readonly someMatchFound: Store; /** * * @param values * @param options */ constructor( values: { show: BaseUIElement, value: T, mainTerm: Record, searchTerms?: Record }[], options?: { mode?: "select-one" | "select-many", selectedElements?: UIEventSource, searchValue?: UIEventSource, onNoMatches?: BaseUIElement, onNoSearchMade?: BaseUIElement, /** * Shows this if there are many (>200) possible mappings */ onManyElements?: BaseUIElement, onManyElementsValue?: UIEventSource, selectIfSingle?: false | boolean, searchAreaClass?: string, hideSearchBar?: false | boolean }) { const search = new TextField({value: options?.searchValue}) const searchBar = options?.hideSearchBar ? undefined : new Combine([Svg.search_svg().SetClass("w-8 normal-background"), search.SetClass("w-full")]) .SetClass("flex items-center border-2 border-black m-2") const searchValue = search.GetValue().map(s => s?.trim()?.toLowerCase()) const selectedElements = options?.selectedElements ?? new UIEventSource([]); const mode = options?.mode ?? "select-one"; const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping const mappedValues: { show: SelfHidingToggle, mainTerm: Record, value: T }[] = values.map(v => { const vIsSelected = new UIEventSource(false); selectedElements.addCallbackAndRunD(selectedElements => { vIsSelected.setData(selectedElements.some(t => t === v.value)) }) vIsSelected.addCallback(selected => { if (selected) { if (mode === "select-one") { selectedElements.setData([v.value]) } else if (!selectedElements.data.some(t => t === v.value)) { selectedElements.data.push(v.value); selectedElements.ping() } } else { for (let i = 0; i < selectedElements.data.length; i++) { const t = selectedElements.data[i] if (t == v.value) { selectedElements.data.splice(i, 1) selectedElements.ping() break; } } } }) const toggle = new SelfHidingToggle(v.show, v.mainTerm, searchValue, { searchTerms: v.searchTerms, selected: vIsSelected, squared: mode === "select-many" }) return { ...v, show: toggle }; }) let totalShown: Store if (options.selectIfSingle) { let forcedSelection : { value: T, show: SelfHidingToggle } = undefined totalShown = searchValue.map(_ => { let totalShown = 0; let lastShownValue: { value: T, show: SelfHidingToggle } for (const mv of mappedValues) { const valueIsShown = mv.show.isShown.data if (valueIsShown) { totalShown++; lastShownValue = mv } } if (totalShown == 1) { if (selectedElements.data?.indexOf(lastShownValue.value) < 0) { selectedElements.setData([lastShownValue.value]) lastShownValue.show.forceSelected.setData(true) forcedSelection = lastShownValue } } else if (forcedSelection != undefined) { forcedSelection?.show?.forceSelected?.setData(false) forcedSelection = undefined; selectedElements.setData([]) } return totalShown }, mappedValues.map(mv => mv.show.GetValue())) } else { totalShown = searchValue.map(_ => mappedValues.filter(mv => mv.show.isShown.data).length, mappedValues.map(mv => mv.show.GetValue())) } const tooMuchElementsCutoff = 200; options?.onManyElementsValue?.map(value => { console.log("Installing toMuchElementsValue", value) if(tooMuchElementsCutoff <= totalShown.data){ selectedElements.setData(value) selectedElements.ping() } }, [totalShown]) super([ searchBar, new VariableUiElement(Locale.language.map(lng => { if(totalShown.data >= 200){ return options?.onManyElements ?? Translations.t.general.useSearch; } if (options?.onNoSearchMade !== undefined && (searchValue.data === undefined || searchValue.data.length === 0)) { return options?.onNoSearchMade } if (totalShown.data == 0) { return onEmpty } mappedValues.sort((a, b) => a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1) return new Combine(mappedValues.map(e => e.show)) .SetClass("flex flex-wrap w-full content-start") .SetClass(options?.searchAreaClass ?? "") }, [totalShown, searchValue])) ]) this.selectedElements = selectedElements; this.someMatchFound = totalShown.map(t => t > 0); } public GetValue(): UIEventSource { return this.selectedElements; } IsValid(t: T[]): boolean { return true; } }