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" ;
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" ;
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" ;
class SimpleTextFieldDef {
public readonly name : string ;
/ *
* An explanation for the theme builder .
* This can indicate which special input element is used , . . .
* * /
public readonly explanation : string ;
public inputmode? : string = undefined
constructor ( explanation : string | BaseUIElement , name? : string ) {
this . name = name ? ? this . constructor . name . toLowerCase ( ) ;
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 ( ) ;
}
}
public reformat ( s : string , country ? : ( ) = > string ) : string {
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-11 20:56:54 +01:00
public postprocess ( s : string ) : string {
return s
}
public undoPostprocess ( s : string ) : string {
return s ;
}
public 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
}
isValid ( s : string , country : ( ( ) = > string ) | undefined ) : boolean {
return true ;
}
getFeedback ( s : string ) : Translation {
return undefined
}
2020-09-26 03:02:19 +02:00
}
2020-09-25 12:44:04 +02:00
2022-02-11 20:56:54 +01:00
class WikidataTextField extends SimpleTextFieldDef {
constructor ( ) {
super ( new Combine ( [
2021-10-29 03:42:33 +02:00
"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" ] ,
[ "removePostfixes" , "remove these snippets of text from the end of the passed string to search" ] ,
]
) ] )
] ] ) ,
new Title ( "Example usage" ) ,
2021-10-29 03:59:28 +02:00
` 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
\ ` \` \`
"freeform" : {
2021-10-29 13:35:33 +02:00
"key" : "name:etymology:wikidata" ,
"type" : "wikidata" ,
"helperArgs" : [
"name" ,
{
"removePostfixes" : [
"street" ,
"boulevard" ,
"path" ,
"square" ,
"plaza" ,
]
}
]
}
\ ` \` \` `
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"
let searchFor = < string > inputHelperOptions . feature ? . properties [ searchKey ] ? . toLowerCase ( )
const options = args [ 1 ]
if ( searchFor !== undefined && options !== undefined ) {
const prefixes = < string [ ] > options [ "removePrefixes" ]
const postfixes = < string [ ] > options [ "removePostfixes" ]
for ( const postfix of postfixes ? ? [ ] ) {
if ( searchFor . endsWith ( postfix ) ) {
searchFor = searchFor . substring ( 0 , searchFor . length - postfix . length )
break ;
}
}
for ( const prefix of prefixes ? ? [ ] ) {
if ( searchFor . startsWith ( prefix ) ) {
searchFor = searchFor . substring ( prefix . length )
break ;
}
}
}
return new WikidataSearchBox ( {
value : currentValue ,
searchText : new UIEventSource < string > ( searchFor )
} )
}
}
2022-02-11 20:56:54 +01:00
class OpeningHoursTextField extends SimpleTextFieldDef {
constructor ( ) {
super ( 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" + `
2021-10-29 13:35:33 +02:00
"freeform" : {
"key" : "access:conditional" ,
"type" : "opening_hours" ,
"helperArgs" : [
{
"prefix" : "no @ (" ,
"postfix" : ")"
}
]
2022-02-11 20:56:54 +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 ) ` " ] ) ,
"opening_hours" ) ;
}
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-11 20:56:54 +01:00
class UrlTextfieldDef extends SimpleTextFieldDef {
2022-01-18 02:26:21 +01:00
inputmode : "url"
2022-02-11 20:56:54 +01:00
constructor ( ) {
super ( "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-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
str = str . toLowerCase ( )
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-11 20:56:54 +01:00
class StringTextField extends SimpleTextFieldDef {
constructor ( ) {
super ( "A simple piece of text" ) ;
}
}
2020-09-25 12:44:04 +02:00
2022-02-11 20:56:54 +01:00
class TextTextField extends SimpleTextFieldDef {
inputmode : "text"
2021-09-09 00:05:51 +02:00
2022-02-11 20:56:54 +01:00
constructor ( ) {
super ( "A longer piece of text" ) ;
}
}
2021-09-09 00:05:51 +02:00
2022-02-11 20:56:54 +01:00
class DateTextField extends SimpleTextFieldDef {
constructor ( ) {
super ( "A date with date picker" ) ;
}
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 )
}
}
class DirectionTextField extends SimpleTextFieldDef {
inputMode = "numeric"
constructor ( ) {
super ( "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)" ) ;
}
isValid = ( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) >= 0 && Number ( str ) <= 360
}
inputHelper = ( value , options ) = > {
const args = options . args ? ? [ ]
let zoom = 19
if ( args [ 0 ] ) {
zoom = Number ( args [ 0 ] )
if ( isNaN ( zoom ) ) {
throw "Invalid zoom level for argument at 'length'-input"
2021-05-11 02:39:51 +02:00
}
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 ( "," ) )
)
}
const di = new DirectionInput ( options . mapBackgroundLayer , location , value )
di . SetStyle ( "max-width: 25rem;" ) ;
return di ;
}
}
class LengthTextField extends SimpleTextFieldDef {
inputMode : "decimal"
constructor ( ) {
super (
"A geographical length 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 )
}
inputHelper = ( value , options ) = > {
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
if ( options . feature !== undefined && options . feature . geometry . type !== "Point" ) {
const lonlat = < [ number , number ] > [ . . . options . location ]
lonlat . reverse ( )
options . location = < [ number , number ] > GeoOperations . nearestPoint ( options . feature , lonlat ) . geometry . coordinates
options . location . reverse ( )
}
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 li = new LengthInput ( options . mapBackgroundLayer , location , value )
li . SetStyle ( "height: 20rem;" )
return li ;
}
}
class IntTextField extends SimpleTextFieldDef {
inputMode = "numeric"
constructor ( ) {
super ( "A number" ) ;
}
isValid = ( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) )
}
reformat = str = > "" + Number ( str )
}
class NatTextField extends SimpleTextFieldDef {
inputMode = "numeric"
constructor ( ) {
super ( "A positive number or zero" ) ;
}
isValid = ( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) >= 0
}
reformat = str = > "" + Number ( str )
}
class PNatTextField extends SimpleTextFieldDef {
inputmode = "numeric"
constructor ( ) {
super ( "A strict positive number" ) ;
}
isValid = ( str ) = > {
str = "" + str ;
return str !== undefined && str . indexOf ( "." ) < 0 && ! isNaN ( Number ( str ) ) && Number ( str ) > 0
}
reformat = str = > "" + Number ( str )
}
class FloatTextField extends SimpleTextFieldDef {
inputmode = "decimal"
constructor ( ) {
super ( "A decimal" ) ;
}
isValid = ( str ) = > ! isNaN ( Number ( str ) ) && ! str . endsWith ( "." ) && ! str . endsWith ( "," )
reformat = str = > "" + Number ( str )
}
class PFloatTextField extends SimpleTextFieldDef {
inputmode = "decimal"
constructor ( ) {
super ( "A positive decimal (inclusive zero)" ) ;
}
isValid = ( str ) = > ! isNaN ( Number ( str ) ) && Number ( str ) >= 0 && ! str . endsWith ( "." ) && ! str . endsWith ( "," )
reformat = str = > "" + Number ( str )
}
class EmailTextField extends SimpleTextFieldDef {
inputmode = "email"
constructor ( ) {
super ( "An email adress" ) ;
}
isValid = ( str ) = > {
if ( str . startsWith ( "mailto:" ) ) {
str = str . substring ( "mailto:" . length )
}
return EmailValidator . validate ( str ) ;
}
reformat = str = > {
if ( str === undefined ) {
return undefined
}
if ( str . startsWith ( "mailto:" ) ) {
str = str . substring ( "mailto:" . length )
}
return str ;
}
}
class PhoneTextField extends SimpleTextFieldDef {
inputmode = "tel"
constructor ( ) {
super ( "A phone number" ) ;
}
isValid = ( str , country : ( ) = > string ) = > {
if ( str === undefined ) {
return false ;
}
if ( str . startsWith ( "tel:" ) ) {
str = str . substring ( "tel:" . length )
}
return parsePhoneNumberFromString ( str , ( country ( ) ) ? . toUpperCase ( ) as any ) ? . isValid ( ) ? ? false
}
reformat = ( str , country : ( ) = > string ) = > {
if ( str . startsWith ( "tel:" ) ) {
str = str . substring ( "tel:" . length )
}
return parsePhoneNumberFromString ( str , ( country ( ) ) ? . toUpperCase ( ) as any ) . formatInternational ( ) ;
}
}
class ColorTextField extends SimpleTextFieldDef {
constructor ( ) {
super ( "Shows a color picker" ) ;
}
inputHelper = ( value ) = > {
return new ColorPicker ( value . map ( color = > {
return Utils . ColourNameToHex ( color ? ? "" ) ;
} , [ ] , str = > Utils . HexToColourName ( str ) ) )
}
}
export default class ValidatedTextField {
private static allTextfieldDefs : SimpleTextFieldDef [ ] = [
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-11 20:56:54 +01:00
public static AllTypes : Map < string , SimpleTextFieldDef > = ValidatedTextField . allTypesDict ( ) ;
2021-07-20 01:33:58 +02:00
2020-09-26 03:02:19 +02:00
public static InputForType ( type : string , options ? : {
2021-06-28 00:45:49 +02:00
placeholder? : string | BaseUIElement ,
2020-09-26 03:02:19 +02:00
value? : UIEventSource < string > ,
2021-05-11 02:39:51 +02:00
htmlType? : string ,
2021-06-16 17:09:32 +02:00
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 ,
2021-06-23 02:15:28 +02:00
location ? : [ number /*lat*/ , number /*lon*/ ] ,
2021-06-25 20:38:12 +02:00
mapBackgroundLayer? : UIEventSource < any > ,
2021-07-20 01:33:58 +02:00
unit? : Unit ,
args ? : ( string | number | boolean ) [ ] // Extra arguments for the inputHelper,
2022-01-21 01:57:16 +01:00
feature? : any ,
inputStyle? : string
2020-09-26 03:02:19 +02:00
} ) : InputElement < string > {
options = options ? ? { } ;
2022-02-11 20:56:54 +01:00
if ( options . placeholder === undefined ) {
options . placeholder = Translations . t . validation [ type ] ? . description ? ? type
2022-02-11 04:28:11 +01:00
}
2022-02-11 20:56:54 +01:00
const tp : SimpleTextFieldDef = ValidatedTextField . AllTypes . get ( 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 ;
}
2021-07-20 01:33:58 +02:00
if ( options . unit ) {
2021-06-25 20:38:12 +02:00
str = options . unit . stripUnitParts ( str )
}
2020-09-26 21:00:03 +02:00
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
}
2021-09-13 01:17:48 +02:00
if ( options . unit !== undefined && isValid !== undefined ) {
// Reformatting is handled by the unit in this case
options . isValid = str = > {
const denom = options . unit . findDenomination ( str ) ;
if ( denom === undefined ) {
return false ;
}
const stripped = denom [ 0 ]
console . log ( "Is valid? " , str , "stripped: " , stripped , "isValid:" , isValid ( stripped ) )
return isValid ( stripped )
}
} else {
options . isValid = isValid ;
}
2021-05-11 02:39:51 +02:00
options . inputMode = tp . inputmode ;
2022-01-18 02:26:21 +01:00
if ( tp . inputmode === "text" ) {
2021-11-07 18:37:42 +01:00
options . htmlType = "area"
}
2021-09-13 01:17:48 +02:00
2020-09-26 03:02:19 +02:00
let input : InputElement < string > = new TextField ( options ) ;
2021-09-13 01:17:48 +02:00
if ( tp . reformat && options . unit === undefined ) {
2020-09-26 21:00:03 +02:00
input . GetValue ( ) . addCallbackAndRun ( str = > {
if ( ! options . isValid ( str , options . country ) ) {
return ;
}
const formatted = tp . reformat ( str , options . country ) ;
input . GetValue ( ) . setData ( formatted ) ;
} )
}
2021-07-20 01:33:58 +02:00
if ( options . unit ) {
2021-06-25 20:38:12 +02:00
// 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
2021-10-08 04:33:39 +02:00
2021-09-13 02:38:20 +02:00
const isSingular = input . GetValue ( ) . map ( str = > str ? . trim ( ) === "1" )
2021-09-13 01:17:48 +02:00
const unitDropDown =
unit . denominations . length === 1 ?
2021-10-08 04:33:39 +02:00
new FixedInputElement ( unit . denominations [ 0 ] . getToggledHuman ( isSingular ) , unit . denominations [ 0 ] )
2021-09-13 01:17:48 +02:00
: new DropDown ( "" ,
unit . denominations . map ( denom = > {
return {
2021-09-13 02:38:20 +02:00
shown : denom.getToggledHuman ( isSingular ) ,
2021-09-13 01:17:48 +02:00
value : denom
}
} )
)
2021-06-25 20:38:12 +02:00
unitDropDown . GetValue ( ) . setData ( unit . defaultDenom )
2021-07-11 15:44:17 +02:00
unitDropDown . SetClass ( "w-min" )
2021-10-08 04:33:39 +02:00
const fixedDenom = unit . denominations . length === 1 ? unit . denominations [ 0 ] : undefined
2021-06-25 20:38:12 +02:00
input = new CombinedInputElement (
input ,
unitDropDown ,
2021-07-04 20:36:19 +02:00
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
2021-09-13 01:17:48 +02:00
( text , denom ) = > {
2021-10-08 04:33:39 +02:00
if ( denom === undefined ) {
2021-09-13 01:17:48 +02:00
return text
}
2021-10-08 04:33:39 +02:00
return denom ? . canonicalValue ( text , true )
2021-09-13 01:17:48 +02:00
} ,
2021-06-25 20:38:12 +02:00
( valueWithDenom : string ) = > {
2021-07-04 20:36:19 +02:00
// Take the value from OSM and feed it into the textfield and the dropdown
const withDenom = unit . findDenomination ( valueWithDenom ) ;
2021-07-20 01:33:58 +02:00
if ( withDenom === undefined ) {
2021-09-13 01:17:48 +02:00
// 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 ]
2021-06-25 20:38:12 +02:00
}
2021-07-04 20:36:19 +02:00
const [ strippedText , denom ] = withDenom
2021-07-20 01:33:58 +02:00
if ( strippedText === undefined ) {
2021-09-13 01:17:48 +02:00
return [ undefined , fixedDenom ]
2021-07-04 20:36:19 +02:00
}
return [ strippedText , denom ]
2021-06-25 20:38:12 +02:00
}
) . SetClass ( "flex" )
}
2022-02-11 20:56:54 +01:00
const helper = tp . inputHelper ( input . GetValue ( ) , {
location : options.location ,
mapBackgroundLayer : options.mapBackgroundLayer ,
args : options.args ,
feature : options.feature
} ) ? . SetClass ( "block" )
if ( helper !== undefined ) {
2021-06-24 02:33:26 +02:00
input = new CombinedInputElement ( input , helper ,
2021-06-24 01:55:45 +02:00
( a , _ ) = > a , // We can ignore b, as they are linked earlier
2021-06-22 03:16:45 +02:00
a = > [ a , a ]
2021-09-13 02:45:51 +02:00
) . SetClass ( "block w-full" ) ;
2020-09-26 03:02:19 +02:00
}
2022-01-18 02:26:21 +01:00
if ( tp . postprocess !== undefined ) {
input = new InputElementMap < string , string > ( input ,
( a , b ) = > a === b ,
tp . postprocess ,
tp . undoPostprocess
)
}
2020-09-26 03:02:19 +02:00
return input ;
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-01-18 02:26:21 +01:00
const explanations : BaseUIElement [ ] =
2022-02-11 20:56:54 +01:00
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" ) )
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 [ ] {
return ValidatedTextField . allTextfieldDefs . map ( tp = > tp . name )
2021-05-11 02:39:51 +02:00
}
2022-02-11 20:56:54 +01:00
private static allTypesDict ( ) : Map < string , SimpleTextFieldDef > {
const types = new Map < string , SimpleTextFieldDef > ( ) ;
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
}