Accessibility: improve keyboard only flow (see #1181); remove some legacy use of Svelte

This commit is contained in:
Pieter Vander Vennet 2023-12-06 17:27:30 +01:00
parent d1a6c11513
commit 4ee83cfe5c
35 changed files with 613 additions and 683 deletions

View file

@ -351,6 +351,7 @@
"if": {
"and": [
"seasonal!=no",
"seasonal~*",
{
"or": [
{

View file

@ -1,33 +1,13 @@
{
"id": "mapcomplete-changes",
"title": {
"en": "Changes made with MapComplete",
"ca": "Canvis fets amb MapComplete",
"cs": "Změny provedené pomocí MapComplete",
"de": "Mit MapComplete erstellte Änderungen",
"es": "Cambios realizados con MapComplete",
"fr": "Changements faits avec MapComplete",
"nl": "Wijzigingen gemaakt met MapComplete",
"pl": "Zmiany wprowadzone za pomocą MapComplete"
"en": "Changes made with MapComplete"
},
"shortDescription": {
"en": "Show changes made with MapComplete",
"ca": "Mostra els canvis fets amb MapComplete",
"cs": "Zobrazení změn provedených pomocí nástroje MapComplete",
"de": "Mit MapComplete erstellte Änderungen anzeigen",
"es": "Mostrar cambios realizados con MapComplete",
"nl": "Toon wijzigingen gemaakt met MapComplete",
"pl": "Pokaż zmiany wprowadzone za pomocą MapComplete"
"en": "Shows changes made by MapComplete"
},
"description": {
"en": "This maps shows all the changes made with MapComplete",
"ca": "Aquest mapa mostra tots els canvis fets amb MapComplete",
"cs": "Tato mapa zobrazuje všechny změny provedené pomocí MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen",
"es": "Este mapa muestra todos los cambios realizados con MapComplete",
"fr": "Cette carte montre tous les changements faits avec MapComplete",
"nl": "Deze kaart toont alle wijzigingen die met MapComplete gemaakt werden",
"pl": "Ta mapa pokazuje wszystkie zmiany wprowadzone za pomocą MapComplete"
"en": "This maps shows all the changes made with MapComplete"
},
"icon": "./assets/svg/logo.svg",
"hideFromOverview": true,
@ -40,13 +20,7 @@
{
"id": "mapcomplete-changes",
"name": {
"en": "Changeset centers",
"ca": "Centre del conjunt de canvis",
"cs": "Centrum změn",
"de": "Zentrum der Änderungssätze",
"es": "Centro del conjunto de cambios",
"nl": "Centerpunt van changeset",
"pl": "Centra zmian"
"en": "Changeset centers"
},
"minzoom": 0,
"source": {
@ -57,85 +31,41 @@
},
"title": {
"render": {
"en": "Changeset for {theme}",
"ca": "Conjunt de canvis per a {theme}",
"cs": "Změna pro {theme}",
"de": "Änderungssatz für {theme}",
"es": "Conjunto de cambios para {theme}",
"fr": "Groupe de modifications pour {theme}",
"pl": "Zestaw zmian dla {theme}"
"en": "Changeset for {theme}"
}
},
"description": {
"en": "Show all MapComplete changes",
"ca": "Mostra tots els canvis de MapComplete",
"cs": "Zobrazit všechny změny MapComplete",
"de": "Alle MapComplete-Änderungen anzeigen",
"es": "Mostrar todos los cambios de MapComplete",
"nl": "Toon alle MapComplete wijzigingen",
"pl": "Wyświetl wszystkie zmiany MapComplete"
"en": "Shows all MapComplete changes"
},
"tagRenderings": [
{
"id": "show_changeset_id",
"render": {
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"ca": "Conjunt de canvi <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"cs": "Změny <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"es": "Conjunto de cambios <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"fr": "Groupe de modifications <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"pl": "Zestaw zmian <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
}
},
{
"id": "contributor",
"question": {
"en": "Which contributor made this change?",
"ca": "Quin col·laborador va fer aquest canvi?",
"cs": "Který přispěvatel tuto změnu provedl?",
"de": "Wer hat diese Änderung vorgenommen?",
"es": "¿Qué contribuidor hizo este cambio?",
"fr": "Quel contributeur a fait cette modification ?",
"nl": "Welke bijdrager maakte deze wijziging?",
"pl": "Który współautor dokonał tej zmiany?"
"en": "What contributor did make this change?"
},
"freeform": {
"key": "user"
},
"render": {
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"ca": "Canvi fet per <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"cs": "Změna provedená <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"de": "Änderung von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"es": "Cambio realizado por <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"fr": "Modification faite par <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"nl": "Wijziging gemaakt door <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"pl": "Zmiana dokonana przez <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
}
},
{
"id": "theme-id",
"question": {
"en": "What theme was used to make this change?",
"ca": "Quin tema es va utilitzar per fer aquest canvi?",
"cs": "Jaké téma bylo použito k provedení této změny?",
"de": "Welches Thema wurde für diese Änderung verwendet?",
"es": "¿Qué tema se utilizó para realizar este cambio?",
"fr": "Quel thème a été utilisé pour faire cette modification ?",
"pl": "Jakiego tematu użyto do wprowadzenia tej zmiany?"
"en": "What theme was used to make this change?"
},
"freeform": {
"key": "theme"
},
"render": {
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"ca": "Canvi amb el tema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"cs": "Změna s motivem <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"de": "Geändert mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"es": "Cambio con tema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"fr": "Modifié avec le thème <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"pl": "Zmiana za pomocą motywu <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
"en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>"
}
},
{
@ -144,45 +74,19 @@
"key": "locale"
},
"question": {
"en": "What locale (language) was this change made in?",
"ca": "Amb quina configuració regional (idioma) s'ha fet aquest canvi?",
"cs": "V jakém národním prostředí (jazyce) byla tato změna provedena?",
"de": "In welcher Benutzersprache wurde diese Änderung vorgenommen?",
"es": "¿En qué configuración regional (idioma) se realizó este cambio?",
"fr": "En quelle langue est-ce que ce changement a été fait ?",
"nl": "In welke locale (taal) werd deze wijziging gemaakt?",
"pl": "W jakim języku wprowadzono tę zmianę?"
"en": "What locale (language) was this change made in?"
},
"render": {
"en": "User locale is {locale}",
"ca": "La configuració regional de l'usuari és {locale}",
"cs": "Uživatelské prostředí je {locale}",
"de": "Benutzersprache {locale}",
"es": "La configuración regional del usuario es {locale}",
"nl": "De gebruikerstaal is {locale}",
"pl": "Ustawienia regionalne użytkownika to {locale}"
"en": "User locale is {locale}"
}
},
{
"id": "host",
"render": {
"en": "Change made with <a href='{host}'>{host}</a>",
"ca": "Canviat fet amb <a href='{host}'>{host}</a>",
"cs": "Změna provedená pomocí <a href='{host}'>{host}</a>",
"de": "Geändert über <a href='{host}'>{host}</a>",
"es": "Cambio realizado con <a href='{host}'>{host}</a>",
"fr": "Modification faite avec <a href='{host}'>{host}</a>",
"nl": "Wijziging gemaakt met <a href='{host}'>{host}</a>",
"pl": "Zmiana dokonana w <a href='{host}'>{host}</a>"
"en": "Change with with <a href='{host}'>{host}</a>"
},
"question": {
"en": "What host (website) was this change made with?",
"ca": "Amb quin amfitrió (lloc web) es va fer aquest canvi?",
"cs": "U jakého hostitele (webové stránky) byla tato změna provedena?",
"de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?",
"es": "¿Con qué host (página web) se realizó este cambio?",
"nl": "Met welke host (website) werd deze wijziging gemaakt?",
"pl": "Na jakim hoście (stronie internetowej) dokonano tej zmiany?"
"en": "What host (website) was this change made with?"
},
"freeform": {
"key": "host"
@ -203,22 +107,10 @@
{
"id": "version",
"question": {
"en": "What version of MapComplete was used to make this change?",
"ca": "Quina versió de MapComplete es va utilitzar per fer aquest canvi?",
"cs": "Jaká verze aplikace MapComplete byla použita k provedení této změny?",
"de": "Mit welcher Version von MapComplete wurde diese Änderung gemacht?",
"es": "¿Qué versión de MapComplete se usó para realizar este cambio?",
"fr": "Quelle version de MapComplete a été utilisée pour faire cette modification ?",
"pl": "Która wersja MapComplete została wykorzystana, aby zrobić tę zmianę?"
"en": "What version of MapComplete was used to make this change?"
},
"render": {
"en": "Made with {editor}",
"ca": "Fet amb {editor}",
"cs": "Vyrobeno pomocí {editor}",
"de": "Erstellt mit {editor}",
"es": "Realizado con {editor}",
"fr": "Fait avec {editor}",
"pl": "Zrobione za pomocą {editor}"
"en": "Made with {editor}"
},
"freeform": {
"key": "editor"
@ -568,13 +460,7 @@
}
],
"question": {
"en": "Theme name contains {search}",
"ca": "El nom del tema conté {search}",
"cs": "Název motivu obsahuje {search}",
"de": "Themenname enthält {search}",
"es": "El nombre del tema contiene {search}",
"nl": "Themenaam bevat {search}",
"pl": "Nazwa tematu zawiera {search}"
"en": "Themename contains {search}"
}
}
]
@ -590,7 +476,7 @@
}
],
"question": {
"en": "Theme name does <b>not</b> contain {search}"
"en": "Themename does <b>not</b> contain {search}"
}
}
]
@ -606,13 +492,7 @@
}
],
"question": {
"en": "Made by contributor {search}",
"ca": "Fet pel col·laborador {search}",
"cs": "Vytvořil přispěvatel {search}",
"de": "Erstellt von {search}",
"es": "Hecho por el colaborador {search}",
"nl": "Gemaakt door bijdrager {search}",
"pl": "Wykonane przez współautora {search}"
"en": "Made by contributor {search}"
}
}
]
@ -628,13 +508,7 @@
}
],
"question": {
"en": "<b>Not</b> made by contributor {search}",
"ca": "<b>No</b> fet pel col·laborador {search}",
"cs": "<b>Není</b> vytvořeno přispěvatelem {search}",
"de": "<b>Nicht</b> erstellt von {search}",
"es": "<b>No</b> hecho por el colaborador {search}",
"nl": "<b>Niet</b> gemaakt door bijdrager {search}",
"pl": "<b>Nie</b> wykonane przez współautora {search}"
"en": "<b>Not</b> made by contributor {search}"
}
}
]
@ -651,13 +525,7 @@
}
],
"question": {
"en": "Made before {search}",
"ca": "Fet abans de {search}",
"cs": "Vytvořeno před {search}",
"de": "Erstellt vor {search}",
"es": "Hecho antes de {search}",
"nl": "Gemaakt voor {search}",
"pl": "Stworzone przed {search}"
"en": "Made before {search}"
}
}
]
@ -674,13 +542,7 @@
}
],
"question": {
"en": "Made after {search}",
"ca": "Fet després de {search}",
"cs": "Vytvořeno po {search}",
"de": "Erstellt nach {search}",
"es": "Hecho después de {search}",
"nl": "Gemaakt na {search}",
"pl": "Stworzone po {search}"
"en": "Made after {search}"
}
}
]
@ -696,14 +558,7 @@
}
],
"question": {
"en": "User language (iso-code) {search}",
"ca": "Idioma de l'usuari (codi iso) {search}",
"cs": "Jazyk uživatele (iso-kód) {search}",
"de": "Benutzersprache (ISO-Code) {search}",
"es": "Use idioma (ISO-code) {search}",
"fr": "Langage utilisateur (code-ISO) {search}",
"nl": "De taal van de bijdrager is {search}",
"pl": "Język użytkownika (kod iso) {search}"
"en": "User language (iso-code) {search}"
}
}
]
@ -719,13 +574,7 @@
}
],
"question": {
"en": "Made with host {search}",
"ca": "Fet amb l'amfitrió {search}",
"cs": "Vytvořeno pomocí hostitele {search}",
"de": "Erstellt mit Host {search}",
"es": "Hecho con el host {search}",
"nl": "Gemaakt met host {search}",
"pl": "Wykonane z hostem {search}"
"en": "Made with host {search}"
}
}
]
@ -736,14 +585,7 @@
{
"osmTags": "add-image>0",
"question": {
"en": "Changeset added at least one image",
"ca": "El conjunt de canvis ha afegit almenys una imatge",
"cs": "Sada změn přidala alespoň jeden obrázek",
"de": "Im Änderungssatz wurde mindestens ein Bild hinzugefügt",
"es": "El conjunto de cambios ha añadido al menos una imagen",
"fr": "Le groupe de modifications a ajouté au moins une image",
"nl": "Changeset bevat minstens één afbeelding",
"pl": "Zestaw zmian dodał co najmniej jedno zdjęcie"
"en": "Changeset added at least one image"
}
}
]
@ -754,7 +596,7 @@
{
"osmTags": "theme!=grb",
"question": {
"en": "Made with host {search}"
"en": "Exclude GRB theme"
}
}
]
@ -765,7 +607,7 @@
{
"osmTags": "theme!=etymology",
"question": {
"en": "Changeset added at least one image"
"en": "Exclude etymology theme"
}
}
]
@ -780,13 +622,7 @@
{
"id": "link_to_more",
"render": {
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
"ca": "Es pot trobar més estadística <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
"cs": "Další statistiky najdete <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
"de": "Mehr Statistiken gibt es <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>",
"es": "Puede encontrar más estadísticas <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
"fr": "D'autres statistiques sont disponibles <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>ici</a>",
"pl": "Więcej statystyk można znaleźć <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>tutaj</a>"
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>"
}
},
{

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.36.1",
"version": "0.36.2",
"lockfileVersion": 2,
"requires": true,
"packages": {

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.36.1",
"version": "0.36.2",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",

View file

@ -777,10 +777,6 @@ video {
float: left;
}
.m-8 {
margin: 2rem;
}
.m-4 {
margin: 1rem;
}
@ -793,6 +789,10 @@ video {
margin: 0px;
}
.m-8 {
margin: 2rem;
}
.m-2 {
margin: 0.5rem;
}
@ -1188,6 +1188,10 @@ video {
max-height: 6rem;
}
.max-h-64 {
max-height: 16rem;
}
.max-h-7 {
max-height: 1.75rem;
}
@ -1266,6 +1270,10 @@ video {
width: 3.5rem;
}
.w-auto {
width: auto;
}
.w-5 {
width: 1.25rem;
}
@ -1283,10 +1291,6 @@ video {
width: 12rem;
}
.w-auto {
width: auto;
}
.max-w-full {
max-width: 100%;
}
@ -1685,6 +1689,10 @@ video {
border-style: dotted;
}
.border-none {
border-style: none;
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity));
@ -2234,6 +2242,11 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif;
}
.focusable {
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
border: 1px solid red
}
svg,
img {
box-sizing: content-box;

View file

@ -4,7 +4,7 @@ import { RegexTag } from "../src/Logic/Tags/RegexTag"
import { ImmutableStore } from "../src/Logic/UIEventSource"
import { BBox } from "../src/Logic/BBox"
import * as fs from "fs"
import { writeFileSync } from "fs"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { Feature } from "geojson"
import ScriptUtils from "./ScriptUtils"
import { Imgur } from "../src/Logic/ImageProviders/Imgur"
@ -181,6 +181,32 @@ export default class GenerateImageAnalysis extends Script {
}
}
async downloadViews(datapath: string): Promise<void> {
const { allImages, imageSource } = this.loadImageUrls(datapath)
console.log("Detected", allImages.size, "images")
const results: [string, number][] = []
const today = new Date().toISOString().substring(0, "YYYY-MM-DD".length)
const viewDir = datapath + "/views_" + today
if (!existsSync(viewDir)) {
mkdirSync(viewDir)
}
for (const image of Array.from(allImages)) {
const cachedView = viewDir + "/" + image.replace(/\\/g, "_")
let attribution: LicenseInfo
if (existsSync(cachedView)) {
attribution = JSON.parse(readFileSync(cachedView, "utf8"))
} else {
attribution = await Imgur.singleton.DownloadAttribution(image)
writeFileSync(cachedView, JSON.stringify(attribution))
}
results.push([image, attribution.views])
}
const targetpath = datapath + "/views.csv"
console.log("Writing views to", targetpath)
fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n"))
}
async downloadImage(url: string, imagePath: string): Promise<boolean> {
const filenameLong = url.replace(/[\/:.\-%]/g, "_") + ".jpg"
const targetPathLong = imagePath + "/" + filenameLong
@ -391,6 +417,7 @@ export default class GenerateImageAnalysis extends Script {
await this.downloadData(datapath, cached)
await this.downloadMetadata(datapath)
await this.downloadViews(datapath)
await this.downloadAllImages(datapath, imageBackupPath)
this.analyze(datapath)
}

View file

@ -21,6 +21,7 @@ interface TagsUpdaterState {
osmObjectDownloader: OsmObjectDownloader
indexedFeatures: IndexedFeatureSource
}
export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([
"timestamp",
@ -42,6 +43,84 @@ export default class SelectedElementTagsUpdater {
})
}
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
try {
const leftRightSensitive = state.layout.isLeftRightSensitive()
if (leftRightSensitive) {
SimpleMetaTagger.removeBothTagging(latestTags)
}
const pendingChanges = state.changes.pendingChanges.data
.filter((change) => change.type + "/" + change.id === id)
.filter((change) => change.tags !== undefined)
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
if (v === undefined || v === "") {
delete latestTags[key]
} else {
latestTags[key] = v
}
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false
const currentTagsSource = state.featureProperties.getStore(id)
if (currentTagsSource === undefined) {
console.warn("No tags store found for", id, "cannot update tags")
return
}
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
if (typeof osmValue === "number") {
osmValue = "" + osmValue
}
const localValue = currentTags[key]
if (localValue !== osmValue) {
somethingChanged = true
currentTags[key] = osmValue
}
}
for (const currentKey in currentTags) {
if (currentKey.startsWith("_")) {
continue
}
if (SelectedElementTagsUpdater.metatags.has(currentKey)) {
continue
}
if (currentKey in latestTags) {
continue
}
console.log("Removing key as deleted upstream", currentKey)
delete currentTags[currentKey]
somethingChanged = true
}
if (somethingChanged) {
console.log(
"Detected upstream changes to the object " +
id +
" when opening it, updating..."
)
currentTagsSource.ping()
} else {
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
return currentTags
} catch (e) {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
}
private installCallback(state: TagsUpdaterState) {
state.selectedElement.addCallbackAndRunD(async (s) => {
let id = s.properties?.id
@ -90,77 +169,4 @@ export default class SelectedElementTagsUpdater {
}
})
}
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
try {
const leftRightSensitive = state.layout.isLeftRightSensitive()
if (leftRightSensitive) {
SimpleMetaTagger.removeBothTagging(latestTags)
}
const pendingChanges = state.changes.pendingChanges.data
.filter((change) => change.type + "/" + change.id === id)
.filter((change) => change.tags !== undefined)
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
if (v === undefined || v === "") {
delete latestTags[key]
} else {
latestTags[key] = v
}
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false
const currentTagsSource = state.featureProperties.getStore(id)
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
if (typeof osmValue === "number") {
osmValue = "" + osmValue
}
const localValue = currentTags[key]
if (localValue !== osmValue) {
somethingChanged = true
currentTags[key] = osmValue
}
}
for (const currentKey in currentTags) {
if (currentKey.startsWith("_")) {
continue
}
if (SelectedElementTagsUpdater.metatags.has(currentKey)) {
continue
}
if (currentKey in latestTags) {
continue
}
console.log("Removing key as deleted upstream", currentKey)
delete currentTags[currentKey]
somethingChanged = true
}
if (somethingChanged) {
console.log(
"Detected upstream changes to the object " +
id +
" when opening it, updating..."
)
currentTagsSource.ping()
} else {
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
return currentTags
} catch (e) {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
}
}

