diff --git a/UI/Base/FilteredCombine.ts b/UI/Base/FilteredCombine.ts new file mode 100644 index 000000000..06da9c88e --- /dev/null +++ b/UI/Base/FilteredCombine.ts @@ -0,0 +1,40 @@ +import BaseUIElement from "../BaseUIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {VariableUiElement} from "./VariableUIElement"; +import Combine from "./Combine"; +import Locale from "../i18n/Locale"; +import {Utils} from "../../Utils"; + +export default class FilteredCombine extends VariableUiElement { + + /** + * Only shows item matching the search + * If predicate of an item is undefined, it will be filtered out as soon as a non-null or non-empty search term is given + * @param entries + * @param searchedValue + * @param options + */ + constructor(entries: { + element: BaseUIElement | string, + predicate?: (s: string) => boolean + }[], + searchedValue: UIEventSource, + options?: { + onEmpty?: BaseUIElement | string, + innerClasses: string + } + ) { + entries = Utils.NoNull(entries) + super(searchedValue.map(searchTerm => { + if(searchTerm === undefined || searchTerm === ""){ + return new Combine(entries.map(e => e.element)).SetClass(options?.innerClasses ?? "") + } + const kept = entries.filter(entry => entry?.predicate !== undefined && entry.predicate(searchTerm)) + if (kept.length === 0) { + return options?.onEmpty + } + return new Combine(kept.map(entry => entry.element)).SetClass(options?.innerClasses ?? "") + }, [Locale.language])) + } + +} \ No newline at end of file diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index f41680d5d..64b18958f 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -13,14 +13,18 @@ import Loading from "./Loading"; export class SubtleButton extends UIElement { private readonly imageUrl: string | BaseUIElement; private readonly message: string | BaseUIElement; - private readonly linkTo: { url: string | UIEventSource; newTab?: boolean }; + private readonly options: { url?: string | UIEventSource; newTab?: boolean ; imgSize?: string}; - constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, linkTo: { url: string | UIEventSource, newTab?: boolean } = undefined) { + constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, options: { + url?: string | UIEventSource, + newTab?: boolean, + imgSize?: "h-11 w-11" | string + } = undefined) { super(); this.imageUrl = imageUrl; this.message = message; - this.linkTo = linkTo; + this.options = options; } protected InnerRender(): string | BaseUIElement { @@ -34,7 +38,7 @@ export class SubtleButton extends UIElement { } else { img = this.imageUrl; } - const image = new Combine([img?.SetClass("block flex items-center justify-center h-11 w-11 flex-shrink0 mr-4")]) + const image = new Combine([img?.SetClass("block flex items-center justify-center "+(this.options?.imgSize ?? "h-11 w-11")+" flex-shrink0 mr-4")]) .SetClass("flex-shrink-0"); message?.SetClass("block overflow-ellipsis no-images") @@ -44,7 +48,7 @@ export class SubtleButton extends UIElement { message ]).SetClass("flex group w-full") - if (this.linkTo == undefined) { + if (this.options?.url == undefined) { this.SetClass(classes) return button } @@ -52,8 +56,8 @@ export class SubtleButton extends UIElement { return new Link( button, - this.linkTo.url, - this.linkTo.newTab ?? false + this.options.url, + this.options.newTab ?? false ).SetClass(classes) } diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index d53a5b9a2..455807f10 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -16,6 +16,9 @@ import {Utils} from "../../Utils"; import Title from "../Base/Title"; import * as themeOverview from "../../assets/generated/theme_overview.json" import {Translation} from "../i18n/Translation"; +import {TextField} from "../Input/TextField"; +import FilteredCombine from "../Base/FilteredCombine"; +import Locale from "../i18n/Locale"; export default class MoreScreen extends Combine { @@ -32,13 +35,31 @@ export default class MoreScreen extends Combine { themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" } - super([ - MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle), - MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle, themeListStyle), - MoreScreen.createUnofficialThemeList(themeButtonStyle, state, themeListStyle), + const search = new TextField({ + placeholder: tr.searchForATheme, + }) + search.GetValue().addCallbackAndRun(d => console.log("Search is ", d)) + const searchBar = new Combine([Svg.search_svg().SetClass("w-8"), search.SetClass("mr-4 w-full")]) + .SetClass("flex rounded-full border-2 border-black w-max items-center my-2 w-1/2") + + + super([ + new Combine([searchBar]).SetClass("flex justify-center"), + MoreScreen.createOfficialThemesList(state, themeButtonStyle, themeListStyle, search.GetValue()), + MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle, themeListStyle, search.GetValue()), + MoreScreen.createUnofficialThemeList(themeButtonStyle, state, themeListStyle, search.GetValue()), tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10") ]); } + + private static NothingFound(search: UIEventSource): BaseUIElement{ + const t = Translations.t.general.morescreen; + return new Combine([ + new Title(t.noMatchingThemes, 5).SetClass("w-max font-bold"), + new SubtleButton(Svg.search_disable_ui(), t.noSearch,{imgSize: "h-8"}).SetClass("h-12 w-max") + .onClick( () => search.setData("")) + ]).SetClass("flex flex-col items-center w-full") + } /** * Creates a button linking to the given theme @@ -152,7 +173,7 @@ export default class MoreScreen extends Combine { } } - private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement { + private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses: string, search : UIEventSource): BaseUIElement { const prefix = "mapcomplete-unofficial-theme-"; var currentIds: UIEventSource = state.osmConnection.preferencesHandler.preferences @@ -171,11 +192,11 @@ export default class MoreScreen extends Combine { var stableIds = UIEventSource.ListStabilized(currentIds) return new VariableUiElement( stableIds.map(ids => { - const allThemes: BaseUIElement[] = [] + const allThemes: { element: BaseUIElement, predicate?: (s:string) => boolean }[] = [] for (const id of ids) { const link = this.createUnofficialButtonFor(state, id) if (link !== undefined) { - allThemes.push(link.SetClass(buttonClass)) + allThemes.push({element: link.SetClass(buttonClass), predicate : s => id.toLowerCase().indexOf(s) >= 0}) } } if (allThemes.length <= 0) { @@ -183,12 +204,15 @@ export default class MoreScreen extends Combine { } return new Combine([ Translations.t.general.customThemeIntro, - new Combine(allThemes).SetClass(themeListClasses) + new FilteredCombine(allThemes, search, { + innerClasses: themeListClasses, + onEmpty: MoreScreen.NothingFound(search) + }) ]); })); } - private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) { + private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string, search: UIEventSource) : BaseUIElement{ const t = Translations.t.general.morescreen const prefix = "mapcomplete-hidden-theme-" const hiddenThemes = themeOverview["default"].filter(layout => layout.hideFromOverview) @@ -206,9 +230,15 @@ export default class MoreScreen extends Combine { } const knownThemeDescriptions = hiddenThemes.filter(theme => knownThemes.has(theme.id)) - .map(theme => MoreScreen.createLinkButton(state, theme)?.SetClass(buttonClass)); + .map(theme => ({element: MoreScreen.createLinkButton(state, theme)?.SetClass(buttonClass), + predicate: MoreScreen.MatchesLayoutFunc(theme) + })); - const knownLayouts = new Combine(knownThemeDescriptions).SetClass(themeListStyle) + const knownLayouts = new FilteredCombine(knownThemeDescriptions, + search, + {innerClasses: themeListStyle, + onEmpty: MoreScreen.NothingFound(search)} + ) return new Combine([ new Title(t.previouslyHiddenTitle), @@ -228,10 +258,50 @@ export default class MoreScreen extends Combine { } - private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource }, buttonClass: string): BaseUIElement { - let officialThemes = themeOverview["default"]; + private static MatchesLayoutFunc(layout: { + id: string, + title: any, + shortDescription: any + }) { + return (search: string) => { + search = search.toLocaleLowerCase() + if (layout.id.toLowerCase().indexOf(search) >= 0) { + return true; + } + for (const lang in layout.shortDescription) { + if (Locale.language.data !== lang) { + continue + } + if (layout.shortDescription[lang].toLowerCase()?.indexOf(search) >= 0) { + return true + } + } - let buttons = officialThemes.map((layout) => { + for (const lang in layout.title) { + if (Locale.language.data !== lang) { + continue + } + if (layout.title[lang].toLowerCase()?.indexOf(search) >= 0) { + return true + } + } + + return false; + } + } + + private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource }, buttonClass: string, themeListStyle: string, search: UIEventSource):BaseUIElement { + let officialThemes: { + id: string, + icon: string, + title: any, + shortDescription: any, + definition?: any, + mustHaveLanguage?: boolean, + hideFromOverview: boolean + }[] = themeOverview["default"]; + + let buttons: { element: BaseUIElement, predicate?: (s: string) => boolean }[] = officialThemes.map((layout) => { if (layout === undefined) { console.trace("Layout is undefined") return undefined @@ -241,7 +311,7 @@ export default class MoreScreen extends Combine { } const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass); if (layout.id === personal.id) { - return new VariableUiElement( + const element = new VariableUiElement( state.osmConnection.userDetails.map(userdetails => userdetails.csCount) .map(csCount => { if (csCount < Constants.userJourney.personalLayoutUnlock) { @@ -251,15 +321,22 @@ export default class MoreScreen extends Combine { } }) ) + return {element} } - return button; + + + return {element: button, predicate: MoreScreen.MatchesLayoutFunc(layout)}; }) const professional = MoreScreen.CreateProffessionalSerivesButton(); const customGeneratorLink = MoreScreen.createCustomGeneratorButton(state) - buttons.splice(0, 0, customGeneratorLink, professional); - - return new Combine(buttons) + buttons.splice(0, 0, + {element: customGeneratorLink}, + {element: professional}); + return new FilteredCombine(buttons, search, { + innerClasses: themeListStyle, + onEmpty: MoreScreen.NothingFound(search) + }); } /* diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index b08e50edf..1b8bd3941 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -44,7 +44,7 @@ export class TextField extends InputElement { el.type = options.htmlType ?? "text" el.inputMode = options.inputMode el.placeholder = placeholder - el.style.cssText = options.inputStyle + el.style.cssText = options.inputStyle ?? "width: 100%;" inputEl = el } diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index a41659488..6ae728e1b 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -1144,18 +1144,14 @@ video { width: 2.75rem; } -.w-16 { - width: 4rem; -} - .w-min { width: -webkit-min-content; width: -moz-min-content; width: min-content; } -.w-auto { - width: auto; +.w-1\/2 { + width: 50%; } .w-max { @@ -1164,6 +1160,14 @@ video { width: max-content; } +.w-16 { + width: 4rem; +} + +.w-auto { + width: auto; +} + .min-w-min { min-width: -webkit-min-content; min-width: -moz-min-content; diff --git a/langs/en.json b/langs/en.json index 4b6f74a0e..692eab3c7 100644 --- a/langs/en.json +++ b/langs/en.json @@ -144,8 +144,11 @@ "createYourOwnTheme": "Create your own MapComplete theme from scratch", "hiddenExplanation": "These themes are only accessible to those with the link. You have discovered {hidden_discovered} of {total_hidden} hidden themes.", "intro": "

More thematic maps?

Do you enjoy collecting geodata?
There are more themes available.", + "noMatchingThemes": "No themes matched your search criteria", + "noSearch": "Show all themes", "previouslyHiddenTitle": "Previously visited hidden themes", "requestATheme": "If you want a custom-built theme, request it in the issue tracker", + "searchForATheme": "Search for a theme", "streetcomplete": "Another, similar application is StreetComplete." }, "nameInlineQuestion": "The name of this {category} is $$$",