More refactoring

This commit is contained in:
Pieter Vander Vennet 2023-03-29 17:21:20 +02:00
parent 5d0fe31c41
commit 41e6a2c760
147 changed files with 1540 additions and 1797 deletions

View file

@ -0,0 +1,7 @@
import { Validator } from "../ValidatedTextField"
export default class ColorValidator extends Validator {
constructor() {
super("color", "Shows a color picker")
}
}

View file

@ -0,0 +1,23 @@
import { Validator } from "../ValidatedTextField"
export default class DateValidator extends Validator {
constructor() {
super("date", "A date with date picker")
}
isValid(str: string): boolean {
return !isNaN(new Date(str).getTime())
}
reformat(str: string) {
const d = new Date(str)
let month = "" + (d.getMonth() + 1)
let day = "" + d.getDate()
const year = d.getFullYear()
if (month.length < 2) month = "0" + month
if (day.length < 2) day = "0" + day
return [year, month, day].join("-")
}
}

View file

@ -0,0 +1,17 @@
import { Validator } from "../ValidatedTextField"
import IntValidator from "./IntValidator";
export default class DirectionValidator extends IntValidator {
constructor() {
super(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"
)
}
reformat(str): string {
const n = Number(str) % 360
return "" + n
}
}

View file

@ -0,0 +1,39 @@
import { Validator } from "../ValidatedTextField.js"
import { Translation } from "../../i18n/Translation.js"
import Translations from "../../i18n/Translations.js"
import * as emailValidatorLibrary from "email-validator"
export default class EmailValidator extends Validator {
constructor() {
super("email", "An email adress", "email")
}
isValid = (str) => {
if (str === undefined) {
return false
}
str = str.trim()
if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length)
}
return emailValidatorLibrary.validate(str)
}
reformat = (str) => {
if (str === undefined) {
return undefined
}
str = str.trim()
if (str.startsWith("mailto:")) {
str = str.substring("mailto:".length)
}
return str
}
getFeedback(s: string): Translation {
if (s.indexOf("@") < 0) {
return Translations.t.validation.email.noAt
}
return super.getFeedback(s)
}
}

View file

@ -0,0 +1,27 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../ValidatedTextField"
export default class FloatValidator extends Validator {
inputmode = "decimal"
constructor(name?: string, explanation?: string) {
super(name ?? "float", explanation ?? "A decimal number", "decimal")
}
isValid(str) {
return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",")
}
reformat(str): string {
return "" + Number(str)
}
getFeedback(s: string): Translation {
if (isNaN(Number(s))) {
return Translations.t.validation.nat.notANumber
}
return undefined
}
}

View file

@ -0,0 +1,29 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../ValidatedTextField"
export default class IntValidator extends Validator {
constructor(name?: string, explanation?: string) {
super(
name ?? "int",
explanation ?? "A whole number, either positive, negative or zero",
"numeric"
)
}
isValid(str): boolean {
str = "" + str
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))
}
getFeedback(s: string): Translation {
const n = Number(s)
if (isNaN(n)) {
return Translations.t.validation.nat.notANumber
}
if (Math.floor(n) !== n) {
return Translations.t.validation.nat.mustBeWhole
}
return undefined
}
}

View file

@ -0,0 +1,16 @@
import { Validator } from "../ValidatedTextField"
export default class LengthValidator extends Validator {
constructor() {
super(
"distance",
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
"decimal"
)
}
isValid = (str) => {
const t = Number(str)
return !isNaN(t)
}
}

View file

