More work on A11y
This commit is contained in:
parent
87aee9e2b7
commit
6da72b80ef
28 changed files with 398 additions and 209 deletions
|
@ -139,7 +139,8 @@
|
||||||
"condition": "_theme:backgroundLayer=",
|
"condition": "_theme:backgroundLayer=",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": "mapcomplete-preferred-background-layer=",
|
"if": "mapcomplete-preferred-background-layer=default",
|
||||||
|
"alsoShowIf": "mapcomplete-preferred-background-layer=",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "Use the default background layer",
|
"en": "Use the default background layer",
|
||||||
"ca": "Utilitzeu la capa de fons predeterminada",
|
"ca": "Utilitzeu la capa de fons predeterminada",
|
||||||
|
|
|
@ -94,10 +94,10 @@
|
||||||
"backToSelect": "Vælg en anden kategori",
|
"backToSelect": "Vælg en anden kategori",
|
||||||
"confirmButton": "Tilføj en {category}<br><div class=\"alert\">Din tilføjelse er synlig for alle</div>",
|
"confirmButton": "Tilføj en {category}<br><div class=\"alert\">Din tilføjelse er synlig for alle</div>",
|
||||||
"confirmLocation": "Bekræft dette sted",
|
"confirmLocation": "Bekræft dette sted",
|
||||||
"confirmTitle": "Tilføj en {titel}?",
|
"confirmTitle": "Tilføj en {title}?",
|
||||||
"disableFilters": "Slå alle filtre fra",
|
"disableFilters": "Slå alle filtre fra",
|
||||||
"disableFiltersExplanation": "Nogle elementer kan være skjult af et filter",
|
"disableFiltersExplanation": "Nogle elementer kan være skjult af et filter",
|
||||||
"enableLayer": "Aktivér lag {navn}",
|
"enableLayer": "Aktivér lag {name}",
|
||||||
"hasBeenImported": "Punktet er allerede importeret",
|
"hasBeenImported": "Punktet er allerede importeret",
|
||||||
"import": {
|
"import": {
|
||||||
"hasBeenImported": "Objektet blev importeret",
|
"hasBeenImported": "Objektet blev importeret",
|
||||||
|
@ -125,8 +125,8 @@
|
||||||
"isApplied": "Ændringerne er anvendt"
|
"isApplied": "Ændringerne er anvendt"
|
||||||
},
|
},
|
||||||
"attribution": {
|
"attribution": {
|
||||||
"attributionBackgroundLayer": "Det nuværende baggrundslag er {navn}",
|
"attributionBackgroundLayer": "Det nuværende baggrundslag er {name}",
|
||||||
"attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er [navn}: {copyright}",
|
"attributionBackgroundLayerWithCopyright": "Det nuværende baggrundslag er {name}: {copyright}",
|
||||||
"attributionContent": "<p>Alle data leveres af <a href=\"https://osm.org\" target=\"_blank\">OpenStreetMap</a>, frit genanvendelige under <a href=\"https://osm.org/copyright\" target=\"_blank\">Open DataBase Licensen</a>.</p>",
|
"attributionContent": "<p>Alle data leveres af <a href=\"https://osm.org\" target=\"_blank\">OpenStreetMap</a>, frit genanvendelige under <a href=\"https://osm.org/copyright\" target=\"_blank\">Open DataBase Licensen</a>.</p>",
|
||||||
"attributionTitle": "Meddelelse om tilskrivning",
|
"attributionTitle": "Meddelelse om tilskrivning",
|
||||||
"codeContributionsBy": "MapComplete er lavet af {contributors} og <a href=\"https://github.com/pietervdvn/MapComplete/graphs/contributors\" target=\"_blank\">{hiddenCount} flere bidragsydere</a>",
|
"codeContributionsBy": "MapComplete er lavet af {contributors} og <a href=\"https://github.com/pietervdvn/MapComplete/graphs/contributors\" target=\"_blank\">{hiddenCount} flere bidragsydere</a>",
|
||||||
|
@ -253,7 +253,7 @@
|
||||||
"pickLanguage": "Vælg et sprog: ",
|
"pickLanguage": "Vælg et sprog: ",
|
||||||
"poweredByOsm": "Drevet af OpenStreetMap",
|
"poweredByOsm": "Drevet af OpenStreetMap",
|
||||||
"questionBox": {
|
"questionBox": {
|
||||||
"answeredMultiple": "Du besvarede [answered} spørgsmål",
|
"answeredMultiple": "Du besvarede {answered} spørgsmål",
|
||||||
"answeredMultipleSkippedMultiple": "Du besvarede {answered} spørgsmål og sprang over {skipped} spørgsmål",
|
"answeredMultipleSkippedMultiple": "Du besvarede {answered} spørgsmål og sprang over {skipped} spørgsmål",
|
||||||
"answeredMultipleSkippedOne": "Du besvarede {answered} spørgsmål og sprang over ét spørgsmål",
|
"answeredMultipleSkippedOne": "Du besvarede {answered} spørgsmål og sprang over ét spørgsmål",
|
||||||
"answeredOne": "Du besvarede ét spørgsmål",
|
"answeredOne": "Du besvarede ét spørgsmål",
|
||||||
|
|
|
@ -398,17 +398,20 @@
|
||||||
"useSearch": "Use the search above to see presets",
|
"useSearch": "Use the search above to see presets",
|
||||||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
"useSearchForMore": "Use the search function to search within {total} more values…",
|
||||||
"visualFeedback": {
|
"visualFeedback": {
|
||||||
"closestFeaturesAre": "{n} features within view",
|
"closestFeaturesAre": "{n} features within viewport.",
|
||||||
"east": "Moving east",
|
"east": "Moving east",
|
||||||
"in": "Zooming in",
|
"in": "Zooming in to level {z}",
|
||||||
"islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.",
|
"islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.",
|
||||||
"locked": "View is now locked to your GPS-location, moving disabled.",
|
"locked": "View is now locked to your GPS-location, moving disabled.",
|
||||||
"navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.",
|
"navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.",
|
||||||
"noCloseFeatures": "No features in view",
|
"noCloseFeatures": "No features in view",
|
||||||
"north": "Moving north",
|
"north": "Moving north",
|
||||||
"out": "Zooming out",
|
"oneFeatureInView": "One feature within viewport.",
|
||||||
|
"out": "Zooming out to level {z}",
|
||||||
"south": "Moving south",
|
"south": "Moving south",
|
||||||
"unlocked": "Moving enabled.",
|
"unlocked": "Moving enabled.",
|
||||||
|
"viewportCenterCloseToGps": "The map is centered around your location.",
|
||||||
|
"viewportCenterDetails": "The viewport center is {distance} away and {bearing} from your location.",
|
||||||
"west": "Moving west"
|
"west": "Moving west"
|
||||||
},
|
},
|
||||||
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
|
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
|
||||||
|
|
|
@ -370,15 +370,15 @@
|
||||||
"useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien",
|
"useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien",
|
||||||
"useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…",
|
"useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…",
|
||||||
"visualFeedback": {
|
"visualFeedback": {
|
||||||
"closestFeaturesAre": "{n} object in in beeld",
|
"closestFeaturesAre": "{n} object in beeld.",
|
||||||
"east": "Naar het oosten",
|
"east": "Naar het oosten",
|
||||||
"in": "Aan het inzoomen",
|
"in": "Aan het inzoomen naar zoomlevel {z}",
|
||||||
"islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.",
|
"islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.",
|
||||||
"locked": "Bewegen vergrendeld rond jouw huidige locatie.",
|
"locked": "Bewegen vergrendeld rond jouw huidige locatie.",
|
||||||
"navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.",
|
"navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.",
|
||||||
"noCloseFeatures": "Niet in beeld",
|
"noCloseFeatures": "Niet in beeld",
|
||||||
"north": "Naar het noorden",
|
"north": "Naar het noorden",
|
||||||
"out": "Aan het uitzoomen",
|
"out": "Aan het uitzoomen naar zoomlevel {z}",
|
||||||
"south": "Naar het zuiden",
|
"south": "Naar het zuiden",
|
||||||
"unlocked": "Bewegen ontgrendeld",
|
"unlocked": "Bewegen ontgrendeld",
|
||||||
"west": "Naar het westen"
|
"west": "Naar het westen"
|
||||||
|
|
|
@ -1342,10 +1342,6 @@ video {
|
||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-none {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appearance-none {
|
.appearance-none {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
@ -1906,6 +1902,11 @@ video {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.text-3xl {
|
.text-3xl {
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
line-height: 2.25rem;
|
line-height: 2.25rem;
|
||||||
|
@ -1916,11 +1917,6 @@ video {
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-sm {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-4xl {
|
.text-4xl {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
|
|
|
@ -288,4 +288,8 @@ export class BBox {
|
||||||
throw "BBOX has NAN"
|
throw "BBOX has NAN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public overlapsWithFeature(f: Feature) {
|
||||||
|
return GeoOperations.calculateOverlap(this.asGeoJson({}), [f]).length > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,32 @@ import { Feature } from "geojson"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import FilteringFeatureSource from "./FilteringFeatureSource"
|
import FilteringFeatureSource from "./FilteringFeatureSource"
|
||||||
import LayerState from "../../State/LayerState"
|
import LayerState from "../../State/LayerState"
|
||||||
|
import { BBox } from "../../BBox"
|
||||||
|
|
||||||
export default class NearbyFeatureSource implements FeatureSource {
|
export default class NearbyFeatureSource implements FeatureSource {
|
||||||
private readonly _result = new UIEventSource<Feature[]>(undefined)
|
|
||||||
|
|
||||||
public readonly features: Store<Feature[]>
|
public readonly features: Store<Feature[]>
|
||||||
|
private readonly _result = new UIEventSource<Feature[]>(undefined)
|
||||||
private readonly _targetPoint: Store<{ lon: number; lat: number }>
|
private readonly _targetPoint: Store<{ lon: number; lat: number }>
|
||||||
private readonly _numberOfNeededFeatures: number
|
private readonly _numberOfNeededFeatures: number
|
||||||
private readonly _layerState?: LayerState
|
private readonly _layerState?: LayerState
|
||||||
private readonly _currentZoom: Store<number>
|
private readonly _currentZoom: Store<number>
|
||||||
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
||||||
|
private readonly _bounds: Store<BBox> | undefined
|
||||||
constructor(
|
constructor(
|
||||||
targetPoint: Store<{ lon: number; lat: number }>,
|
targetPoint: Store<{ lon: number; lat: number }>,
|
||||||
sources: ReadonlyMap<string, FilteringFeatureSource>,
|
sources: ReadonlyMap<string, FilteringFeatureSource>,
|
||||||
numberOfNeededFeatures?: number,
|
options?: {
|
||||||
layerState?: LayerState,
|
bounds?: Store<BBox>
|
||||||
currentZoom?: Store<number>
|
numberOfNeededFeatures?: number
|
||||||
|
layerState?: LayerState
|
||||||
|
currentZoom?: Store<number>
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
this._layerState = layerState
|
this._layerState = options?.layerState
|
||||||
this._targetPoint = targetPoint.stabilized(100)
|
this._targetPoint = targetPoint.stabilized(100)
|
||||||
this._numberOfNeededFeatures = numberOfNeededFeatures
|
this._numberOfNeededFeatures = options?.numberOfNeededFeatures
|
||||||
this._currentZoom = currentZoom.stabilized(500)
|
this._currentZoom = options?.currentZoom.stabilized(500)
|
||||||
|
this._bounds = options?.bounds
|
||||||
|
|
||||||
this.features = Stores.ListStabilized(this._result)
|
this.features = Stores.ListStabilized(this._result)
|
||||||
|
|
||||||
|
@ -53,6 +57,10 @@ export default class NearbyFeatureSource implements FeatureSource {
|
||||||
private update() {
|
private update() {
|
||||||
let features: { feat: Feature; d: number }[] = []
|
let features: { feat: Feature; d: number }[] = []
|
||||||
for (const src of this._allSources) {
|
for (const src of this._allSources) {
|
||||||
|
if (src.data === undefined) {
|
||||||
|
this._result.setData(undefined)
|
||||||
|
return // We cannot yet calculate all the features
|
||||||
|
}
|
||||||
features.push(...src.data)
|
features.push(...src.data)
|
||||||
}
|
}
|
||||||
features.sort((a, b) => a.d - b.d)
|
features.sort((a, b) => a.d - b.d)
|
||||||
|
@ -80,6 +88,15 @@ export default class NearbyFeatureSource implements FeatureSource {
|
||||||
if (this._currentZoom.data < minZoom) {
|
if (this._currentZoom.data < minZoom) {
|
||||||
return empty
|
return empty
|
||||||
}
|
}
|
||||||
|
if (this._bounds) {
|
||||||
|
const bbox = this._bounds.data
|
||||||
|
if (!bbox) {
|
||||||
|
// We have a 'bounds' store, but the bounds store itself is still empty
|
||||||
|
// As such, we cannot yet calculate which features are within the store
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
feats = feats.filter((f) => bbox.overlapsWithFeature(f))
|
||||||
|
}
|
||||||
const point = this._targetPoint.data
|
const point = this._targetPoint.data
|
||||||
const lonLat = <[number, number]>[point.lon, point.lat]
|
const lonLat = <[number, number]>[point.lon, point.lat]
|
||||||
const withDistance = feats.map((feat) => ({
|
const withDistance = feats.map((feat) => ({
|
||||||
|
@ -95,7 +112,7 @@ export default class NearbyFeatureSource implements FeatureSource {
|
||||||
}
|
}
|
||||||
return withDistance
|
return withDistance
|
||||||
},
|
},
|
||||||
[this._targetPoint, isActive, this._currentZoom]
|
[this._targetPoint, isActive, this._currentZoom, this._bounds]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,7 @@ export class GeoOperations {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect wether or not the given point is located in the feature
|
* Detect whether or not the given point is located in the feature
|
||||||
*
|
*
|
||||||
* // Should work with a normal polygon
|
* // Should work with a normal polygon
|
||||||
* const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}};
|
* const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}};
|
||||||
|
@ -985,4 +985,87 @@ export class GeoOperations {
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeoOperations.distanceToHuman(52.8) // => "53m"
|
||||||
|
* GeoOperations.distanceToHuman(2800) // => "2.8km"
|
||||||
|
* GeoOperations.distanceToHuman(12800) // => "13km"
|
||||||
|
*
|
||||||
|
* @param meters
|
||||||
|
*/
|
||||||
|
public static distanceToHuman(meters: number): string {
|
||||||
|
meters = Math.round(meters)
|
||||||
|
if (meters < 1000) {
|
||||||
|
return meters + "m"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meters >= 10000) {
|
||||||
|
const km = Math.round(meters / 1000)
|
||||||
|
return km + "km"
|
||||||
|
}
|
||||||
|
|
||||||
|
meters = Math.round(meters / 100)
|
||||||
|
const kmStr = "" + meters
|
||||||
|
|
||||||
|
return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const
|
||||||
|
private static readonly directionsRelative = [
|
||||||
|
"straight",
|
||||||
|
"slight_right",
|
||||||
|
"right",
|
||||||
|
"sharp_right",
|
||||||
|
"behind",
|
||||||
|
"sharp_left",
|
||||||
|
"left",
|
||||||
|
"slight_left",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeoOperations.bearingToHuman(0) // => "N"
|
||||||
|
* GeoOperations.bearingToHuman(-9) // => "N"
|
||||||
|
* GeoOperations.bearingToHuman(-10) // => "N"
|
||||||
|
* GeoOperations.bearingToHuman(-180) // => "S"
|
||||||
|
* GeoOperations.bearingToHuman(181) // => "S"
|
||||||
|
* GeoOperations.bearingToHuman(46) // => "NE"
|
||||||
|
*/
|
||||||
|
public static bearingToHuman(
|
||||||
|
bearing: number
|
||||||
|
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
|
||||||
|
while (bearing < 0) {
|
||||||
|
bearing += 360
|
||||||
|
}
|
||||||
|
bearing %= 360
|
||||||
|
bearing += 22.5
|
||||||
|
const segment = Math.floor(bearing / 45) % GeoOperations.directions.length
|
||||||
|
return GeoOperations.directions[segment]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeoOperations.bearingToHuman(0) // => "N"
|
||||||
|
* GeoOperations.bearingToHuman(-10) // => "N"
|
||||||
|
* GeoOperations.bearingToHuman(-180) // => "S"
|
||||||
|
* GeoOperations.bearingToHuman(181) // => "S"
|
||||||
|
* GeoOperations.bearingToHuman(46) // => "NE"
|
||||||
|
*/
|
||||||
|
public static bearingToHumanRelative(
|
||||||
|
bearing: number
|
||||||
|
):
|
||||||
|
| "straight"
|
||||||
|
| "slight_right"
|
||||||
|
| "right"
|
||||||
|
| "sharp_right"
|
||||||
|
| "behind"
|
||||||
|
| "sharp_left"
|
||||||
|
| "left"
|
||||||
|
| "slight_left" {
|
||||||
|
while (bearing < 0) {
|
||||||
|
bearing += 360
|
||||||
|
}
|
||||||
|
bearing %= 360
|
||||||
|
bearing += 22.5
|
||||||
|
const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length
|
||||||
|
return GeoOperations.directionsRelative[segment]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,14 @@
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
||||||
export class ThemeMetaTagging {
|
export class ThemeMetaTagging {
|
||||||
public static readonly themeName = "usersettings"
|
public static readonly themeName = "usersettings"
|
||||||
|
|
||||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||||
feat.properties._description
|
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||||
?.at(1)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||||
)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||||
Utils.AddLazyProperty(
|
feat.properties['__current_backgroun'] = 'initial_value'
|
||||||
feat.properties,
|
}
|
||||||
"_d",
|
}
|
||||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
|
||||||
((feat) => {
|
|
||||||
const e = document.createElement("div")
|
|
||||||
e.innerHTML = feat.properties._d
|
|
||||||
return Array.from(e.getElementsByTagName("a")).filter(
|
|
||||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
|
||||||
)[0]?.href
|
|
||||||
})(feat)
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
|
||||||
((feat) => {
|
|
||||||
const e = document.createElement("div")
|
|
||||||
e.innerHTML = feat.properties._d
|
|
||||||
return Array.from(e.getElementsByTagName("a")).filter(
|
|
||||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
|
||||||
)[0]?.href
|
|
||||||
})(feat)
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(
|
|
||||||
feat.properties,
|
|
||||||
"_mastodon_candidate",
|
|
||||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
|
||||||
)
|
|
||||||
feat.properties["__current_backgroun"] = "initial_value"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,6 +10,9 @@ export class Stores {
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
source.setData(new Date())
|
source.setData(new Date())
|
||||||
|
if (Utils.runningFromConsole) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (asLong === undefined || asLong()) {
|
if (asLong === undefined || asLong()) {
|
||||||
window.setTimeout(run, millis)
|
window.setTimeout(run, millis)
|
||||||
}
|
}
|
||||||
|
@ -104,7 +107,8 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
M
|
M
|
||||||
public mapD<J>(
|
public mapD<J>(
|
||||||
f: (t: Exclude<T, undefined | null>) => J,
|
f: (t: Exclude<T, undefined | null>) => J,
|
||||||
extraStoresToWatch?: Store<any>[]
|
extraStoresToWatch?: Store<any>[],
|
||||||
|
callbackDestroyFunction?: (f: () => void) => void
|
||||||
): Store<J> {
|
): Store<J> {
|
||||||
return this.map((t) => {
|
return this.map((t) => {
|
||||||
if (t === undefined) {
|
if (t === undefined) {
|
||||||
|
@ -263,7 +267,7 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
/**
|
/**
|
||||||
* Converts the uiEventSource into a promise.
|
* Converts the uiEventSource into a promise.
|
||||||
* The promise will return the value of the store if the given condition evaluates to true
|
* The promise will return the value of the store if the given condition evaluates to true
|
||||||
* @param condition: an optional condition, default to 'store.value !== undefined'
|
* @param condition an optional condition, default to 'store.value !== undefined'
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
public AsPromise(condition?: (t: T) => boolean): Promise<T> {
|
public AsPromise(condition?: (t: T) => boolean): Promise<T> {
|
||||||
|
@ -482,7 +486,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
||||||
stores = []
|
stores = []
|
||||||
}
|
}
|
||||||
if (extraStores?.length > 0) {
|
if (extraStores?.length > 0) {
|
||||||
stores.push(...extraStores)
|
stores?.push(...extraStores)
|
||||||
}
|
}
|
||||||
if (this._extraStores?.length > 0) {
|
if (this._extraStores?.length > 0) {
|
||||||
this._extraStores?.forEach((store) => {
|
this._extraStores?.forEach((store) => {
|
||||||
|
@ -767,9 +771,9 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
/**
|
/**
|
||||||
* Monoidal map which results in a read-only store
|
* Monoidal map which results in a read-only store
|
||||||
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
|
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
|
||||||
* @param f: The transforming function
|
* @param f The transforming function
|
||||||
* @param extraSources: also trigger the update if one of these sources change
|
* @param extraSources also trigger the update if one of these sources change
|
||||||
* @param onDestroy: a callback that can trigger the destroy function
|
* @param onDestroy a callback that can trigger the destroy function
|
||||||
*
|
*
|
||||||
* const src = new UIEventSource<number>(10)
|
* const src = new UIEventSource<number>(10)
|
||||||
* const store = src.map(i => i * 2)
|
* const store = src.map(i => i * 2)
|
||||||
|
@ -802,7 +806,8 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
*/
|
*/
|
||||||
public mapD<J>(
|
public mapD<J>(
|
||||||
f: (t: Exclude<T, undefined | null>) => J,
|
f: (t: Exclude<T, undefined | null>) => J,
|
||||||
extraSources: Store<any>[] = []
|
extraSources: Store<any>[] = [],
|
||||||
|
callbackDestroyFunction?: (f: () => void) => void
|
||||||
): Store<J | undefined> {
|
): Store<J | undefined> {
|
||||||
return new MappedStore(
|
return new MappedStore(
|
||||||
this,
|
this,
|
||||||
|
@ -819,17 +824,18 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
this._callbacks,
|
this._callbacks,
|
||||||
this.data === undefined || this.data === null
|
this.data === undefined || this.data === null
|
||||||
? <undefined | null>this.data
|
? <undefined | null>this.data
|
||||||
: f(<any>this.data)
|
: f(<any>this.data),
|
||||||
|
callbackDestroyFunction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Two way sync with functions in both directions
|
* Two way sync with functions in both directions
|
||||||
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
|
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
|
||||||
* @param f: The transforming function
|
* @param f The transforming function
|
||||||
* @param extraSources: also trigger the update if one of these sources change
|
* @param extraSources also trigger the update if one of these sources change
|
||||||
* @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData
|
* @param g a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData
|
||||||
* @param allowUnregister: if set, the update will be halted if no listeners are registered
|
* @param allowUnregister if set, the update will be halted if no listeners are registered
|
||||||
*/
|
*/
|
||||||
public sync<J>(
|
public sync<J>(
|
||||||
f: (t: T) => J,
|
f: (t: T) => J,
|
||||||
|
|
|
@ -105,6 +105,12 @@ export interface MappingConfigJson {
|
||||||
*/
|
*/
|
||||||
hideInAnswer?: boolean | TagConfigJson
|
hideInAnswer?: boolean | TagConfigJson
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Also show this 'then'-option if the feature matches these tags.
|
||||||
|
* Ideal for outdated tags.
|
||||||
|
*/
|
||||||
|
alsoShowIf?: TagConfigJson
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* question: What tags should be applied if this mapping is _not_ chosen?
|
* question: What tags should be applied if this mapping is _not_ chosen?
|
||||||
*
|
*
|
||||||
|
|
|
@ -168,6 +168,7 @@ export interface TagRenderingConfigJson {
|
||||||
* This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'}
|
* This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'}
|
||||||
*/
|
*/
|
||||||
if: TagConfigJson
|
if: TagConfigJson
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* question: What text should be shown?
|
* question: What text should be shown?
|
||||||
*
|
*
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface Icon {}
|
||||||
|
|
||||||
export interface Mapping {
|
export interface Mapping {
|
||||||
readonly if: UploadableTag
|
readonly if: UploadableTag
|
||||||
|
readonly alsoShowIf: Tag | undefined
|
||||||
readonly ifnot?: UploadableTag
|
readonly ifnot?: UploadableTag
|
||||||
readonly then: TypedTranslation<object>
|
readonly then: TypedTranslation<object>
|
||||||
readonly icon: string
|
readonly icon: string
|
||||||
|
@ -383,7 +384,9 @@ export default class TagRenderingConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const prioritySearch =
|
const prioritySearch =
|
||||||
mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined
|
mapping.priorityIf !== undefined
|
||||||
|
? TagUtils.Tag(mapping.priorityIf, `${ctx}.priorityIf`)
|
||||||
|
: undefined
|
||||||
const mp = <Mapping>{
|
const mp = <Mapping>{
|
||||||
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
|
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
|
||||||
ifnot:
|
ifnot:
|
||||||
|
@ -391,6 +394,10 @@ export default class TagRenderingConfig {
|
||||||
? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`)
|
? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`)
|
||||||
: undefined,
|
: undefined,
|
||||||
then: Translations.T(mapping.then, `${ctx}.then`),
|
then: Translations.T(mapping.then, `${ctx}.then`),
|
||||||
|
alsoShowIf:
|
||||||
|
mapping.alsoShowIf !== undefined
|
||||||
|
? TagUtils.Tag(mapping.alsoShowIf, `${ctx}.alsoShowIf`)
|
||||||
|
: undefined,
|
||||||
hideInAnswer,
|
hideInAnswer,
|
||||||
icon,
|
icon,
|
||||||
iconClass,
|
iconClass,
|
||||||
|
@ -530,6 +537,9 @@ export default class TagRenderingConfig {
|
||||||
if (mapping.if.matchesProperties(tags)) {
|
if (mapping.if.matchesProperties(tags)) {
|
||||||
return mapping
|
return mapping
|
||||||
}
|
}
|
||||||
|
if (mapping.alsoShowIf?.matchesProperties(tags)) {
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -818,6 +828,7 @@ export default class TagRenderingConfig {
|
||||||
for (const m of this.mappings ?? []) {
|
for (const m of this.mappings ?? []) {
|
||||||
tags.push(m.if)
|
tags.push(m.if)
|
||||||
tags.push(m.priorityIf)
|
tags.push(m.priorityIf)
|
||||||
|
tags.push(m.alsoShowIf)
|
||||||
tags.push(...(m.addExtraTags ?? []))
|
tags.push(...(m.addExtraTags ?? []))
|
||||||
if (typeof m.hideInAnswer !== "boolean") {
|
if (typeof m.hideInAnswer !== "boolean") {
|
||||||
tags.push(m.hideInAnswer)
|
tags.push(m.hideInAnswer)
|
||||||
|
|
|
@ -119,6 +119,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly previewedImage = new UIEventSource<ProvidedImage>(undefined)
|
readonly previewedImage = new UIEventSource<ProvidedImage>(undefined)
|
||||||
|
|
||||||
readonly addNewPoint: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
readonly addNewPoint: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||||
|
/**
|
||||||
|
* When using arrow keys to move, the accessibility mode is activated, which has a small rectangle set.
|
||||||
|
* This is the 'viewport' which 'closestFeatures' uses to filter wilt
|
||||||
|
*/
|
||||||
|
readonly visualFeedbackViewportBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
|
||||||
|
|
||||||
readonly lastClickObject: LastClickFeatureSource
|
readonly lastClickObject: LastClickFeatureSource
|
||||||
readonly overlayLayerStates: ReadonlyMap<
|
readonly overlayLayerStates: ReadonlyMap<
|
||||||
|
@ -351,9 +356,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.closestFeatures = new NearbyFeatureSource(
|
this.closestFeatures = new NearbyFeatureSource(
|
||||||
this.mapProperties.location,
|
this.mapProperties.location,
|
||||||
this.perLayerFiltered,
|
this.perLayerFiltered,
|
||||||
3,
|
{
|
||||||
this.layerState,
|
currentZoom: this.mapProperties.zoom,
|
||||||
this.mapProperties.zoom
|
layerState: this.layerState,
|
||||||
|
bounds: this.visualFeedbackViewportBounds,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
||||||
this.imageUploadManager = new ImageUploadManager(
|
this.imageUploadManager = new ImageUploadManager(
|
||||||
|
@ -476,8 +483,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
*/
|
*/
|
||||||
private selectClosestAtCenter(i: number = 0) {
|
private selectClosestAtCenter(i: number = 0) {
|
||||||
this.visualFeedback.setData(true)
|
this.visualFeedback.setData(true)
|
||||||
const toSelect = this.closestFeatures.features.data[i]
|
const toSelect = this.closestFeatures.features?.data?.[i]
|
||||||
if (!toSelect) {
|
if (!toSelect) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
const toSelect = this.closestFeatures.features?.data?.[i]
|
||||||
|
if (!toSelect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const layer = this.layout.getMatchingLayer(toSelect.properties)
|
||||||
|
this.selectedElement.setData(undefined)
|
||||||
|
this.selectedLayer.setData(layer)
|
||||||
|
this.selectedElement.setData(toSelect)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const layer = this.layout.getMatchingLayer(toSelect.properties)
|
const layer = this.layout.getMatchingLayer(toSelect.properties)
|
||||||
|
|
47
src/UI/Base/DirectionIndicator.svelte
Normal file
47
src/UI/Base/DirectionIndicator.svelte
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An A11Y feature which indicates how far away and in what direction the feature lies.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
export let state: ThemeViewState
|
||||||
|
export let feature: Feature
|
||||||
|
|
||||||
|
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 dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
|
||||||
|
return { bearing, dist }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
</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)}
|
||||||
|
</div>
|
||||||
|
{#if $bearingFromGps !== undefined}
|
||||||
|
<div class={twMerge("absolute top-0 left-0 rounded-full border border-gray-500", size)}>
|
||||||
|
<Compass_arrow class={size}
|
||||||
|
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - $compass}deg) );`} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span>{$bearingAndDist.bearing}° {GeoOperations.bearingToHuman($bearingAndDist.bearing)} {GeoOperations.bearingToHumanRelative($bearingAndDist.bearing - $compass)}</span>
|
|
@ -47,7 +47,8 @@ export default class Hotkeys {
|
||||||
onUp?: boolean
|
onUp?: boolean
|
||||||
},
|
},
|
||||||
documentation: string | Translation,
|
documentation: string | Translation,
|
||||||
action: () => void | false
|
action: () => void | false,
|
||||||
|
alsoTriggeredOn?: Translation[]
|
||||||
) {
|
) {
|
||||||
const type = key["onUp"] ? "keyup" : "keypress"
|
const type = key["onUp"] ? "keyup" : "keypress"
|
||||||
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
|
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
title={ariaLabel}
|
||||||
target={newTab ? "_blank" : undefined}
|
target={newTab ? "_blank" : undefined}
|
||||||
{download}
|
{download}
|
||||||
class={classnames}
|
class={classnames}
|
||||||
|
|
36
src/UI/BigComponents/MapCenterDetails.svelte
Normal file
36
src/UI/BigComponents/MapCenterDetails.svelte
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates how far away the viewport center is from the current user location
|
||||||
|
*/
|
||||||
|
export let state: ThemeViewState
|
||||||
|
const t = Translations.t.general.visualFeedback
|
||||||
|
let map = state.mapProperties
|
||||||
|
|
||||||
|
let currentLocation = state.geolocation.geolocationState.currentGPSLocation
|
||||||
|
let distanceToCurrentLocation: Store<{ distance: string, distanceInMeters: number, bearing: number }> = map.location.mapD(({ lon, lat }) => {
|
||||||
|
const current = currentLocation.data
|
||||||
|
if (!current) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const gps: [number, number] = [current.longitude, current.latitude]
|
||||||
|
const mapCenter: [number, number] = [lon, lat]
|
||||||
|
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 }
|
||||||
|
}, [currentLocation])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $currentLocation !== undefined}
|
||||||
|
{#if $distanceToCurrentLocation.distanceInMeters < 20}
|
||||||
|
<Tr t={t.viewportCenterCloseToGps} />
|
||||||
|
{:else}
|
||||||
|
<Tr t={t.viewportCenterDetails.Subs($distanceToCurrentLocation)} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
|
@ -8,8 +8,11 @@
|
||||||
import Hotkeys from "../Base/Hotkeys"
|
import Hotkeys from "../Base/Hotkeys"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Locale from "../i18n/Locale"
|
import Locale from "../i18n/Locale"
|
||||||
|
import MapCenterDetails from "./MapCenterDetails.svelte"
|
||||||
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let mapProperties: MapProperties
|
export let state: ThemeViewState
|
||||||
|
let mapProperties = state.mapProperties
|
||||||
let lastDisplayed: Date = undefined
|
let lastDisplayed: Date = undefined
|
||||||
let currentLocation: string = undefined
|
let currentLocation: string = undefined
|
||||||
|
|
||||||
|
@ -51,8 +54,9 @@
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
class="normal-background border-interactive rounded-full px-2"
|
class="normal-background border-interactive rounded-full px-2 flex flex-col items-center"
|
||||||
>
|
>
|
||||||
{currentLocation}
|
{currentLocation}
|
||||||
|
<MapCenterDetails {state}/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -31,8 +31,11 @@
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<!-- Title element-->
|
<!-- Title element-->
|
||||||
<h3>
|
<h3>
|
||||||
|
<a href={`#${$tags.id}`}>
|
||||||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
||||||
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1"
|
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 p-1 pt-0.5 sm:pt-1"
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
|
@ -13,30 +12,13 @@
|
||||||
let tags = state.featureProperties.getStore(id)
|
let tags = state.featureProperties.getStore(id)
|
||||||
let layer: LayerConfig = state.layout.getMatchingLayer(tags.data)
|
let layer: LayerConfig = state.layout.getMatchingLayer(tags.data)
|
||||||
|
|
||||||
function select() {
|
|
||||||
state.selectedElement.setData(undefined)
|
|
||||||
state.selectedLayer.setData(layer)
|
|
||||||
state.selectedElement.setData(feature)
|
|
||||||
}
|
|
||||||
|
|
||||||
let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map(
|
|
||||||
(l) => {
|
|
||||||
let fcenter = GeoOperations.centerpointCoordinates(feature)
|
|
||||||
let mapCenter = [l.lon, l.lat]
|
|
||||||
|
|
||||||
let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter))
|
|
||||||
let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
|
|
||||||
return { bearing, dist }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="small flex cursor-pointer" on:click={() => select()}>
|
<a class="small flex space-x-1 cursor-pointer w-fit" href={`#${feature.properties.id}`}>
|
||||||
<span class="flex">
|
{#if i !== undefined}
|
||||||
{#if i !== undefined}
|
<span class="font-bold">{i + 1} </span>
|
||||||
<span class="font-bold">{i + 1}.</span>
|
{/if}
|
||||||
{/if}
|
<TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state}
|
||||||
<TagRenderingAnswer config={layer.title} {layer} selectedElement={feature} {state} {tags} />
|
{tags} />
|
||||||
{$bearingAndDist.dist}m {$bearingAndDist.bearing}°
|
<DirectionIndicator {feature} {state} />
|
||||||
</span>
|
</a>
|
||||||
</div>
|
|
||||||
|
|
|
@ -7,18 +7,24 @@
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
import Summary from "./Summary.svelte"
|
import Summary from "./Summary.svelte"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import type { KeyNavigationEvent } from "../../Models/MapProperties"
|
import type { KeyNavigationEvent } from "../../Models/MapProperties"
|
||||||
import type { Feature } from "geojson"
|
import MapCenterDetails from "./MapCenterDetails.svelte"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
export let featuresInViewPort: Store<Feature[]>
|
|
||||||
console.log("Visual feedback panel:", featuresInViewPort)
|
|
||||||
const t = Translations.t.general.visualFeedback
|
const t = Translations.t.general.visualFeedback
|
||||||
|
let map = state.mapProperties
|
||||||
let centerFeatures = state.closestFeatures.features
|
let centerFeatures = state.closestFeatures.features
|
||||||
|
let translationWithLength = centerFeatures.mapD(cf => cf.length).mapD(n => {
|
||||||
|
if (n === 1) {
|
||||||
|
return t.oneFeatureInView
|
||||||
|
}
|
||||||
|
return t.closestFeaturesAre.Subs({ n })
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
let lastAction: UIEventSource<KeyNavigationEvent> = new UIEventSource<KeyNavigationEvent>(
|
let lastAction: UIEventSource<KeyNavigationEvent> = new UIEventSource<KeyNavigationEvent>(
|
||||||
undefined
|
undefined,
|
||||||
)
|
)
|
||||||
state.mapProperties.onKeyNavigationEvent((event) => {
|
state.mapProperties.onKeyNavigationEvent((event) => {
|
||||||
lastAction.setData(event)
|
lastAction.setData(event)
|
||||||
|
@ -26,17 +32,23 @@
|
||||||
lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined))
|
lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div aria-live="assertive" class="p-1" role="alert">
|
<div aria-live="assertive" class="p-1 interactive" role="alert">
|
||||||
{#if $lastAction !== undefined}
|
{#if $lastAction?.key === "out"}
|
||||||
|
<Tr t={t.out.Subs({z: map.zoom.data - 1})} />
|
||||||
|
{:else if $lastAction?.key === "in"}
|
||||||
|
<Tr t={t.out.Subs({z: map.zoom.data + 1})} />
|
||||||
|
{:else if $lastAction !== undefined}
|
||||||
<Tr t={t[$lastAction.key]} />
|
<Tr t={t[$lastAction.key]} />
|
||||||
{:else if $centerFeatures.length === 0}
|
{:else if $centerFeatures?.length === 0}
|
||||||
<Tr t={t.noCloseFeatures} />
|
<Tr t={t.noCloseFeatures} />
|
||||||
{:else}
|
<MapCenterDetails {state} />
|
||||||
|
{:else if $centerFeatures !== undefined}
|
||||||
<div class="pointer-events-auto">
|
<div class="pointer-events-auto">
|
||||||
<Tr t={t.closestFeaturesAre.Subs({ n: $featuresInViewPort?.length })} />
|
<Tr t={$translationWithLength} />
|
||||||
<ol class="list-none">
|
<MapCenterDetails {state} />
|
||||||
{#each $centerFeatures as feat, i (feat.properties.id)}
|
<ol>
|
||||||
<li class="flex">
|
{#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)}
|
||||||
|
<li>
|
||||||
<Summary {state} feature={feat} {i} />
|
<Summary {state} feature={feat} {i} />
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -33,15 +33,7 @@
|
||||||
const coord = GeoOperations.centerpointCoordinates(feature)
|
const coord = GeoOperations.centerpointCoordinates(feature)
|
||||||
const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => {
|
const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => {
|
||||||
let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat]))
|
let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat]))
|
||||||
|
return GeoOperations.distanceToHuman(meters)
|
||||||
if (meters < 1000) {
|
|
||||||
return meters + "m"
|
|
||||||
}
|
|
||||||
|
|
||||||
meters = Math.round(meters / 100)
|
|
||||||
const kmStr = "" + meters
|
|
||||||
|
|
||||||
return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km"
|
|
||||||
})
|
})
|
||||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -952,17 +952,21 @@ export class ToTextualDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OH.weekdaysIdentical(ranges, 0, 4)) {
|
if (OH.weekdaysIdentical(ranges, 0, 4)) {
|
||||||
result.push(
|
if (ranges[0].length > 0) {
|
||||||
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) })
|
result.push(
|
||||||
)
|
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) })
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
addRange(0, 4)
|
addRange(0, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OH.weekdaysIdentical(ranges, 5, 6)) {
|
if (OH.weekdaysIdentical(ranges, 5, 6)) {
|
||||||
result.push(
|
if (ranges[5].length > 0) {
|
||||||
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) })
|
result.push(
|
||||||
)
|
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) })
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
addRange(5, 6)
|
addRange(5, 6)
|
||||||
}
|
}
|
||||||
|
@ -983,7 +987,6 @@ export class ToTextualDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createRangeFor(range: OpeningRange): Translation {
|
private static createRangeFor(range: OpeningRange): Translation {
|
||||||
console.log(">>>", range)
|
|
||||||
return Translations.t.general.opening_hours.ranges.Subs({
|
return Translations.t.general.opening_hours.ranges.Subs({
|
||||||
starttime: ToTextualDescription.timeString(range.startDate),
|
starttime: ToTextualDescription.timeString(range.startDate),
|
||||||
endtime: ToTextualDescription.timeString(range.endDate),
|
endtime: ToTextualDescription.timeString(range.endDate),
|
||||||
|
@ -991,6 +994,9 @@ export class ToTextualDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createRangesFor(ranges: OpeningRange[]): Translation {
|
private static createRangesFor(ranges: OpeningRange[]): Translation {
|
||||||
|
if (ranges.length === 0) {
|
||||||
|
// return undefined
|
||||||
|
}
|
||||||
let tr = ToTextualDescription.createRangeFor(ranges[0])
|
let tr = ToTextualDescription.createRangeFor(ranges[0])
|
||||||
for (let i = 1; i < ranges.length; i++) {
|
for (let i = 1; i < ranges.length; i++) {
|
||||||
tr = Translations.t.general.opening_hours.rangescombined.Subs({
|
tr = Translations.t.general.opening_hours.rangescombined.Subs({
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
export let score: number
|
export let score: number
|
||||||
export let cutoff: number
|
export let cutoff: number
|
||||||
export let starSize = "w-h h-4"
|
export let starSize = "w-h h-4"
|
||||||
|
export let i: number
|
||||||
|
|
||||||
let dispatch = createEventDispatcher<{ hover: { score: number } }>()
|
let dispatch = createEventDispatcher<{ hover: { score: number }, click: { score: number } }>()
|
||||||
let container: HTMLElement
|
let container: HTMLElement
|
||||||
|
|
||||||
function getScore(e: MouseEvent): number {
|
function getScore(e: MouseEvent): number {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
* Number between 0 and 100. Every 10 points, another half star is added
|
* Number between 0 and 100. Every 10 points, another half star is added
|
||||||
*/
|
*/
|
||||||
export let score: number
|
export let score: number
|
||||||
let dispatch = createEventDispatcher<{ hover: number; click: number }>()
|
|
||||||
|
|
||||||
let cutoffs = [20, 40, 60, 80, 100]
|
let cutoffs = [20, 40, 60, 80, 100]
|
||||||
export let starSize = "w-h h-4"
|
export let starSize = "w-h h-4"
|
||||||
|
@ -14,8 +13,8 @@
|
||||||
|
|
||||||
{#if score !== undefined}
|
{#if score !== undefined}
|
||||||
<div class="flex" on:mouseout>
|
<div class="flex" on:mouseout>
|
||||||
{#each cutoffs as cutoff}
|
{#each cutoffs as cutoff, i}
|
||||||
<StarElement {score} {cutoff} {starSize} on:hover on:click />
|
<StarElement {score} {i} {cutoff} {starSize} on:hover on:click />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -13,13 +13,7 @@
|
||||||
import type { MapProperties } from "../Models/MapProperties"
|
import type { MapProperties } from "../Models/MapProperties"
|
||||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||||
import Translations from "./i18n/Translations"
|
import Translations from "./i18n/Translations"
|
||||||
import {
|
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
CogIcon,
|
|
||||||
EyeIcon,
|
|
||||||
HeartIcon,
|
|
||||||
MenuIcon,
|
|
||||||
XCircleIcon,
|
|
||||||
} from "@rgossiaux/svelte-heroicons/solid"
|
|
||||||
import Tr from "./Base/Tr.svelte"
|
import Tr from "./Base/Tr.svelte"
|
||||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||||
import FloatOver from "./Base/FloatOver.svelte"
|
import FloatOver from "./Base/FloatOver.svelte"
|
||||||
|
@ -72,9 +66,6 @@
|
||||||
import FilterPanel from "./BigComponents/FilterPanel.svelte"
|
import FilterPanel from "./BigComponents/FilterPanel.svelte"
|
||||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
||||||
import { BBox } from "../Logic/BBox"
|
import { BBox } from "../Logic/BBox"
|
||||||
import { GeoOperations } from "../Logic/GeoOperations"
|
|
||||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
|
||||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
|
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
let layout = state.layout
|
let layout = state.layout
|
||||||
|
@ -102,37 +93,38 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) =>
|
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) =>
|
||||||
state.layout.getMatchingLayer(element.properties)
|
state.layout.getMatchingLayer(element.properties),
|
||||||
)
|
)
|
||||||
let currentZoom = state.mapProperties.zoom
|
let currentZoom = state.mapProperties.zoom
|
||||||
let showCrosshair = state.userRelatedState.showCrosshair
|
let showCrosshair = state.userRelatedState.showCrosshair
|
||||||
let visualFeedback = state.visualFeedback
|
let visualFeedback = state.visualFeedback
|
||||||
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
|
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
|
||||||
let featuresInViewPort: UIEventSource<Feature[]> = new UIEventSource<Feature[]>(undefined)
|
|
||||||
viewport.addCallbackAndRunD((viewport) => {
|
|
||||||
state.featuresInView.features.addCallbackAndRunD((features: Feature[]) => {
|
|
||||||
const rect = viewport.getBoundingClientRect()
|
|
||||||
const mlmap = state.map.data
|
|
||||||
if (!mlmap) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const topLeft = mlmap.unproject([rect.left, rect.top])
|
|
||||||
const bottomRight = mlmap.unproject([rect.right, rect.bottom])
|
|
||||||
const bbox = new BBox([
|
|
||||||
[topLeft.lng, topLeft.lat],
|
|
||||||
[bottomRight.lng, bottomRight.lat],
|
|
||||||
])
|
|
||||||
const bboxGeo = bbox.asGeoJson({})
|
|
||||||
console.log("BBOX:", bboxGeo)
|
|
||||||
|
|
||||||
const filtered = features.filter((f: Feature) => {
|
|
||||||
console.log(f, bboxGeo)
|
|
||||||
return GeoOperations.calculateOverlap(bboxGeo, [f]).length > 0
|
|
||||||
})
|
|
||||||
featuresInViewPort.setData(filtered)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
let mapproperties: MapProperties = state.mapProperties
|
let mapproperties: MapProperties = state.mapProperties
|
||||||
|
|
||||||
|
function updateViewport() {
|
||||||
|
const rect = viewport.data?.getBoundingClientRect()
|
||||||
|
if (!rect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mlmap = state.map.data
|
||||||
|
if (!mlmap) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const topLeft = mlmap.unproject([rect.left, rect.top])
|
||||||
|
const bottomRight = mlmap.unproject([rect.right, rect.bottom])
|
||||||
|
const bbox = new BBox([
|
||||||
|
[topLeft.lng, topLeft.lat],
|
||||||
|
[bottomRight.lng, bottomRight.lat],
|
||||||
|
])
|
||||||
|
state.visualFeedbackViewportBounds.setData(bbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport.addCallbackAndRunD(_ => {
|
||||||
|
updateViewport()
|
||||||
|
})
|
||||||
|
mapproperties.bounds.addCallbackAndRunD(_ => {
|
||||||
|
updateViewport()
|
||||||
|
})
|
||||||
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
||||||
let availableLayers = state.availableLayers
|
let availableLayers = state.availableLayers
|
||||||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
||||||
|
@ -142,7 +134,7 @@
|
||||||
onDestroy(
|
onDestroy(
|
||||||
rasterLayer.addCallbackAndRunD((l) => {
|
rasterLayer.addCallbackAndRunD((l) => {
|
||||||
rasterLayerName = l.properties.name
|
rasterLayerName = l.properties.name
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
let previewedImage = state.previewedImage
|
let previewedImage = state.previewedImage
|
||||||
|
|
||||||
|
@ -173,8 +165,14 @@
|
||||||
|
|
||||||
<div class="pointer-events-none absolute top-0 left-0 w-full">
|
<div class="pointer-events-none absolute top-0 left-0 w-full">
|
||||||
<!-- Top components -->
|
<!-- Top components -->
|
||||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
|
||||||
<div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2">
|
<div class="pointer-events-auto float-right mt-1 px-1 max-[480px]:w-full sm:m-2 flex flex-col">
|
||||||
|
<If condition={state.visualFeedback}>
|
||||||
|
<div class="w-fit">
|
||||||
|
<VisualFeedbackPanel {state} />
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||||
<Geosearch
|
<Geosearch
|
||||||
bounds={state.mapProperties.bounds}
|
bounds={state.mapProperties.bounds}
|
||||||
on:searchCompleted={() => {
|
on:searchCompleted={() => {
|
||||||
|
@ -183,8 +181,8 @@
|
||||||
perLayer={state.perLayer}
|
perLayer={state.perLayer}
|
||||||
selectedElement={state.selectedElement}
|
selectedElement={state.selectedElement}
|
||||||
/>
|
/>
|
||||||
</div>
|
</If>
|
||||||
</If>
|
</div>
|
||||||
<div class="float-left m-1 flex flex-col sm:mt-2">
|
<div class="float-left m-1 flex flex-col sm:mt-2">
|
||||||
<MapControlButton
|
<MapControlButton
|
||||||
on:click={() => state.guistate.themeIsOpened.setData(true)}
|
on:click={() => state.guistate.themeIsOpened.setData(true)}
|
||||||
|
@ -229,7 +227,7 @@
|
||||||
<!-- Flex and w-full are needed for the positioning -->
|
<!-- Flex and w-full are needed for the positioning -->
|
||||||
<!-- Centermessage -->
|
<!-- Centermessage -->
|
||||||
<StateIndicator {state} />
|
<StateIndicator {state} />
|
||||||
<ReverseGeocoding mapProperties={mapproperties} />
|
<ReverseGeocoding {state} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -280,9 +278,6 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<If condition={state.visualFeedback}>
|
|
||||||
<VisualFeedbackPanel {state} {featuresInViewPort} />
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-end">
|
<div class="flex flex-col items-end">
|
||||||
<!-- bottom right elements -->
|
<!-- bottom right elements -->
|
||||||
|
|
|
@ -1,28 +1,21 @@
|
||||||
import { Translation } from "../UI/i18n/Translation"
|
import { Translation } from "../UI/i18n/Translation"
|
||||||
import Locale from "../UI/i18n/Locale"
|
import { Store } from "../Logic/UIEventSource"
|
||||||
|
|
||||||
export function ariaLabel(htmlElement: Element, t: Translation) {
|
export function ariaLabel(htmlElement: Element, t: Translation) {
|
||||||
|
ariaLabelStore(htmlElement, t?.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ariaLabelStore(htmlElement: Element, t: Store<string>) {
|
||||||
if (!t) {
|
if (!t) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let destroy: () => void = undefined
|
let destroy: () => void = undefined
|
||||||
|
|
||||||
Locale.language.map((language) => {
|
t?.mapD(
|
||||||
if (!t.translations[language]) {
|
|
||||||
console.log(
|
|
||||||
"No aria label in",
|
|
||||||
language,
|
|
||||||
"for",
|
|
||||||
t.context,
|
|
||||||
"; en is",
|
|
||||||
t.translations["en"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.current.map(
|
|
||||||
(label) => {
|
(label) => {
|
||||||
htmlElement.setAttribute("aria-label", label)
|
htmlElement.setAttribute("aria-label", label)
|
||||||
|
// Set the tooltip, which is the 'title' attribute of an html-element
|
||||||
|
htmlElement.setAttribute("title", label)
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
(f) => {
|
(f) => {
|
||||||
|
|
Loading…
Reference in a new issue