Add QR-code to all popups, add direction indicator to popup and visual feedback, make reviews accessible to screenreaders (both to read them and to make them)

This commit is contained in:
Pieter Vander Vennet 2023-12-24 05:01:10 +01:00
parent 5567869bb4
commit bfd818cb38
33 changed files with 415 additions and 98 deletions

View file

@ -8,20 +8,25 @@
import { GeoOperations } from "../../Logic/GeoOperations"
import { Store } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import ThemeViewState from "../../Models/ThemeViewState"
import Compass_arrow from "../../assets/svg/Compass_arrow.svelte"
import { twMerge } from "tailwind-merge"
import { Orientation } from "../../Sensors/Orientation"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
import Locale from "../i18n/Locale"
import { ariaLabelStore } from "../../Utils/ariaLabel"
import type { SpecialVisualizationState } from "../SpecialVisualization"
export let state: ThemeViewState
export let state: SpecialVisualizationState
export let feature: Feature
export let size = "w-8 h-8"
let fcenter = GeoOperations.centerpointCoordinates(feature)
// Bearing and distance relative to the map center
let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map(
(l) => {
let mapCenter = [l.lon, l.lat]
let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter))
let bearing = Math.round(GeoOperations.bearing(mapCenter, fcenter))
let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
return { bearing, dist }
},
@ -29,19 +34,87 @@
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(coordinate => {
return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter)
})
let compass = Orientation.singleton.alpha.map(compass => compass ?? 0)
export let size = "w-8 h-8"
let compass = Orientation.singleton.alpha
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
let absoluteDirections = Translations.t.general.visualFeedback.directionsAbsolute
let closeToCurrentLocation = state.geolocation.geolocationState.currentGPSLocation.map(gps => {
if (!gps) {
return false
}
let l = state.mapProperties.location.data
let mapCenter = [l.lon, l.lat]
const dist = GeoOperations.distanceBetween([gps.longitude, gps.latitude], mapCenter)
return dist < Constants.viewportCenterCloseToGpsCutoff
},
[state.mapProperties.location],
)
let labelFromCenter: Store<string> = bearingAndDist.mapD(({ bearing, dist }) => {
const distHuman = GeoOperations.distanceToHuman(dist)
const lang = Locale.language.data
const t = absoluteDirections[GeoOperations.bearingToHuman(bearing)]
const mainTr = Translations.t.general.visualFeedback.fromMapCenter.Subs({
distance: distHuman,
direction: t.textFor(lang),
})
return mainTr.textFor(lang)
}, [compass, Locale.language])
// Bearing and distance relative to the map center
let bearingAndDistGps: Store<{
bearing: number;
dist: number
} | undefined> = state.geolocation.geolocationState.currentGPSLocation.mapD(
({ longitude, latitude }) => {
let gps = [longitude, latitude]
let bearing = Math.round(GeoOperations.bearing(gps, fcenter))
let dist = Math.round(GeoOperations.distanceBetween(fcenter, gps))
return { bearing, dist }
},
)
let labelFromGps: Store<string | undefined> = bearingAndDistGps.mapD(({ bearing, dist }) => {
const distHuman = GeoOperations.distanceToHuman(dist)
const lang = Locale.language.data
let bearingHuman: string
if (compass.data !== undefined) {
console.log("compass:", compass.data)
const bearingRelative = bearing - compass.data
const t = relativeDirections[GeoOperations.bearingToHumanRelative(bearingRelative)]
bearingHuman = t.textFor(lang)
} else {
bearingHuman = absoluteDirections[GeoOperations.bearingToHuman(bearing)].textFor(lang)
}
const mainTr = Translations.t.general.visualFeedback.fromGps.Subs({
distance: distHuman,
direction: bearingHuman,
})
return mainTr.textFor(lang)
}, [compass, Locale.language])
let label = labelFromCenter.map(labelFromCenter => {
if (labelFromGps.data !== undefined) {
if(closeToCurrentLocation.data){
return labelFromGps.data
}
return labelFromCenter + ", " + labelFromGps.data
}
return labelFromCenter
}, [labelFromGps])
function focusMap(){
state.mapProperties.location.setData({ lon: fcenter[0], lat: fcenter[1] })
}
</script>
<div class={twMerge("relative", size)}>
<div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm",size)}>
{GeoOperations.distanceToHuman($bearingAndDist.dist)}
<button class={twMerge("relative rounded-full soft", size)} use:ariaLabelStore={label} on:click={() => focusMap()}>
<div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm break-words",size)}>
{GeoOperations.distanceToHuman($bearingAndDistGps.dist)}
</div>
{#if $bearingFromGps !== undefined}
<div class={twMerge("absolute top-0 left-0 rounded-full border border-gray-500", size)}>
<div class={twMerge("absolute top-0 left-0 rounded-full", size)}>
<Compass_arrow class={size}
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - $compass}deg) );`} />
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - ($compass ?? 0)}deg) );`} />
</div>
{/if}
</div>
<span>{$bearingAndDist.bearing}° {GeoOperations.bearingToHuman($bearingAndDist.bearing)} {GeoOperations.bearingToHumanRelative($bearingAndDist.bearing - $compass)}</span>
</button>

