forked from MapComplete/MapComplete
		
	More work on searchable mappings
This commit is contained in:
		
							parent
							
								
									dd992a1e0d
								
							
						
					
					
						commit
						f9ce1e4db4
					
				
					 9 changed files with 398 additions and 227 deletions
				
			
		
							
								
								
									
										184
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										184
									
								
								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[] = <any>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<boolean> { | ||||
|     private readonly _shown: BaseUIElement; | ||||
|     private readonly _searchTerms: Record<string, string[]>; | ||||
|     private readonly _search: Store<string>; | ||||
| 
 | ||||
|     private readonly _selected: UIEventSource<boolean> | ||||
| 
 | ||||
|     public constructor( | ||||
|         shown: string | BaseUIElement, | ||||
|         mainTerm: Record<string, string>, | ||||
|         search: Store<string>, | ||||
|         searchTerms?: Record<string, string[]>, | ||||
|         selected: UIEventSource<boolean> = new UIEventSource<boolean>(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<boolean> { | ||||
|         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<T> extends Combine implements InputElement<T[]> { | ||||
|     private selectedElements: UIEventSource<T[]>; | ||||
| 
 | ||||
|     constructor( | ||||
|         values: { show: BaseUIElement, value: T, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]> }[], | ||||
|         mode: "select-one" | "select-many", | ||||
|         selectedElements: UIEventSource<T[]> = new UIEventSource<T[]>([])) { | ||||
| 
 | ||||
|         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<T[]> { | ||||
|         return this.selectedElements; | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: T[]): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function fromMapping(m: Mapping): { show: BaseUIElement, value: TagsFilter, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]> } { | ||||
|     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") | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue