@@ -4,6 +4,8 @@ import { format as dateFormat } from "date-fns/format";
44import * as Loader from "../elements/loader" ;
55import * as Notifications from "../elements/notifications" ;
66import * as ConnectionState from "../states/connection" ;
7+ import { InputIndicator } from "../elements/input-indicator" ;
8+ import { debounce } from "throttle-debounce" ;
79
810type CommonInput < TType , TValue > = {
911 type : TType ;
@@ -14,6 +16,25 @@ type CommonInput<TType, TValue> = {
1416 optional ?: boolean ;
1517 label ?: string ;
1618 oninput ?: ( event : Event ) => void ;
19+ /**
20+ * Validate the input value and indicate the validation result next to the input.
21+ * If the schema is defined it is always checked first.
22+ * Only if the schema validaton is passed or missing the `isValid` method is called.
23+ */
24+ validation ?: {
25+ /**
26+ * Zod schema to validate the input value against.
27+ * The indicator will show the error messages from the schema.
28+ */
29+ schema ?: Zod . Schema < TValue > ;
30+ /**
31+ * Custom async validation method.
32+ * This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
33+ * @param value current input value
34+ * @returns true if the `value` is valid, an errorMessage as string if it is invalid.
35+ */
36+ isValid ?: ( value : string ) => Promise < true | string > ;
37+ } ;
1738} ;
1839
1940export type TextInput = CommonInput < "text" , string > ;
@@ -67,6 +88,9 @@ export type ExecReturn = {
6788 afterHide ?: ( ) => void ;
6889} ;
6990
91+ type CommonInputTypeWithIndicator = CommonInputType & {
92+ indicator ?: InputIndicator ;
93+ } ;
7094type SimpleModalOptions = {
7195 id : string ;
7296 title : string ;
@@ -90,7 +114,7 @@ export class SimpleModal {
90114 modal : AnimatedModal ;
91115 id : string ;
92116 title : string ;
93- inputs : CommonInputType [ ] ;
117+ inputs : CommonInputTypeWithIndicator [ ] ;
94118 text ?: string ;
95119 textAllowHtml : boolean ;
96120 buttonText : string ;
@@ -286,10 +310,77 @@ export class SimpleModal {
286310 }
287311 inputs . append ( buildTag ( { tagname, classes, attributes } ) ) ;
288312 }
313+ const element = document . querySelector (
314+ "#" + attributes [ "id" ]
315+ ) as HTMLInputElement ;
289316 if ( input . oninput !== undefined ) {
290- (
291- document . querySelector ( "#" + attributes [ "id" ] ) as HTMLElement
292- ) . oninput = input . oninput ;
317+ element . oninput = input . oninput ;
318+ }
319+ if ( input . validation !== undefined ) {
320+ const indicator = new InputIndicator ( element , {
321+ valid : {
322+ icon : "fa-check" ,
323+ level : 1 ,
324+ } ,
325+ invalid : {
326+ icon : "fa-times" ,
327+ level : - 1 ,
328+ } ,
329+ checking : {
330+ icon : "fa-circle-notch" ,
331+ spinIcon : true ,
332+ level : 0 ,
333+ } ,
334+ } ) ;
335+ input . indicator = indicator ;
336+
337+ const debouceIsValid = debounce ( 1000 , async ( value : string ) => {
338+ const result = await input . validation ?. isValid ?.( value ) ;
339+
340+ if ( element . value !== value ) {
341+ //value of the input has changed in the meantime. discard
342+ return ;
343+ }
344+
345+ if ( result === true ) {
346+ indicator . show ( "valid" ) ;
347+ } else {
348+ indicator . show ( "invalid" , result ) ;
349+ }
350+ } ) ;
351+
352+ const validateInput = async ( value : string ) : Promise < void > => {
353+ if ( value === undefined || value === "" ) {
354+ indicator . hide ( ) ;
355+ return ;
356+ }
357+ if ( input . validation ?. schema !== undefined ) {
358+ const schemaResult = input . validation . schema . safeParse ( value ) ;
359+ if ( ! schemaResult . success ) {
360+ indicator . show (
361+ "invalid" ,
362+ schemaResult . error . errors . map ( ( err ) => err . message ) . join ( ", " )
363+ ) ;
364+ return ;
365+ }
366+ }
367+
368+ if ( input . validation ?. isValid !== undefined ) {
369+ indicator . show ( "checking" ) ;
370+ void debouceIsValid ( value ) ;
371+ return ;
372+ }
373+
374+ indicator . show ( "valid" ) ;
375+ } ;
376+
377+ element . oninput = async ( event ) => {
378+ const value = ( event . target as HTMLInputElement ) . value ;
379+ await validateInput ( value ) ;
380+
381+ //call original handler if defined
382+ input . oninput ?.( event ) ;
383+ } ;
293384 }
294385 } ) ;
295386
@@ -307,14 +398,14 @@ export class SimpleModal {
307398 }
308399 }
309400
310- type CommonInputWithCurrentValue = CommonInputType & {
401+ type CommonInputWithCurrentValue = CommonInputTypeWithIndicator & {
311402 currentValue : string | undefined ;
312403 } ;
313404
314405 const inputsWithCurrentValue : CommonInputWithCurrentValue [ ] = [ ] ;
315406 for ( let i = 0 ; i < this . inputs . length ; i ++ ) {
316407 inputsWithCurrentValue . push ( {
317- ...( this . inputs [ i ] as CommonInputType ) ,
408+ ...( this . inputs [ i ] as CommonInputTypeWithIndicator ) ,
318409 currentValue : vals [ i ] ,
319410 } ) ;
320411 }
@@ -328,6 +419,11 @@ export class SimpleModal {
328419 return ;
329420 }
330421
422+ if ( inputsWithCurrentValue . some ( ( i ) => i . indicator ?. get ( ) === "invalid" ) ) {
423+ Notifications . add ( "Please solve all validation errors" , 0 ) ;
424+ return ;
425+ }
426+
331427 this . disableInputs ( ) ;
332428 Loader . show ( ) ;
333429 void this . execFn ( this , ...vals ) . then ( ( res ) => {
0 commit comments