2020-07-20 15:54:50 +02:00
import { InputElement } from "./InputElement"
2022-06-30 03:07:54 +02:00
import { Store , UIEventSource } from "../../Logic/UIEventSource"
2021-06-10 01:36:20 +02:00
import BaseUIElement from "../BaseUIElement"
2022-06-30 03:07:54 +02:00
import { Translation } from "../i18n/Translation"
import Locale from "../i18n/Locale"
interface TextFieldOptions {
placeholder? : string | Store < string > | Translation
value? : UIEventSource < string >
htmlType ? : "area" | "text" | "time" | string
inputMode? : string
label? : BaseUIElement
textAreaRows? : number
inputStyle? : string
isValid ? : ( s : string ) = > boolean
}
2020-08-22 02:12:46 +02:00
2020-09-25 12:44:04 +02:00
export class TextField extends InputElement < string > {
2020-08-30 01:13:18 +02:00
public readonly enterPressed = new UIEventSource < string > ( undefined )
2021-06-12 02:58:32 +02:00
private readonly value : UIEventSource < string >
2022-04-28 02:04:25 +02:00
private _actualField : HTMLElement
2022-03-02 15:59:06 +01:00
private readonly _isValid : ( s : string ) = > boolean
2022-06-30 03:07:54 +02:00
private readonly _rawValue : UIEventSource < string >
2022-04-28 02:04:25 +02:00
private _isFocused = false
2022-06-30 03:07:54 +02:00
private readonly _options : TextFieldOptions
2022-09-08 21:40:48 +02:00
2022-06-30 03:07:54 +02:00
constructor ( options? : TextFieldOptions ) {
2021-06-10 01:36:20 +02:00
super ( )
2022-06-30 03:07:54 +02:00
this . _options = options ? ? { }
2020-09-25 12:44:04 +02:00
options = options ? ? { }
this . value = options ? . value ? ? new UIEventSource < string > ( undefined )
2022-02-12 02:53:41 +01:00
this . _rawValue = new UIEventSource < string > ( "" )
2021-06-10 01:36:20 +02:00
this . _isValid = options . isValid ? ? ( ( _ ) = > true )
2022-06-30 03:07:54 +02:00
}
private static SetCursorPosition ( textfield : HTMLElement , i : number ) {
if ( textfield === undefined || textfield === null ) {
return
}
if ( i === - 1 ) {
// @ts-ignore
i = textfield . value . length
}
textfield . focus ( )
// @ts-ignore
textfield . setSelectionRange ( i , i )
}
GetValue ( ) : UIEventSource < string > {
return this . value
}
2022-09-08 21:40:48 +02:00
2022-06-30 03:07:54 +02:00
GetRawValue ( ) : UIEventSource < string > {
return this . _rawValue
}
2022-09-08 21:40:48 +02:00
2022-06-30 03:07:54 +02:00
IsValid ( t : string ) : boolean {
if ( t === undefined || t === null ) {
return false
}
return this . _isValid ( t )
}
/ * *
*
* // should update placeholders dynamically
* const placeholder = new UIEventSource < string > ( "placeholder" )
* const tf = new TextField ( {
* placeholder
* } )
* const html = < HTMLInputElement > tf . InnerConstructElement ( ) . children [ 0 ] ;
* html . placeholder // => 'placeholder'
* placeholder . setData ( "another piece of text" )
* html . placeholder // => "another piece of text"
*
* // should update translated placeholders dynamically
* const placeholder = new Translation ( { nl : "Nederlands" , en : "English" } )
* Locale . language . setData ( "nl" ) ;
* const tf = new TextField ( {
* placeholder
* } )
* const html = < HTMLInputElement > tf . InnerConstructElement ( ) . children [ 0 ] ;
* html . placeholder // => "Nederlands"
* Locale . language . setData ( "en" ) ;
* html . placeholder // => 'English'
* /
protected InnerConstructElement ( ) : HTMLElement {
const options = this . _options
const self = this
let placeholderStore : Store < string >
let placeholder : string = ""
if ( options . placeholder ) {
if ( typeof options . placeholder === "string" ) {
placeholder = options . placeholder
placeholderStore = undefined
} else {
if (
options . placeholder instanceof Store &&
options . placeholder [ "data" ] !== undefined
) {
placeholderStore = options . placeholder
} else if (
options . placeholder instanceof Translation &&
options . placeholder [ "translations" ] !== undefined
) {
placeholderStore = < Store < string > > (
Locale . language . map ( ( l ) = > ( < Translation > options . placeholder ) . textFor ( l ) )
2022-09-08 21:40:48 +02:00
)
2022-06-30 03:07:54 +02:00
}
placeholder = placeholderStore ? . data ? ? placeholder ? ? ""
}
}
2021-06-10 01:36:20 +02:00
this . SetClass ( "form-text-field" )
2021-06-12 02:58:32 +02:00
let inputEl : HTMLElement
if ( options . htmlType === "area" ) {
2021-07-11 15:44:17 +02:00
this . SetClass ( "w-full box-border max-w-full" )
2021-06-10 01:36:20 +02:00
const el = document . createElement ( "textarea" )
el . placeholder = placeholder
el . rows = options . textAreaRows
el . cols = 50
2021-08-20 00:15:55 +02:00
el . style . width = "100%"
2022-11-14 00:45:08 +01:00
el . dir = "auto"
2021-06-10 01:36:20 +02:00
inputEl = el
2022-06-30 03:07:54 +02:00
if ( placeholderStore ) {
placeholderStore . addCallbackAndRunD ( ( placeholder ) = > ( el . placeholder = placeholder ) )
}
2021-06-12 02:58:32 +02:00
} else {
2021-06-10 01:36:20 +02:00
const el = document . createElement ( "input" )
2021-06-12 02:58:32 +02:00
el . type = options . htmlType ? ? "text"
el . inputMode = options . inputMode
2021-06-10 01:36:20 +02:00
el . placeholder = placeholder
2022-04-24 01:32:19 +02:00
el . style . cssText = options . inputStyle ? ? "width: 100%;"
2022-11-14 00:45:08 +01:00
el . dir = "auto"
2021-06-10 01:36:20 +02:00
inputEl = el
2022-06-30 03:07:54 +02:00
if ( placeholderStore ) {
placeholderStore . addCallbackAndRunD ( ( placeholder ) = > ( el . placeholder = placeholder ) )
}
2021-06-10 01:36:20 +02:00
}
const form = document . createElement ( "form" )
2021-06-12 02:58:32 +02:00
form . appendChild ( inputEl )
2021-06-10 01:36:20 +02:00
form . onsubmit = ( ) = > false
2021-06-12 02:58:32 +02:00
if ( options . label ) {
form . appendChild ( options . label . ConstructElement ( ) )
}
2021-06-10 01:36:20 +02:00
const field = inputEl
2021-06-30 15:38:14 +02:00
this . value . addCallbackAndRunD ( ( value ) = > {
2021-09-09 00:05:51 +02:00
// We leave the textfield as is in the case of undefined or null (handled by addCallbackAndRunD) - make sure we do not erase it!
2021-06-16 21:23:03 +02:00
field [ "value" ] = value
2021-06-12 02:58:32 +02:00
if ( self . IsValid ( value ) ) {
2021-06-10 01:36:20 +02:00
self . RemoveClass ( "invalid" )
2021-06-12 02:58:32 +02:00
} else {
2021-06-10 01:36:20 +02:00
self . SetClass ( "invalid" )
}
} )
2020-09-25 12:44:04 +02:00
2020-07-01 02:12:33 +02:00
field . oninput = ( ) = > {
2020-09-26 21:00:03 +02:00
// How much characters are on the right, not including spaces?
2020-09-26 01:10:10 +02:00
// @ts-ignore
2021-06-12 02:58:32 +02:00
const endDistance = field . value . substring ( field . selectionEnd ) . replace ( / /g , "" ) . length
2020-07-05 18:59:47 +02:00
// @ts-ignore
2020-09-26 21:00:03 +02:00
let val : string = field . value
2022-02-12 02:53:41 +01:00
self . _rawValue . setData ( val )
2020-09-26 03:02:19 +02:00
if ( ! self . IsValid ( val ) ) {
self . value . setData ( undefined )
} else {
self . value . setData ( val )
}
2020-09-26 01:10:10 +02:00
// Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change
// See https://github.com/pietervdvn/MapComplete/issues/103
2020-09-26 03:02:19 +02:00
// We reread the field value - it might have changed!
2021-06-10 01:36:20 +02:00
2020-09-26 03:02:19 +02:00
// @ts-ignore
2020-09-26 21:00:03 +02:00
val = field . value
2020-09-27 00:20:48 +02:00
let newCursorPos = val . length - endDistance
2021-06-12 02:58:32 +02:00
while (
newCursorPos >= 0 &&
2020-09-26 21:00:03 +02:00
// We count the number of _actual_ characters (non-space characters) on the right of the new value
// This count should become bigger then the end distance
2020-09-27 00:20:48 +02:00
val . substr ( newCursorPos ) . replace ( / /g , "" ) . length < endDistance
2021-06-12 02:58:32 +02:00
) {
newCursorPos --
2020-09-26 21:00:03 +02:00
}
2021-06-15 16:18:58 +02:00
TextField . SetCursorPosition ( field , newCursorPos )
2020-07-01 02:12:33 +02:00
}
2021-06-12 02:58:32 +02:00
2020-07-01 02:12:33 +02:00
field . addEventListener ( "keyup" , function ( event ) {
if ( event . key === "Enter" ) {
2020-07-05 18:59:47 +02:00
// @ts-ignore
2020-07-01 02:12:33 +02:00
self . enterPressed . setData ( field . value )
}
2021-06-12 02:58:32 +02:00
} )
2022-06-30 03:07:54 +02:00
2022-04-28 02:04:25 +02:00
if ( this . _isFocused ) {
field . focus ( )
}
2020-07-20 15:54:50 +02:00
2022-06-30 03:07:54 +02:00
this . _actualField = field
return form
2021-06-12 02:58:32 +02:00
}
2022-04-28 02:04:25 +02:00
public focus() {
if ( this . _actualField === undefined ) {
this . _isFocused = true
} else {
this . _actualField . focus ( )
}
}
2020-09-27 00:20:48 +02:00
}