forked from MapComplete/MapComplete
More refactoring
This commit is contained in:
parent
5d0fe31c41
commit
41e6a2c760
147 changed files with 1540 additions and 1797 deletions
70
UI/InputElement/Helpers/DirectionInput.svelte
Normal file
70
UI/InputElement/Helpers/DirectionInput.svelte
Normal file
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { MapProperties } from "../../../Models/MapProperties";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import Svg from "../../../Svg.js";
|
||||
|
||||
/**
|
||||
* A visualisation to pick a direction on a map background
|
||||
*/
|
||||
export let value: UIEventSource<undefined | number>;
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
mla.allowMoving.setData(false)
|
||||
mla.allowZooming.setData(false)
|
||||
let directionElem: HTMLElement | undefined;
|
||||
$: value.addCallbackAndRunD(degrees => {
|
||||
console.log("Degrees are", degrees, directionElem);
|
||||
if (directionElem === undefined) {
|
||||
return;
|
||||
}
|
||||
directionElem.style.rotate = degrees + "deg";
|
||||
});
|
||||
|
||||
let mainElem : HTMLElement
|
||||
function onPosChange(x: number, y: number) {
|
||||
const rect = mainElem.getBoundingClientRect();
|
||||
const dx = -(rect.left + rect.right) / 2 + x;
|
||||
const dy = (rect.top + rect.bottom) / 2 - y;
|
||||
const angle = (180 * Math.atan2(dy, dx)) / Math.PI;
|
||||
const angleGeo = Math.floor((450 - angle) % 360);
|
||||
value.setData(angleGeo);
|
||||
}
|
||||
|
||||
let isDown = false;
|
||||
</script>
|
||||
|
||||
<div bind:this={mainElem} class="relative w-48 h-48 cursor-pointer overflow-hidden"
|
||||
on:click={e => onPosChange(e.x, e.y)}
|
||||
on:mousedown={e => {
|
||||
isDown = true
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
} }
|
||||
on:mousemove={e => {
|
||||
if(isDown){
|
||||
onPosChange(e.clientX, e.clientY)
|
||||
|
||||
}}}
|
||||
|
||||
on:mouseup={() => {
|
||||
isDown = false
|
||||
} }
|
||||
on:touchmove={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
|
||||
|
||||
on:touchstart={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}>
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div bind:this={directionElem} class="absolute w-full h-full top-0 left-0 border border-red-500">
|
||||
|
||||
<ToSvelte construct={ Svg.direction_stroke_svg}>
|
||||
|
||||
</ToSvelte>
|
||||
</div>
|
||||
</div>
|
42
UI/InputElement/Helpers/LocationInput.svelte
Normal file
42
UI/InputElement/Helpers/LocationInput.svelte
Normal file
|
@ -0,0 +1,42 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { MapProperties } from "../../../Models/MapProperties";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor";
|
||||
import MaplibreMap from "../../Map/MaplibreMap.svelte";
|
||||
import Svg from "../../../Svg";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import DragInvitation from "../../Base/DragInvitation.svelte";
|
||||
|
||||
/**
|
||||
* A visualisation to pick a direction on a map background
|
||||
*/
|
||||
export let value: UIEventSource<{lon: number, lat: number}>;
|
||||
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
|
||||
/**
|
||||
* Called when setup is done, cna be used to add layrs to the map
|
||||
*/
|
||||
export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
|
||||
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||
mla.allowMoving.setData(true)
|
||||
mla.allowZooming.setData(true)
|
||||
|
||||
if(onCreated){
|
||||
onCreated(value, map, mla)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative h-32 cursor-pointer overflow-hidden">
|
||||
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50">
|
||||
<ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte>
|
||||
</div>
|
||||
|
||||
<DragInvitation></DragInvitation>
|
||||
|
||||
</div>
|
13
UI/InputElement/InputHelper.svelte
Normal file
13
UI/InputElement/InputHelper.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Constructs an input helper element for the given type.
|
||||
* Note that all values are stringified
|
||||
*/
|
||||
|
||||
import { AvailableInputHelperType } from "./InputHelpers";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
|
||||
export let type : AvailableInputHelperType
|
||||
export let value : UIEventSource<string>
|
||||
|
||||
</script>
|
16
UI/InputElement/InputHelpers.ts
Normal file
16
UI/InputElement/InputHelpers.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
|
||||
export type AvailableInputHelperType = typeof InputHelpers.AvailableInputHelpers[number]
|
||||
|
||||
export default class InputHelpers {
|
||||
public static readonly AvailableInputHelpers = [] as const
|
||||
/**
|
||||
* To port
|
||||
* direction
|
||||
* opening_hours
|
||||
* color
|
||||
* length
|
||||
* date
|
||||
* wikidata
|
||||
*/
|
||||
}
|
119
UI/InputElement/ValidatedTextField.ts
Normal file
119
UI/InputElement/ValidatedTextField.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import WikidataValidator from "./Validators/WikidataValidator"
|
||||
import StringValidator from "./Validators/StringValidator"
|
||||
import TextValidator from "./Validators/TextValidator"
|
||||
import DateValidator from "./Validators/DateValidator"
|
||||
import LengthValidator from "./Validators/LengthValidator"
|
||||
import IntValidator from "./Validators/IntValidator"
|
||||
import EmailValidator from "./Validators/EmailValidator"
|
||||
import DirectionValidator from "./Validators/DirectionValidator"
|
||||
import NatValidator from "./Validators/NatValidator"
|
||||
import OpeningHoursValidator from "./Validators/OpeningHoursValidator"
|
||||
import PFloatValidator from "./Validators/PFloatValidator"
|
||||
import ColorValidator from "./Validators/ColorValidator"
|
||||
import PhoneValidator from "./Validators/PhoneValidator"
|
||||
import UrlValidator from "./Validators/UrlValidator"
|
||||
import FloatValidator from "./Validators/FloatValidator"
|
||||
import PNatValidator from "./Validators/PNatValidator"
|
||||
|
||||
/**
|
||||
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
|
||||
* They also double as an index of supported types for textfields in MapComplete
|
||||
*/
|
||||
export abstract class Validator {
|
||||
public readonly name: string
|
||||
/*
|
||||
* An explanation for the theme builder.
|
||||
* This can indicate which special input element is used, ...
|
||||
* */
|
||||
public readonly explanation: string
|
||||
/**
|
||||
* What HTML-inputmode to use
|
||||
*/
|
||||
public readonly inputmode?: string
|
||||
|
||||
constructor(name: string, explanation: string | BaseUIElement, inputmode?: string) {
|
||||
this.name = name
|
||||
this.inputmode = inputmode
|
||||
if (this.name.endsWith("textfield")) {
|
||||
this.name = this.name.substr(0, this.name.length - "TextField".length)
|
||||
}
|
||||
if (this.name.endsWith("textfielddef")) {
|
||||
this.name = this.name.substr(0, this.name.length - "TextFieldDef".length)
|
||||
}
|
||||
if (typeof explanation === "string") {
|
||||
this.explanation = explanation
|
||||
} else {
|
||||
this.explanation = explanation.AsMarkdown()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'.
|
||||
* However, inheritors might overwrite this to give more specific feedback
|
||||
* @param s
|
||||
*/
|
||||
public getFeedback(s: string): Translation {
|
||||
const tr = Translations.t.validation[this.name]
|
||||
if (tr !== undefined) {
|
||||
return tr["feedback"]
|
||||
}
|
||||
}
|
||||
|
||||
public isValid(string: string, requestCountry: () => string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
public reformat(s: string, country?: () => string): string {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
export default class Validators {
|
||||
private static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
new StringValidator(),
|
||||
new TextValidator(),
|
||||
new DateValidator(),
|
||||
new NatValidator(),
|
||||
new IntValidator(),
|
||||
new LengthValidator(),
|
||||
new DirectionValidator(),
|
||||
new WikidataValidator(),
|
||||
new PNatValidator(),
|
||||
new FloatValidator(),
|
||||
new PFloatValidator(),
|
||||
new EmailValidator(),
|
||||
new UrlValidator(),
|
||||
new PhoneValidator(),
|
||||
new OpeningHoursValidator(),
|
||||
new ColorValidator(),
|
||||
]
|
||||
public static allTypes: Map<string, Validator> = Validators.allTypesDict()
|
||||
|
||||
public static HelpText(): BaseUIElement {
|
||||
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
|
||||
new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")
|
||||
)
|
||||
return new Combine([
|
||||
new Title("Available types for text fields", 1),
|
||||
"The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them",
|
||||
...explanations,
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
public static AvailableTypes(): string[] {
|
||||
return Validators.AllValidators.map((tp) => tp.name)
|
||||
}
|
||||
|
||||
private static allTypesDict(): Map<string, Validator> {
|
||||
const types = new Map<string, Validator>()
|
||||
for (const tp of Validators.AllValidators) {
|
||||
types.set(tp.name, tp)
|
||||
}
|
||||
return types
|
||||
}
|
||||
}
|
7
UI/InputElement/Validators/ColorValidator.ts
Normal file
7
UI/InputElement/Validators/ColorValidator.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class ColorValidator extends Validator {
|
||||
constructor() {
|
||||
super("color", "Shows a color picker")
|
||||
}
|
||||
}
|
23
UI/InputElement/Validators/DateValidator.ts
Normal file
23
UI/InputElement/Validators/DateValidator.ts
Normal 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("-")
|
||||
}
|
||||
}
|
17
UI/InputElement/Validators/DirectionValidator.ts
Normal file
17
UI/InputElement/Validators/DirectionValidator.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
39
UI/InputElement/Validators/EmailValidator.ts
Normal file
39
UI/InputElement/Validators/EmailValidator.ts
Normal 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)
|
||||
}
|
||||
}
|
27
UI/InputElement/Validators/FloatValidator.ts
Normal file
27
UI/InputElement/Validators/FloatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
29
UI/InputElement/Validators/IntValidator.ts
Normal file
29
UI/InputElement/Validators/IntValidator.ts
Normal 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
|
||||
}
|
||||
}
|
16
UI/InputElement/Validators/LengthValidator.ts
Normal file
16
UI/InputElement/Validators/LengthValidator.ts
Normal 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)
|
||||
}
|
||||
}
|
30
UI/InputElement/Validators/NatValidator.ts
Normal file
30
UI/InputElement/Validators/NatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
54
UI/InputElement/Validators/OpeningHoursValidator.ts
Normal file
54
UI/InputElement/Validators/OpeningHoursValidator.ts
Normal 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 )`",
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
23
UI/InputElement/Validators/PFloatValidator.ts
Normal file
23
UI/InputElement/Validators/PFloatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
27
UI/InputElement/Validators/PNatValidator.ts
Normal file
27
UI/InputElement/Validators/PNatValidator.ts
Normal 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
|
||||
}
|
||||
}
|
32
UI/InputElement/Validators/PhoneValidator.ts
Normal file
32
UI/InputElement/Validators/PhoneValidator.ts
Normal 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()
|
||||
}
|
||||
}
|
8
UI/InputElement/Validators/StringValidator.ts
Normal file
8
UI/InputElement/Validators/StringValidator.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Validator } from "../ValidatedTextField"
|
||||
|
||||
export default class StringValidator extends Validator {
|
||||
constructor() {
|
||||
super("string", "A simple piece of text")
|
||||
}
|
||||
|
||||
}
|
7
UI/InputElement/Validators/TextValidator.ts
Normal file
7
UI/InputElement/Validators/TextValidator.ts
Normal 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")
|
||||
}
|
||||
}
|
75
UI/InputElement/Validators/UrlValidator.ts
Normal file
75
UI/InputElement/Validators/UrlValidator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
179
UI/InputElement/Validators/WikidataValidator.ts
Normal file
179
UI/InputElement/Validators/WikidataValidator.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue