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 Combine from "../Base/Combine"
|
||||||
import { FlowPanelFactory, FlowStep } from "../ImportFlow/FlowStep"
|
import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||||
import { InputElement } from "../Input/InputElement"
|
import {InputElement} from "../Input/InputElement"
|
||||||
import { SvgToPdf, SvgToPdfOptions } from "../../Utils/svgToPdf"
|
import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf"
|
||||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
import {FixedInputElement} from "../Input/FixedInputElement"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||||
import FileSelectorButton from "../Input/FileSelectorButton"
|
import FileSelectorButton from "../Input/FileSelectorButton"
|
||||||
import InputElementMap from "../Input/InputElementMap"
|
import InputElementMap from "../Input/InputElementMap"
|
||||||
import { RadioButton } from "../Input/RadioButton"
|
import {RadioButton} from "../Input/RadioButton"
|
||||||
import { Utils } from "../../Utils"
|
import {Utils} from "../../Utils"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||||
import Loading from "../Base/Loading"
|
import Loading from "../Base/Loading"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import Img from "../Base/Img"
|
import Img from "../Base/Img"
|
||||||
import Title from "../Base/Title"
|
import Title from "../Base/Title"
|
||||||
import { CheckBox } from "../Input/Checkboxes"
|
import {CheckBox} from "../Input/Checkboxes"
|
||||||
import Minimap from "../Base/Minimap"
|
import Minimap from "../Base/Minimap"
|
||||||
import SearchAndGo from "./SearchAndGo"
|
import SearchAndGo from "./SearchAndGo"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
|
@ -25,9 +25,7 @@ import Toggleable from "../Base/Toggleable"
|
||||||
import Lazy from "../Base/Lazy"
|
import Lazy from "../Base/Lazy"
|
||||||
import LinkToWeblate from "../Base/LinkToWeblate"
|
import LinkToWeblate from "../Base/LinkToWeblate"
|
||||||
import Link from "../Base/Link"
|
import Link from "../Base/Link"
|
||||||
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
|
import {AllLanguagesSelector} from "../Popup/AllLanguagesSelector";
|
||||||
import * as languages from "../../assets/language_translations.json"
|
|
||||||
import { Translation } from "../i18n/Translation"
|
|
||||||
|
|
||||||
class SelectTemplate extends Combine implements FlowStep<{ title: string; pages: string[] }> {
|
class SelectTemplate extends Combine implements FlowStep<{ title: string; pages: string[] }> {
|
||||||
readonly IsValid: Store<boolean>
|
readonly IsValid: Store<boolean>
|
||||||
|
@ -203,23 +201,7 @@ class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf; langu
|
||||||
|
|
||||||
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
|
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
|
||||||
const svgToPdf = new SvgToPdf(title, pages, options)
|
const svgToPdf = new SvgToPdf(title, pages, options)
|
||||||
const languageOptions = [
|
const languageSelector = new AllLanguagesSelector( )
|
||||||
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 isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
|
const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
|
||||||
|
|
||||||
super([
|
super([
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import { UIElement } from "../UIElement"
|
import {UIElement} from "../UIElement"
|
||||||
import { InputElement } from "./InputElement"
|
import {InputElement} from "./InputElement"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Locale from "../i18n/Locale"
|
import Locale from "../i18n/Locale"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import { TextField } from "./TextField"
|
import {TextField} from "./TextField"
|
||||||
import Svg from "../../Svg"
|
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
|
* A single 'pill' which can hide itself if the search criteria is not met
|
||||||
*/
|
*/
|
||||||
class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
||||||
private readonly _shown: BaseUIElement
|
|
||||||
public readonly _selected: UIEventSource<boolean>
|
public readonly _selected: UIEventSource<boolean>
|
||||||
public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true)
|
public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true)
|
||||||
|
public readonly matchesSearchCriteria: Store<boolean>
|
||||||
public readonly forceSelected: UIEventSource<boolean>
|
public readonly forceSelected: UIEventSource<boolean>
|
||||||
|
private readonly _shown: BaseUIElement
|
||||||
private readonly _squared: boolean
|
private readonly _squared: boolean
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
shown: string | BaseUIElement,
|
shown: string | BaseUIElement,
|
||||||
mainTerm: Record<string, string>,
|
mainTerm: Record<string, string>,
|
||||||
|
@ -26,7 +28,9 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
||||||
searchTerms?: Record<string, string[]>
|
searchTerms?: Record<string, string[]>
|
||||||
selected?: UIEventSource<boolean>
|
selected?: UIEventSource<boolean>
|
||||||
forceSelected?: UIEventSource<boolean>
|
forceSelected?: UIEventSource<boolean>
|
||||||
squared?: boolean
|
squared?: boolean,
|
||||||
|
/* Hide, if not selected*/
|
||||||
|
hide?: Store<boolean>
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -49,24 +53,31 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
||||||
const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false))
|
const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false))
|
||||||
const forceSelected = (this.forceSelected =
|
const forceSelected = (this.forceSelected =
|
||||||
options?.forceSelected ?? new UIEventSource<boolean>(false))
|
options?.forceSelected ?? new UIEventSource<boolean>(false))
|
||||||
this.isShown = search.map(
|
this.matchesSearchCriteria = search.map(s => {
|
||||||
(s) => {
|
if (s === undefined || s.length === 0) {
|
||||||
if (s === undefined || s.length === 0) {
|
return true
|
||||||
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
|
||||||
|
})
|
||||||
|
this.isShown = this.matchesSearchCriteria.map(
|
||||||
|
(matchesSearch) => {
|
||||||
if (selected.data && !forceSelected.data) {
|
if (selected.data && !forceSelected.data) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
s = s?.trim()?.toLowerCase()
|
if (options?.hide?.data) {
|
||||||
if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) {
|
return false
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) {
|
return matchesSearch
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
[selected, Locale.language]
|
[selected, Locale.language, options?.hide]
|
||||||
)
|
)
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
|
@ -128,13 +139,12 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
|
||||||
* A searchfield can be used to filter the values
|
* A searchfield can be used to filter the values
|
||||||
*/
|
*/
|
||||||
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
|
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
|
||||||
private readonly selectedElements: UIEventSource<T[]>
|
|
||||||
|
|
||||||
public readonly someMatchFound: Store<boolean>
|
public readonly someMatchFound: Store<boolean>
|
||||||
|
private readonly selectedElements: UIEventSource<T[]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param values
|
* @param values: the values that can be selected
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -142,38 +152,57 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
|
||||||
show: BaseUIElement
|
show: BaseUIElement
|
||||||
value: T
|
value: T
|
||||||
mainTerm: Record<string, string>
|
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?: {
|
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"
|
mode?: "select-one" | "select-many"
|
||||||
|
/**
|
||||||
|
* The values of the selected elements.
|
||||||
|
* Use this to tie input elements together
|
||||||
|
*/
|
||||||
selectedElements?: UIEventSource<T[]>
|
selectedElements?: UIEventSource<T[]>
|
||||||
|
/**
|
||||||
|
* The search bar. Use this to seed the search value or to tie to another value
|
||||||
|
*/
|
||||||
searchValue?: UIEventSource<string>
|
searchValue?: UIEventSource<string>
|
||||||
|
/**
|
||||||
|
* What is shown if the search yielded no results.
|
||||||
|
* By default: a translated "no search results"
|
||||||
|
*/
|
||||||
onNoMatches?: BaseUIElement
|
onNoMatches?: BaseUIElement
|
||||||
|
/**
|
||||||
|
* An element that is shown if no search is entered
|
||||||
|
* Default behaviour is to show all options
|
||||||
|
*/
|
||||||
onNoSearchMade?: BaseUIElement
|
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
|
onManyElements?: BaseUIElement
|
||||||
onManyElementsValue?: UIEventSource<T[]>
|
|
||||||
selectIfSingle?: false | boolean
|
|
||||||
searchAreaClass?: string
|
searchAreaClass?: string
|
||||||
hideSearchBar?: false | boolean
|
hideSearchBar?: false | boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const search = new TextField({ value: options?.searchValue })
|
const search = new TextField({value: options?.searchValue})
|
||||||
|
|
||||||
const searchBar = options?.hideSearchBar
|
const searchBar = options?.hideSearchBar
|
||||||
? undefined
|
? undefined
|
||||||
: new Combine([
|
: new Combine([
|
||||||
Svg.search_svg().SetClass("w-8 normal-background"),
|
Svg.search_svg().SetClass("w-8 normal-background"),
|
||||||
search.SetClass("w-full"),
|
search.SetClass("w-full"),
|
||||||
]).SetClass("flex items-center border-2 border-black m-2")
|
]).SetClass("flex items-center border-2 border-black m-2")
|
||||||
|
|
||||||
const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase())
|
const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase())
|
||||||
const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([])
|
const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([])
|
||||||
const mode = options?.mode ?? "select-one"
|
const mode = options?.mode ?? "select-one"
|
||||||
const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
|
const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
|
||||||
|
const forceHide = new UIEventSource(false)
|
||||||
const mappedValues: {
|
const mappedValues: {
|
||||||
show: SelfHidingToggle
|
show: SelfHidingToggle
|
||||||
mainTerm: Record<string, string>
|
mainTerm: Record<string, string>
|
||||||
|
@ -209,6 +238,7 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
|
||||||
searchTerms: v.searchTerms,
|
searchTerms: v.searchTerms,
|
||||||
selected: vIsSelected,
|
selected: vIsSelected,
|
||||||
squared: mode === "select-many",
|
squared: mode === "select-many",
|
||||||
|
hide: v.hasPriority === undefined ? forceHide : forceHide.map(fh => fh && !v.hasPriority?.data, [v.hasPriority])
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
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>
|
let totalShown: Store<number>
|
||||||
if (options.selectIfSingle) {
|
totalShown = searchValue.map((_) => mappedValues.filter((mv) => mv.show.matchesSearchCriteria.data).length)
|
||||||
let forcedSelection: { value: T; show: SelfHidingToggle } = undefined
|
const tooMuchElementsCutoff = 40
|
||||||
totalShown = searchValue.map(
|
totalShown.addCallbackAndRunD(shown => forceHide.setData(tooMuchElementsCutoff < shown))
|
||||||
(_) => {
|
|
||||||
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([
|
super([
|
||||||
searchBar,
|
searchBar,
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
Locale.language.map(
|
Locale.language.map(
|
||||||
(lng) => {
|
(lng) => {
|
||||||
if (totalShown.data >= 200) {
|
|
||||||
return options?.onManyElements ?? Translations.t.general.useSearch
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
options?.onNoSearchMade !== undefined &&
|
options?.onNoSearchMade !== undefined &&
|
||||||
(searchValue.data === undefined || searchValue.data.length === 0)
|
(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))
|
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("flex flex-wrap w-full content-start")
|
||||||
.SetClass(options?.searchAreaClass ?? "")
|
.SetClass(options?.searchAreaClass ?? "")
|
||||||
|
|
||||||
|
if (totalShown.data >= tooMuchElementsCutoff) {
|
||||||
|
pills = new Combine([options?.onManyElements ?? Translations.t.general.useSearch, pills])
|
||||||
|
}
|
||||||
|
return pills
|
||||||
},
|
},
|
||||||
[totalShown, searchValue]
|
[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 { Unit } from "../../Models/Unit"
|
||||||
import Lazy from "../Base/Lazy"
|
import Lazy from "../Base/Lazy"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
|
import {EditButton} from "./SaveButton";
|
||||||
|
|
||||||
export default class EditableTagRendering extends Toggle {
|
export default class EditableTagRendering extends Toggle {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -70,16 +71,9 @@ export default class EditableTagRendering extends Toggle {
|
||||||
// We have a question and editing is enabled
|
// We have a question and editing is enabled
|
||||||
const answerWithEditButton = new Combine([
|
const answerWithEditButton = new Combine([
|
||||||
answer,
|
answer,
|
||||||
new Toggle(
|
new EditButton(state.osmConnection,() => {
|
||||||
new Combine([Svg.pencil_ui()])
|
editMode.setData(true)
|
||||||
.SetClass("block relative h-10 w-10 p-2 float-right")
|
}),
|
||||||
.SetStyle("border: 1px solid black; border-radius: 0.7em")
|
|
||||||
.onClick(() => {
|
|
||||||
editMode.setData(true)
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
state.osmConnection.isLoggedIn
|
|
||||||
),
|
|
||||||
]).SetClass("flex justify-between w-full")
|
]).SetClass("flex justify-between w-full")
|
||||||
|
|
||||||
const question = new Lazy(
|
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 { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
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 {
|
export class SaveButton extends Toggle {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data)
|
TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data)
|
||||||
)
|
)
|
||||||
if (selection) {
|
if (selection) {
|
||||||
;(state?.changes)
|
(state?.changes)
|
||||||
.applyAction(
|
.applyAction(
|
||||||
new ChangeTagAction(tags.data.id, selection, tags.data, {
|
new ChangeTagAction(tags.data.id, selection, tags.data, {
|
||||||
theme: state?.layoutToUse?.id ?? "unkown",
|
theme: state?.layoutToUse?.id ?? "unkown",
|
||||||
|
@ -288,14 +288,16 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
value: number
|
value: number
|
||||||
mainTerm: Record<string, string>
|
mainTerm: Record<string, string>
|
||||||
searchTerms?: Record<string, string[]>
|
searchTerms?: Record<string, string[]>
|
||||||
original: Mapping
|
original: Mapping,
|
||||||
|
hasPriority?: Store<boolean>
|
||||||
}[] {
|
}[] {
|
||||||
const values: {
|
const values: {
|
||||||
show: BaseUIElement
|
show: BaseUIElement
|
||||||
value: number
|
value: number
|
||||||
mainTerm: Record<string, string>
|
mainTerm: Record<string, string>
|
||||||
searchTerms?: Record<string, string[]>
|
searchTerms?: Record<string, string[]>
|
||||||
original: Mapping
|
original: Mapping,
|
||||||
|
hasPriority?: Store<boolean>
|
||||||
}[] = []
|
}[] = []
|
||||||
const addIcons = applicableMappings.some((m) => m.icon !== undefined)
|
const addIcons = applicableMappings.some((m) => m.icon !== undefined)
|
||||||
for (let i = 0; i < applicableMappings.length; i++) {
|
for (let i = 0; i < applicableMappings.length; i++) {
|
||||||
|
@ -317,6 +319,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
mainTerm: tr.translations,
|
mainTerm: tr.translations,
|
||||||
searchTerms: mapping.searchTerms,
|
searchTerms: mapping.searchTerms,
|
||||||
original: mapping,
|
original: mapping,
|
||||||
|
hasPriority: tagsSource.map(tags => mapping.priorityIf?.matchesProperties(tags))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
|
@ -397,7 +400,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
const values = TagRenderingQuestion.MappingToPillValue(
|
const values = TagRenderingQuestion.MappingToPillValue(
|
||||||
applicableMappings,
|
applicableMappings,
|
||||||
tagsSource,
|
tagsSource,
|
||||||
state
|
state,
|
||||||
)
|
)
|
||||||
|
|
||||||
const searchValue: UIEventSource<string> =
|
const searchValue: UIEventSource<string> =
|
||||||
|
@ -411,47 +414,12 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
}
|
}
|
||||||
const mode = configuration.multiAnswer ? "select-many" : "select-one"
|
const mode = configuration.multiAnswer ? "select-many" : "select-one"
|
||||||
|
|
||||||
const tooMuchElementsValue = new UIEventSource<number[]>([])
|
|
||||||
|
|
||||||
let priorityPresets: BaseUIElement = undefined
|
|
||||||
const classes = "h-64 overflow-scroll"
|
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, {
|
const presetSearch = new SearchablePillsSelector<number>(values, {
|
||||||
selectIfSingle: true,
|
|
||||||
mode,
|
mode,
|
||||||
searchValue,
|
searchValue,
|
||||||
onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
|
onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
|
||||||
searchAreaClass: classes,
|
searchAreaClass: classes
|
||||||
onManyElementsValue: tooMuchElementsValue,
|
|
||||||
onManyElements: priorityPresets,
|
|
||||||
})
|
})
|
||||||
const fallbackTag = searchValue.map((s) => {
|
const fallbackTag = searchValue.map((s) => {
|
||||||
if (s === undefined || ff?.key === undefined) {
|
if (s === undefined || ff?.key === undefined) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue