@@ -10,7 +10,7 @@ import * as OutOfFocus from "../test/out-of-focus";
1010import * as ActivePage from "../states/active-page" ;
1111import { focusWords } from "../test/test-ui" ;
1212import * as Loader from "../elements/loader" ;
13- import { Command , CommandsSubgroup } from "./types" ;
13+ import { Command , CommandsSubgroup , CommandWithValidation } from "./types" ;
1414import { areSortedArraysEqual } from "../utils/arrays" ;
1515import { parseIntOptional } from "../utils/numbers" ;
1616import { debounce } from "throttle-debounce" ;
@@ -21,6 +21,10 @@ type InputModeParams = {
2121 placeholder : string | null ;
2222 value : string | null ;
2323 icon : string | null ;
24+ validation ?: {
25+ status : "checking" | "success" | "failed" ;
26+ errorMessage ?: string ;
27+ } ;
2428} ;
2529
2630let activeIndex = 0 ;
@@ -175,6 +179,7 @@ function hide(clearModalChain = false): void {
175179 void modal . hide ( {
176180 clearModalChain,
177181 afterAnimation : async ( ) => {
182+ hideWarning ( ) ;
178183 addCommandlineBackground ( ) ;
179184 if ( ActivePage . get ( ) === "test" ) {
180185 const isWordsFocused = $ ( "#wordsInput" ) . is ( ":focus" ) ;
@@ -202,6 +207,7 @@ async function goBackOrHide(): Promise<void> {
202207 await filterSubgroup ( ) ;
203208 await showCommands ( ) ;
204209 await updateActiveCommand ( ) ;
210+ hideWarning ( ) ;
205211 return ;
206212 }
207213
@@ -212,6 +218,7 @@ async function goBackOrHide(): Promise<void> {
212218 await filterSubgroup ( ) ;
213219 await showCommands ( ) ;
214220 await updateActiveCommand ( ) ;
221+ hideWarning ( ) ;
215222 } else {
216223 hide ( ) ;
217224 }
@@ -562,10 +569,36 @@ function handleInputSubmit(): void {
562569 if ( inputModeParams . command === null ) {
563570 throw new Error ( "Can't handle input submit - command is null" ) ;
564571 }
565- inputModeParams . command . exec ?.( {
566- commandlineModal : modal ,
567- input : inputValue ,
568- } ) ;
572+
573+ if ( inputModeParams . validation ?. status === "checking" ) {
574+ //validation ongoing, ignore the submit
575+ return ;
576+ } else if ( inputModeParams . validation ?. status === "failed" ) {
577+ const cmdLine = $ ( "#commandLine .modal" ) ;
578+ cmdLine
579+ . stop ( true , true )
580+ . addClass ( "hasError" )
581+ . animate ( { undefined : 1 } , 500 , ( ) => {
582+ cmdLine . removeClass ( "hasError" ) ;
583+ } ) ;
584+ return ;
585+ }
586+
587+ if ( "inputValueConvert" in inputModeParams . command ) {
588+ inputModeParams . command . exec ?.( {
589+ commandlineModal : modal ,
590+
591+ // @ts -expect-error this is fine
592+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
593+ input : inputModeParams . command . inputValueConvert ( inputValue ) ,
594+ } ) ;
595+ } else {
596+ inputModeParams . command . exec ?.( {
597+ commandlineModal : modal ,
598+ input : inputValue ,
599+ } ) ;
600+ }
601+
569602 void AnalyticsController . log ( "usedCommandLine" , {
570603 command : inputModeParams . command . id ,
571604 } ) ;
@@ -695,6 +728,109 @@ async function decrementActiveIndex(): Promise<void> {
695728 await updateActiveCommand ( ) ;
696729}
697730
731+ function showWarning ( message : string ) : void {
732+ const warningEl = modal . getModal ( ) . querySelector < HTMLElement > ( ".warning" ) ;
733+ const warningTextEl = modal
734+ . getModal ( )
735+ . querySelector < HTMLElement > ( ".warning .text" ) ;
736+ if ( warningEl === null || warningTextEl === null ) {
737+ throw new Error ( "Commandline warning element not found" ) ;
738+ }
739+ warningEl . classList . remove ( "hidden" ) ;
740+ warningTextEl . textContent = message ;
741+ }
742+
743+ const showCheckingIcon = debounce ( 200 , async ( ) => {
744+ const checkingiconEl = modal
745+ . getModal ( )
746+ . querySelector < HTMLElement > ( ".checkingicon" ) ;
747+ if ( checkingiconEl === null ) {
748+ throw new Error ( "Commandline checking icon element not found" ) ;
749+ }
750+ checkingiconEl . classList . remove ( "hidden" ) ;
751+ } ) ;
752+
753+ function hideCheckingIcon ( ) : void {
754+ showCheckingIcon . cancel ( { upcomingOnly : true } ) ;
755+
756+ const checkingiconEl = modal
757+ . getModal ( )
758+ . querySelector < HTMLElement > ( ".checkingicon" ) ;
759+ if ( checkingiconEl === null ) {
760+ throw new Error ( "Commandline checking icon element not found" ) ;
761+ }
762+ checkingiconEl . classList . add ( "hidden" ) ;
763+ }
764+
765+ function hideWarning ( ) : void {
766+ const warningEl = modal . getModal ( ) . querySelector < HTMLElement > ( ".warning" ) ;
767+ if ( warningEl === null ) {
768+ throw new Error ( "Commandline warning element not found" ) ;
769+ }
770+ warningEl . classList . add ( "hidden" ) ;
771+ }
772+
773+ function updateValidationResult (
774+ validation : NonNullable < InputModeParams [ "validation" ] >
775+ ) : void {
776+ inputModeParams . validation = validation ;
777+ if ( validation . status === "checking" ) {
778+ showCheckingIcon ( ) ;
779+ } else if (
780+ validation . status === "failed" &&
781+ validation . errorMessage !== undefined
782+ ) {
783+ showWarning ( validation . errorMessage ) ;
784+ hideCheckingIcon ( ) ;
785+ } else {
786+ hideWarning ( ) ;
787+ hideCheckingIcon ( ) ;
788+ }
789+ }
790+
791+ async function isValid (
792+ checkValue : unknown ,
793+ originalValue : string ,
794+ originalInput : HTMLInputElement ,
795+ validation : CommandWithValidation < unknown > [ "validation" ]
796+ ) : Promise < void > {
797+ updateValidationResult ( { status : "checking" } ) ;
798+
799+ if ( validation . schema !== undefined ) {
800+ const schemaResult = validation . schema . safeParse ( checkValue ) ;
801+
802+ if ( ! schemaResult . success ) {
803+ updateValidationResult ( {
804+ status : "failed" ,
805+ errorMessage : schemaResult . error . errors
806+ . map ( ( err ) => err . message )
807+ . join ( ", " ) ,
808+ } ) ;
809+ return ;
810+ }
811+ }
812+
813+ if ( validation . isValid === undefined ) {
814+ updateValidationResult ( { status : "success" } ) ;
815+ return ;
816+ }
817+
818+ const result = await validation . isValid ( checkValue ) ;
819+ if ( originalInput . value !== originalValue ) {
820+ //value has change in the meantime, discard result
821+ return ;
822+ }
823+
824+ if ( result === true ) {
825+ updateValidationResult ( { status : "success" } ) ;
826+ } else {
827+ updateValidationResult ( {
828+ status : "failed" ,
829+ errorMessage : result ,
830+ } ) ;
831+ }
832+ }
833+
698834const modal = new AnimatedModal ( {
699835 dialogId : "commandLine" ,
700836 customEscapeHandler : ( ) : void => {
@@ -785,6 +921,35 @@ const modal = new AnimatedModal({
785921 }
786922 } ) ;
787923
924+ input . addEventListener (
925+ "input" ,
926+ debounce ( 100 , async ( e ) => {
927+ if (
928+ inputModeParams === null ||
929+ inputModeParams . command === null ||
930+ ! ( "validation" in inputModeParams . command )
931+ ) {
932+ return ;
933+ }
934+
935+ const originalInput = ( e as InputEvent ) . target as HTMLInputElement ;
936+ const currentValue = originalInput . value ;
937+ let checkValue : unknown = currentValue ;
938+ const command =
939+ inputModeParams . command as CommandWithValidation < unknown > ;
940+
941+ if ( "inputValueConvert" in command ) {
942+ checkValue = command . inputValueConvert ( currentValue ) ;
943+ }
944+ await isValid (
945+ checkValue ,
946+ currentValue ,
947+ originalInput ,
948+ command . validation
949+ ) ;
950+ } )
951+ ) ;
952+
788953 modalEl . addEventListener ( "mousemove" , ( _e ) => {
789954 mouseMode = true ;
790955 } ) ;
0 commit comments