forked from MapComplete/MapComplete
		
	Simplify priority behaviour of PillsSelector, create LanguageChooser-element, update usages
This commit is contained in:
		
							parent
							
								
									efb54782ca
								
							
						
					
					
						commit
						84eee064b2
					
				
					 7 changed files with 385 additions and 162 deletions
				
			
		| 
						 | 
				
			
			@ -1,20 +1,20 @@
 | 
			
		|||
import Combine from "../Base/Combine"
 | 
			
		||||
import { FlowPanelFactory, FlowStep } from "../ImportFlow/FlowStep"
 | 
			
		||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
import { InputElement } from "../Input/InputElement"
 | 
			
		||||
import { SvgToPdf, SvgToPdfOptions } from "../../Utils/svgToPdf"
 | 
			
		||||
import { FixedInputElement } from "../Input/FixedInputElement"
 | 
			
		||||
import { FixedUiElement } from "../Base/FixedUiElement"
 | 
			
		||||
import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep"
 | 
			
		||||
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"
 | 
			
		||||
import {InputElement} from "../Input/InputElement"
 | 
			
		||||
import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf"
 | 
			
		||||
import {FixedInputElement} from "../Input/FixedInputElement"
 | 
			
		||||
import {FixedUiElement} from "../Base/FixedUiElement"
 | 
			
		||||
import FileSelectorButton from "../Input/FileSelectorButton"
 | 
			
		||||
import InputElementMap from "../Input/InputElementMap"
 | 
			
		||||
import { RadioButton } from "../Input/RadioButton"
 | 
			
		||||
import { Utils } from "../../Utils"
 | 
			
		||||
import { VariableUiElement } from "../Base/VariableUIElement"
 | 
			
		||||
import {RadioButton} from "../Input/RadioButton"
 | 
			
		||||
import {Utils} from "../../Utils"
 | 
			
		||||
import {VariableUiElement} from "../Base/VariableUIElement"
 | 
			
		||||
import Loading from "../Base/Loading"
 | 
			
		||||
import BaseUIElement from "../BaseUIElement"
 | 
			
		||||
import Img from "../Base/Img"
 | 
			
		||||
import Title from "../Base/Title"
 | 
			
		||||
import { CheckBox } from "../Input/Checkboxes"
 | 
			
		||||
import {CheckBox} from "../Input/Checkboxes"
 | 
			
		||||
import Minimap from "../Base/Minimap"
 | 
			
		||||
import SearchAndGo from "./SearchAndGo"
 | 
			
		||||
import Toggle from "../Input/Toggle"
 | 
			
		||||
| 
						 | 
				
			
			@ -25,9 +25,7 @@ import Toggleable from "../Base/Toggleable"
 | 
			
		|||
import Lazy from "../Base/Lazy"
 | 
			
		||||
import LinkToWeblate from "../Base/LinkToWeblate"
 | 
			
		||||
import Link from "../Base/Link"
 | 
			
		||||
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
 | 
			
		||||
import * as languages from "../../assets/language_translations.json"
 | 
			
		||||
import { Translation } from "../i18n/Translation"
 | 
			
		||||
import {AllLanguagesSelector} from "../Popup/AllLanguagesSelector";
 | 
			
		||||
 | 
			
		||||
class SelectTemplate extends Combine implements FlowStep<{ title: string; pages: string[] }> {
 | 
			
		||||
    readonly IsValid: Store<boolean>
 | 
			
		||||
| 
						 | 
				
			
			@ -203,23 +201,7 @@ class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf; langu
 | 
			
		|||
 | 
			
