Refactoring: convert language input element into svelte,remove many obsolete classes

This commit is contained in:
Pieter Vander Vennet 2024-01-25 03:13:18 +01:00
parent e68b31e267
commit 2e8b44659a
36 changed files with 1038 additions and 1412 deletions

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
import { Translation, TypedTranslation } from "../../i18n/Translation"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import * as all_languages from "../../../assets/language_translations.json"
/**
* Visualizes a list of the known languages
*/
export let languages: Store<string[]>
export let single_render: string
export let item_render: string
export let render_all: string // Should contain one `{list()}`
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig | undefined
let [beforeListing, afterListing] = (render_all ?? "{list()}").split("{list()}")
</script>
{#if $languages.length === 1}
<SpecialTranslation {state} {tags} {feature} {layer}
t={new TypedTranslation({"*": single_render}).PartialSubsTr(
"language()",
new Translation(all_languages[$languages[0]], undefined)
)}/>
{:else}
{beforeListing}
<ul>
{#each $languages as language}
<li>
<SpecialTranslation {state} {tags} {feature} {layer} t={
new TypedTranslation({"*": item_render}).PartialSubsTr("language()",
new Translation(all_languages[language], undefined) )}
/>
</li>
{/each}
</ul>
{afterListing}
{/if}

View file

@ -0,0 +1,69 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource"
import LanguageQuestion from "./LanguageQuestion.svelte"
import LanguageAnswer from "./LanguageAnswer.svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import EditButton from "../TagRendering/EditButton.svelte"
export let key: string
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState
export let feature: Feature
export let layer: LayerConfig | undefined
export let question: string
export let on_no_known_languages: string = undefined
export let single_render: string
export let item_render: string
export let render_all: string // Should contain one `{list()}`
let prefix = key + ":"
let foundLanguages = tags.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)
</script>
{#if $foundLanguages.length === 0 && on_no_known_languages && !$forceInputMode}
<div class="p-1 flex items-center justify-between low-interaction rounded">
<div>
{on_no_known_languages}
</div>
<EditButton on:click={_ => forceInputMode.setData(true)} />
</div>
{:else if $forceInputMode || $foundLanguages.length === 0}
<LanguageQuestion {question} {foundLanguages} {prefix} {state} {tags} {feature} {layer}
on:save={_ => forceInputMode.setData(false)}>
<span slot="cancel-button">
{#if $forceInputMode}
<button on:click={_ => forceInputMode.setData(false)}>
<Tr t={Translations.t.general.cancel} />
</button>
{/if}
</span>
</LanguageQuestion>
{:else}
<div class="p-2 flex items-center justify-between low-interaction rounded">
<div>
<LanguageAnswer {single_render} {item_render} {render_all} languages={foundLanguages} {state} {tags} { feature}
{layer} />
</div>
<EditButton on:click={_ => forceInputMode.setData(true)} />
</div>
{/if}

View file

@ -0,0 +1,107 @@
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import BaseUIElement from "../../BaseUIElement"
import { UIEventSource } from "../../../Logic/UIEventSource"
import SvelteUIElement from "../../Base/SvelteUIElement"
import { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { default as LanguageElementSvelte } from "./LanguageElement.svelte"
export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser"
needsUrls = []
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",
},
]
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()}"}
}
\`\`\`
`
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages] =
argument
if (item_render === undefined || item_render.trim() === "") {
item_render = "{language()}"
}
if (all_render === undefined || all_render.length == 0) {
all_render = "{list()}"
}
if (single_render.indexOf("{language()") < 0) {
throw (
"Error while calling language_chooser: render_single_language must contain '{language()}' but it is " +
single_render
)
}
if (item_render.indexOf("{language()") < 0) {
throw (
"Error while calling language_chooser: render_list_item must contain '{language()}' but it is " +
item_render
)
}
if (all_render.indexOf("{list()") < 0) {
throw "Error while calling language_chooser: render_all must contain '{list()}'"
}
if (on_no_known_languages === "") {
on_no_known_languages = undefined
}
return new SvelteUIElement(LanguageElementSvelte, {
key,
tags: tagSource,
state,
feature,
layer,
question,
on_no_known_languages,
single_render,
item_render,
})
}
}

View file

