2020-09-25 12:44:04 +02:00
import { DropDown } from "./DropDown"
import * as EmailValidator from "email-validator"
import { parsePhoneNumberFromString } from "libphonenumber-js"
import { InputElement } from "./InputElement"
import { TextField } from "./TextField"
2022-06-19 19:10:56 +02:00
import { Store , UIEventSource } from "../../Logic/UIEventSource"
2020-09-26 03:02:19 +02:00
import CombinedInputElement from "./CombinedInputElement"
import SimpleDatePicker from "./SimpleDatePicker"
2021-01-02 16:04:16 +01:00
import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"
2020-11-15 03:10:44 +01:00
import DirectionInput from "./DirectionInput"
2021-05-11 02:39:51 +02:00
import ColorPicker from "./ColorPicker"
import { Utils } from "../../Utils"
2021-06-23 02:15:28 +02:00
import Loc from "../../Models/Loc"
2021-06-28 00:45:49 +02:00
import BaseUIElement from "../BaseUIElement"
2021-07-20 01:33:58 +02:00
import LengthInput from "./LengthInput"
import { GeoOperations } from "../../Logic/GeoOperations"
2021-08-07 23:11:34 +02:00
import { Unit } from "../../Models/Unit"
2021-09-13 01:17:48 +02:00
import { FixedInputElement } from "./FixedInputElement"
2021-10-08 04:33:39 +02:00
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"
2021-10-09 22:40:52 +02:00
import Wikidata from "../../Logic/Web/Wikidata"
2021-10-15 14:52:11 +02:00
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
2021-10-29 03:42:33 +02:00
import Table from "../Base/Table"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
2022-01-18 02:26:21 +01:00
import InputElementMap from "./InputElementMap"
2022-02-11 04:28:11 +01:00
import Translations from "../i18n/Translations"
2022-02-11 20:56:54 +01:00
import { Translation } from "../i18n/Translation"
2022-06-06 19:37:22 +02:00
import BaseLayer from "../../Models/BaseLayer"
2022-06-23 03:06:51 +02:00
import Locale from "../i18n/Locale"
2022-02-11 20:56:54 +01:00
2022-02-12 02:53:41 +01:00
export class TextFieldDef {
2022-02-11 20:56:54 +01:00
public readonly name : string
/ *
* An explanation for the theme builder .
* This can indicate which special input element is used , . . .
* * /
public readonly explanation : string
2022-02-12 02:53:41 +01:00
protected inputmode? : string = undefined
2022-02-11 20:56:54 +01:00
2022-02-12 02:53:41 +01:00
constructor ( name : string , explanation : string | BaseUIElement ) {
this . name = name
2022-02-11 20:56:54 +01:00
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 ( )
}
}
2022-02-12 02:53:41 +01:00
public getFeedback ( s : string ) : Translation {
const tr = Translations . t . validation [ this . name ]
if ( tr !== undefined ) {
return tr [ "feedback" ]
}
}
public ConstructInputElement (
options : {
value? : UIEventSource < string >
inputStyle? : string
feedback? : UIEventSource < Translation >
2022-06-30 03:07:54 +02:00
placeholder? : string | Translation | UIEventSource < string >
2022-02-12 02:53:41 +01:00
country ? : ( ) = > string
location ? : [ number /*lat*/ , number /*lon*/ ]
2022-06-06 19:37:22 +02:00
mapBackgroundLayer? : UIEventSource < /*BaseLayer*/ any >
2022-02-12 02:53:41 +01:00
unit? : Unit
2022-06-23 03:06:51 +02:00
args ? : ( string | number | boolean | any ) [ ] // Extra arguments for the inputHelper,
2022-02-12 02:53:41 +01:00
feature? : any
} = { }
) : InputElement < string > {
if ( options . placeholder === undefined ) {
options . placeholder = Translations . t . validation [ this . name ] ? . description ? ? this . name
}
options [ "textArea" ] = this . name === "text"
2023-02-09 00:30:21 +01:00
if ( this . name === "text" ) {
options [ "htmlType" ] = "area"
}
2022-02-12 02:53:41 +01:00
const self = this
if ( options . unit !== undefined ) {
// Reformatting is handled by the unit in this case
options [ "isValid" ] = ( str ) = > {
2022-08-18 19:17:15 +02:00
const denom = options . unit . findDenomination ( str , options ? . country )
2022-02-12 02:53:41 +01:00
if ( denom === undefined ) {
return false
}
const stripped = denom [ 0 ]
return self . isValid ( stripped , options . country )
}
} else {
2022-03-02 15:59:06 +01:00
options [ "isValid" ] = ( str ) = > self . isValid ( str , options . country )
2022-02-12 02:53:41 +01:00
}
2022-03-13 02:46:42 +01:00
options [ "cssText" ] = "width: 100%;"
2022-02-12 02:53:41 +01:00
options [ "inputMode" ] = this . inputmode
if ( this . inputmode === "text" ) {
options [ "htmlType" ] = "area"
2022-03-13 02:46:42 +01:00
options [ "textAreaRows" ] = 4
2022-02-12 02:53:41 +01:00
}
const textfield = new TextField ( options )
let input : InputElement < string > = textfield
if ( options . feedback ) {
textfield . GetRawValue ( ) . addCallback ( ( v ) = > {
if ( self . isValid ( v , options . country ) ) {
options . feedback . setData ( undefined )
} else {
options . feedback . setData ( self . getFeedback ( v ) )
}
} )
}
if ( this . reformat && options . unit === undefined ) {
input . GetValue ( ) . addCallbackAndRun ( ( str ) = > {
if ( ! options [ "isValid" ] ( str , options . country ) ) {
return
}
const formatted = this . reformat ( str , options . country )
input . GetValue ( ) . setData ( formatted )
} )
}
if ( options . unit ) {
// We need to apply a unit.
// This implies:
// We have to create a dropdown with applicable denominations, and fuse those values
const unit = options . unit
const isSingular = input . GetValue ( ) . map ( ( str ) = > str ? . trim ( ) === "1" )
const unitDropDown =
unit . denominations . length === 1
? new FixedInputElement (
unit . denominations [ 0 ] . getToggledHuman ( isSingular ) ,
unit . denominations [ 0 ]
)
: new DropDown (
"" ,
unit . denominations . map ( ( denom ) = > {
return {
shown : denom.getToggledHuman ( isSingular ) ,
value : denom ,
}
} )
)
2022-08-18 19:17:15 +02:00
unitDropDown . GetValue ( ) . setData ( unit . getDefaultInput ( options . country ) )
2022-02-12 02:53:41 +01:00
unitDropDown . SetClass ( "w-min" )
const fixedDenom = unit . denominations . length === 1 ? unit . denominations [ 0 ] : undefined
input = new CombinedInputElement (
input ,
unitDropDown ,
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
( text , denom ) = > {
if ( denom === undefined ) {
return text
}
return denom ? . canonicalValue ( text , true )
} ,
( valueWithDenom : string ) = > {
// Take the value from OSM and feed it into the textfield and the dropdown
2022-08-18 19:17:15 +02:00
const withDenom = unit . findDenomination ( valueWithDenom , options ? . country )
2022-02-12 02:53:41 +01:00
if ( withDenom === undefined ) {
// Not a valid value at all - we give it undefined and leave the details up to the other elements (but we keep the previous denomination)
return [ undefined , fixedDenom ]
}
const [ strippedText , denom ] = withDenom
if ( strippedText === undefined ) {
return [ undefined , fixedDenom ]
}
return [ strippedText , denom ]
}
) . SetClass ( "flex" )
}
const helper = this . inputHelper ( input . GetValue ( ) , {
location : options.location ,
mapBackgroundLayer : options.mapBackgroundLayer ,
args : options.args ,
feature : options.feature ,
} ) ? . SetClass ( "block" )
if ( helper !== undefined ) {
input = new CombinedInputElement (
input ,
helper ,
( a , _ ) = > a , // We can ignore b, as they are linked earlier
( a ) = > [ a , a ]
) . SetClass ( "block w-full" )
}
if ( this . postprocess !== undefined ) {
input = new InputElementMap < string , string > (
input ,
( a , b ) = > a === b ,
this . postprocess ,
this . undoPostprocess
)
}
return input
}
protected isValid ( string : string , requestCountry : ( ) = > string ) : boolean {
return true
}
protected reformat ( s : string , country ? : ( ) = > string ) : string {
2022-02-11 20:56:54 +01:00
return s
}
2020-09-26 03:02:19 +02:00
2022-01-18 02:26:21 +01:00
/ * *
* Modification to make before the string is uploaded to OSM
* /
2022-02-12 02:53:41 +01:00
protected postprocess ( s : string ) : string {
2022-02-11 20:56:54 +01:00
return s
}
2022-02-12 02:53:41 +01:00
protected undoPostprocess ( s : string ) : string {
2022-02-11 20:56:54 +01:00
return s
}
2022-02-12 02:53:41 +01:00
protected inputHelper (
value : UIEventSource < string > ,
options ? : {
2021-06-23 02:15:28 +02:00
location : [ number , number ]
2021-07-20 01:33:58 +02:00
mapBackgroundLayer? : UIEventSource < any >
2021-10-29 03:42:33 +02:00
args : ( string | number | boolean | any ) [ ]
2021-07-20 01:33:58 +02:00
feature? : any
2022-02-11 20:56:54 +01:00
}
) : InputElement < string > {
return undefined
}
2020-09-26 03:02:19 +02:00
}
2020-09-25 12:44:04 +02:00
2022-02-12 02:53:41 +01:00
class WikidataTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
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" ] ,
2022-06-23 03:06:51 +02:00
[
2022-09-08 21:40:48 +02:00
[
2022-06-23 03:06:51 +02:00
"removePrefixes" ,
2023-03-06 00:21:42 +01:00
"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" ,
2022-06-23 03:06:51 +02:00
] ,
[
"removePostfixes" ,
2023-03-06 00:21:42 +01:00
"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." ,
2022-04-22 01:45:54 +02:00
] ,
[
"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" ,
2022-09-08 21:40:48 +02:00
] ,
2022-04-22 01:45:54 +02:00
[
"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" ,
2022-09-08 21:40:48 +02:00
] ,
2022-02-12 02:53:41 +01:00
]
) ,
] ) ,
2022-09-08 21:40:48 +02:00
] ,
]
) ,
2022-02-12 02:53:41 +01:00
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
2021-10-29 03:59:28 +02:00
2022-04-22 01:45:54 +02:00
\ ` \` \` json
2021-10-29 03:59:28 +02:00
"freeform" : {
2021-10-29 13:35:33 +02:00
"key" : "name:etymology:wikidata" ,
"type" : "wikidata" ,
"helperArgs" : [
"name" ,
{
2022-06-23 03:06:51 +02:00
"removePostfixes" : { "en" : [
2021-10-29 13:35:33 +02:00
"street" ,
"boulevard" ,
"path" ,
"square" ,
"plaza" ,
2022-04-22 01:45:54 +02:00
] ,
2023-03-06 00:21:42 +01:00
"nl" : [ "straat" , "plein" , "pad" , "weg" , laan " ] ,
"fr" : [ "route (de|de la|de l'| de le)" ]
2022-06-23 03:06:51 +02:00
} ,
2023-01-17 02:50:13 +01:00
2022-04-22 01:45:54 +02:00
"#" : "Remove streets and parks from the search results:"
2023-01-17 02:50:13 +01:00
"notInstanceOf" : [ "Q79007" , "Q22698" ]
2021-10-29 13:35:33 +02:00
}
2023-01-17 02:50:13 +01:00
2021-10-29 13:35:33 +02:00
]
}
2022-04-22 01:45:54 +02:00
\ ` \` \`
Another example is to search for species and trees :
\ ` \` \` json
"freeform" : {
"key" : "species:wikidata" ,
"type" : "wikidata" ,
"helperArgs" : [
"species" ,
{
"instanceOf" : [ 10884 , 16521 ]
} ]
}
\ ` \` \`
` ,
2022-02-12 02:53:41 +01:00
] )
2022-09-08 21:40:48 +02:00
)
2022-02-11 20:56:54 +01:00
}
2021-10-29 03:42:33 +02:00
2022-02-11 20:56:54 +01:00
public isValid ( str ) : boolean {
2021-10-29 03:42:33 +02:00
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"
2022-06-23 03:06:51 +02:00
const searchFor = < string > (
( inputHelperOptions . feature ? . properties [ searchKey ] ? . toLowerCase ( ) ? ? "" )
2022-09-08 21:40:48 +02:00
)
2021-10-29 03:42:33 +02:00
2022-06-23 03:06:51 +02:00
let searchForValue : UIEventSource < string > = new UIEventSource ( searchFor )
2022-04-22 01:45:54 +02:00
const options : any = args [ 1 ]
2021-10-29 03:42:33 +02:00
if ( searchFor !== undefined && options !== undefined ) {
2022-06-23 03:06:51 +02:00
const prefixes = < string [ ] | Record < string , string [ ] > > options [ "removePrefixes" ] ? ? [ ]
const postfixes = < string [ ] | Record < string , string [ ] > > options [ "removePostfixes" ] ? ? [ ]
2023-03-06 00:21:42 +01:00
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
2022-06-23 03:06:51 +02:00
}
2023-03-06 00:21:42 +01:00
}
2021-10-29 03:42:33 +02:00
2023-03-06 00:21:42 +01:00
for ( const prefix of prefixesUnrwapped ) {
const match = searchFor . match ( prefix )
if ( match !== null ) {
clipped = searchFor . substring ( match [ 0 ] . length )
break
2022-06-23 03:06:51 +02:00
}
2023-03-06 00:21:42 +01:00
}
return clipped
} )
defaultValueCandidate . addCallbackAndRun ( ( clipped ) = > searchForValue . setData ( clipped ) )
2021-10-29 03:42:33 +02:00
}
2022-06-23 03:06:51 +02:00
2022-04-22 01:45:54 +02:00
let instanceOf : number [ ] = Utils . NoNull (
( options ? . instanceOf ? ? [ ] ) . map ( ( i ) = > Wikidata . QIdToNumber ( i ) )
2022-09-08 21:40:48 +02:00
)
2022-04-22 01:45:54 +02:00
let notInstanceOf : number [ ] = Utils . NoNull (
( options ? . notInstanceOf ? ? [ ] ) . map ( ( i ) = > Wikidata . QIdToNumber ( i ) )
2022-09-08 21:40:48 +02:00
)
2021-10-29 03:42:33 +02:00
return new WikidataSearchBox ( {
value : currentValue ,
2022-06-23 03:06:51 +02:00
searchText : searchForValue ,
2022-04-22 01:45:54 +02:00
instanceOf ,
notInstanceOf ,
2021-10-29 03:42:33 +02:00
} )
}
}
2022-02-12 02:53:41 +01:00
class OpeningHoursTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
super (
"opening_hours" ,
new Combine ( [
2022-02-11 20:56:54 +01:00
"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" ,
2023-03-06 00:21:42 +01:00
"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." ,
2022-02-11 20:56:54 +01:00
] ,
[
"postfix" ,
"Piece of text that will always be added to the end of the generated opening hours" ,
] ,
]
2022-09-08 21:40:48 +02:00
) ,
2022-02-11 20:56:54 +01:00
] ) ,
2022-09-08 21:40:48 +02:00
] ,
]
) ,
2022-02-11 20:56:54 +01:00
new Title ( "Example usage" ) ,
"To add a conditional (based on time) access restriction:\n\n```\n" +
`
2021-10-29 13:35:33 +02:00
"freeform" : {
"key" : "access:conditional" ,
"type" : "opening_hours" ,
"helperArgs" : [
{
"prefix" : "no @ (" ,
"postfix" : ")"
}
]
2022-02-12 02:53:41 +01:00
} ` +
"\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 )`" ,
] )
2022-09-08 21:40:48 +02:00
)
2022-02-11 20:56:54 +01:00
}
2021-10-29 03:42:33 +02:00
isValid() {
return true
}
reformat ( str ) {
return str
}
inputHelper (
value : UIEventSource < string > ,
inputHelperOptions : {
location : [ number , number ]
mapBackgroundLayer? : UIEventSource < any >
args : ( string | number | boolean | any ) [ ]
feature? : any
}
) {
const args = ( inputHelperOptions . args ? ? [ ] ) [ 0 ]
const prefix = < string > args ? . prefix ? ? ""
const postfix = < string > args ? . postfix ? ? ""
return new OpeningHoursInput ( value , prefix , postfix )
}
}
2022-01-18 02:26:21 +01:00
2022-02-12 02:53:41 +01:00
class UrlTextfieldDef extends TextFieldDef {
2023-01-17 02:50:13 +01:00
declare inputmode : "url"
2022-01-18 02:26:21 +01:00
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
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"
)
2022-02-11 20:56:54 +01:00
}
2022-01-18 02:26:21 +01:00
postprocess ( str : string ) {
if ( str === undefined ) {
return undefined
}
if ( ! str . startsWith ( "http://" ) || ! str . startsWith ( "https://" ) ) {
return "https://" + str
}
return str
}
undoPostprocess ( str : string ) {
if ( str === undefined ) {
return undefined
}
if ( str . startsWith ( "http://" ) ) {
return str . substr ( "http://" . length )
}
if ( str . startsWith ( "https://" ) ) {
return str . substr ( "https://" . length )
}
return str
}
reformat ( str : string ) : string {
try {
let url : URL
2022-04-18 11:52:23 +02:00
// str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
2022-01-18 02:26:21 +01:00
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" ,
2022-01-26 21:40:38 +01:00
"campaignid" ,
"campaign" ,
"AdGroupId" ,
"AdGroup" ,
"TargetId" ,
"msclkid" ,
]
2022-01-18 02:26:21 +01:00
for ( const dontLike of blacklistedTrackingParams ) {
2022-01-26 21:40:38 +01:00
url . searchParams . delete ( dontLike . toLowerCase ( ) )
2022-01-18 02:26:21 +01:00
}
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 ( 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 ( "." )
2022-01-26 21:40:38 +01:00
return dotIndex > 0 && url . host [ url . host . length - 1 ] !== "."
2022-01-18 02:26:21 +01:00
} catch ( e ) {
return false
}
}
}
2022-02-12 02:53:41 +01:00
class StringTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "string" , "A simple piece of text" )
2022-02-11 20:56:54 +01:00
}
}
2020-09-25 12:44:04 +02:00
2022-02-12 02:53:41 +01:00
class TextTextField extends TextFieldDef {
2023-01-17 02:50:13 +01:00
declare inputmode : "text"
2022-02-11 20:56:54 +01:00
constructor ( ) {
2023-02-09 00:30:21 +01:00
super ( "text" , "A longer piece of text. Uses an textArea instead of a textField" )
2022-02-11 20:56:54 +01:00
}
}
2021-09-09 00:05:51 +02:00
2022-02-12 02:53:41 +01:00
class DateTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "date" , "A date with date picker" )
2022-02-11 20:56:54 +01:00
}
2021-06-16 17:09:32 +02:00
2022-02-11 20:56:54 +01:00
isValid = ( str ) = > {
return ! isNaN ( new Date ( str ) . getTime ( ) )
}
reformat ( str ) {
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 ( "-" )
}
inputHelper ( value ) {
return new SimpleDatePicker ( value )
}
}
2022-02-12 02:53:41 +01:00
class LengthTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
inputMode : "decimal"
constructor ( ) {
super (
2022-06-20 03:14:44 +02:00
"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"]'
2020-09-25 12:44:04 +02:00
)
2022-02-11 20:56:54 +01:00
}
isValid = ( str ) = > {
const t = Number ( str )
return ! isNaN ( t )
}
2022-06-19 19:10:56 +02:00
inputHelper = (
value : UIEventSource < string > ,
options : {
location ? : [ number , number ]
args? : string [ ]
feature? : any
mapBackgroundLayer? : Store < BaseLayer >
}
) = > {
2022-02-12 02:53:41 +01:00
options = options ? ? { }
options . location = options . location ? ? [ 0 , 0 ]
2022-02-11 20:56:54 +01:00
const args = options . args ? ? [ ]
let zoom = 19
if ( args [ 0 ] ) {
zoom = Number ( args [ 0 ] )
if ( isNaN ( zoom ) ) {
console . error (
"Invalid zoom level for argument at 'length'-input. The offending argument is: " ,
args [ 0 ] ,
" (using 19 instead)"
)
zoom = 19
}
}
// Bit of a hack: we project the centerpoint to the closes point on the road - if available
2022-02-12 02:53:41 +01:00
if ( options ? . feature !== undefined && options . feature . geometry . type !== "Point" ) {
2022-02-11 20:56:54 +01:00
const lonlat = < [ number , number ] > [ . . . options . location ]
2022-02-22 14:13:41 +01:00
lonlat . reverse ( /*Changes a clone, this is safe */ )
2022-02-11 20:56:54 +01:00
options . location = < [ number , number ] > (
GeoOperations . nearestPoint ( options . feature , lonlat ) . geometry . coordinates
2022-09-08 21:40:48 +02:00
)
2022-02-22 14:13:41 +01:00
options . location . reverse ( /*Changes a clone, this is safe */ )
2022-02-11 20:56:54 +01:00
}
const location = new UIEventSource < Loc > ( {
lat : options.location [ 0 ] ,
lon : options.location [ 1 ] ,
zoom : zoom ,
} )
if ( args [ 1 ] ) {
// We have a prefered map!
options . mapBackgroundLayer = AvailableBaseLayers . SelectBestLayerAccordingTo (
location ,
new UIEventSource < string [ ] > ( args [ 1 ] . split ( "," ) )
)
}
2022-06-19 19:10:56 +02:00
const background = options ? . mapBackgroundLayer
const li = new LengthInput ( new UIEventSource < BaseLayer > ( background . data ) , location , value )
2022-02-11 20:56:54 +01:00
li . SetStyle ( "height: 20rem;" )
return li
}
}
2022-02-12 02:53:41 +01:00
class FloatTextField extends TextFieldDef {
inputmode = "decimal"
2022-02-11 20:56:54 +01:00
2022-02-12 02:53:41 +01:00
constructor ( name? : string , explanation? : string ) {
super ( name ? ? "float" , explanation ? ? "A decimal" )
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
isValid ( str ) {
return ! isNaN ( Number ( str ) ) && ! str . endsWith ( "." ) && ! str . endsWith ( "," )
}
reformat ( str ) : string {
return "" + Number ( str )
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
getFeedback ( s : string ) : Translation {
if ( isNaN ( Number ( s ) ) ) {
return Translations . t . validation . nat . notANumber
}
return undefined
}
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
class IntTextField extends FloatTextField {
2022-02-11 20:56:54 +01:00
inputMode = "numeric"
2022-02-12 02:53:41 +01:00
constructor ( name? : string , explanation? : string ) {
super ( name ? ? "int" , explanation ? ? "A number" )
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
isValid ( str ) : boolean {
2022-02-11 20:56:54 +01:00
str = "" + str
2022-02-12 02:53:41 +01:00
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
2022-02-11 20:56:54 +01:00
}
}
2022-02-12 02:53:41 +01:00
class NatTextField extends IntTextField {
inputMode = "numeric"
2022-02-11 20:56:54 +01:00
2022-02-12 02:53:41 +01:00
constructor ( name? : string , explanation? : string ) {
super ( name ? ? "nat" , explanation ? ? "A positive number or zero" )
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
isValid ( str ) : boolean {
if ( str === undefined ) {
return false
}
2022-02-11 20:56:54 +01:00
str = "" + str
2022-02-12 02:53:41 +01:00
return str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) >= 0
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
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
}
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
class PNatTextField extends NatTextField {
inputmode = "numeric"
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "pnat" , "A strict positive number" )
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
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
}
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
class PFloatTextField extends FloatTextField {
2022-02-11 20:56:54 +01:00
inputmode = "decimal"
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "pfloat" , "A positive decimal (inclusive zero)" )
2022-02-11 20:56:54 +01:00
}
isValid = ( str ) = >
! isNaN ( Number ( str ) ) && Number ( str ) >= 0 && ! str . endsWith ( "." ) && ! str . endsWith ( "," )
2022-02-12 02:53:41 +01:00
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
}
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
class EmailTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
inputmode = "email"
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "email" , "An email adress" )
2022-02-11 20:56:54 +01:00
}
isValid = ( str ) = > {
2022-02-12 02:53:41 +01:00
if ( str === undefined ) {
return false
}
2022-06-08 12:27:01 +02:00
str = str . trim ( )
2022-02-11 20:56:54 +01:00
if ( str . startsWith ( "mailto:" ) ) {
str = str . substring ( "mailto:" . length )
}
return EmailValidator . validate ( str )
}
reformat = ( str ) = > {
if ( str === undefined ) {
return undefined
}
2022-06-08 12:27:01 +02:00
str = str . trim ( )
2022-02-11 20:56:54 +01:00
if ( str . startsWith ( "mailto:" ) ) {
str = str . substring ( "mailto:" . length )
}
return str
}
2022-09-08 21:40:48 +02:00
2022-02-12 02:53:41 +01:00
getFeedback ( s : string ) : Translation {
if ( s . indexOf ( "@" ) < 0 ) {
return Translations . t . validation . email . noAt
}
2022-09-08 21:40:48 +02:00
2022-02-12 02:53:41 +01:00
return super . getFeedback ( s )
}
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
class PhoneTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
inputmode = "tel"
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "phone" , "A phone number" )
2022-02-11 20:56:54 +01:00
}
2022-02-12 02:53:41 +01:00
isValid ( str , country : ( ) = > string ) : boolean {
2022-02-11 20:56:54 +01:00
if ( str === undefined ) {
return false
}
if ( str . startsWith ( "tel:" ) ) {
str = str . substring ( "tel:" . length )
}
2022-02-12 02:53:41 +01:00
let countryCode = undefined
if ( country !== undefined ) {
countryCode = country ( ) ? . toUpperCase ( )
}
return parsePhoneNumberFromString ( str , countryCode ) ? . isValid ( ) ? ? false
2022-02-11 20:56:54 +01:00
}
reformat = ( str , country : ( ) = > string ) = > {
if ( str . startsWith ( "tel:" ) ) {
str = str . substring ( "tel:" . length )
}
2022-03-02 15:59:06 +01:00
return parsePhoneNumberFromString (
str ,
country ( ) ? . toUpperCase ( ) as any
) ? . formatInternational ( )
2022-02-11 20:56:54 +01:00
}
}
2022-02-12 02:53:41 +01:00
class ColorTextField extends TextFieldDef {
2022-02-11 20:56:54 +01:00
constructor ( ) {
2022-02-12 02:53:41 +01:00
super ( "color" , "Shows a color picker" )
2022-02-11 20:56:54 +01:00
}
inputHelper = ( value ) = > {
return new ColorPicker (
value . map (
( color ) = > {
return Utils . ColourNameToHex ( color ? ? "" )
} ,
[ ] ,
( str ) = > Utils . HexToColourName ( str )
2022-09-08 21:40:48 +02:00
)
)
2022-02-11 20:56:54 +01:00
}
}
2022-02-12 02:53:41 +01:00
class DirectionTextField extends IntTextField {
inputMode = "numeric"
constructor ( ) {
super (
"direction" ,
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"
2022-09-08 21:40:48 +02:00
)
2022-02-12 02:53:41 +01:00
}
2022-09-08 21:40:48 +02:00
2022-02-12 02:53:41 +01:00
reformat ( str ) : string {
const n = Number ( str ) % 360
return "" + n
}
inputHelper = ( value , options ) = > {
const args = options . args ? ? [ ]
options . location = options . location ? ? [ 0 , 0 ]
let zoom = 19
if ( args [ 0 ] ) {
zoom = Number ( args [ 0 ] )
if ( isNaN ( zoom ) ) {
throw "Invalid zoom level for argument at 'length'-input"
}
}
const location = new UIEventSource < Loc > ( {
lat : options.location [ 0 ] ,
lon : options.location [ 1 ] ,
zoom : zoom ,
} )
if ( args [ 1 ] ) {
// We have a prefered map!
options . mapBackgroundLayer = AvailableBaseLayers . SelectBestLayerAccordingTo (
location ,
new UIEventSource < string [ ] > ( args [ 1 ] . split ( "," ) )
)
}
const di = new DirectionInput ( options . mapBackgroundLayer , location , value )
di . SetStyle ( "max-width: 25rem;" )
return di
}
}
2022-02-11 20:56:54 +01:00
export default class ValidatedTextField {
2022-02-12 02:53:41 +01:00
private static AllTextfieldDefs : TextFieldDef [ ] = [
2022-02-11 20:56:54 +01:00
new StringTextField ( ) ,
new TextTextField ( ) ,
new DateTextField ( ) ,
new NatTextField ( ) ,
new IntTextField ( ) ,
new LengthTextField ( ) ,
new DirectionTextField ( ) ,
new WikidataTextField ( ) ,
new PNatTextField ( ) ,
new FloatTextField ( ) ,
new PFloatTextField ( ) ,
new EmailTextField ( ) ,
new UrlTextfieldDef ( ) ,
new PhoneTextField ( ) ,
new OpeningHoursTextField ( ) ,
new ColorTextField ( ) ,
2020-09-25 12:44:04 +02:00
]
2022-02-12 02:53:41 +01:00
public static allTypes : Map < string , TextFieldDef > = ValidatedTextField . allTypesDict ( )
public static ForType ( type : string = "string" ) : TextFieldDef {
2022-06-19 19:10:56 +02:00
const def = ValidatedTextField . allTypes . get ( type )
if ( def === undefined ) {
2022-06-23 03:06:51 +02:00
console . warn (
"Something tried to load a validated text field named" ,
type ,
"but this type does not exist"
)
2022-06-19 19:10:56 +02:00
return this . ForType ( )
}
return def
2020-09-25 12:44:04 +02:00
}
2021-07-20 01:33:58 +02:00
2021-11-30 22:50:48 +01:00
public static HelpText ( ) : BaseUIElement {
2022-02-12 02:53:41 +01:00
const explanations : BaseUIElement [ ] = ValidatedTextField . AllTextfieldDefs . map ( ( type ) = >
2022-01-18 02:26:21 +01:00
new Combine ( [ new Title ( type . name , 3 ) , type . explanation ] ) . SetClass ( "flex flex-col" )
2022-09-08 21:40:48 +02:00
)
2021-10-29 13:53:00 +02:00
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" ,
2021-11-30 22:50:48 +01:00
. . . explanations ,
] ) . SetClass ( "flex flex-col" )
2021-05-11 02:39:51 +02:00
}
2022-02-11 20:56:54 +01:00
public static AvailableTypes ( ) : string [ ] {
2022-02-12 02:53:41 +01:00
return ValidatedTextField . AllTextfieldDefs . map ( ( tp ) = > tp . name )
2021-05-11 02:39:51 +02:00
}
2022-02-12 02:53:41 +01:00
private static allTypesDict ( ) : Map < string , TextFieldDef > {
const types = new Map < string , TextFieldDef > ( )
for ( const tp of ValidatedTextField . AllTextfieldDefs ) {
2021-05-11 02:39:51 +02:00
types [ tp . name ] = tp
2022-01-07 17:31:39 +01:00
types . set ( tp . name , tp )
2021-05-11 02:39:51 +02:00
}
return types
}
2020-09-25 12:44:04 +02:00
}