View file

@ -108,7 +108,7 @@ export class ImageUploadManager {
description,
file,
targetKey,
tags.data["_orig_theme"]
tags?.data?.["_orig_theme"]
)
if (!isNaN(Number(featureId))) {
// This is a map note

View file

@ -97,7 +97,10 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(f: (t: T) => J): Store<J>
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J>
public mapD<J>(f: (t: Exclude<T, undefined | null>) => J, extraStoresToWatch?: Store<any>[]): Store<J> {
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<any>[]
): Store<J> {
return this.map((t) => {
if (t === undefined) {
return undefined
@ -105,7 +108,7 @@ export abstract class Store<T> implements Readable<T> {
if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>> t)
return f(<Exclude<T, undefined | null>>t)
}, extraStoresToWatch)
}
@ -201,24 +204,36 @@ export abstract class Store<T> implements Readable<T> {
mapped.addCallbackAndRun((newEventSource) => {
if (newEventSource === null) {
sink.setData(null)
} else if (newEventSource === undefined) {
return
}
if (newEventSource === undefined) {
sink.setData(undefined)
} else if (!seenEventSources.has(newEventSource)) {
return
}
if (seenEventSources.has(newEventSource)) {
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
return
}
seenEventSources.add(newEventSource)
newEventSource.addCallbackAndRun((resultData) => {
if (mapped.data === newEventSource) {
sink.setData(resultData)
}
})
} else {
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
}
})
return sink
}
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> {
return this.bind((t) => {
if (t === undefined || t === null) {
return <undefined | null>t
}
return f(<Exclude<T, undefined | null>>t)
})
}
public stabilized(millisToStabilize): Store<T> {
if (Utils.runningFromConsole) {
return this
@ -771,7 +786,10 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* Monoidal map which results in a read-only store. 'undefined' is passed 'as is'
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
*/
public mapD<J>(f: (t: Exclude<T, undefined | null>) => J, extraSources: Store<any>[] = []): Store<J | undefined> {
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraSources: Store<any>[] = []
): Store<J | undefined> {
return new MappedStore(
this,
(t) => {
@ -781,11 +799,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>> t)
return f(<Exclude<T, undefined | null>>t)
},
extraSources,
this._callbacks,
(this.data === undefined || this.data === null) ?(<undefined | null> this.data) : f(<any> this.data)
this.data === undefined || this.data === null
? <undefined | null>this.data
: f(<any>this.data)
)
}

View file

@ -79,6 +79,11 @@ export class MenuState {
this.highlightedUserSetting.setData(undefined)
}
})
this.menuViewTab.addCallbackD((tab) => {
if (tab !== "settings") {
this.highlightedUserSetting.setData(undefined)
}
})
this.themeViewTab.addCallbackAndRun((tab) => {
if (tab !== "filters") {
this.highlightedLayerInFilters.setData(undefined)

View file

@ -475,9 +475,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
{ nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar,
() => {
if (this.previewedImage.data !== undefined) {
this.previewedImage.setData(undefined)
return
}
this.selectedElement.setData(undefined)
this.guistate.closeAll()
this.previewedImage.setData(undefined)
this.focusOnMap()
}
)

View file

@ -36,7 +36,9 @@
dispatcher("submit", e.dataTransfer.files)
}}
>
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput" + id}>
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")}
tabindex="0" for={"fileinput" + id}
on:click={() => {console.log("Clicked", inputElement); inputElement.click()}}>
<slot />
</label>
<input

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { createEventDispatcher, onMount } from "svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twMerge } from "tailwind-merge"
@ -9,6 +9,12 @@
const dispatch = createEventDispatcher<{ close }>()
export let extraClasses = "p-4 md:p-6"
let mainContent: HTMLElement
onMount(() => {
console.log("Mounting floatover")
mainContent?.focus()
})
</script>
<div
@ -18,7 +24,7 @@
dispatch("close")
}}
>
<div class="content normal-background" on:click|stopPropagation={() => {}}>
<div bind:this={mainContent} class="content normal-background" on:click|stopPropagation={() => {}}>
<div class="h-full rounded-xl">
<slot />
</div>