View file

@ -11,11 +11,12 @@
export let cls: string = ""
// Text for the current language
let txt: Store<string | undefined> = t?.current
$: {txt = t?.current}
</script>
{#if $txt}
<span class={cls}>
<FromHtml src={$txt} />
<WeblateLink context={t.context} />
<WeblateLink context={t?.context} />
</span>
{/if}

View file

@ -4,12 +4,16 @@
import ThemeViewState from "../../Models/ThemeViewState"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import { Orientation } from "../../Sensors/Orientation"
import { Translation } from "../i18n/Translation"
import Constants from "../../Models/Constants"
/**
* Indicates how far away the viewport center is from the current user location
*/
export let state: ThemeViewState
const t = Translations.t.general.visualFeedback
const relativeDir = t.directionsRelative
let map = state.mapProperties
let currentLocation = state.geolocation.geolocationState.currentGPSLocation
@ -23,14 +27,29 @@
const distanceInMeters = Math.round(GeoOperations.distanceBetween(gps, mapCenter))
const distance = GeoOperations.distanceToHuman(distanceInMeters)
const bearing = Math.round(GeoOperations.bearing(gps, mapCenter))
return { distance, bearing, distanceInMeters }
const bearingDirection = GeoOperations.bearingToHuman(bearing)
return { distance, bearing, distanceInMeters, bearingDirection }
}, [currentLocation])
let hasCompass = Orientation.singleton.gotMeasurement
let compass = Orientation.singleton.alpha
let relativeBearing: Store<{distance: string, bearing: Translation}> =
compass.mapD(compass => {
const bearing: Translation = relativeDir[GeoOperations.bearingToHumanRelative(distanceToCurrentLocation.data.bearing - compass)]
return {bearing, distance: distanceToCurrentLocation.data.distance}
}, [distanceToCurrentLocation])
let viewportCenterDetails = Translations.DynamicSubstitute(t.viewportCenterDetails, relativeBearing)
let viewportCenterDetailsAbsolute = Translations.DynamicSubstitute(t.viewportCenterDetails, distanceToCurrentLocation.map(({distance, bearing}) => {
return {distance, bearing: t.directionsAbsolute[GeoOperations.bearingToHuman(bearing)]}
}))
</script>
{#if $currentLocation !== undefined}
{#if $distanceToCurrentLocation.distanceInMeters < 20}
{#if $distanceToCurrentLocation.distanceInMeters < Constants.viewportCenterCloseToGpsCutoff}
<Tr t={t.viewportCenterCloseToGps} />
{:else if $hasCompass}
{$viewportCenterDetails}
{:else}
<Tr t={t.viewportCenterDetails.Subs($distanceToCurrentLocation)} />
{$viewportCenterDetailsAbsolute}
{/if}
{/if}

View file

@ -55,11 +55,10 @@
{#if currentLocation}
<div
role="alert"
aria-live="assertive"
class="normal-background border-interactive rounded-full px-2 flex flex-col items-center"
>
{currentLocation}
{currentLocation}.
<MapCenterDetails {state}/>
</div>
{/if}

View file

@ -4,6 +4,7 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
export let state: SpecialVisualizationState
export let feature: Feature
@ -14,11 +15,14 @@
</script>
<a class="small flex space-x-1 cursor-pointer w-fit" href={`#${feature.properties.id}`}>
<span class="inline-flex gap-x-1">
<a class="small flex space-x-0.5 cursor-pointer w-fit items-center" href={`#${feature.properties.id}`}>
{#if i !== undefined}
<span class="font-bold">{i + 1} &nbsp; </span>
{/if}
<TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state}
{tags} />
<DirectionIndicator {feature} {state} />
</a>
<DirectionIndicator {feature} {state} />
</span>

View file

@ -32,7 +32,7 @@
lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined))
</script>
<div aria-live="assertive" class="p-1 interactive" role="alert">
<div aria-live="assertive" class="p-1 bg-white m-1 rounded">
{#if $lastAction?.key === "out"}
<Tr t={t.out.Subs({z: map.zoom.data - 1})} />
{:else if $lastAction?.key === "in"}
@ -46,13 +46,11 @@
<div class="pointer-events-auto">
<Tr t={$translationWithLength} />
<MapCenterDetails {state} />
<ol>
<div class="grid grid-cols-3 space-x-1 space-y-0.5">
{#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)}
<li>
<Summary {state} feature={feat} {i} />
</li>
<Summary {state} feature={feat} {i} />
{/each}
</ol>
</div>
</div>
{/if}
</div>

View file

@ -2,9 +2,9 @@
import type { SpecialVisualizationState } from "../SpecialVisualization"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import type { Feature } from "geojson"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
import { UIEventSource } from "../../Logic/UIEventSource"
import { GeoOperations } from "../../Logic/GeoOperations"
import Center from "../../assets/svg/Center.svelte"
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
export let feature: Feature
let properties: Record<string, string> = feature.properties
@ -30,11 +30,6 @@
center()
}
const coord = GeoOperations.centerpointCoordinates(feature)
const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => {
let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat]))
return GeoOperations.distanceToHuman(meters)
})
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
</script>
@ -57,7 +52,7 @@
class="title-icons links-as-button flex flex-wrap items-center gap-x-0.5 self-end justify-self-end p-1 pt-0.5 sm:pt-1"
>
{#each favConfig.titleIcons as titleIconConfig}
{#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...properties, ...state.userRelatedState.preferencesAsTags.data } ) ?? true) && titleIconConfig.IsKnown(properties)}
{#if titleIconBlacklist.indexOf(titleIconConfig.id) < 0 && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)}
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer
config={titleIconConfig}
@ -71,12 +66,7 @@
{/if}
{/each}
<button class="p-1" on:click={() => center()}>
<Center class="h-6 w-6" />
</button>
<div class="w-14">
{$distance}
</div>
<DirectionIndicator {state} {feature} />
</div>
</div>
{/if}

View file

@ -48,7 +48,7 @@
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
<Tr t={Translations.t.favouritePoi.intro.Subs({ length: $favourites?.length ?? 0 })} />
<Tr t={Translations.t.favouritePoi.privacy} />
<Tr t={Translations.t.favouritePoi.priintroPrivacyvacy} />
{#each $favourites as feature (feature.properties.id)}
<FavouriteSummary {feature} {state} />

View file

@ -57,7 +57,11 @@
validator = Validators.get(type ?? "string")
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
if(_value.data?.length > 0){
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
}else{
feedback?.setData(undefined)
}
initValueAndDenom()
}
@ -65,9 +69,14 @@
function setValues() {
// Update the value stores
const v = _value.data
if (!validator?.isValid(v, getCountry) || v === "") {
if(v === ""){
value.setData(undefined)
feedback.setData(undefined)
return
}
if (!validator?.isValid(v, getCountry)) {
feedback?.setData(validator?.getFeedback(v, getCountry))
value.setData("")
value.setData(undefined)
return
}

View file

@ -16,15 +16,22 @@ export default class NatValidator extends IntValidator {
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}
/**
*
* const validator = new NatValidator()
* validator.getFeedback(-4).textFor("en") // => "This number should be positive"
*/
getFeedback(s: string): Translation {
console.log("Getting feedback for", s)
const n = Number(s)
if (!isNaN(n) && n < 0) {
return Translations.t.validation.nat.mustBePositive
}
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
const n = Number(s)
if (n < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -3,16 +3,20 @@
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import DynamicIcon from "./DynamicIcon.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { Orientation } from "../../Sensors/Orientation"
/**
* Renders a 'marker', which consists of multiple 'icons'
*/
export let marker: IconConfig[] = config?.marker
export let marker: IconConfig[]
export let tags: Store<Record<string, string>>
export let rotation: TagRenderingConfig = undefined
let _rotation = rotation
let _rotation: Store<string> = rotation
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
: new ImmutableStore(0)
: new ImmutableStore("0deg")
if(rotation?.render?.txt === "{alpha}deg"){
_rotation = Orientation.singleton.alpha.map(alpha => alpha ? (alpha)+"deg" : "0deg ")
}
</script>
{#if marker && marker}

View file

@ -27,6 +27,7 @@
import Confirm from "../../assets/svg/Confirm.svelte"
import Not_found from "../../assets/svg/Not_found.svelte"
import { twMerge } from "tailwind-merge"
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
/**
* Renders a single icon.
@ -100,6 +101,8 @@
<HeartOutlineIcon class={clss} />
{:else if icon === "confirm"}
<Confirm class={clss} {color} />
{:else if icon === "direction"}
<Direction_gradient class={clss} {color} />
{:else if icon === "not_found"}
<Not_found class={twMerge(clss, "no-image-background")} {color} />
{:else}

View file

@ -171,6 +171,7 @@ class PointRenderingLayer {
store
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any>pitchAligment))
if (feature.geometry.type === "Point") {
// When the tags get 'pinged', check that the location didn't change
store.addCallbackAndRunD(() => {

View file

@ -4,13 +4,11 @@
import StarsBar from "./StarsBar.svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
export let review: Review & { madeByLoggedInUser: Store<boolean> }
export let review: Review & { kid: string,signature: string, madeByLoggedInUser: Store<boolean> }
let name = review.metadata.nickname
name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim()
if (name.length === 0) {
name = "Anonymous"
}
name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim()
let d = new Date()
d.setTime(review.iat * 1000)
let date = d.toDateString()
@ -19,18 +17,32 @@
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
<div class="flex items-center justify-between">
<StarsBar score={review.rating} />
<div tabindex="0" use:ariaLabel={Translations.t.reviews.rated.Subs({n: ""+(Math.round(review.rating / 10)/2)})}>
<StarsBar readonly={true} score={review.rating} />
</div>
<div class="flex flex-wrap space-x-2">
<div class="font-bold">
{name}
</div>
<a href={`https://mangrove.reviews/list?kid=${encodeURIComponent(review.kid)}`} rel="noopener"
target="_blank">
{#if !name}
<i>Anonymous</i>
{:else}
<span class="font-bold">
{name}
</span>
{/if}
</a>
<span class="subtle">
{date}
</span>
</div>
</div>
{#if review.opinion}
{review.opinion}
<div class="link-no-underline">
<a target="_blank" rel="noopener nofollow"
href={`https://mangrove.reviews/list?signature=${encodeURIComponent(review.signature)}`}>
{review.opinion}
</a>
</div>
{/if}
{#if review.metadata.is_affiliated}
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />

View file

@ -1,31 +1,32 @@
<script lang="ts">
import ToSvelte from "../Base/ToSvelte.svelte"
import Svg from "../../Svg"
import { createEventDispatcher } from "svelte"
import Star from "../../assets/svg/Star.svelte"
import Star_half from "../../assets/svg/Star_half.svelte"
import Star_outline from "../../assets/svg/Star_outline.svelte"
import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel"
import Translations from "../i18n/Translations"
export let score: number
export let cutoff: number
export let starSize = "w-h h-4"
export let i: number
export let readonly = false
let dispatch = createEventDispatcher<{ hover: { score: number }, click: { score: number } }>()
let dispatch = createEventDispatcher<{ hover: { score: number }, click: { score: number } }>()
let container: HTMLElement
function getScore(e: MouseEvent): number {
if (e.clientX === 0 && e.clientY === 0) {
// Keyboard triggered 'click' -> return max value
return cutoff
}
const x = e.clientX - e.target.getBoundingClientRect().x
const w = container.getClientRects()[0]?.width
return x / w < 0.5 ? cutoff - 10 : cutoff
}
</script>
<div
bind:this={container}
on:click={(e) => dispatch("click", { score: getScore(e) })}
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}
>
{#if readonly}
{#if score >= cutoff}
<Star class={starSize} />
{:else if score + 10 >= cutoff}
@ -33,4 +34,22 @@
{:else}
<Star_outline class={starSize} />
{/if}
</div>
{:else}
<button
use:ariaLabel={Translations.t.reviews.rate.Subs({n: i+1})}
class="small soft rounded-full no-image-background"
style="padding: 0; border: none;"
bind:this={container}
on:click={(e) => dispatch("click", { score: getScore(e) })}
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}
>
{#if score >= cutoff}
<Star class={starSize} />
{:else if score + 10 >= cutoff}
<Star_half class={starSize} />
{:else}
<Star_outline class={starSize} />
{/if}
</button>
{/if}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import StarElement from "./StarElement.svelte"
/**
@ -9,12 +8,13 @@
let cutoffs = [20, 40, 60, 80, 100]
export let starSize = "w-h h-4"
export let readonly = false
</script>
{#if score !== undefined}
<div class="flex" on:mouseout>
{#each cutoffs as cutoff, i}
<StarElement {score} {i} {cutoff} {starSize} on:hover on:click />
<StarElement {readonly} {score} {i} {cutoff} {starSize} on:hover on:click />
{/each}
</div>
{/if}

View file

@ -1,10 +1,16 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource"
import StarsBar from "./StarsBar.svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
import Translations from "../i18n/Translations"
export let score: Store<number>
let scoreRounded = score.mapD(count => Math.round(count / 10) / 2)
</script>
{#if $score !== undefined && $score !== null}
<StarsBar score={$score} />
<div tabindex="0"
use:ariaLabel={Translations.t.reviews.averageRating.Subs({n: $scoreRounded})}>
<StarsBar readonly={true} score={$score} />
</div>
{/if}

View file

@ -19,6 +19,7 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { OsmTags } from "../Models/OsmFeature"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
/**
* The state needed to render a special Visualisation.
@ -87,6 +88,7 @@ export interface SpecialVisualizationState {
readonly imageUploadManager: ImageUploadManager
readonly previewedImage: UIEventSource<ProvidedImage>
readonly geolocation: GeoLocationHandler
}
export interface SpecialVisualization {

View file

@ -85,6 +85,9 @@ import { Unit } from "../Models/Unit"
import Link from "./Base/Link.svelte"
import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte"
import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte"
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
import Img from "./Base/Img"
import Qr from "../Utils/Qr"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -1539,6 +1542,43 @@ export default class SpecialVisualizations {
})
},
},
{
funcName: "direction_indicator",
args: [],
needsUrls: [],
docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature })
},
},
{
funcName: "qr_code",
args: [],
needsUrls: [],
docs: "Generates a QR-code to share the selected object",
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const url =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
"#" +
feature.properties.id
return new Img(new Qr(url).toImageElement(75)).SetStyle("width: 75px")
},
},
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -4,6 +4,9 @@ import BaseUIElement from "../BaseUIElement"
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
import LanguageUtils from "../../Utils/LanguageUtils"
import { ClickableToggle } from "../Input/Toggle"
import { Store } from "../../Logic/UIEventSource"
import Locale from "./Locale"
import { Utils } from "../../Utils"
export default class Translations {
static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t
@ -130,6 +133,29 @@ export default class Translations {
}
}
public static DynamicSubstitute<T extends Record<string, string | Translation>>(
translation: TypedTranslation<T>,
t: Store<T>
): Store<string> {
return Locale.language.map(
(lang) => {
const tags: Record<string, string> = {}
for (const k in t.data) {
let v = t.data[k]
if (!v) {
continue
}
if (v["textFor"] !== undefined) {
v = v["textFor"](lang)
}
tags[k] = <string>v
}
return Utils.SubstituteKeys(translation.textFor(lang), t.data)
},
[t]
)
}
static isProbablyATranslation(transl: any) {
if (!transl || typeof transl !== "object") {
return false