@ -0,0 +1,133 @@
<script lang="ts">/**
* An input element which allows to select one or more langauges
*/
import { UIEventSource } from "../../../Logic/UIEventSource"
import all_languages from "../../../assets/language_translations.json"
import { Translation } from "../../i18n/Translation"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Locale from "../../i18n/Locale"
/**
* Will contain one or more ISO-language codes
*/
export let selectedLanguages: UIEventSource<string[]>
/**
* The country (countries) that the point lies in.
* Note that a single place might be claimed by multiple countries
*/
export let countries: Set<string>
let searchValue: UIEventSource<string> = new UIEventSource<string>("")
let searchLC = searchValue.mapD(search => search.toLowerCase())
const knownLanguagecodes = Object.keys(all_languages)
let probableLanguages = []
let isChecked = {}
for (const lng of knownLanguagecodes) {
const lngInfo = all_languages[lng]
if (lngInfo._meta?.countries?.some(l => countries.has(l))) {
probableLanguages.push(lng)
}
isChecked[lng] = false
}
let newlyChecked: UIEventSource<string[]> = new UIEventSource<string[]>([])
function update(isChecked: Record<string, boolean>) {
const currentlyChecked = new Set<string>(selectedLanguages.data)
const languages: string[] = []
for (const lng in isChecked) {
if (isChecked[lng]) {
languages.push(lng)
if (!currentlyChecked.has(lng)) {
newlyChecked.data.push(lng)
newlyChecked.ping()
}
}
}
selectedLanguages.setData(languages)
}
function matchesSearch(lng: string, searchLc: string | undefined): boolean {
if(!searchLc){
return
}
if(lng.indexOf(searchLc) >= 0){
return true
}
const languageInfo = all_languages[lng]
const native : string = languageInfo[lng]?.toLowerCase()
if(native?.indexOf(searchLc) >= 0){
return true
}
const current : string = languageInfo[Locale.language.data]?.toLowerCase()
if(current?.indexOf(searchLc) >= 0){
return true
}
return false
}
function onEnter(){
// we select the first match which is not yet checked
for (const lng of knownLanguagecodes) {
if(lng === searchLC.data){
isChecked[lng] = true
return
}
}
for (const lng of knownLanguagecodes) {
if(matchesSearch(lng, searchLC.data)){
isChecked[lng] = true
return
}
}
}
$: {
update(isChecked)
}
searchValue.addCallback(_ => {
newlyChecked.setData([])
})
</script>
<form on:submit|preventDefault={() => onEnter()}>
{#each probableLanguages as lng}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/each}
<label class="block relative neutral-label m-4 mx-16">
<SearchIcon class="w-6 h-6 absolute right-0" />
<input bind:value={$searchValue} type="text" />
<Tr t={Translations.t.general.useSearch} />
</label>
<div class="overflow-auto" style="max-height: 25vh">
{#each knownLanguagecodes as lng}
{#if (isChecked[lng]) && $newlyChecked.indexOf(lng) < 0 && probableLanguages.indexOf(lng) < 0}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/if}
{/each}
{#each knownLanguagecodes as lng}
{#if $searchLC.length > 0 && matchesSearch(lng, $searchLC) && (!isChecked[lng] || $newlyChecked.indexOf(lng) >= 0) && probableLanguages.indexOf(lng) < 0}
<label class="no-image-background flex items-center gap-1">
<input bind:checked={isChecked[lng]} type="checkbox" />
<Tr t={new Translation(all_languages[lng])} />
<span class="subtle">({lng})</span>
</label>
{/if}
{/each}
</div>
</form>

View file

@ -0,0 +1,87 @@
<script lang="ts">/**
* The 'languageQuestion' is a special element which asks about the (possible) languages of a feature
* (e.g. which speech output an ATM has, in what language(s) the braille writing is or what languages are spoken at a school)
*
* This is written into a `key`.
*
*/
import { Translation } from "../../i18n/Translation"
import SpecialTranslation from "../TagRendering/SpecialTranslation.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Store } from "../../../Logic/UIEventSource"
import { UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import LanguageOptions from "./LanguageOptions.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { createEventDispatcher } from "svelte"
import { Tag } from "../../../Logic/Tags/Tag"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../../Logic/Tags/And"
export let question: string
export let prefix: string
export let foundLanguages: Store<string[]>
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig | undefined
let dispatch = createEventDispatcher<{ save }>()
let selectedLanguages: UIEventSource<string[]> = new UIEventSource<string[]>([])
let countries: Store<Set<string>> = tags.mapD(tags => new Set<string>(tags["_country"]?.toUpperCase()?.split(";") ?? []))
async function applySelectedLanguages() {
const selectedLngs = selectedLanguages.data
const selection: Tag[] = selectedLanguages.data.map((ln) => new Tag(prefix + ln, "yes"))
if (selection.length === 0) {
return
}
const currentLanguages = foundLanguages.data
for (const currentLanguage of currentLanguages) {
if (selectedLngs.indexOf(currentLanguage) >= 0) {
continue
}
// Erase languages that are not spoken anymore
selection.push(new Tag(prefix + currentLanguage, ""))
}
if (state === undefined || state?.featureSwitchIsTesting?.data) {
for (const tag of selection) {
tags.data[tag.key] = tag.value
}
tags.ping()
} else if (state.changes) {
await state.changes
.applyAction(
new ChangeTagAction(
tags.data.id,
new And(selection),
tags.data,
{
theme: state?.layout?.id ?? "unkown",
changeType: "answer",
},
),
)
}
dispatch("save")
}
</script>
<div class="flex flex-col disable-links interactive border-interactive p-2">
<div class="interactive justify-between pt-1 font-bold">
<SpecialTranslation {feature} {layer} {state} t={new Translation({"*":question})} {tags} />
</div>
<LanguageOptions {selectedLanguages} countries={$countries}/>
<div class="flex justify-end flex-wrap-reverse w-full">
<slot name="cancel-button"></slot>
<button class="primary" class:disabled={$selectedLanguages.length === 0} on:click={_ => applySelectedLanguages()}>
<Tr t={Translations.t.general.save} />
</button>
</div>
</div>