View file

@ -1,25 +1,36 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { createEventDispatcher, onMount } from "svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Utils } from "../../Utils";
/**
* The slotted element will be shown on the right side
*/
const dispatch = createEventDispatcher<{ close }>()
const dispatch = createEventDispatcher<{ close }>();
let mainContent: HTMLElement;
onMount(() => {
window.setTimeout(
() => Utils.focusOnFocusableChild(mainContent), 250
)
})
</script>
<div
bind:this={mainContent}
class="absolute top-0 right-0 h-screen w-full overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
style="max-width: 100vw; max-height: 100vh"
>
<div class="normal-background m-0 flex flex-col">
<slot name="close-button">
<div
<button
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
on:click={() => dispatch("close")}
>
<XCircleIcon />
</div>
</button>
</slot>
<slot />
</div>

View file

@ -30,7 +30,7 @@
}
</script>
<div class="tabbedgroup flex h-full w-full">
<div class="tabbedgroup flex h-full w-full focusable">
<TabGroup
class="flex h-full w-full flex-col"
defaultIndex={1}

View file

@ -19,6 +19,7 @@
href={LinkToWeblate.hrefToWeblate($language, context)}
target="_blank"
class="weblate-link mx-1"
tabindex="-1"
>
<Translate class="font-gray" />
</a>
@ -27,6 +28,7 @@
href={LinkToWeblate.hrefToWeblate($language, context)}
class="weblate-link hidden-on-mobile mx-1"
target="_blank"
tabindex="-1"
>
<Translate class="font-gray inline-block" />
</a>

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ToSvelte from "../Base/ToSvelte.svelte"
import Svg from "../../Svg.js"
import Translations from "../i18n/Translations"
@ -15,7 +14,6 @@
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
export let selectedElement: UIEventSource<Feature> | undefined = undefined
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined
export let clearAfterView: boolean = true
@ -34,9 +32,12 @@
let feedback: string = undefined
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
feedback = undefined
requestAnimationFrame(() => {
inputElement?.focus()
inputElement?.select()
})
})
const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>()
$: {
@ -73,8 +74,12 @@
const layers = Array.from(perLayer?.values() ?? [])
for (const layer of layers) {
const found = layer.features.data.find((f) => f.properties.id === id)
selectedElement?.setData(found)
selectedLayer?.setData(layer.layer.layerDef)
if (found === undefined) {
continue;
}
selectedElement?.setData(found);
console.log("Found an element that probably matches:", selectedElement?.data);
break;
}
}
if (clearAfterView) {

View file

@ -1,35 +1,25 @@
<script lang="ts">
import type { Feature } from "geojson"
import { UIEventSource } from "../../Logic/UIEventSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
import { onDestroy } from "svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import type { Feature } from "geojson";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let selectedElement: Feature
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState;
export let layer: LayerConfig;
export let selectedElement: Feature;
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id);
$: {
tags = state.featureProperties.getStore(selectedElement.properties.id);
}
let _tags: Record<string, string>
onDestroy(
tags.addCallbackAndRun((tags) => {
_tags = tags
})
)
let _metatags: Record<string, string>
onDestroy(
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
_metatags = tags
})
)
let metatags: Store<Record<string, string>> = state.userRelatedState.preferencesAsTags;
</script>
{#if _tags._deleted === "yes"}
{#if $tags._deleted === "yes"}
<Tr t={Translations.t.delete.isDeleted} />
{:else}
<div
@ -44,7 +34,7 @@
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"
>
{#each layer.titleIcons as titleIconConfig}
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)}
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer
config={titleIconConfig}
@ -59,10 +49,10 @@
{/each}
</div>
</div>
<XCircleIcon
class="h-8 w-8 cursor-pointer"
on:click={() => state.selectedElement.setData(undefined)}
/>
<button on:click={() => state.selectedElement.setData(undefined)} class="border-none p-0">
<XCircleIcon class="h-8 w-8" />
</button>
</div>
{/if}

View file

@ -1,19 +1,23 @@
<script lang="ts">
import type { Feature } from "geojson"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import { onDestroy } from "svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let selectedElement: Feature
export let tags: UIEventSource<Record<string, string>>
export let highlightedRendering: UIEventSource<string> = undefined
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id)
$: {
tags = state.featureProperties.getStore(selectedElement.properties.id)
}
let _metatags: Record<string, string>
onDestroy(
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
@ -21,20 +25,12 @@
})
)
let knownTagRenderings = layer.tagRenderings.filter(
let knownTagRenderings: Store<TagRenderingConfig[]> = tags.mapD(tgs => layer.tagRenderings.filter(
(config) =>
(config.condition?.matchesProperties($tags) ?? true) &&
config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true) &&
config.IsKnown($tags)
)
$: {
knownTagRenderings = layer.tagRenderings.filter(
(config) =>
(config.condition?.matchesProperties($tags) ?? true) &&
config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true) &&
config.IsKnown($tags)
)
}
(config.condition?.matchesProperties(tgs) ?? true) &&
config.metacondition?.matchesProperties({ ...tgs, ..._metatags } ?? true) &&
config.IsKnown(tgs)
))
</script>
{#if $tags._deleted === "yes"}
@ -43,8 +39,8 @@
<Tr t={Translations.t.general.returnToTheMap} />
</button>
{:else}
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2">
{#each knownTagRenderings as config (config.id)}
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2 focusable" tabindex="-1">
{#each $knownTagRenderings as config (config.id)}
<TagRenderingEditable
{tags}
{config}
@ -52,7 +48,7 @@
{selectedElement}
{layer}
{highlightedRendering}
clss={knownTagRenderings.length === 1 ? "h-full" : "tr-length-" + knownTagRenderings.length}
clss={$knownTagRenderings.length === 1 ? "h-full" : "tr-length-" + $knownTagRenderings.length}
/>
{/each}
</div>

View file

@ -23,7 +23,6 @@
export let state: ThemeViewState
let layout = state.layout
let selectedElement = state.selectedElement
let selectedLayer = state.selectedLayer
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
let searchEnabled = false
@ -116,7 +115,6 @@
}}
perLayer={state.perLayer}
{selectedElement}
{selectedLayer}
{triggerSearch}
/>
</div>

View file

@ -27,7 +27,6 @@
}
function select() {
state.selectedLayer.setData(favConfig);
state.selectedElement.setData(feature);
center();
}