@ -0,0 +1,30 @@
import IntValidator from "./IntValidator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class NatValidator extends IntValidator {
constructor(name?: string, explanation?: string) {
super(name ?? "nat", explanation ?? "A whole, positive number or zero")
}
isValid(str): boolean {
if (str === undefined) {
return false
}
str = "" + str
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
}
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
const n = Number(s)
if (n < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -0,0 +1,54 @@
import { Validator } from "../ValidatedTextField"
import Combine from "../../Base/Combine"
import Title from "../../Base/Title"
import Table from "../../Base/Table"
export default class OpeningHoursValidator extends Validator {
constructor() {
super(
"opening_hours",
new Combine([
"Has extra elements to easily input when a POI is opened.",
new Title("Helper arguments"),
new Table(
["name", "doc"],
[
[
"options",
new Combine([
"A JSON-object of type `{ prefix: string, postfix: string }`. ",
new Table(
["subarg", "doc"],
[
[
"prefix",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
],
[
"postfix",
"Piece of text that will always be added to the end of the generated opening hours",
],
]
),
]),
],
]
),
new Title("Example usage"),
"To add a conditional (based on time) access restriction:\n\n```\n" +
`
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
"helperArgs": [
{
"prefix":"no @ (",
"postfix":")"
}
]
}` +
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
])
)
}
}

View file

@ -0,0 +1,23 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../ValidatedTextField"
export default class PFloatValidator extends Validator {
constructor() {
super("pfloat", "A positive decimal number or zero")
}
isValid = (str) =>
!isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",")
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
if (Number(s) < 0) {
return Translations.t.validation.nat.mustBePositive
}
return undefined
}
}

View file

@ -0,0 +1,27 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import NatValidator from "./NatValidator"
export default class PNatValidator extends NatValidator {
constructor() {
super("pnat", "A strict positive number")
}
getFeedback(s: string): Translation {
const spr = super.getFeedback(s)
if (spr !== undefined) {
return spr
}
if (Number(s) === 0) {
return Translations.t.validation.pnat.noZero
}
return undefined
}
isValid = (str) => {
if (!super.isValid(str)) {
return false
}
return Number(str) > 0
}
}

View file

@ -0,0 +1,32 @@
import { Validator } from "../ValidatedTextField"
import { parsePhoneNumberFromString } from "libphonenumber-js"
export default class PhoneValidator extends Validator {
constructor() {
super("phone", "A phone number", "tel")
}
isValid(str, country: () => string): boolean {
if (str === undefined) {
return false
}
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
let countryCode = undefined
if (country !== undefined) {
countryCode = country()?.toUpperCase()
}
return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false
}
reformat = (str, country: () => string) => {
if (str.startsWith("tel:")) {
str = str.substring("tel:".length)
}
return parsePhoneNumberFromString(
str,
country()?.toUpperCase() as any
)?.formatInternational()
}
}

View file

@ -0,0 +1,8 @@
import { Validator } from "../ValidatedTextField"
export default class StringValidator extends Validator {
constructor() {
super("string", "A simple piece of text")
}
}

View file

@ -0,0 +1,7 @@
import { Validator } from "../ValidatedTextField"
export default class TextValidator extends Validator {
constructor() {
super("text", "A longer piece of text. Uses an textArea instead of a textField", "text")
}
}

View file

@ -0,0 +1,75 @@
import { Validator } from "../ValidatedTextField"
export default class UrlValidator extends Validator {
constructor() {
super(
"url",
"The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed",
"url"
)
}
reformat(str: string): string {
try {
let url: URL
// str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
if (
!str.startsWith("http://") &&
!str.startsWith("https://") &&
!str.startsWith("http:")
) {
url = new URL("https://" + str)
} else {
url = new URL(str)
}
const blacklistedTrackingParams = [
"fbclid", // Oh god, how I hate the fbclid. Let it burn, burn in hell!
"gclid",
"cmpid",
"agid",
"utm",
"utm_source",
"utm_medium",
"campaignid",
"campaign",
"AdGroupId",
"AdGroup",
"TargetId",
"msclkid",
]
for (const dontLike of blacklistedTrackingParams) {
url.searchParams.delete(dontLike.toLowerCase())
}
let cleaned = url.toString()
if (cleaned.endsWith("/") && !str.endsWith("/")) {
// Do not add a trailing '/' if it wasn't typed originally
cleaned = cleaned.substr(0, cleaned.length - 1)
}
if (!str.startsWith("http") && cleaned.startsWith("https://")) {
cleaned = cleaned.substr("https://".length)
}
return cleaned
} catch (e) {
console.error(e)
return undefined
}
}
isValid(str: string): boolean {
try {
if (
!str.startsWith("http://") &&
!str.startsWith("https://") &&
!str.startsWith("http:")
) {
str = "https://" + str
}
const url = new URL(str)
const dotIndex = url.host.indexOf(".")
return dotIndex > 0 && url.host[url.host.length - 1] !== "."
} catch (e) {
return false
}
}
}

