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,24 +53,31 @@ 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) => {
|
||||
if (s === undefined || s.length === 0) {
|
||||
return true
|
||||
}
|
||||
this.matchesSearchCriteria = search.map(s => {
|
||||
if (s === undefined || s.length === 0) {
|
||||
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) {
|
||||
return true
|
||||
}
|
||||
s = s?.trim()?.toLowerCase()
|
||||
if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) {
|
||||
return true
|
||||
if (options?.hide?.data) {
|
||||
return false
|
||||
}
|
||||
if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) {
|
||||
return true
|
||||
}
|
||||
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,38 +152,57 @@ 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
|
||||
: new Combine([
|
||||
Svg.search_svg().SetClass("w-8 normal-background"),
|
||||
search.SetClass("w-full"),
|
||||
]).SetClass("flex items-center border-2 border-black m-2")
|
||||
Svg.search_svg().SetClass("w-8 normal-background"),
|
||||
search.SetClass("w-full"),
|
||||
]).SetClass("flex items-center border-2 border-black m-2")
|
||||
|
||||
const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase())
|
||||
const selectedElements = options?.selectedElements ?? new UIEventSource<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(() => {
|
||||
editMode.setData(true)
|
||||
}),
|
||||
undefined,
|
||||
state.osmConnection.isLoggedIn
|
||||
),
|
||||
new EditButton(state.osmConnection,() => {
|
||||
editMode.setData(true)
|
||||
}),
|
||||
]).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…
Reference in a new issue