View file

@ -13,11 +13,12 @@
}
let imgEl: HTMLImageElement
export let imgClass: string = undefined
</script>
<div class="relative">
<img bind:this={imgEl} src={image.url} on:error={(event) => {
<img bind:this={imgEl} src={image.url} class={imgClass ?? ""} on:error={(event) => {
if(fallbackImage){
imgEl.src = fallbackImage
}

View file

@ -7,9 +7,10 @@ import ImageAttribution from "./ImageAttribution.svelte"
import ImagePreview from "./ImagePreview.svelte"
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Utils } from "../../Utils"
import { twMerge } from "tailwind-merge";
export let image: ProvidedImage
export let clss: string = undefined
async function download() {
const response = await fetch(image.url)
const blob = await response.blob()
@ -20,7 +21,7 @@ async function download() {
</script>
<div class="w-full h-full relative">
<div class={twMerge("w-full h-full relative", clss)}>
<div class="absolute top-0 left-0 w-full h-full overflow-hidden">
<ImagePreview image={image} />
</div>

View file

@ -13,7 +13,7 @@
$: {
if (panzoomEl) {
panzoomInstance = panzoom(panzoomEl, { bounds: true,
boundsPadding: 1,
boundsPadding: 0.49,
minZoom: 1,
maxZoom: 25,
initialZoom: 1.2

View file

@ -64,7 +64,9 @@
</script>
<div class="flex w-fit shrink-0 flex-col">
<AttributedImage image={providedImage} />
<div on:click={() => state.previewedImage.setData(providedImage)}>
<AttributedImage image={providedImage} imgClass="max-h-64 w-auto"/>
</div>
{#if linkable}
<label>
<input bind:checked={isLinked} type="checkbox" />

View file

@ -1,44 +1,41 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import NearbyImages from "./NearbyImages.svelte"
import Svg from "../../Svg"
import ToSvelte from "../Base/ToSvelte.svelte"
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
import exp from "constants"
import Camera_plus from "../../assets/svg/Camera_plus.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
import { Store } from "../../Logic/UIEventSource";
import type { OsmTags } from "../../Models/OsmFeature";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import NearbyImages from "./NearbyImages.svelte";
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
import Camera_plus from "../../assets/svg/Camera_plus.svelte";
import LoginToggle from "../Base/LoginToggle.svelte";
export let tags: Store<OsmTags>
export let state: SpecialVisualizationState
export let lon: number
export let lat: number
export let feature: Feature
export let tags: Store<OsmTags>;
export let state: SpecialVisualizationState;
export let lon: number;
export let lat: number;
export let feature: Feature;
export let linkable: boolean = true
export let layer: LayerConfig
const t = Translations.t.image.nearby
export let linkable: boolean = true;
export let layer: LayerConfig;
const t = Translations.t.image.nearby;
let expanded = false
let expanded = false;
</script>
<LoginToggle {state}>
{#if expanded}
{#if expanded}
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable}>
<XCircleIcon
slot="corner"
class="h-6 w-6 cursor-pointer"
<button slot="corner"
class="h-6 w-6 cursor-pointer no-image-background p-0 border-none"
on:click={() => {
expanded = false
}}
/>
}}>
<XCircleIcon />
</button>
</NearbyImages>
{:else}
{:else}
<button
class="flex w-full items-center"
on:click={() => {
@ -48,5 +45,5 @@
<Camera_plus class="mr-2 block h-8 w-8 p-1" />
<Tr t={t.seeNearby} />
</button>
{/if}
{/if}
</LoginToggle>

View file

@ -56,9 +56,9 @@
multiple={true}
on:submit={(e) => handleFiles(e.detail)}
>
<div class="flex items-center">
<div class="flex items-center" >
{#if image !== undefined}
<img src={image} />
<img src={image} aria-hidden="true" />
{:else}
<Camera_plus class="block h-12 w-12 p-1 text-4xl" />
{/if}
@ -70,15 +70,15 @@
</div>
</FileSelector>
<div class="text-sm">
<Tr t={t.respectPrivacy} />
<a
class="cursor-pointer"
<button
class="link small "
on:click={() => {
state.guistate.openUsersettings("picture-license")
}}
>
<Tr t={t.currentLicense.Subs({ license: $licenseStore })} />
</a>
</button>
<Tr t={t.respectPrivacy} />
</div>
</div>
</LoginToggle>

View file

@ -302,19 +302,23 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
rescaleIcons: number,
pixelRatio: number
) {
const marker = element
const style = marker.style.transform
let x = marker.getBoundingClientRect().x
let y = marker.getBoundingClientRect().y
marker.style.transform = ""
const style = element.style.transform
let x = element.getBoundingClientRect().x
let y = element.getBoundingClientRect().y
element.style.transform = ""
const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
const w = marker.style.width
const w = element.style.width
const h = element.style.height
// Force a wider view for icon badges
marker.style.width = marker.getBoundingClientRect().width * 4 + "px"
const svgSource = await htmltoimage.toSvg(marker)
element.style.width = element.getBoundingClientRect().width * 4 + "px"
element.style.height = element.getBoundingClientRect().height + "px"
const svgSource = await htmltoimage.toSvg(element)
const img = await MapLibreAdaptor.createImage(svgSource)
marker.style.width = w
element.style.width = w
element.style.height = h
if (offset && rescaleIcons !== 1) {
const [_, __, relYStr] = offset
const relY = Number(relYStr)

View file

@ -410,9 +410,14 @@ class LineRenderingLayer {
this._listenerInstalledOn.add(id)
tags.addCallbackAndRunD((properties) => {
// Make sure to use 'getSource' here, the layer names are different!
try {
if (map.getSource(this._layername) === undefined) {
return true
}
} catch (e) {
console.debug("Could not fetch source for", this._layername)
return
}
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(properties)

View file

@ -5,14 +5,13 @@
import TagRenderingMapping from "./TagRenderingMapping.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type { Feature } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
import { onDestroy } from "svelte"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { twMerge } from "tailwind-merge"
export let tags: UIEventSource<Record<string, string> | undefined>
let _tags: Record<string, string>
let trs: { then: Translation; icon?: string; iconClass?: string }[]
export let state: SpecialVisualizationState
export let selectedElement: Feature
@ -23,22 +22,18 @@
if (config === undefined) {
throw "Config is undefined in tagRenderingAnswer"
}
onDestroy(
tags.addCallbackAndRun((tags) => {
_tags = tags
trs = Utils.NoNull(config?.GetRenderValues(_tags))
})
)
let trs : Store<{then: Translation, icon?: string, iconClass?: string}[]> = tags.mapD(tags => Utils.NoNull(config?.GetRenderValues(tags)))
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(_tags))}
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
<div class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
{#if trs.length === 1}
<TagRenderingMapping mapping={trs[0]} {tags} {state} {selectedElement} {layer} />
{#if $trs.length === 1}
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
{/if}
{#if trs.length > 1}
{#if $trs.length > 1}
<ul>
{#each trs as mapping}
{#each $trs as mapping}
<li>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} />
</li>

View file

@ -1,40 +1,41 @@
<script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import { onDestroy } from "svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../../Utils"
import { twMerge } from "tailwind-merge"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature | undefined
export let state: SpecialVisualizationState
export let layer: LayerConfig = undefined
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
import type { Feature } from "geojson";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
import { PencilAltIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import { onDestroy } from "svelte";
import Tr from "../../Base/Tr.svelte";
import Translations from "../../i18n/Translations.js";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import { Utils } from "../../../Utils";
import { twMerge } from "tailwind-merge";
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature | undefined;
export let state: SpecialVisualizationState;
export let layer: LayerConfig = undefined;
export let highlightedRendering: UIEventSource<string> = undefined
export let clss
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge;
export let highlightedRendering: UIEventSource<string> = undefined;
export let clss;
/**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/
export let editMode = !config.IsKnown(tags.data) // || showQuestionIfUnknown;
export let editMode = !config.IsKnown(tags.data); // || showQuestionIfUnknown;
if (tags) {
onDestroy(
tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags)
editMode = !config.IsKnown(tags);
})
)
);
}
let htmlElem: HTMLDivElement
let htmlElem: HTMLDivElement;
$: {
if (editMode && htmlElem !== undefined && config.IsKnown(tags)) {
// EditMode switched to true yet the answer is already known, so the person wants to make a change
@ -42,32 +43,36 @@
// Some delay is applied to give Svelte the time to render the _question_
window.setTimeout(() => {
Utils.scrollIntoView(<any>htmlElem)
}, 50)
Utils.scrollIntoView(<any>htmlElem);
}, 50);
}
}
const _htmlElement = new UIEventSource<HTMLElement>(undefined)
$: _htmlElement.setData(htmlElem)
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
$: _htmlElement.setData(htmlElem);
function setHighlighting() {
if (highlightedRendering === undefined) {
return
return;
}
if (htmlElem === undefined) {
return
return;
}
const highlighted = highlightedRendering.data
const highlighted = highlightedRendering.data;
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow")
htmlElem.classList.add("glowing-shadow");
htmlElem.tabIndex = "-1";
console.log("Scrolling to", htmlElem);
htmlElem.scrollIntoView({ behavior: "smooth" });
Utils.focusOnFocusableChild(htmlElem);
} else {
htmlElem.classList.remove("glowing-shadow")
htmlElem.classList.remove("glowing-shadow");
}
}
if (highlightedRendering) {
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()));
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()));
}
</script>
@ -84,13 +89,13 @@
>
<Tr t={Translations.t.general.cancel} />
</button>
<XCircleIcon
slot="upper-right"
class="h-8 w-8 cursor-pointer"
<button slot="upper-right"
class="h-8 w-8 cursor-pointer border-none p-0"
on:click={() => {
editMode = false
}}
/>
}}>
<XCircleIcon />
</button>
</TagRenderingQuestion>
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">

View file

@ -136,7 +136,6 @@
function onSave() {
if (selectedTags === undefined) {
console.log("SelectedTags is undefined, ignoring 'onSave'-event");
return;
}
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {

View file

@ -1,138 +1,116 @@
<script lang="ts">
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte"
import { GeolocationControl } from "./BigComponents/GeolocationControl"
import type { Feature } from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState"
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations"
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte"
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import { VariableUiElement } from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte"
import ShareScreen from "./BigComponents/ShareScreen.svelte"
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
import Cross from "../assets/svg/Cross.svelte"
import Summary from "./BigComponents/Summary.svelte"
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
import Mastodon from "../assets/svg/Mastodon.svelte"
import Bug from "../assets/svg/Bug.svelte"
import Liberapay from "../assets/svg/Liberapay.svelte"
import OpenJosm from "./Base/OpenJosm.svelte"
import Min from "../assets/svg/Min.svelte"
import Plus from "../assets/svg/Plus.svelte"
import Filter from "../assets/svg/Filter.svelte"
import Add from "../assets/svg/Add.svelte"
import Statistics from "../assets/svg/Statistics.svelte"
import Community from "../assets/svg/Community.svelte"
import Download from "../assets/svg/Download.svelte"
import Share from "../assets/svg/Share.svelte"
import Favourites from "./Favourites/Favourites.svelte"
import ImageOperations from "./Image/ImageOperations.svelte"
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type { MapProperties } from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte";
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
import Constants from "../Models/Constants";
import TabbedGroup from "./Base/TabbedGroup.svelte";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel";
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
import ModalRight from "./Base/ModalRight.svelte";
import { Utils } from "../Utils";
import Hotkeys from "./Base/Hotkeys";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type { RasterLayerPolygon } from "../Models/RasterLayers";
import { AvailableRasterLayers } from "../Models/RasterLayers";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
import { onDestroy } from "svelte";
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
import ShareScreen from "./BigComponents/ShareScreen.svelte";
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte";
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte";
import Cross from "../assets/svg/Cross.svelte";
import Summary from "./BigComponents/Summary.svelte";
import LanguagePicker from "./InputElement/LanguagePicker.svelte";
import Mastodon from "../assets/svg/Mastodon.svelte";
import Bug from "../assets/svg/Bug.svelte";
import Liberapay from "../assets/svg/Liberapay.svelte";
import OpenJosm from "./Base/OpenJosm.svelte";
import Min from "../assets/svg/Min.svelte";
import Plus from "../assets/svg/Plus.svelte";
import Filter from "../assets/svg/Filter.svelte";
import Add from "../assets/svg/Add.svelte";
import Statistics from "../assets/svg/Statistics.svelte";
import Community from "../assets/svg/Community.svelte";
import Download from "../assets/svg/Download.svelte";
import Share from "../assets/svg/Share.svelte";
import Favourites from "./Favourites/Favourites.svelte";
import ImageOperations from "./Image/ImageOperations.svelte";
export let state: ThemeViewState
let layout = state.layout
export let state: ThemeViewState;
let layout = state.layout;
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
let maplibremap: UIEventSource<MlMap> = state.map;
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined);
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
let centerFeatures = state.closestFeatures.features
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined
state.selectedElement.addCallback(selected => {
if(!selected){
selectedElement.setData(selected)
return
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined
if(selected !== selectedElement.data){
// We first set the selected element to 'undefined' to force the popup to close...
selectedElement.setData(undefined)
}
// ... we give svelte some time to update with requestAnimationFrame ...
window.requestAnimationFrame(() => {
// ... and we force a fresh popup window
selectedElement.setData(selected)
})
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, {
state,
layer,
selectedElement,
tags,
}).SetClass("h-full w-full")
},
[selectedLayer],
)
})
const selectedElementTitle = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined
}
let selectedLayer: UIEventSource<LayerConfig> = state.selectedElement.mapD(element => state.layout.getMatchingLayer(element.properties));
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
},
[selectedLayer],
)
let currentZoom = state.mapProperties.zoom;
let showCrosshair = state.userRelatedState.showCrosshair;
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
let centerFeatures = state.closestFeatures.features;
let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers
let userdetails = state.osmConnection.userDetails
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let mapproperties: MapProperties = state.mapProperties;
let featureSwitches: FeatureSwitchState = state.featureSwitches;
let availableLayers = state.availableLayers;
let userdetails = state.osmConnection.userDetails;
let currentViewLayer = layout.layers.find((l) => l.id === "current_view");
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer;
let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name;
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
}),
)
let previewedImage = state.previewedImage
rasterLayerName = l.properties.name;
})
);
let previewedImage = state.previewedImage;
</script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
@ -146,7 +124,7 @@
<Geosearch
bounds={state.mapProperties.bounds}
perLayer={state.perLayer}
{selectedElement}
selectedElement={state.selectedElement}
{selectedLayer}
/>
</div>
@ -264,9 +242,7 @@
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
<ToSvelte
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass(
"block w-8 h-8"
)}
construct={new GeolocationControl(state.geolocation, mapproperties).SetClass("block w-8 h-8")}
/>
</MapControlButton>
</If>
@ -285,66 +261,52 @@
</LoginToggle>
<If condition={state.previewedImage.map(i => i!==undefined)}>
<FloatOver on:close={() => state.previewedImage.setData(undefined)} extraClasses="">
<FloatOver extraClasses="p-1" on:close={() => state.previewedImage.setData(undefined)}>
<div
slot="close-button"
class="absolute right-4 top-4 h-8 w-8 cursor-pointer rounded-full hover:bg-white bg-white/50 transition-colors duration-200"
on:click={() => previewedImage.setData(undefined)}
slot="close-button"
>
<XCircleIcon />
</div>
<ImageOperations image={$previewedImage} />
<ImageOperations clss="focusable" image={$previewedImage} />
</FloatOver>
</If>
<If
condition={selectedElementView.map(
(v) =>
v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover,
[selectedLayer]
)}
>
{#if $selectedElement !== undefined && $selectedLayer !== undefined && !($selectedLayer.popupInFloatover)}
<!-- right modal with the selected element view -->
<ModalRight
on:close={() => {
selectedElement.setData(undefined)
}}
>
<div slot="close-button"/>
<div class="normal-background absolute flex h-full w-full flex-col">
<ToSvelte construct={new VariableUiElement(selectedElementTitle)}>
<!-- Title -->
</ToSvelte>
<ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("overflow-auto")}>
<!-- Main view -->
</ToSvelte>
<SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement} />
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
</div>
</ModalRight>
</If>
{/if}
<If
condition={selectedElementView.map(
(v) =>
v !== undefined && selectedLayer.data !== undefined && selectedLayer.data.popupInFloatover,
[selectedLayer]
)}
>
{#if $selectedElement !== undefined && $selectedLayer !== undefined && $selectedLayer.popupInFloatover}
<!-- Floatover with the selected element, if applicable -->
<FloatOver
on:close={() => {
selectedElement.setData(undefined)
}}
>
<ToSvelte
construct={new VariableUiElement(selectedElementView).SetClass("h-full w-full flex")}
/>
<div class="h-full w-full flex focusable">
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
</div>
</FloatOver>
</If>
{/if}
<If condition={state.guistate.themeIsOpened}>
<!-- Theme menu -->
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
<span slot="close-button"><!-- Disable the close button --></span>
<TabbedGroup
condition1={state.featureSwitches.featureSwitchFilter}
tab={state.guistate.themeViewTabIndex}
>
@ -421,7 +383,7 @@
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}}
>
<div class="h-full p-2">
<div class="h-full p-2 focusable">
<RasterLayerOverview
{availableLayers}
map={state.map}

View file

@ -11,7 +11,7 @@ export class Utils {
public static readonly assets_path = "./assets/svg/"
public static externalDownloadFunction: (
url: string,
headers?: any,
headers?: any
) => Promise<{ content: string } | { redirect: string }>
public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`.
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
@ -150,7 +150,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (Utils.runningFromConsole) {
return
}
DOMPurify.addHook("afterSanitizeAttributes", function(node) {
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
// set all elements owning target to target=_blank + add noopener noreferrer
const target = node.getAttribute("target")
if (target) {
@ -172,7 +172,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
*/
public static ParseVisArgs(
specs: { name: string; defaultValue?: string }[],
args: string[],
args: string[]
): Record<string, string> {
const parsed: Record<string, string> = {}
if (args.length > specs.length) {
@ -324,7 +324,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
object: any,
name: string,
init: () => any,
whenDone?: () => void,
whenDone?: () => void
) {
Object.defineProperty(object, name, {
enumerable: false,
@ -347,7 +347,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
object: any,
name: string,
init: () => Promise<any>,
whenDone?: () => void,
whenDone?: () => void
) {
Object.defineProperty(object, name, {
enumerable: false,
@ -487,7 +487,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static SubstituteKeys(
txt: string | undefined,
tags: Record<string, any> | undefined,
useLang?: string,
useLang?: string
): string | undefined {
if (txt === undefined) {
return undefined
@ -523,7 +523,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is",
key,
"\nThe value is",
v,
v
)
v = v.InnerConstructElement()?.textContent
}
@ -636,7 +636,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (!Array.isArray(targetV)) {
throw new Error(
"Cannot concatenate: value to add is not an array: " +
JSON.stringify(targetV),
JSON.stringify(targetV)
)
}
if (Array.isArray(sourceV)) {
@ -646,7 +646,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"Could not merge concatenate " +
JSON.stringify(sourceV) +
" and " +
JSON.stringify(targetV),
JSON.stringify(targetV)
)
}
} else {
@ -691,7 +691,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
path: string[],
object: any,
replaceLeaf: (leaf: any, travelledPath: string[]) => any,
travelledPath: string[] = [],
travelledPath: string[] = []
): void {
if (object == null) {
return
@ -722,7 +722,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
if (Array.isArray(sub)) {
sub.forEach((el, i) =>
Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]),
Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i])
)
return
}
@ -739,7 +739,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
path: string[],
object: any,
collectedList: { leaf: any; path: string[] }[] = [],
travelledPath: string[] = [],
travelledPath: string[] = []
): { leaf: any; path: string[] }[] {
if (object === undefined || object === null) {
return collectedList
@ -769,7 +769,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (Array.isArray(sub)) {
sub.forEach((el, i) =>
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]),
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i])
)
return collectedList
}
@ -813,7 +813,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
json: any,
f: (v: object | number | string | boolean | undefined, path: string[]) => any,
isLeaf: (object) => boolean = undefined,
path: string[] = [],
path: string[] = []
) {
if (json === undefined || json === null) {
return f(json, path)
@ -852,7 +852,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
json: any,
collect: (v: number | string | boolean | undefined, path: string[]) => any,
isLeaf: (object) => boolean = undefined,
path = [],
path = []
): void {
if (json === undefined) {
return
@ -927,7 +927,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
continue
}
const i = part.charCodeAt(0)
result += "\"" + keys[i] + "\":" + part.substring(1)
result += '"' + keys[i] + '":' + part.substring(1)
}
return result
@ -954,7 +954,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
url: string,
headers?: any,
method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET",
content?: string,
content?: string
): Promise<
| { content: string }
| { redirect: string }
@ -1019,7 +1019,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonCached(
url: string,
maxCacheTimeMs: number,
headers?: any,
headers?: any
): Promise<any> {
const result = await Utils.downloadJsonAdvanced(url, headers)
if (result["content"]) {
@ -1031,7 +1031,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonCachedAdvanced(
url: string,
maxCacheTimeMs: number,
headers?: any,
headers?: any
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
const cached = Utils._download_cache.get(url)
if (cached !== undefined) {
@ -1042,7 +1042,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
const promise =
/*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced(
url,
headers,
headers
)
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
return await promise
@ -1058,7 +1058,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonAdvanced(
url: string,
headers?: any,
headers?: any
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
const injected = Utils.injectedDownloads[url]
if (injected !== undefined) {
@ -1067,7 +1067,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
const result = await Utils.downloadAdvanced(
url,
Utils.Merge({ accept: "application/json" }, headers ?? {}),
Utils.Merge({ accept: "application/json" }, headers ?? {})
)
if (result["error"] !== undefined) {
return <{ error: string; url: string; statuscode?: number }>result
@ -1087,7 +1087,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"due to",
e,
"\n",
e.stack,
e.stack
)
return { error: "malformed", url }
}
@ -1108,7 +1108,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
| "{gpx=application/gpx+xml}"
| "application/json"
| "image/png"
},
}
) {
const element = document.createElement("a")
let file
@ -1212,7 +1212,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
e.preventDefault()
return false
},
false,
false
)
}
@ -1296,7 +1296,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static sortedByLevenshteinDistance<T>(
reference: string,
ts: T[],
getName: (t: T) => string,
getName: (t: T) => string
): T[] {
const withDistance: [T, number][] = ts.map((t) => [
t,
@ -1322,7 +1322,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
track[j][i] = Math.min(
track[j][i - 1] + 1, // deletion
track[j - 1][i] + 1, // insertion
track[j - 1][i - 1] + indicator, // substitution
track[j - 1][i - 1] + indicator // substitution
)
}
}
@ -1331,7 +1331,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static MapToObj<V, T>(
d: Map<string, V>,
onValue: (t: V, key: string) => T,
onValue: (t: V, key: string) => T
): Record<string, T> {
const o = {}
const keys = Array.from(d.keys())
@ -1348,7 +1348,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Utils.TransposeMap({"a" : ["b", "c"], "x" : ["b", "y"]}) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]}
*/
public static TransposeMap<K extends string, V extends string>(
d: Record<K, V[]>,
d: Record<K, V[]>
): Record<V, K[]> {
const newD: Record<V, K[]> = <any>{}
@ -1422,7 +1422,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
public static asDict(
tags: { key: string; value: string | number }[],
tags: { key: string; value: string | number }[]
): Map<string, string | number> {
const d = new Map<string, string | number>()
@ -1529,7 +1529,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
*
*/
public static splitIntoSubstitutionParts(
template: string,
template: string
): ({ message: string } | { subs: string })[] {
const preparts = template.split("{")
const spec: ({ message: string } | { subs: string })[] = []
@ -1591,7 +1591,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
public static RemoveDiacritics(str?: string): string {
if(!str){
if (!str) {
return str
}
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "")
@ -1638,6 +1638,41 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return newObj
}
/**
* Searches a child that can be focused on, by first selecting a 'focusable', then a button, then a link
*
* Returns the focussed element
* @param el
*/
public static focusOnFocusableChild(el: HTMLElement): undefined {
if (!el) {
return
}
requestAnimationFrame(() => {
let childs = el.getElementsByClassName("focusable")
if (childs.length == 0) {
childs = el.getElementsByTagName("button")
if (childs.length === 0) {
childs = el.getElementsByTagName("a")
}
}
const child = <HTMLElement>childs.item(0)
if (child === null) {
console.log("Focussing on child element: no child element found for", el)
return undefined
}
if (
child.tagName !== "button" &&
child.tagName !== "a" &&
child.hasAttribute("tabindex")
) {
child.setAttribute("tabindex", "-1")
}
console.log("Focussing on", child)
child?.focus()
})
}
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
// Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) {
@ -1655,7 +1690,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
private static colorDiff(
c0: { r: number; g: number; b: number },
c1: { r: number; g: number; b: number },
c1: { r: number; g: number; b: number }
) {
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b)
}

View file

@ -70,6 +70,11 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif;
}
.focusable {
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
border: 1px solid red
}
svg,
img {
box-sizing: content-box;
@ -159,7 +164,6 @@ input[type=text] {
border-radius: 0.5rem;
}
/******************* Styling of input elements **********************/
/**