forked from MapComplete/MapComplete
		
	Refactoring: convert language input element into svelte,remove many obsolete classes
This commit is contained in:
		
							parent
							
								
									e68b31e267
								
							
						
					
					
						commit
						2e8b44659a
					
				
					 36 changed files with 1038 additions and 1412 deletions
				
			
		
							
								
								
									
										46
									
								
								src/UI/Popup/LanguageElement/LanguageAnswer.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/UI/Popup/LanguageElement/LanguageAnswer.svelte
									
										
									
									
									
										Normal 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} | ||||
							
								
								
									
										69
									
								
								src/UI/Popup/LanguageElement/LanguageElement.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/UI/Popup/LanguageElement/LanguageElement.svelte
									
										
									
									
									
										Normal 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} | ||||
							
								
								
									
										107
									
								
								src/UI/Popup/LanguageElement/LanguageElement.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/UI/Popup/LanguageElement/LanguageElement.ts
									
										
									
									
									
										Normal 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, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										133
									
								
								src/UI/Popup/LanguageElement/LanguageOptions.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/UI/Popup/LanguageElement/LanguageOptions.svelte
									
										
									
									
									
										Normal 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> | ||||
							
								
								
									
										87
									
								
								src/UI/Popup/LanguageElement/LanguageQuestion.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/UI/Popup/LanguageElement/LanguageQuestion.svelte
									
										
									
									
									
										Normal 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> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue