From f9ce1e4db40626cb5e580258dbfa7fdfa8b87d0a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 10 Jul 2022 03:58:07 +0200 Subject: [PATCH] More work on searchable mappings --- UI/Input/InputElementMap.ts | 4 +- UI/Input/SearchableMappingsSelector.ts | 241 +++++++++++++++++++++++ UI/Popup/TagRenderingQuestion.ts | 79 +++++++- assets/layers/shops/shops.json | 7 + assets/themes/healthcare/healthcare.json | 40 ++-- css/index-tailwind-output.css | 63 +++--- index.css | 6 + langs/en.json | 1 + test.ts | 184 +---------------- 9 files changed, 398 insertions(+), 227 deletions(-) create mode 100644 UI/Input/SearchableMappingsSelector.ts diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts index 3cb1a60cc..0f0d74d0c 100644 --- a/UI/Input/InputElementMap.ts +++ b/UI/Input/InputElementMap.ts @@ -1,5 +1,5 @@ import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import {Store, UIEventSource} from "../../Logic/UIEventSource"; export default class InputElementMap extends InputElement { @@ -13,7 +13,7 @@ export default class InputElementMap extends InputElement { isSame: (x0: X, x1: X) => boolean, toX: (t: T) => X, fromX: (x: X) => T, - extraSources: UIEventSource[] = [] + extraSources: Store[] = [] ) { super(); this.isSame = isSame; diff --git a/UI/Input/SearchableMappingsSelector.ts b/UI/Input/SearchableMappingsSelector.ts new file mode 100644 index 000000000..02ab40dc3 --- /dev/null +++ b/UI/Input/SearchableMappingsSelector.ts @@ -0,0 +1,241 @@ +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 + public constructor( + shown: string | BaseUIElement, + mainTerm: Record, + search: Store, + options?: { + searchTerms?: Record, + selected?: UIEventSource, + forceSelected?: UIEventSource + } + ) { + super(); + this._shown = Translations.W(shown); + 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() + return searchTerms[Locale.language.data].some(t => t.indexOf(s) >= 0); + }, [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") + } + }) + + el.onClick(() => selected.setData(!selected.data)) + + return el.SetClass("border border-black rounded-full 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 selectedElements: UIEventSource; + + public readonly someMatchFound: Store; + + constructor( + values: { show: BaseUIElement, value: T, mainTerm: Record, searchTerms?: Record }[], + options?: { + mode?: "select-one" | "select-many", + selectedElements?: UIEventSource, + searchValue?: UIEventSource, + onNoMatches?: BaseUIElement, + onNoSearchMade?: BaseUIElement, + selectIfSingle?: false | boolean, + searchAreaClass?: string + }) { + + const search = new TextField({value: options?.searchValue}) + + const searchBar = 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 + }) + + + return { + ...v, + show: toggle + }; + }) + + + let somethingShown: Store + if (options.selectIfSingle) { + let forcedSelection : { value: T, show: SelfHidingToggle }= undefined + somethingShown = 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 (this.selectedElements.data.indexOf(lastShownValue.value) < 0) { + this.selectedElements.setData([lastShownValue.value]) + lastShownValue.show.forceSelected.setData(true) + forcedSelection = lastShownValue + } + } else if (forcedSelection != undefined) { + this.selectedElements.setData([]) + forcedSelection.show.forceSelected.setData(false) + forcedSelection = undefined; + } + + return totalShown > 0 + }, mappedValues.map(mv => mv.show.GetValue())) + } else { + somethingShown = searchValue.map(_ => mappedValues.some(mv => mv.show.isShown.data), mappedValues.map(mv => mv.show.GetValue())) + + } + + super([ + searchBar, + new VariableUiElement(Locale.language.map(lng => { + if (options?.onNoSearchMade !== undefined && (searchValue.data === undefined || searchValue.data.length === 0)) { + return options?.onNoSearchMade + } + if (!somethingShown.data) { + 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") + .SetClass(options?.searchAreaClass ?? "") + }, [somethingShown, searchValue])) + + ]) + this.selectedElements = selectedElements; + this.someMatchFound = somethingShown; + + } + + public GetValue(): UIEventSource { + return this.selectedElements; + } + + IsValid(t: T[]): boolean { + return true; + } + + +} + diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 9278a86c7..14104f463 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -31,6 +31,7 @@ import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; import Title from "../Base/Title"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import {GeoOperations} from "../../Logic/GeoOperations"; +import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector"; /** * Shows the question element. @@ -141,19 +142,25 @@ export default class TagRenderingQuestion extends Combine { private static GenerateInputElement( state: FeaturePipelineState, configuration: TagRenderingConfig, - applicableMappings: { if: TagsFilter, then: TypedTranslation, icon?: string, ifnot?: TagsFilter, addExtraTags: Tag[] }[], + applicableMappings: { if: TagsFilter, then: TypedTranslation, icon?: string, ifnot?: TagsFilter, addExtraTags: Tag[], searchTerms?: Record }[], applicableUnit: Unit, tagsSource: UIEventSource, feedback: UIEventSource ): ReadonlyInputElement { - // FreeForm input will be undefined if not present; will already contain a special input element if applicable - const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback); - + const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0 let inputEls: InputElement[]; const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined) + + if(applicableMappings.length > 8 && !ifNotsPresent && (configuration.freeform?.type === undefined || configuration.freeform?.type === "string")){ + return TagRenderingQuestion.GenerateSearchableSelector(state, configuration, applicableMappings, tagsSource) + } + + + // FreeForm input will be undefined if not present; will already contain a special input element if applicable + const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback); function allIfNotsExcept(excludeIndex: number): TagsFilter[] { if (configuration.mappings === undefined || configuration.mappings.length === 0) { @@ -221,6 +228,64 @@ export default class TagRenderingQuestion extends Combine { } + private static GenerateSearchableSelector( + state: FeaturePipelineState, + configuration: TagRenderingConfig, + applicableMappings: { if: TagsFilter; then: TypedTranslation; icon?: string; iconClass?: string, addExtraTags: Tag[], searchTerms?: Record }[], tagsSource: UIEventSource): InputElement{ + const values : { show: BaseUIElement, value: TagsFilter, mainTerm: Record, searchTerms?: Record }[] = [] + for (const mapping of applicableMappings) { + const tr = mapping.then.Subs(tagsSource.data) + const patchedMapping = <{iconClass: "small-height", then: TypedTranslation}> {...mapping, iconClass: "small-height"} + const fancy = TagRenderingQuestion.GenerateMappingContent(patchedMapping, tagsSource, state).SetClass("normal-background") + values.push({ + show: fancy, + value: mapping.if, + mainTerm: tr.translations, + searchTerms: mapping.searchTerms + }) + } + + const searchValue: UIEventSource = new UIEventSource(undefined) + const ff = configuration.freeform + let onEmpty : BaseUIElement = undefined + if(ff !== undefined){ + onEmpty = new VariableUiElement(searchValue.map(search => configuration.render.Subs({[ff.key] : search}))) + } + + const classes = "h-32 overflow-scroll" + const presetSearch = new SearchablePillsSelector(values,{ + selectIfSingle: true, + mode: configuration.multiAnswer ? "select-many" : "select-one", + searchValue, + onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"), + searchAreaClass:classes + }) + return new InputElementMap(presetSearch, + (x0, x1) => false, + arr => { + console.log("Arr is ", arr) + if(arr[0] !== undefined){ + return new And(arr) + } + if(ff !== undefined && searchValue.data?.length > 0 && !presetSearch.someMatchFound.data){ + const t = new Tag(ff.key, searchValue.data) + if(ff.addExtraTags){ + return new And([t, ...ff.addExtraTags]) + } + return t; + + } + return undefined; + }, + tf => { + if(tf["and"] !== undefined){ + return tf["and"]; + } + return [tf]; + }, + [searchValue, presetSearch.someMatchFound] + ); + } private static GenerateMultiAnswer( configuration: TagRenderingConfig, @@ -337,7 +402,7 @@ export default class TagRenderingQuestion extends Combine { then: Translation, addExtraTags: Tag[], icon?: string, - iconClass?: string + iconClass?: "small" | "medium" | "large" | "small-height" }, ifNot?: TagsFilter[]): InputElement { let tagging: TagsFilter = mapping.if; @@ -358,13 +423,13 @@ export default class TagRenderingQuestion extends Combine { private static GenerateMappingContent(mapping: { then: Translation, icon?: string, - iconClass?: string + iconClass?: "small" | "medium" | "large" | "small-height" }, tagsSource: UIEventSource, state: FeaturePipelineState): BaseUIElement { const text = new SubstitutedTranslation(mapping.then, tagsSource, state) if (mapping.icon === undefined) { return text; } - return new Combine([new Img(mapping.icon).SetClass("mapping-icon-"+(mapping.iconClass ?? "small")), text]).SetClass("flex") + return new Combine([new Img(mapping.icon).SetClass("mr-1 mapping-icon-"+(mapping.iconClass ?? "small")), text]).SetClass("flex") } private static GenerateFreeform(state: FeaturePipelineState, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource, feedback: UIEventSource) diff --git a/assets/layers/shops/shops.json b/assets/layers/shops/shops.json index d72121c86..594c788da 100644 --- a/assets/layers/shops/shops.json +++ b/assets/layers/shops/shops.json @@ -105,6 +105,13 @@ "question": { "en": "What kind of shop is this?", "nl": "Wat voor soort winkel is dit?" + }, + "render": { + "en":"This is a {shop}" + }, + "freeform": { + "key": "shop", + "addExtraTags": ["fixme=freeform shop key used, to be reviewed"] } } }, diff --git a/assets/themes/healthcare/healthcare.json b/assets/themes/healthcare/healthcare.json index b527c884a..482d21396 100644 --- a/assets/themes/healthcare/healthcare.json +++ b/assets/themes/healthcare/healthcare.json @@ -1,22 +1,22 @@ { - "id": "healthcare", - "title": { - "en": "Healthcare" - }, - "description": { - "en": "On this map, various healthcare related items are shown" - }, - "maintainer": "MapComplete", - "icon": "./assets/layers/doctors/doctors.svg", - "version": "0", - "startLat": 50.8465573, - "defaultBackgroundId": "CartoDB.Voyager", - "startLon": 4.351697, - "startZoom": 16, - "widenFactor": 2, - "layers": [ - "doctors", - "hospital", - "pharmacy" - ] + "id": "healthcare", + "title": { + "en": "Healthcare" + }, + "description": { + "en": "On this map, various healthcare related items are shown" + }, + "maintainer": "MapComplete", + "icon": "./assets/layers/doctors/doctors.svg", + "version": "0", + "startLat": 50.8465573, + "defaultBackgroundId": "CartoDB.Voyager", + "startLon": 4.351697, + "startZoom": 16, + "widenFactor": 2, + "layers": [ + "doctors", + "hospital", + "pharmacy" + ] } \ No newline at end of file diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 2514d1a9b..4dd901a29 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -858,10 +858,6 @@ video { margin-bottom: 0.75rem; } -.mr-4 { - margin-right: 1rem; -} - .ml-3 { margin-left: 0.75rem; } @@ -890,6 +886,10 @@ video { margin-bottom: 6rem; } +.mr-4 { + margin-right: 1rem; +} + .mt-2 { margin-top: 0.5rem; } @@ -950,6 +950,10 @@ video { margin-top: -3rem; } +.mr-1 { + margin-right: 0.25rem; +} + .mb-0 { margin-bottom: 0px; } @@ -1126,8 +1130,8 @@ video { width: 2rem; } -.w-1\/2 { - width: 50%; +.w-1 { + width: 0.25rem; } .w-24 { @@ -1167,6 +1171,10 @@ video { width: min-content; } +.w-1\/2 { + width: 50%; +} + .w-max { width: -webkit-max-content; width: max-content; @@ -1400,10 +1408,6 @@ video { border-bottom-left-radius: 0.25rem; } -.border-4 { - border-width: 4px; -} - .border { border-width: 1px; } @@ -1412,6 +1416,10 @@ video { border-width: 2px; } +.border-4 { + border-width: 4px; +} + .border-l-4 { border-left-width: 4px; } @@ -1420,16 +1428,16 @@ video { border-bottom-width: 1px; } -.border-black { - --tw-border-opacity: 1; - border-color: rgba(0, 0, 0, var(--tw-border-opacity)); -} - .border-gray-500 { --tw-border-opacity: 1; border-color: rgba(107, 114, 128, var(--tw-border-opacity)); } +.border-black { + --tw-border-opacity: 1; + border-color: rgba(0, 0, 0, var(--tw-border-opacity)); +} + .border-gray-400 { --tw-border-opacity: 1; border-color: rgba(156, 163, 175, var(--tw-border-opacity)); @@ -1508,14 +1516,14 @@ video { padding: 0.75rem; } -.p-1 { - padding: 0.25rem; -} - .p-4 { padding: 1rem; } +.p-1 { + padding: 0.25rem; +} + .p-2 { padding: 0.5rem; } @@ -1528,16 +1536,16 @@ video { padding: 0.125rem; } -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - .px-0 { padding-left: 0px; padding-right: 0px; } +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + .pr-2 { padding-right: 0.5rem; } @@ -2455,6 +2463,13 @@ input { /* Additional class on the first layer filter */ } +.mapping-icon-small-height { + /* A mapping icon type */ + height: 1.5rem; + margin-right: 0.5rem; + width: unset; +} + .mapping-icon-small { /* A mapping icon type */ width: 1.5rem; diff --git a/index.css b/index.css index 7e8799923..cbba97a37 100644 --- a/index.css +++ b/index.css @@ -620,6 +620,12 @@ input { /* Additional class on the first layer filter */ } +.mapping-icon-small-height { + /* A mapping icon type */ + height: 1.5rem; + margin-right: 0.5rem; + width: unset; +} .mapping-icon-small { /* A mapping icon type */ diff --git a/langs/en.json b/langs/en.json index 45b119476..e893fb262 100644 --- a/langs/en.json +++ b/langs/en.json @@ -160,6 +160,7 @@ }, "nameInlineQuestion": "The name of this {category} is $$$", "next": "Next", + "noMatchingMapping": "No entries mapped your search…", "noNameCategory": "{category} without a name", "noTagsSelected": "No tags selected", "notValid": "Select a valid value to continue", diff --git a/test.ts b/test.ts index 916a46654..e25a2ad54 100644 --- a/test.ts +++ b/test.ts @@ -2,187 +2,18 @@ import * as shops from "./assets/generated/layers/shops.json" import Combine from "./UI/Base/Combine"; import Img from "./UI/Base/Img"; import BaseUIElement from "./UI/BaseUIElement"; -import Svg from "./Svg"; -import {TextField} from "./UI/Input/TextField"; -import {Store, UIEventSource} from "./Logic/UIEventSource"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import Locale from "./UI/i18n/Locale"; import LanguagePicker from "./UI/LanguagePicker"; -import {InputElement} from "./UI/Input/InputElement"; -import {UIElement} from "./UI/UIElement"; -import Translations from "./UI/i18n/Translations"; import TagRenderingConfig, {Mapping} from "./Models/ThemeConfig/TagRenderingConfig"; import {MappingConfigJson} from "./Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; import {FixedUiElement} from "./UI/Base/FixedUiElement"; import {TagsFilter} from "./Logic/Tags/TagsFilter"; +import {SearchablePillsSelector} from "./UI/Input/SearchableMappingsSelector"; +import {UIEventSource} from "./Logic/UIEventSource"; const mappingsRaw: MappingConfigJson[] = shops.tagRenderings.find(tr => tr.id == "shop_types").mappings const mappings = mappingsRaw.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, "test", "test")) - -export class SelfHidingToggle extends UIElement implements InputElement { - private readonly _shown: BaseUIElement; - private readonly _searchTerms: Record; - private readonly _search: Store; - - private readonly _selected: UIEventSource - - public constructor( - shown: string | BaseUIElement, - mainTerm: Record, - search: Store, - searchTerms?: Record, - selected: UIEventSource = new UIEventSource(false) - ) { - super(); - this._shown = Translations.W(shown); - this._search = search; - this._searchTerms = {}; - for (const lng in searchTerms ?? []) { - if (lng === "_context") { - continue - } - this._searchTerms[lng] = searchTerms[lng].map(t => t.trim().toLowerCase()) - } - for (const lng in mainTerm) { - if (lng === "_context") { - continue - } - this._searchTerms[lng] = [mainTerm[lng]].concat(this._searchTerms[lng] ?? []) - } - this._selected = selected; - } - - - GetValue(): UIEventSource { - return this._selected - } - - IsValid(t: boolean): boolean { - return true; - } - - protected InnerRender(): string | BaseUIElement { - let el: BaseUIElement = this._shown; - const selected = this._selected; - const search = this._search; - const terms = this._searchTerms; - const applySearch = () => { - const s = search.data?.trim()?.toLowerCase() - if (s === undefined || s.length === 0 || selected.data) { - el.RemoveClass("hidden") - return; - } - - if (terms[Locale.language.data].some(t => t.toLowerCase().indexOf(s) >= 0)) { - el.RemoveClass("hidden"); - return; - } - - el.SetClass("hidden") - } - search.addCallbackAndRun(_ => { - applySearch() - }) - Locale.language.addCallback(_ => { - applySearch() - }) - - selected.addCallbackAndRun(selected => { - if (selected) { - el.SetClass("border-4") - el.RemoveClass("border") - el.SetStyle("margin: calc( 0.25rem )") - } else { - el.SetStyle("margin: calc( 0.25rem + 3px )") - el.SetClass("border") - el.RemoveClass("border-4") - } - applySearch() - }) - - el.onClick(() => selected.setData(!selected.data)) - - return el.SetClass("border border-black rounded-full p-1 px-4") - } -} - - -class SearchablePresets extends Combine implements InputElement { - private selectedElements: UIEventSource; - - constructor( - values: { show: BaseUIElement, value: T, mainTerm: Record, searchTerms?: Record }[], - mode: "select-one" | "select-many", - selectedElements: UIEventSource = new UIEventSource([])) { - - const search = new TextField({}) - - const searchBar = new Combine([Svg.search_svg().SetClass("w-8"), search.SetClass("mr-4 w-full")]) - .SetClass("flex rounded-full border-2 border-black items-center my-2 w-1/2") - - const searchValue = search.GetValue().map(s => s?.trim()?.toLowerCase()) - - - values = 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; - } - } - } - }) - - return { - ...v, - show: new SelfHidingToggle(v.show, v.mainTerm, searchValue, v.searchTerms, vIsSelected) - }; - }) - - super([ - searchBar, - new VariableUiElement(Locale.language.map(lng => { - values.sort((a, b) => a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1) - return new Combine(values.map(e => e.show)) - .SetClass("flex flex-wrap w-full") - })) - - ]) - this.selectedElements = selectedElements; - - } - - public GetValue(): UIEventSource { - return this.selectedElements; - } - - IsValid(t: T[]): boolean { - return true; - } - - -} - - function fromMapping(m: Mapping): { show: BaseUIElement, value: TagsFilter, mainTerm: Record, searchTerms?: Record } { const el: BaseUIElement = m.then let icon: BaseUIElement @@ -199,10 +30,15 @@ function fromMapping(m: Mapping): { show: BaseUIElement, value: TagsFilter, main return {show, mainTerm: m.then.translations, searchTerms: m.searchTerms, value: m.if}; } - -const sp = new SearchablePresets( +const search = new UIEventSource("") +const sp = new SearchablePillsSelector( mappings.map(m => fromMapping(m)), - "select-one" + { + noMatchFound: new VariableUiElement(search.map(s => "Mark this a `"+s+"`")), + onNoSearch: new FixedUiElement("Search in "+mappingsRaw.length+" categories"), + selectIfSingle: true, + searchValue: search + } ) sp.AttachTo("maindiv")