forked from MapComplete/MapComplete
Fix: chromium browsers don't handle spaces in tel:
-links well
This commit is contained in:
parent
b79835074f
commit
4168ef01e3
5 changed files with 315 additions and 279 deletions
|
@ -175,13 +175,23 @@
|
||||||
"cs": "Jaké je telefonní číslo {title()}?"
|
"cs": "Jaké je telefonní číslo {title()}?"
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"*": "<a href='tel:{phone}'>{phone}</a>"
|
"special": {
|
||||||
|
"type": "link",
|
||||||
|
"href": "tel:{phone}",
|
||||||
|
"text": "{phone}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"icon": "./assets/layers/questions/phone.svg",
|
"icon": "./assets/layers/questions/phone.svg",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": "contact:phone~*",
|
"if": "contact:phone~*",
|
||||||
"then": "<a href='tel:{contact:phone}'>{contact:phone}</a>",
|
"then":{
|
||||||
|
"special": {
|
||||||
|
"type": "link",
|
||||||
|
"href": "tel:{contact:phone}",
|
||||||
|
"text": "{contact:phone}"
|
||||||
|
}
|
||||||
|
},
|
||||||
"hideInAnswer": true,
|
"hideInAnswer": true,
|
||||||
"icon": "./assets/layers/questions/phone.svg"
|
"icon": "./assets/layers/questions/phone.svg"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||||
import Icon from "../Map/Icon.svelte"
|
import Icon from "../Map/Icon.svelte"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export let text: Store<string>
|
export let text: Store<string>
|
||||||
export let href: Store<string>
|
export let href: Store<string>
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={$href}
|
href={Utils.prepareHref($href)}
|
||||||
aria-label={$ariaLabel}
|
aria-label={$ariaLabel}
|
||||||
title={$ariaLabel}
|
title={$ariaLabel}
|
||||||
target={$newTab ? "_blank" : undefined}
|
target={$newTab ? "_blank" : undefined}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export let text: string
|
export let text: string
|
||||||
export let href: string
|
export let href: string
|
||||||
|
|
||||||
|
|
||||||
export let classnames: string = undefined
|
export let classnames: string = undefined
|
||||||
export let download: string = undefined
|
export let download: string = undefined
|
||||||
export let ariaLabel: string = undefined
|
export let ariaLabel: string = undefined
|
||||||
|
@ -9,7 +13,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
{href}
|
href={Utils.prepareHref(href)}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
title={ariaLabel}
|
title={ariaLabel}
|
||||||
target={newTab ? "_blank" : undefined}
|
target={newTab ? "_blank" : undefined}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
79
src/Utils.ts
79
src/Utils.ts
|
@ -114,7 +114,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
"version",
|
"version",
|
||||||
"wayHandling",
|
"wayHandling",
|
||||||
"widenFactor",
|
"widenFactor",
|
||||||
"width",
|
"width"
|
||||||
]
|
]
|
||||||
private static extraKeys = [
|
private static extraKeys = [
|
||||||
"nl",
|
"nl",
|
||||||
|
@ -133,7 +133,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
"yes",
|
"yes",
|
||||||
"no",
|
"no",
|
||||||
"true",
|
"true",
|
||||||
"false",
|
"false"
|
||||||
]
|
]
|
||||||
private static injectedDownloads = {}
|
private static injectedDownloads = {}
|
||||||
private static _download_cache = new Map<
|
private static _download_cache = new Map<
|
||||||
|
@ -150,7 +150,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
|
DOMPurify.addHook("afterSanitizeAttributes", function(node) {
|
||||||
// set all elements owning target to target=_blank + add noopener noreferrer
|
// set all elements owning target to target=_blank + add noopener noreferrer
|
||||||
const target = node.getAttribute("target")
|
const target = node.getAttribute("target")
|
||||||
if (target) {
|
if (target) {
|
||||||
|
@ -163,7 +163,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
public static purify(src: string): string {
|
public static purify(src: string): string {
|
||||||
return DOMPurify.sanitize(src, {
|
return DOMPurify.sanitize(src, {
|
||||||
USE_PROFILES: { html: true },
|
USE_PROFILES: { html: true },
|
||||||
ADD_ATTR: ["target"], // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener'
|
ADD_ATTR: ["target"] // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,7 +344,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
console.error("Error while calculating a lazy property", e)
|
console.error("Error while calculating a lazy property", e)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,7 +368,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
whenDone()
|
whenDone()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,7 +651,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
if (!Array.isArray(targetV)) {
|
if (!Array.isArray(targetV)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Cannot concatenate: value to add is not an array: " +
|
"Cannot concatenate: value to add is not an array: " +
|
||||||
JSON.stringify(targetV)
|
JSON.stringify(targetV)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (Array.isArray(sourceV)) {
|
if (Array.isArray(sourceV)) {
|
||||||
|
@ -659,9 +659,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Could not merge concatenate " +
|
"Could not merge concatenate " +
|
||||||
JSON.stringify(sourceV) +
|
JSON.stringify(sourceV) +
|
||||||
" and " +
|
" and " +
|
||||||
JSON.stringify(targetV)
|
JSON.stringify(targetV)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -922,7 +922,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const i = part.charCodeAt(0)
|
const i = part.charCodeAt(0)
|
||||||
result += '"' + keys[i] + '":' + part.substring(1)
|
result += "\"" + keys[i] + "\":" + part.substring(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -960,6 +960,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
if (!result["error"]) {
|
if (!result["error"]) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
const error = result.error
|
||||||
|
if (error.statuscode === 429 || error.statuscode === 509) {
|
||||||
|
// rate limited
|
||||||
|
return result
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`Request to ${url} failed, Trying again in a moment. Attempt ${
|
`Request to ${url} failed, Trying again in a moment. Attempt ${
|
||||||
i + 1
|
i + 1
|
||||||
|
@ -1000,7 +1005,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
resolve({
|
resolve({
|
||||||
error: "other error: " + xhr.statusText + ", " + xhr.responseText,
|
error: "other error: " + xhr.statusText + ", " + xhr.responseText,
|
||||||
url,
|
url,
|
||||||
statuscode: xhr.status,
|
statuscode: xhr.status
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1014,12 +1019,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
xhr.onerror = (ev: ProgressEvent<EventTarget>) =>
|
xhr.onerror = (ev: ProgressEvent<EventTarget>) =>
|
||||||
reject(
|
reject(
|
||||||
"Could not get " +
|
"Could not get " +
|
||||||
url +
|
url +
|
||||||
", xhr status code is " +
|
", xhr status code is " +
|
||||||
xhr.status +
|
xhr.status +
|
||||||
" (" +
|
" (" +
|
||||||
xhr.statusText +
|
xhr.statusText +
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1077,12 +1082,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
}
|
}
|
||||||
const promise =
|
const promise =
|
||||||
/*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced<T>(
|
/*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced<T>(
|
||||||
url,
|
url,
|
||||||
headers
|
headers
|
||||||
)
|
)
|
||||||
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
|
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
|
||||||
return await promise
|
return await promise
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async downloadJson<T = object | []>(
|
public static async downloadJson<T = object | []>(
|
||||||
url: string,
|
url: string,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
|
@ -1271,7 +1277,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
): T[] {
|
): T[] {
|
||||||
const withDistance: [T, number][] = ts.map((t) => [
|
const withDistance: [T, number][] = ts.map((t) => [
|
||||||
t,
|
t,
|
||||||
Utils.levenshteinDistance(getName(t), reference),
|
Utils.levenshteinDistance(getName(t), reference)
|
||||||
])
|
])
|
||||||
withDistance.sort(([_, a], [__, b]) => a - b)
|
withDistance.sort(([_, a], [__, b]) => a - b)
|
||||||
return withDistance.map((n) => n[0])
|
return withDistance.map((n) => n[0])
|
||||||
|
@ -1393,7 +1399,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return {
|
return {
|
||||||
r: Utils.percentageToNumber(match[1]),
|
r: Utils.percentageToNumber(match[1]),
|
||||||
g: Utils.percentageToNumber(match[2]),
|
g: Utils.percentageToNumber(match[2]),
|
||||||
b: Utils.percentageToNumber(match[3]),
|
b: Utils.percentageToNumber(match[3])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1404,14 +1410,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return {
|
return {
|
||||||
r: parseInt(hex.substr(1, 1), 16),
|
r: parseInt(hex.substr(1, 1), 16),
|
||||||
g: parseInt(hex.substr(2, 1), 16),
|
g: parseInt(hex.substr(2, 1), 16),
|
||||||
b: parseInt(hex.substr(3, 1), 16),
|
b: parseInt(hex.substr(3, 1), 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
r: parseInt(hex.substr(1, 2), 16),
|
r: parseInt(hex.substr(1, 2), 16),
|
||||||
g: parseInt(hex.substr(3, 2), 16),
|
g: parseInt(hex.substr(3, 2), 16),
|
||||||
b: parseInt(hex.substr(5, 2), 16),
|
b: parseInt(hex.substr(5, 2), 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1586,7 +1592,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
line: Number(line),
|
line: Number(line),
|
||||||
column: Number(column),
|
column: Number(column),
|
||||||
markdownLocation,
|
markdownLocation,
|
||||||
filename: path.substring(path.lastIndexOf("/") + 1),
|
filename: path.substring(path.lastIndexOf("/") + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1705,6 +1711,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly _metrixPrefixes = ["", "k", "M", "G", "T", "P", "E"]
|
private static readonly _metrixPrefixes = ["", "k", "M", "G", "T", "P", "E"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a big number (e.g. 1000000) into a rounded postfixed verion (e.g. 1M)
|
* Converts a big number (e.g. 1000000) into a rounded postfixed verion (e.g. 1M)
|
||||||
*
|
*
|
||||||
|
@ -1727,6 +1734,26 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes or rewrites some characters in links, as some blink/chromium based browsers are picky about them
|
||||||
|
*
|
||||||
|
* Utils.prepareHref("tel:+32 123 456") // => "tel:+32123456"
|
||||||
|
* Utils.prepareHref("https://osm.org/user/User Name") // => "https://osm.org/user/User%20Name"
|
||||||
|
*/
|
||||||
|
static prepareHref(href: string): string {
|
||||||
|
if (href.startsWith("tel:")) {
|
||||||
|
// Telephone numbers are not allowed to contain spaces in chromium-based browsers
|
||||||
|
href = "tel:" + href.replaceAll(/[^+0-9]/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chromium based browsers eat the spaces */
|
||||||
|
href = href.replaceAll(
|
||||||
|
/ /g,
|
||||||
|
"%20"
|
||||||
|
)
|
||||||
|
return href
|
||||||
|
}
|
||||||
|
|
||||||
private static emojiRegex = /[\p{Extended_Pictographic}🛰️]$/u
|
private static emojiRegex = /[\p{Extended_Pictographic}🛰️]$/u
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue