UX: fix #1729: ignore accents in theme search

This commit is contained in:
Pieter Vander Vennet 2023-12-01 14:52:50 +01:00
parent 8e73369308
commit 4eb8b086e2
2 changed files with 93 additions and 85 deletions

View file

@ -118,7 +118,7 @@ export default class MoreScreen extends Combine {
if (search === undefined) { if (search === undefined) {
return true return true
} }
search = search.toLocaleLowerCase() search = Utils.RemoveDiacritics(search.toLocaleLowerCase())
if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) {
return true return true
} }
@ -131,7 +131,7 @@ export default class MoreScreen extends Combine {
continue continue
} }
const term = entity["*"] ?? entity[Locale.language.data] const term = entity["*"] ?? entity[Locale.language.data]
if (term?.toLowerCase()?.indexOf(search) >= 0) { if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) {
return true return true
} }
} }

View file

@ -11,7 +11,7 @@ export class Utils {
public static readonly assets_path = "./assets/svg/" public static readonly assets_path = "./assets/svg/"
public static externalDownloadFunction: ( public static externalDownloadFunction: (
url: string, url: string,
headers?: any headers?: any,
) => Promise<{ content: string } | { redirect: string }> ) => 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\`. 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. 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) { 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) {
@ -172,7 +172,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
*/ */
public static ParseVisArgs( public static ParseVisArgs(
specs: { name: string; defaultValue?: string }[], specs: { name: string; defaultValue?: string }[],
args: string[] args: string[],
): Record<string, string> { ): Record<string, string> {
const parsed: Record<string, string> = {} const parsed: Record<string, string> = {}
if (args.length > specs.length) { if (args.length > specs.length) {
@ -320,7 +320,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
object: any, object: any,
name: string, name: string,
init: () => any, init: () => any,
whenDone?: () => void whenDone?: () => void,
) { ) {
Object.defineProperty(object, name, { Object.defineProperty(object, name, {
enumerable: false, enumerable: false,
@ -343,7 +343,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
object: any, object: any,
name: string, name: string,
init: () => Promise<any>, init: () => Promise<any>,
whenDone?: () => void whenDone?: () => void,
) { ) {
Object.defineProperty(object, name, { Object.defineProperty(object, name, {
enumerable: false, enumerable: false,
@ -483,7 +483,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static SubstituteKeys( public static SubstituteKeys(
txt: string | undefined, txt: string | undefined,
tags: Record<string, any> | undefined, tags: Record<string, any> | undefined,
useLang?: string useLang?: string,
): string | undefined { ): string | undefined {
if (txt === undefined) { if (txt === undefined) {
return undefined return undefined
@ -519,7 +519,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", "SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is",
key, key,
"\nThe value is", "\nThe value is",
v v,
) )
v = v.InnerConstructElement()?.textContent v = v.InnerConstructElement()?.textContent
} }
@ -561,38 +561,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
target.push(...source) target.push(...source)
} }
/**
* Recursively rewrites all keys from `+key`, `key+` and `=key` into `key
*
* Utils.CleanMergeObject({"condition":{"and+":["xyz"]}} // => {"condition":{"and":["xyz"]}}
* @param obj
* @constructor
* @private
*/
private static CleanMergeObject(obj: any) {
if (Array.isArray(obj)) {
const result = []
for (const el of obj) {
result.push(Utils.CleanMergeObject(el))
}
return result
}
if (typeof obj !== "object") {
return obj
}
const newObj = {}
for (let objKey in obj) {
let cleanKey = objKey
if (objKey.startsWith("+") || objKey.startsWith("=")) {
cleanKey = objKey.substring(1)
} else if (objKey.endsWith("+") || objKey.endsWith("=")) {
cleanKey = objKey.substring(0, objKey.length - 1)
}
newObj[cleanKey] = Utils.CleanMergeObject(obj[objKey])
}
return newObj
}
/** /**
* Copies all key-value pairs of the source into the target. This will change the target * Copies all key-value pairs of the source into the target. This will change the target
* If the key starts with a '+', the values of the list will be appended to the target instead of overwritten * If the key starts with a '+', the values of the list will be appended to the target instead of overwritten
@ -664,7 +632,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)) {
@ -672,9 +640,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 {
@ -719,7 +687,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
path: string[], path: string[],
object: any, object: any,
replaceLeaf: (leaf: any, travelledPath: string[]) => any, replaceLeaf: (leaf: any, travelledPath: string[]) => any,
travelledPath: string[] = [] travelledPath: string[] = [],
): void { ): void {
if (object == null) { if (object == null) {
return return
@ -750,7 +718,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
if (Array.isArray(sub)) { if (Array.isArray(sub)) {
sub.forEach((el, i) => 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 return
} }
@ -767,7 +735,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
path: string[], path: string[],
object: any, object: any,
collectedList: { leaf: any; path: string[] }[] = [], collectedList: { leaf: any; path: string[] }[] = [],
travelledPath: string[] = [] travelledPath: string[] = [],
): { leaf: any; path: string[] }[] { ): { leaf: any; path: string[] }[] {
if (object === undefined || object === null) { if (object === undefined || object === null) {
return collectedList return collectedList
@ -797,7 +765,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (Array.isArray(sub)) { if (Array.isArray(sub)) {
sub.forEach((el, i) => sub.forEach((el, i) =>
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]) Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]),
) )
return collectedList return collectedList
} }
@ -841,7 +809,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
json: any, json: any,
f: (v: object | number | string | boolean | undefined, path: string[]) => any, f: (v: object | number | string | boolean | undefined, path: string[]) => any,
isLeaf: (object) => boolean = undefined, isLeaf: (object) => boolean = undefined,
path: string[] = [] path: string[] = [],
) { ) {
if (json === undefined || json === null) { if (json === undefined || json === null) {
return f(json, path) return f(json, path)
@ -880,7 +848,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
json: any, json: any,
collect: (v: number | string | boolean | undefined, path: string[]) => any, collect: (v: number | string | boolean | undefined, path: string[]) => any,
isLeaf: (object) => boolean = undefined, isLeaf: (object) => boolean = undefined,
path = [] path = [],
): void { ): void {
if (json === undefined) { if (json === undefined) {
return return
@ -955,7 +923,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
@ -982,7 +950,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
url: string, url: string,
headers?: any, headers?: any,
method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET", method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET",
content?: string content?: string,
): Promise< ): Promise<
| { content: string } | { content: string }
| { redirect: string } | { redirect: string }
@ -1047,7 +1015,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonCached( public static async downloadJsonCached(
url: string, url: string,
maxCacheTimeMs: number, maxCacheTimeMs: number,
headers?: any headers?: any,
): Promise<any> { ): Promise<any> {
const result = await Utils.downloadJsonAdvanced(url, headers) const result = await Utils.downloadJsonAdvanced(url, headers)
if (result["content"]) { if (result["content"]) {
@ -1059,7 +1027,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonCachedAdvanced( public static async downloadJsonCachedAdvanced(
url: string, url: string,
maxCacheTimeMs: number, maxCacheTimeMs: number,
headers?: any headers?: any,
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> { ): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
const cached = Utils._download_cache.get(url) const cached = Utils._download_cache.get(url)
if (cached !== undefined) { if (cached !== undefined) {
@ -1069,9 +1037,9 @@ 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( /*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced(
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
} }
@ -1086,7 +1054,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonAdvanced( public static async downloadJsonAdvanced(
url: string, url: string,
headers?: any headers?: any,
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> { ): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
const injected = Utils.injectedDownloads[url] const injected = Utils.injectedDownloads[url]
if (injected !== undefined) { if (injected !== undefined) {
@ -1095,7 +1063,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
const result = await Utils.downloadAdvanced( const result = await Utils.downloadAdvanced(
url, url,
Utils.Merge({ accept: "application/json" }, headers ?? {}) Utils.Merge({ accept: "application/json" }, headers ?? {}),
) )
if (result["error"] !== undefined) { if (result["error"] !== undefined) {
return <{ error: string; url: string; statuscode?: number }>result return <{ error: string; url: string; statuscode?: number }>result
@ -1115,7 +1083,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"due to", "due to",
e, e,
"\n", "\n",
e.stack e.stack,
) )
return { error: "malformed", url } return { error: "malformed", url }
} }
@ -1136,7 +1104,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
| "{gpx=application/gpx+xml}" | "{gpx=application/gpx+xml}"
| "application/json" | "application/json"
| "image/png" | "image/png"
} },
) { ) {
const element = document.createElement("a") const element = document.createElement("a")
let file let file
@ -1240,7 +1208,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
e.preventDefault() e.preventDefault()
return false return false
}, },
false false,
) )
} }
@ -1324,7 +1292,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static sortedByLevenshteinDistance<T>( public static sortedByLevenshteinDistance<T>(
reference: string, reference: string,
ts: T[], ts: T[],
getName: (t: T) => string getName: (t: T) => string,
): T[] { ): T[] {
const withDistance: [T, number][] = ts.map((t) => [ const withDistance: [T, number][] = ts.map((t) => [
t, t,
@ -1350,7 +1318,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
track[j][i] = Math.min( track[j][i] = Math.min(
track[j][i - 1] + 1, // deletion track[j][i - 1] + 1, // deletion
track[j - 1][i] + 1, // insertion track[j - 1][i] + 1, // insertion
track[j - 1][i - 1] + indicator // substitution track[j - 1][i - 1] + indicator, // substitution
) )
} }
} }
@ -1359,7 +1327,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static MapToObj<V, T>( public static MapToObj<V, T>(
d: Map<string, V>, d: Map<string, V>,
onValue: (t: V, key: string) => T onValue: (t: V, key: string) => T,
): Record<string, T> { ): Record<string, T> {
const o = {} const o = {}
const keys = Array.from(d.keys()) const keys = Array.from(d.keys())
@ -1376,7 +1344,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"]} * Utils.TransposeMap({"a" : ["b", "c"], "x" : ["b", "y"]}) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]}
*/ */
public static TransposeMap<K extends string, V extends string>( public static TransposeMap<K extends string, V extends string>(
d: Record<K, V[]> d: Record<K, V[]>,
): Record<V, K[]> { ): Record<V, K[]> {
const newD: Record<V, K[]> = <any>{} const newD: Record<V, K[]> = <any>{}
@ -1450,7 +1418,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
public static asDict( public static asDict(
tags: { key: string; value: string | number }[] tags: { key: string; value: string | number }[],
): Map<string, string | number> { ): Map<string, string | number> {
const d = new Map<string, string | number>() const d = new Map<string, string | number>()
@ -1491,21 +1459,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
element.scrollIntoView({ behavior: "smooth", block: "nearest" }) element.scrollIntoView({ behavior: "smooth", block: "nearest" })
} }
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
// Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) {
return element
}
// If the element does not have scrolling, check if it has a parent element
if (!element.parentElement) {
return null
}
// If the element has a parent, repeat the process for the parent element
return Utils.findParentWithScrolling(<HTMLBaseElement>element.parentElement)
}
/** /**
* Returns true if the contents of `a` are the same (and in the same order) as `b`. * Returns true if the contents of `a` are the same (and in the same order) as `b`.
* Might have false negatives in some cases * Might have false negatives in some cases
@ -1572,7 +1525,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* *
*/ */
public static splitIntoSubstitutionParts( public static splitIntoSubstitutionParts(
template: string template: string,
): ({ message: string } | { subs: string })[] { ): ({ message: string } | { subs: string })[] {
const preparts = template.split("{") const preparts = template.split("{")
const spec: ({ message: string } | { subs: string })[] = [] const spec: ({ message: string } | { subs: string })[] = []
@ -1633,6 +1586,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
} }
public static RemoveDiacritics(str?: string): string {
if(!str){
return str
}
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "")
}
public static randomString(length: number): string { public static randomString(length: number): string {
let result = "" let result = ""
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
@ -1641,9 +1601,57 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return result return result
} }
/**
* Recursively rewrites all keys from `+key`, `key+` and `=key` into `key
*
* Utils.CleanMergeObject({"condition":{"and+":["xyz"]}} // => {"condition":{"and":["xyz"]}}
* @param obj
* @constructor
* @private
*/
private static CleanMergeObject(obj: any) {
if (Array.isArray(obj)) {
const result = []
for (const el of obj) {
result.push(Utils.CleanMergeObject(el))
}
return result
}
if (typeof obj !== "object") {
return obj
}
const newObj = {}
for (let objKey in obj) {
let cleanKey = objKey
if (objKey.startsWith("+") || objKey.startsWith("=")) {
cleanKey = objKey.substring(1)
} else if (objKey.endsWith("+") || objKey.endsWith("=")) {
cleanKey = objKey.substring(0, objKey.length - 1)
}
newObj[cleanKey] = Utils.CleanMergeObject(obj[objKey])
}
return newObj
}
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
// Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) {
return element
}
// If the element does not have scrolling, check if it has a parent element
if (!element.parentElement) {
return null
}
// If the element has a parent, repeat the process for the parent element
return Utils.findParentWithScrolling(<HTMLBaseElement>element.parentElement)
}
private static colorDiff( private static colorDiff(
c0: { r: number; g: number; b: number }, 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) return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b)
} }