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 } }