2020-09-25 12:44:04 +02:00
import { DropDown } from "./DropDown" ;
import * as EmailValidator from "email-validator" ;
import { parsePhoneNumberFromString } from "libphonenumber-js" ;
import InputElementMap from "./InputElementMap" ;
import { InputElement } from "./InputElement" ;
import { TextField } from "./TextField" ;
import { UIElement } from "../UIElement" ;
import { 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" ;
2020-09-26 03:02:19 +02:00
interface TextFieldDef {
name : string ,
explanation : string ,
2021-05-11 02:39:51 +02:00
isValid : ( ( s : string , country ? : ( ) = > string ) = > boolean ) ,
2020-12-05 03:22:17 +01:00
reformat ? : ( ( s : string , country ? : ( ) = > string ) = > string ) ,
2020-11-17 02:22:48 +01:00
inputHelper ? : ( value : UIEventSource < string > , options ? : {
location : [ number , number ]
} ) = > InputElement < string > ,
2021-05-11 02:39:51 +02:00
inputmode? : string
2020-09-26 03:02:19 +02:00
}
2020-09-25 12:44:04 +02:00
export default class ValidatedTextField {
2020-09-26 03:02:19 +02:00
public static tpList : TextFieldDef [ ] = [
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"string" ,
"A basic string" ) ,
2020-10-27 01:01:34 +01:00
ValidatedTextField . tp (
"text" ,
2021-05-11 02:39:51 +02:00
"A string, but allows input of longer strings more comfortably (a text area)" ,
undefined ,
undefined ,
undefined ,
"text" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"date" ,
2020-09-26 03:02:19 +02:00
"A date" ,
( str ) = > {
const time = Date . parse ( str ) ;
return ! isNaN ( time ) ;
} ,
( 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 ( '-' ) ;
} ,
( value ) = > new SimpleDatePicker ( value ) ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"wikidata" ,
"A wikidata identifier, e.g. Q42" ) ,
ValidatedTextField . tp (
"int" ,
"A number" ,
( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) )
2021-05-11 02:39:51 +02:00
} ,
undefined ,
undefined ,
"numeric" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"nat" ,
"A positive number or zero" ,
( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) >= 0
2021-05-11 02:39:51 +02:00
} ,
undefined ,
undefined ,
"numeric" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"pnat" ,
"A strict positive number" ,
( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) > 0
2021-05-11 02:39:51 +02:00
} ,
undefined ,
undefined ,
"numeric" ) ,
2020-11-15 03:10:44 +01:00
ValidatedTextField . tp (
"direction" ,
2020-11-15 16:08:17 +01:00
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)" ,
2020-11-15 03:10:44 +01:00
( str ) = > {
str = "" + str ;
2020-11-15 16:54:41 +01:00
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) >= 0 && Number ( str ) <= 360
2021-05-11 02:39:51 +02:00
} , str = > str ,
2020-11-15 03:10:44 +01:00
( value ) = > {
2021-05-11 02:39:51 +02:00
return new DirectionInput ( value ) ;
} ,
"numeric"
2020-11-15 03:10:44 +01:00
) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"float" ,
"A decimal" ,
2021-05-11 02:39:51 +02:00
( str ) = > ! isNaN ( Number ( str ) ) ,
undefined ,
undefined ,
"decimal" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"pfloat" ,
"A positive decimal (incl zero)" ,
2021-05-11 02:39:51 +02:00
( str ) = > ! isNaN ( Number ( str ) ) && Number ( str ) >= 0 ,
undefined ,
undefined ,
"decimal" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"email" ,
"An email adress" ,
2021-05-11 02:39:51 +02:00
( str ) = > EmailValidator . validate ( str ) ,
undefined ,
undefined ,
"email" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"url" ,
2020-09-26 03:02:19 +02:00
"A url" ,
( str ) = > {
try {
new URL ( str ) ;
return true ;
} catch ( e ) {
return false ;
}
2021-05-11 02:39:51 +02:00
} ,
( str ) = > {
2020-09-26 03:02:19 +02:00
try {
const url = new URL ( str ) ;
const blacklistedTrackingParams = [
"fbclid" , // Oh god, how I hate the fbclid. Let it burn, burn in hell!
"gclid" ,
2021-05-11 02:39:51 +02:00
"cmpid" , "agid" , "utm" , "utm_source" , "utm_medium" ]
2020-09-26 03:02:19 +02:00
for ( const dontLike of blacklistedTrackingParams ) {
url . searchParams . delete ( dontLike )
}
2021-05-10 23:49:46 +02:00
let cleaned = url . toString ( ) ;
2021-05-11 02:39:51 +02:00
if ( cleaned . endsWith ( "/" ) && ! str . endsWith ( "/" ) ) {
2021-05-10 23:49:46 +02:00
// Do not add a trailing '/' if it wasn't typed originally
cleaned = cleaned . substr ( 0 , cleaned . length - 1 )
}
return cleaned ;
2020-09-26 03:02:19 +02:00
} catch ( e ) {
console . error ( e )
return undefined ;
}
2021-05-11 02:39:51 +02:00
} ,
undefined ,
"url" ) ,
2020-09-25 12:44:04 +02:00
ValidatedTextField . tp (
"phone" ,
"A phone number" ,
2020-12-05 03:22:17 +01:00
( str , country : ( ) = > string ) = > {
2020-09-26 01:43:20 +02:00
if ( str === undefined ) {
return false ;
}
2020-12-05 03:22:17 +01:00
return parsePhoneNumberFromString ( str , ( country ( ) ) ? . toUpperCase ( ) as any ) ? . isValid ( ) ? ? false
2020-09-25 12:44:04 +02:00
} ,
2021-05-11 02:39:51 +02:00
( str , country : ( ) = > string ) = > parsePhoneNumberFromString ( str , ( country ( ) ) ? . toUpperCase ( ) as any ) . formatInternational ( ) ,
undefined ,
"tel"
2020-10-04 12:55:44 +02:00
) ,
ValidatedTextField . tp (
"opening_hours" ,
"Has extra elements to easily input when a POI is opened" ,
2021-05-11 02:39:51 +02:00
( ) = > true ,
str = > str ,
2020-10-04 12:55:44 +02:00
( value ) = > {
2020-10-08 19:03:00 +02:00
return new OpeningHoursInput ( value ) ;
2020-10-04 12:55:44 +02:00
}
2021-05-11 02:39:51 +02:00
) ,
ValidatedTextField . tp (
"color" ,
"Shows a color picker" ,
( ) = > true ,
str = > str ,
( value ) = > {
return new ColorPicker ( value . map ( color = > {
return Utils . ColourNameToHex ( color ? ? "" ) ;
} , [ ] , str = > Utils . HexToColourName ( str ) ) )
}
2020-09-25 12:44:04 +02:00
)
]
2021-05-11 02:39:51 +02:00
/ * *
* { string ( typename ) -- > TextFieldDef }
* /
public static AllTypes = ValidatedTextField . allTypesDict ( ) ;
2020-09-25 12:44:04 +02:00
public static TypeDropdown ( ) : DropDown < string > {
const values : { value : string , shown : string } [ ] = [ ] ;
const expl = ValidatedTextField . tpList ;
for ( const key in expl ) {
2020-10-10 13:44:10 +02:00
values . push ( { value : expl [ key ] . name , shown : ` ${ expl [ key ] . name } - ${ expl [ key ] . explanation } ` } )
2020-09-25 12:44:04 +02:00
}
return new DropDown < string > ( "" , values )
}
2020-09-26 03:02:19 +02:00
public static InputForType ( type : string , options ? : {
placeholder? : string | UIElement ,
value? : UIEventSource < string > ,
2021-05-11 02:39:51 +02:00
htmlType? : string ,
textArea? :boolean ,
inputMode? :string ,
2020-09-26 03:02:19 +02:00
textAreaRows? : number ,
2020-12-05 03:22:17 +01:00
isValid ? : ( ( s : string , country : ( ) = > string ) = > boolean ) ,
country ? : ( ) = > string ,
2020-11-17 02:22:48 +01:00
location ? : [ number /*lat*/ , number /*lon*/ ]
2020-09-26 03:02:19 +02:00
} ) : InputElement < string > {
options = options ? ? { } ;
options . placeholder = options . placeholder ? ? type ;
const tp : TextFieldDef = ValidatedTextField . AllTypes [ type ]
2020-09-26 21:00:03 +02:00
const isValidTp = tp . isValid ;
let isValid ;
2020-10-27 01:01:34 +01:00
options . textArea = options . textArea ? ? type === "text" ;
2020-09-26 03:02:19 +02:00
if ( options . isValid ) {
const optValid = options . isValid ;
isValid = ( str , country ) = > {
2021-05-11 02:39:51 +02:00
if ( str === undefined ) {
2020-09-26 21:00:03 +02:00
return false ;
}
return isValidTp ( str , country ? ? options . country ) && optValid ( str , country ? ? options . country ) ;
2020-09-26 03:02:19 +02:00
}
2021-05-11 02:39:51 +02:00
} else {
2020-09-26 21:00:03 +02:00
isValid = isValidTp ;
2020-09-26 03:02:19 +02:00
}
options . isValid = isValid ;
2021-05-11 02:39:51 +02:00
options . inputMode = tp . inputmode ;
2020-09-26 03:02:19 +02:00
let input : InputElement < string > = new TextField ( options ) ;
2020-09-26 21:00:03 +02:00
if ( tp . reformat ) {
input . GetValue ( ) . addCallbackAndRun ( str = > {
if ( ! options . isValid ( str , options . country ) ) {
return ;
}
const formatted = tp . reformat ( str , options . country ) ;
input . GetValue ( ) . setData ( formatted ) ;
} )
}
2020-09-26 03:02:19 +02:00
if ( tp . inputHelper ) {
2021-05-11 02:39:51 +02:00
input = new CombinedInputElement ( input , tp . inputHelper ( input . GetValue ( ) , {
2020-11-17 02:22:48 +01:00
location : options.location
} ) ) ;
2020-09-26 03:02:19 +02:00
}
return input ;
2020-09-25 12:44:04 +02:00
}
public static NumberInput ( type : string = "int" , extraValidation : ( number : Number ) = > boolean = undefined ) : InputElement < number > {
const isValid = ValidatedTextField . AllTypes [ type ] . isValid ;
extraValidation = extraValidation ? ? ( ( ) = > true )
const fromString = str = > {
if ( ! isValid ( str ) ) {
return undefined ;
}
const n = Number ( str ) ;
if ( ! extraValidation ( n ) ) {
return undefined ;
}
return n ;
} ;
const toString = num = > {
if ( num === undefined ) {
return undefined ;
}
return "" + num ;
} ;
const textField = ValidatedTextField . InputForType ( type ) ;
return new InputElementMap ( textField , ( n0 , n1 ) = > n0 === n1 , fromString , toString )
}
2021-05-11 02:39:51 +02:00
2020-09-25 12:44:04 +02:00
public static KeyInput ( allowEmpty : boolean = false ) : InputElement < string > {
function fromString ( str ) {
if ( str ? . match ( /^[a-zA-Z][a-zA-Z0-9:_-]*$/ ) ) {
return str ;
}
if ( str === "" && allowEmpty ) {
return "" ;
}
return undefined
}
const toString = str = > str
function isSame ( str0 , str1 ) {
return str0 === str1 ;
}
const textfield = new TextField ( {
placeholder : "key" ,
isValid : str = > fromString ( str ) !== undefined ,
value : new UIEventSource < string > ( "" )
} ) ;
return new InputElementMap ( textfield , isSame , fromString , toString ) ;
}
static Mapped < T > ( fromString : ( str ) = > T , toString : ( T ) = > string , options ? : {
placeholder? : string | UIElement ,
2020-09-26 03:02:19 +02:00
type ? : string ,
2020-09-25 12:44:04 +02:00
value? : UIEventSource < string > ,
startValidated? : boolean ,
textArea? : boolean ,
textAreaRows? : number ,
2020-09-26 21:00:03 +02:00
isValid ? : ( ( string : string ) = > boolean ) ,
2020-12-05 03:22:17 +01:00
country ? : ( ) = > string
2020-09-25 12:44:04 +02:00
} ) : InputElement < T > {
2020-09-26 03:02:19 +02:00
let textField : InputElement < string > ;
2020-09-27 20:51:37 +02:00
if ( options ? . type ) {
2020-09-26 21:00:03 +02:00
textField = ValidatedTextField . InputForType ( options . type , options ) ;
2020-09-26 03:02:19 +02:00
} else {
textField = new TextField ( options ) ;
}
2020-09-25 12:44:04 +02:00
return new InputElementMap (
textField , ( a , b ) = > a === b ,
fromString , toString
) ;
}
2021-05-11 02:39:51 +02:00
public static HelpText ( ) : string {
const explanations = ValidatedTextField . tpList . map ( type = > [ "## " + type . name , "" , type . explanation ] . join ( "\n" ) ) . join ( "\n\n" )
return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations
}
private static tp ( name : string ,
explanation : string ,
isValid ? : ( ( s : string , country ? : ( ) = > string ) = > boolean ) ,
reformat ? : ( ( s : string , country ? : ( ) = > string ) = > string ) ,
inputHelper ? : ( value : UIEventSource < string > , options ? : {
location : [ number , number ]
} ) = > InputElement < string > ,
inputmode? : string ) : TextFieldDef {
if ( isValid === undefined ) {
isValid = ( ) = > true ;
}
if ( reformat === undefined ) {
reformat = ( str , _ ) = > str ;
}
return {
name : name ,
explanation : explanation ,
isValid : isValid ,
reformat : reformat ,
inputHelper : inputHelper ,
inputmode : inputmode
}
}
private static allTypesDict() {
const types = { } ;
for ( const tp of ValidatedTextField . tpList ) {
types [ tp . name ] = tp ;
}
return types ;
}
2020-09-25 12:44:04 +02:00
}