View file

@ -0,0 +1,179 @@
import Combine from "../../Base/Combine"
import Title from "../../Base/Title"
import Table from "../../Base/Table"
import Wikidata from "../../../Logic/Web/Wikidata"
import { UIEventSource } from "../../../Logic/UIEventSource"
import Locale from "../../i18n/Locale"
import { Utils } from "../../../Utils"
import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox"
import { Validator } from "../ValidatedTextField"
export default class WikidataValidator extends Validator {
constructor() {
super(
"wikidata",
new Combine([
"A wikidata identifier, e.g. Q42.",
new Title("Helper arguments"),
new Table(
["name", "doc"],
[
["key", "the value of this tag will initialize search (default: name)"],
[
"options",
new Combine([
"A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
new Table(
["subarg", "doc"],
[
[
"removePrefixes",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
],
[
"removePostfixes",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
],
[
"instanceOf",
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
],
[
"notInstanceof",
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
],
]
),
]),
],
]
),
new Title("Example usage"),
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`json
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
"helperArgs": [
"name",
{
"removePostfixes": {"en": [
"street",
"boulevard",
"path",
"square",
"plaza",
],
"nl": ["straat","plein","pad","weg",laan"],
"fr":["route (de|de la|de l'| de le)"]
},
"#": "Remove streets and parks from the search results:"
"notInstanceOf": ["Q79007","Q22698"]
}
]
}
\`\`\`
Another example is to search for species and trees:
\`\`\`json
"freeform": {
"key": "species:wikidata",
"type": "wikidata",
"helperArgs": [
"species",
{
"instanceOf": [10884, 16521]
}]
}
\`\`\`
`,
])
)
}
public isValid(str): boolean {
if (str === undefined) {
return false
}
if (str.length <= 2) {
return false
}
return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined)
}
public reformat(str) {
if (str === undefined) {
return undefined
}
let out = str
.split(";")
.map((str) => Wikidata.ExtractKey(str))
.join("; ")
if (str.endsWith(";")) {
out = out + ";"
}
return out
}
public inputHelper(currentValue, inputHelperOptions) {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
const searchFor = <string>(
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
)
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
const options: any = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? []
const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? []
const defaultValueCandidate = Locale.language.map((lg) => {
const prefixesUnrwapped: RegExp[] = (
Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? []
).map((s) => new RegExp("^" + s, "i"))
const postfixesUnwrapped: RegExp[] = (
Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? []
).map((s) => new RegExp(s + "$", "i"))
let clipped = searchFor
for (const postfix of postfixesUnwrapped) {
const match = searchFor.match(postfix)
if (match !== null) {
clipped = searchFor.substring(0, searchFor.length - match[0].length)
break
}
}
for (const prefix of prefixesUnrwapped) {
const match = searchFor.match(prefix)
if (match !== null) {
clipped = searchFor.substring(match[0].length)
break
}
}
return clipped
})
defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
}
let instanceOf: number[] = Utils.NoNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Utils.NoNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
return new WikidataSearchBox({
value: currentValue,
searchText: searchForValue,
instanceOf,
notInstanceOf,
})
}
}