		||||
    constructor(title: string, pages: string[], options: SvgToPdfOptions) {
 | 
			
		||||
        const svgToPdf = new SvgToPdf(title, pages, options)
 | 
			
		||||
        const languageOptions = [
 | 
			
		||||
            new FixedInputElement("Nederlands", "nl"),
 | 
			
		||||
            new FixedInputElement("English", "en"),
 | 
			
		||||
        ]
 | 
			
		||||
        const langs: string[] = Array.from(Object.keys(languages["default"] ?? languages))
 | 
			
		||||
        console.log("Available languages are:", langs)
 | 
			
		||||
        const languageSelector = new SearchablePillsSelector(
 | 
			
		||||
            langs.map((l) => ({
 | 
			
		||||
                show: new Translation(languages[l]),
 | 
			
		||||
                value: l,
 | 
			
		||||
                mainTerm: languages[l],
 | 
			
		||||
            })),
 | 
			
		||||
            {
 | 
			
		||||
                mode: "select-many",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const languageSelector = new AllLanguagesSelector(          )
 | 
			
		||||
        const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
 | 
			
		||||
 | 
			
		||||
        super([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,25 @@
 | 
			
		|||
import { UIElement } from "../UIElement"
 | 
			
		||||
import { InputElement } from "./InputElement"
 | 
			
		||||
import {UIElement} from "../UIElement"
 | 
			
		||||
import {InputElement} from "./InputElement"
 | 
			
		||||
import BaseUIElement from "../BaseUIElement"
 | 
			
		||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
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 {TextField} from "./TextField"
 | 
			
		||||
import Svg from "../../Svg"
 | 
			
		||||
import { VariableUiElement } from "../Base/VariableUIElement"
 | 
			
		||||
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<boolean> {
 | 
			
		||||
    private readonly _shown: BaseUIElement
 | 
			
		||||
    public readonly _selected: UIEventSource<boolean>
 | 
			
		||||
    public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true)
 | 
			
		||||
    public readonly matchesSearchCriteria: Store<boolean>
 | 
			
		||||
    public readonly forceSelected: UIEventSource<boolean>
 | 
			
		||||
    private readonly _shown: BaseUIElement
 | 
			
		||||
    private readonly _squared: boolean
 | 
			
		||||
 | 
			
		||||
    public constructor(
 | 
			
		||||
        shown: string | BaseUIElement,
 | 
			
		||||
        mainTerm: Record<string, string>,
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +28,9 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
 | 
			
		|||
            searchTerms?: Record<string, string[]>
 | 
			
		||||
            selected?: UIEventSource<boolean>
 | 
			
		||||
            forceSelected?: UIEventSource<boolean>
 | 
			
		||||
            squared?: boolean
 | 
			
		||||
            squared?: boolean,
 | 
			
		||||
            /* Hide, if not selected*/
 | 
			
		||||
            hide?: Store<boolean>
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        super()
 | 
			
		||||
| 
						 | 
				
			
			@ -49,14 +53,11 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
 | 
			
		|||
        const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false))
 | 
			
		||||
        const forceSelected = (this.forceSelected =
 | 
			
		||||
            options?.forceSelected ?? new UIEventSource<boolean>(false))
 | 
			
		||||
        this.isShown = search.map(
 | 
			
		||||
            (s) => {
 | 
			
		||||
        this.matchesSearchCriteria = 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
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +66,18 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
 | 
			
		|||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            return false
 | 
			
		||||
        })
 | 
			
		||||
        this.isShown = this.matchesSearchCriteria.map(
 | 
			
		||||
            (matchesSearch) => {
 | 
			
		||||
                if (selected.data && !forceSelected.data) {
 | 
			
		||||
                    return true
 | 
			
		||||
                }
 | 
			
		||||
                if (options?.hide?.data) {
 | 
			
		||||
                    return false
 | 
			
		||||
                }
 | 
			
		||||
                return matchesSearch
 | 
			
		||||
            },
 | 
			
		||||
            [selected, Locale.language]
 | 
			
		||||
            [selected, Locale.language, options?.hide]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const self = this
 | 
			
		||||
| 
						 | 
				
			
			@ -128,13 +139,12 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
 | 
			
		|||
 * A searchfield can be used to filter the values
 | 
			
		||||
 */
 | 
			
		||||
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
 | 
			
		||||
    private readonly selectedElements: UIEventSource<T[]>
 | 
			
		||||
 | 
			
		||||
    public readonly someMatchFound: Store<boolean>
 | 
			
		||||
    private readonly selectedElements: UIEventSource<T[]>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     *
 | 
			
		||||
     * @param values
 | 
			
		||||
     * @param values: the values that can be selected
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    constructor(
 | 
			
		||||
| 
						 | 
				
			
			@ -142,25 +152,44 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
 | 
			
		|||
            show: BaseUIElement
 | 
			
		||||
            value: T
 | 
			
		||||
            mainTerm: Record<string, string>
 | 
			
		||||
            searchTerms?: Record<string, string[]>
 | 
			
		||||
            searchTerms?: Record<string, string[]>,
 | 
			
		||||
            /* If there are more then 200 elements, should this element still be shown? */
 | 
			
		||||
            hasPriority?: Store<boolean>
 | 
			
		||||
        }[],
 | 
			
		||||
        options?: {
 | 
			
		||||
            /*
 | 
			
		||||
            * If one single value can be selected (like a radio button) or if many values can be selected (like checkboxes)
 | 
			
		||||
             */
 | 
			
		||||
            mode?: "select-one" | "select-many"
 | 
			
		||||
            /**
 | 
			
		||||
             * The values of the selected elements.
 | 
			
		||||
             * Use this to tie input elements together
 | 
			
		||||
             */
 | 
			
		||||
            selectedElements?: UIEventSource<T[]>
 | 
			
		||||
            /**
 | 
			
		||||
             * The search bar. Use this to seed the search value or to tie to another value
 | 
			
		||||
             */
 | 
			
		||||
            searchValue?: UIEventSource<string>
 | 
			
		||||
            /**
 | 
			
		||||
             * What is shown if the search yielded no results.
 | 
			
		||||
             * By default: a translated "no search results"
 | 
			
		||||
             */
 | 
			
		||||
            onNoMatches?: BaseUIElement
 | 
			
		||||
            /**
 | 
			
		||||
             * An element that is shown if no search is entered
 | 
			
		||||
             * Default behaviour is to show all options
 | 
			
		||||
             */
 | 
			
		||||
            onNoSearchMade?: BaseUIElement
 | 
			
		||||
            /**
 | 
			
		||||
             * Shows this if there are many (>200) possible mappings
 | 
			
		||||
             * Extra element to show if there are many (>200) possible mappings and when non-priority mappings are hidden
 | 
			
		||||
             *
 | 
			
		||||
             */
 | 
			
		||||
            onManyElements?: BaseUIElement
 | 
			
		||||
            onManyElementsValue?: UIEventSource<T[]>
 | 
			
		||||
            selectIfSingle?: false | boolean
 | 
			
		||||
            searchAreaClass?: string
 | 
			
		||||
            hideSearchBar?: false | boolean
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        const search = new TextField({ value: options?.searchValue })
 | 
			
		||||
        const search = new TextField({value: options?.searchValue})
 | 
			
		||||
 | 
			
		||||
        const searchBar = options?.hideSearchBar
 | 
			
		||||
            ? undefined
 | 
			
		||||
| 
						 | 
				
			
			@ -173,7 +202,7 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
 | 
			
		|||
        const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([])
 | 
			
		||||
        const mode = options?.mode ?? "select-one"
 | 
			
		||||
        const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
 | 
			
		||||
 | 
			
		||||
        const forceHide = new UIEventSource(false)
 | 
			
		||||
        const mappedValues: {
 | 
			
		||||
            show: SelfHidingToggle
 | 
			
		||||
            mainTerm: Record<string, string>
 | 
			
		||||
| 
						 | 
				
			
			@ -209,6 +238,7 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
 | 
			
		|||
                searchTerms: v.searchTerms,
 | 
			
		||||
                selected: vIsSelected,
 | 
			
		||||
                squared: mode === "select-many",
 | 
			
		||||
                hide: v.hasPriority === undefined ? forceHide : forceHide.map(fh => fh && !v.hasPriority?.data, [v.hasPriority])
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
| 
						 | 
				
			
			@ -217,62 +247,18 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
 | 
			
		|||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // The total number of elements that would be displayed based on the search criteria alone
 | 
			
		||||
        let totalShown: Store<number>
 | 
			
		||||
        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]
 | 
			
		||||
        )
 | 
			
		||||
        totalShown = searchValue.map((_) => mappedValues.filter((mv) => mv.show.matchesSearchCriteria.data).length)
 | 
			
		||||
        const tooMuchElementsCutoff = 40
 | 
			
		||||
        totalShown.addCallbackAndRunD(shown => forceHide.setData(tooMuchElementsCutoff < shown))
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
| 
						 | 
				
			
			@ -284,9 +270,14 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
 | 
			
		|||
                        }
 | 
			
		||||
 | 
			
		||||
                        mappedValues.sort((a, b) => (a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1))
 | 
			
		||||
                        return new Combine(mappedValues.map((e) => e.show))
 | 
			
		||||
                        let pills = new Combine(mappedValues.map((e) => e.show))
 | 
			
		||||
                            .SetClass("flex flex-wrap w-full content-start")
 | 
			
		||||
                            .SetClass(options?.searchAreaClass ?? "")
 | 
			
		||||
 | 
			
		||||
                        if (totalShown.data >= tooMuchElementsCutoff) {
 | 
			
		||||
                            pills = new Combine([options?.onManyElements ?? Translations.t.general.useSearch, pills])
 | 
			
		||||
                        }
 | 
			
		||||
                        return pills
 | 
			
		||||
                    },
 | 
			
		||||
                    [totalShown, searchValue]
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										45
									
								
								UI/Popup/AllLanguagesSelector.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								UI/Popup/AllLanguagesSelector.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
 | 
			
		||||
import {Store} from "../../Logic/UIEventSource";
 | 
			
		||||
import BaseUIElement from "../BaseUIElement";
 | 
			
		||||
import * as all_languages from "../../assets/language_translations.json";
 | 
			
		||||
import {Translation} from "../i18n/Translation";
 | 
			
		||||
 | 
			
		||||
export class AllLanguagesSelector extends SearchablePillsSelector <string> {
 | 
			
		||||
 | 
			
		||||
    constructor(options?: {
 | 
			
		||||
                    mode?: "select-many" | "select-one"
 | 
			
		||||
                    currentCountry?: Store<string>,
 | 
			
		||||
                    supportedLanguages?: Record<string, string> & { _meta?: { countries?: string[] } }
 | 
			
		||||
                }) {
 | 
			
		||||
 | 
			
		||||
        const possibleValues: {
 | 
			
		||||
            show: BaseUIElement
 | 
			
		||||
            value: string
 | 
			
		||||
            mainTerm: Record<string, string>
 | 
			
		||||
            searchTerms?: Record<string, string[]>,
 | 
			
		||||
            hasPriority?: Store<boolean>
 | 
			
		||||
        }[] = []
 | 
			
		||||
 | 
			
		||||
        const langs = options?.supportedLanguages ?? all_languages["default"] ?? all_languages
 | 
			
		||||
        for (const ln in langs) {
 | 
			
		||||
            let languageInfo: Record<string, string> & { _meta?: { countries: string[] } } = all_languages[ln]
 | 
			
		||||
            const countries = languageInfo._meta?.countries?.map(c => c.toLowerCase())
 | 
			
		||||
            languageInfo = {...languageInfo}
 | 
			
		||||
            delete languageInfo._meta
 | 
			
		||||
            const term = {
 | 
			
		||||
                show: new Translation(languageInfo),
 | 
			
		||||
                value: ln,
 | 
			
		||||
                mainTerm: languageInfo,
 | 
			
		||||
                searchTerms: {"*": [ln]},
 | 
			
		||||
                hasPriority: countries === undefined ? undefined : options?.currentCountry?.map(country => countries?.indexOf(country.toLowerCase()) >= 0)
 | 
			
		||||
            }
 | 
			
		||||
            possibleValues.push(term)
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        super(possibleValues,
 | 
			
		||||
            {
 | 
			
		||||
                mode: options?.mode ?? 'select-many'
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
 | 
			
		|||
import { Unit } from "../../Models/Unit"
 | 
			
		||||
import Lazy from "../Base/Lazy"
 | 
			
		||||
import { FixedUiElement } from "../Base/FixedUiElement"
 | 
			
		||||
import {EditButton} from "./SaveButton";
 | 
			
		||||
 | 
			
		||||
export default class EditableTagRendering extends Toggle {
 | 
			
		||||
    constructor(
 | 
			
		||||
| 
						 | 
				
			
			@ -70,16 +71,9 @@ export default class EditableTagRendering extends Toggle {
 | 
			
		|||
            // We have a question and editing is enabled
 | 
			
		||||
            const answerWithEditButton = new Combine([
 | 
			
		||||
                answer,
 | 
			
		||||
                new Toggle(
 | 
			
		||||
                    new Combine([Svg.pencil_ui()])
 | 
			
		||||
                        .SetClass("block relative h-10 w-10 p-2 float-right")
 | 
			
		||||
                        .SetStyle("border: 1px solid black; border-radius: 0.7em")
 | 
			
		||||
                        .onClick(() => {
 | 
			
		||||
                new EditButton(state.osmConnection,() => {
 | 
			
		||||
                    editMode.setData(true)
 | 
			
		||||
                }),
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    state.osmConnection.isLoggedIn
 | 
			
		||||
                ),
 | 
			
		||||
            ]).SetClass("flex justify-between w-full")
 | 
			
		||||
 | 
			
		||||
            const question = new Lazy(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										228
									
								
								UI/Popup/LanguageElement.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								UI/Popup/LanguageElement.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,228 @@
 | 
			
		|||
import {SpecialVisualization} from "../SpecialVisualization";
 | 
			
		||||
import BaseUIElement from "../BaseUIElement";
 | 
			
		||||
import {UIEventSource} from "../../Logic/UIEventSource";
 | 
			
		||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
 | 
			
		||||
import {VariableUiElement} from "../Base/VariableUIElement";
 | 
			
		||||
import {OsmTags} from "../../Models/OsmFeature";
 | 
			
		||||
import * as all_languages from "../../assets/language_translations.json"
 | 
			
		||||
import {Translation} from "../i18n/Translation";
 | 
			
		||||
import Combine from "../Base/Combine";
 | 
			
		||||
import Title from "../Base/Title";
 | 
			
		||||
import Lazy from "../Base/Lazy";
 | 
			
		||||
import {SubstitutedTranslation} from "../SubstitutedTranslation";
 | 
			
		||||
import List from "../Base/List";
 | 
			
		||||
import {AllLanguagesSelector} from "./AllLanguagesSelector";
 | 
			
		||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
 | 
			
		||||
import {And} from "../../Logic/Tags/And";
 | 
			
		||||
import {Tag} from "../../Logic/Tags/Tag";
 | 
			
		||||
import {EditButton, SaveButton} from "./SaveButton";
 | 
			
		||||
import {FixedUiElement} from "../Base/FixedUiElement";
 | 
			
		||||
import Translations from "../i18n/Translations";
 | 
			
		||||
import Toggle from "../Input/Toggle";
 | 
			
		||||
import {On} from "../../Models/ThemeConfig/Conversion/Conversion";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export class LanguageElement implements SpecialVisualization {
 | 
			
		||||
    funcName: string = "language_chooser"
 | 
			
		||||
 | 
			
		||||
    docs: string | BaseUIElement = "The language element allows to show and pick all known (modern) languages. The key can be set";
 | 
			
		||||
 | 
			
		||||
    args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] =
 | 
			
		||||
        [{
 | 
			
		||||
            name: "key",
 | 
			
		||||
            required: true,
 | 
			
		||||
            doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked "
 | 
			
		||||
        },
 | 
			
		||||
            {
 | 
			
		||||
                name: "question",
 | 
			
		||||
                required: true,
 | 
			
		||||
                doc: "What to ask if no questions are known"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: "render_list_item",
 | 
			
		||||
                doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
 | 
			
		||||
                defaultValue: "{language()}"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: "render_single_language",
 | 
			
		||||
                doc: "What will be shown if the feature only supports a single language",
 | 
			
		||||
                required: true
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: "render_all",
 | 
			
		||||
                doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
 | 
			
		||||
                defaultValue: "{list()}"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: "no_known_languages",
 | 
			
		||||
                doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'mode',
 | 
			
		||||
                doc: "If one or many languages can be selected. Should be 'multi' or 'single'",
 | 
			
		||||
                defaultValue: 'multi'
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    example: `
 | 
			
		||||
    \`\`\`json
 | 
			
		||||
     {"special":
 | 
			
		||||
       "type": "language_chooser",
 | 
			
		||||
       "key": "school:language",
 | 
			
		||||
       "question": {"en": "What are the main (and administrative) languages spoken in this school?"},
 | 
			
		||||
       "render_single_language": {"en": "{language()} is spoken on this school"},
 | 
			
		||||
       "render_list_item": {"en": "{language()}"},
 | 
			
		||||
       "render_all": {"en": "The following languages are spoken here:{list()}"}
 | 
			
		||||
       "mode":"multi"
 | 
			
		||||
     }
 | 
			
		||||
     \`\`\`
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
    constr(state: FeaturePipelineState, tagSource: UIEventSource<OsmTags>, argument: string[]): BaseUIElement {
 | 
			
		||||
        let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] = argument
 | 
			
		||||
        if (mode === undefined || mode.length == 0) {
 | 
			
		||||
            mode = "multi"
 | 
			
		||||
        }
 | 
			
		||||
        if (item_render === undefined) {
 | 
			
		||||
            item_render = "{language()}"
 | 
			
		||||
        }
 | 
			
		||||
        if (all_render === undefined || all_render.length == 0) {
 | 
			
		||||
            all_render = "{list()}"
 | 
			
		||||
        }
 | 
			
		||||
        if (mode !== "single" && mode !== "multi") {
 | 
			
		||||
            throw "Error while calling language_chooser: mode must be either 'single' or 'multi' but it is " + mode
 | 
			
		||||
        }
 | 
			
		||||
        if (single_render.indexOf("{language()") < 0 || item_render.indexOf("{language()") < 0) {
 | 
			
		||||
            throw "Error while calling language_chooser: render_single_language and render_list_item must contain '{language()}'"
 | 
			
		||||
        }
 | 
			
		||||
        if (all_render.indexOf("{list()") < 0) {
 | 
			
		||||
            throw "Error while calling language_chooser: render_all must contain '{list()}'"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const prefix = key + ":"
 | 
			
		||||
        const foundLanguages = tagSource
 | 
			
		||||
            .map(tags => {
 | 
			
		||||
                const foundLanguages: string[] = []
 | 
			
		||||
                for (const k in tags) {
 | 
			
		||||
                    const v = tags[k]
 | 
			
		||||
                    if (v !== "yes") {
 | 
			
		||||
                        continue
 | 
			
		||||
                    }
 | 
			
		||||
                    if (k.startsWith(prefix)) {
 | 
			
		||||
                        foundLanguages.push(k.substring(prefix.length))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return foundLanguages
 | 
			
		||||
            })
 | 
			
		||||
        const forceInputMode = new UIEventSource(false);
 | 
			
		||||
        const inputEl = new Lazy(() => {
 | 
			
		||||
            const selector = new AllLanguagesSelector(
 | 
			
		||||
                {
 | 
			
		||||
                    mode: mode === "single" ? "select-one" : "select-many",
 | 
			
		||||
                    currentCountry: tagSource.map(tgs => tgs["_country"])
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            const cancelButton = Toggle.If(forceInputMode,
 | 
			
		||||
                () => Translations.t.general.cancel
 | 
			
		||||
                    .Clone()
 | 
			
		||||
                    .SetClass("btn btn-secondary").onClick(() => forceInputMode.setData(false)))
 | 
			
		||||
 | 
			
		||||
            const saveButton = new SaveButton(
 | 
			
		||||
                selector.GetValue().map(lngs => lngs.length > 0 ? "true" : undefined),
 | 
			
		||||
                state.osmConnection,
 | 
			
		||||
            ).onClick(() => {
 | 
			
		||||
                const selectedLanguages = selector.GetValue().data
 | 
			
		||||
                const currentLanguages = foundLanguages.data
 | 
			
		||||
                const selection: Tag[] = selectedLanguages.map(ln => new Tag(prefix + ln, "yes"));
 | 
			
		||||
 | 
			
		||||
                for (const currentLanguage of currentLanguages) {
 | 
			
		||||
                    if (selectedLanguages.indexOf(currentLanguage) >= 0) {
 | 
			
		||||
                        continue
 | 
			
		||||
                    }
 | 
			
		||||
                    // Erase language that is not spoken anymore
 | 
			
		||||
                    selection.push(new Tag(prefix + currentLanguage, ""))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if (state.featureSwitchIsTesting.data) {
 | 
			
		||||
                    for (const tag of selection) {
 | 
			
		||||
                        tagSource.data[tag.key] = tag.value
 | 
			
		||||
                    }
 | 
			
		||||
                    tagSource.ping()
 | 
			
		||||
                } else {
 | 
			
		||||
                    (state?.changes)
 | 
			
		||||
                        .applyAction(
 | 
			
		||||
                            new ChangeTagAction(tagSource.data.id, new And(selection), tagSource.data, {
 | 
			
		||||
                                theme: state?.layoutToUse?.id ?? "unkown",
 | 
			
		||||
                                changeType: "answer",
 | 
			
		||||
                            })
 | 
			
		||||
                        )
 | 
			
		||||
                        .then((_) => {
 | 
			
		||||
                            console.log("Tagchanges applied")
 | 
			
		||||
                        })
 | 
			
		||||
                }
 | 
			
		||||
                forceInputMode.setData(false)
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            return new Combine([new Title(question), selector,
 | 
			
		||||
                new Combine([cancelButton, saveButton]).SetClass("flex justify-end")
 | 
			
		||||
            ]).SetClass("flex flex-col question disable-links");
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const editButton = new EditButton(state.osmConnection, () => forceInputMode.setData(true))
 | 
			
		||||
 | 
			
		||||
        return new VariableUiElement(foundLanguages
 | 
			
		||||
            .map(foundLanguages => {
 | 
			
		||||
 | 
			
		||||
                if (forceInputMode.data) {
 | 
			
		||||
                    return inputEl
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (foundLanguages.length === 0) {
 | 
			
		||||
                    // No languages found - we show the question and the input element
 | 
			
		||||
                    if (on_no_known_languages !== undefined && on_no_known_languages.length > 0) {
 | 
			
		||||
                        return new Combine([on_no_known_languages, editButton]).SetClass("flex justify-end")
 | 
			
		||||
                    }
 | 
			
		||||
                    return inputEl
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let rendered: BaseUIElement;
 | 
			
		||||
                if (foundLanguages.length === 1) {
 | 
			
		||||
                    const ln = foundLanguages[0]
 | 
			
		||||
                    let mapping = new Map<string, BaseUIElement>();
 | 
			
		||||
                    mapping.set("language", new Translation(all_languages[ln]))
 | 
			
		||||
                    rendered = new SubstitutedTranslation(
 | 
			
		||||
                        new Translation({"*": single_render}, undefined),
 | 
			
		||||
                        tagSource, state, mapping
 | 
			
		||||
                    )
 | 
			
		||||
                } else {
 | 
			
		||||
 | 
			
		||||
                    let mapping = new Map<string, BaseUIElement>();
 | 
			
		||||
                    const languagesList = new List(
 | 
			
		||||
                        foundLanguages.map(ln => {
 | 
			
		||||
                            let mappingLn = new Map<string, BaseUIElement>();
 | 
			
		||||
                            mappingLn.set("language", new Translation(all_languages[ln]))
 | 
			
		||||
                            return new SubstitutedTranslation(
 | 
			
		||||
                                new Translation({"*": item_render}, undefined),
 | 
			
		||||
                                tagSource, state, mappingLn
 | 
			
		||||
                            )
 | 
			
		||||
                        })
 | 
			
		||||
                    );
 | 
			
		||||
                    mapping.set("list", languagesList)
 | 
			
		||||
                    rendered = new SubstitutedTranslation(
 | 
			
		||||
                        new Translation({'*': all_render}, undefined), tagSource,
 | 
			
		||||
                        state, mapping
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                return new Combine([rendered, editButton]).SetClass("flex justify-between")
 | 
			
		||||
 | 
			
		||||
            }, [forceInputMode]));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,21 @@ import Translations from "../i18n/Translations"
 | 
			
		|||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
 | 
			
		||||
import Toggle from "../Input/Toggle"
 | 
			
		||||
import BaseUIElement from "../BaseUIElement"
 | 
			
		||||
import Combine from "../Base/Combine";
 | 
			
		||||
import Svg from "../../Svg";
 | 
			
		||||
 | 
			
		||||
export class EditButton extends Toggle {
 | 
			
		||||
    constructor(osmConnection: OsmConnection, onClick: () => void) {
 | 
			
		||||
        super(
 | 
			
		||||
            new Combine([Svg.pencil_ui()])
 | 
			
		||||
                .SetClass("block relative h-10 w-10 p-2 float-right")
 | 
			
		||||
                .SetStyle("border: 1px solid black; border-radius: 0.7em")
 | 
			
		||||
                .onClick(onClick),
 | 
			
		||||
            undefined,
 | 
			
		||||
            osmConnection.isLoggedIn
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SaveButton extends Toggle {
 | 
			
		||||
    constructor(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,7 +105,7 @@ export default class TagRenderingQuestion extends Combine {
 | 
			
		|||
                TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data)
 | 
			
		||||
            )
 | 
			
		||||
            if (selection) {
 | 
			
		||||
                ;(state?.changes)
 | 
			
		||||
                (state?.changes)
 | 
			
		||||
                    .applyAction(
 | 
			
		||||
                        new ChangeTagAction(tags.data.id, selection, tags.data, {
 | 
			
		||||
                            theme: state?.layoutToUse?.id ?? "unkown",
 | 
			
		||||
| 
						 | 
				
			
			@ -288,14 +288,16 @@ export default class TagRenderingQuestion extends Combine {
 | 
			
		|||
        value: number
 | 
			
		||||
        mainTerm: Record<string, string>
 | 
			
		||||
        searchTerms?: Record<string, string[]>
 | 
			
		||||
        original: Mapping
 | 
			
		||||
        original: Mapping,
 | 
			
		||||
        hasPriority?: Store<boolean>
 | 
			
		||||
    }[] {
 | 
			
		||||
        const values: {
 | 
			
		||||
            show: BaseUIElement
 | 
			
		||||
            value: number
 | 
			
		||||
            mainTerm: Record<string, string>
 | 
			
		||||
            searchTerms?: Record<string, string[]>
 | 
			
		||||
            original: Mapping
 | 
			
		||||
            original: Mapping,
 | 
			
		||||
            hasPriority?: Store<boolean>
 | 
			
		||||
        }[] = []
 | 
			
		||||
        const addIcons = applicableMappings.some((m) => m.icon !== undefined)
 | 
			
		||||
        for (let i = 0; i < applicableMappings.length; i++) {
 | 
			
		||||
| 
						 | 
				
			
			@ -317,6 +319,7 @@ export default class TagRenderingQuestion extends Combine {
 | 
			
		|||
                mainTerm: tr.translations,
 | 
			
		||||
                searchTerms: mapping.searchTerms,
 | 
			
		||||
                original: mapping,
 | 
			
		||||
                hasPriority: tagsSource.map(tags => mapping.priorityIf?.matchesProperties(tags))
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        return values
 | 
			
		||||
| 
						 | 
				
			
			@ -397,7 +400,7 @@ export default class TagRenderingQuestion extends Combine {
 | 
			
		|||
        const values = TagRenderingQuestion.MappingToPillValue(
 | 
			
		||||
            applicableMappings,
 | 
			
		||||
            tagsSource,
 | 
			
		||||
            state
 | 
			
		||||
            state,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const searchValue: UIEventSource<string> =
 | 
			
		||||
| 
						 | 
				
			
			@ -411,47 +414,12 @@ export default class TagRenderingQuestion extends Combine {
 | 
			
		|||
        }
 | 
			
		||||
        const mode = configuration.multiAnswer ? "select-many" : "select-one"
 | 
			
		||||
 | 
			
		||||
        const tooMuchElementsValue = new UIEventSource<number[]>([])
 | 
			
		||||
 | 
			
		||||
        let priorityPresets: BaseUIElement = undefined
 | 
			
		||||
        const classes = "h-64 overflow-scroll"
 | 
			
		||||
 | 
			
		||||
        if (applicableMappings.some((m) => m.priorityIf !== undefined)) {
 | 
			
		||||
            const priorityValues = tagsSource.map((tags) =>
 | 
			
		||||
                TagRenderingQuestion.MappingToPillValue(
 | 
			
		||||
                    applicableMappings,
 | 
			
		||||
                    tagsSource,
 | 
			
		||||
                    state
 | 
			
		||||
                ).filter((v) => v.original.priorityIf?.matchesProperties(tags))
 | 
			
		||||
            )
 | 
			
		||||
            priorityPresets = new VariableUiElement(
 | 
			
		||||
                priorityValues.map((priority) => {
 | 
			
		||||
                    if (priority.length === 0) {
 | 
			
		||||
                        return Translations.t.general.useSearch
 | 
			
		||||
                    }
 | 
			
		||||
                    return new Combine([
 | 
			
		||||
                        Translations.t.general.useSearchForMore.Subs({
 | 
			
		||||
                            total: applicableMappings.length,
 | 
			
		||||
                        }),
 | 
			
		||||
                        new SearchablePillsSelector(priority, {
 | 
			
		||||
                            selectedElements: tooMuchElementsValue,
 | 
			
		||||
                            hideSearchBar: true,
 | 
			
		||||
                            mode,
 | 
			
		||||
                        }),
 | 
			
		||||
                    ])
 | 
			
		||||
                        .SetClass("flex flex-col items-center ")
 | 
			
		||||
                        .SetClass(classes)
 | 
			
		||||
                })
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        const presetSearch = new SearchablePillsSelector<number>(values, {
 | 
			
		||||
            selectIfSingle: true,
 | 
			
		||||
            mode,
 | 
			
		||||
            searchValue,
 | 
			
		||||
            onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
 | 
			
		||||
            searchAreaClass: classes,
 | 
			
		||||
            onManyElementsValue: tooMuchElementsValue,
 | 
			
		||||
            onManyElements: priorityPresets,
 | 
			
		||||
            searchAreaClass: classes
 | 
			
		||||
        })
 | 
			
		||||
        const fallbackTag = searchValue.map((s) => {
 | 
			
		||||
            if (s === undefined || ff?.key === undefined) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue