11import {
2+ createSyntheticEvent ,
23 decrementTimeUnitValue ,
34 EventEmitter ,
45 Hours ,
@@ -13,6 +14,7 @@ import {
1314 zeroTime ,
1415 zeroTimeUnit ,
1516} from "@vuu-ui/vuu-utils" ;
17+ import { ChangeEventHandler } from "react" ;
1618
1719export type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
1820
@@ -32,12 +34,12 @@ const FullSelection: Selection = { end: 0, start: 8 };
3234const CursorAtEnd : Selection = { end : 8 , start : 8 } ;
3335
3436export type MaskedInputEvents = {
35- change : ( value : TimeString ) => void ;
37+ change : ChangeEventHandler < HTMLInputElement > ;
3638} ;
3739
3840export class MaskedInput extends EventEmitter < MaskedInputEvents > {
3941 #controlled = false ;
40- #input: HTMLInputElement | null ;
42+ #input: HTMLInputElement | null = null ;
4143 #isFocused = false ;
4244 #isIncrementalChange = false ;
4345 #selectionStart = - 1 ;
@@ -51,14 +53,35 @@ export class MaskedInput extends EventEmitter<MaskedInputEvents> {
5153 inputEl : HTMLInputElement | null = null ,
5254 ) {
5355 super ( ) ;
54- this . #input = inputEl ;
56+ if ( inputEl ) {
57+ this . input = inputEl ;
58+ }
5559 this . #value = defaultValue ?? zeroTime ;
5660 }
5761
5862 set input ( el : HTMLInputElement ) {
63+ if ( this . #input) {
64+ throw Error (
65+ "MaskedInput cannot be reused, create a new instance for a new input" ,
66+ ) ;
67+ }
5968 this . #input = el ;
69+ el . addEventListener ( "change" , this . emitSyntheticChangeEvent ) ;
6070 }
6171
72+ /**
73+ * The change event is fired programatically. This will only be handled
74+ * by a native event handler ( not a React handler). We handle this event
75+ * and convert to a React (Synthetic) event.
76+ */
77+ private emitSyntheticChangeEvent = ( e : Event ) => {
78+ const syntheticEvent = createSyntheticEvent (
79+ e ,
80+ ) as React . ChangeEvent < HTMLInputElement > ;
81+
82+ this . emit ( "change" , syntheticEvent ) ;
83+ } ;
84+
6285 get cursorPos ( ) {
6386 return this . selectionStart ;
6487 }
@@ -92,11 +115,23 @@ export class MaskedInput extends EventEmitter<MaskedInputEvents> {
92115 if ( ! this . #controlled) {
93116 this . #isIncrementalChange = false ;
94117 this . #value = value ;
95- if ( this . #input) {
96- this . #input. value = value ;
97- }
98118 }
99- this . emit ( "change" , value ) ;
119+
120+ if ( this . #input) {
121+ // HM this updateds the input value, even if we are controlled.
122+ // Thats not right, but if we don't update it, the event will
123+ // not carry thwe right value. I don;t thibnk we can simulate
124+ // the correct behaviour
125+ this . #input. value = value ;
126+ // this triggers the native change event, which we convert
127+ // // to synthetic event and emit in input setter above
128+ this . #input. dispatchEvent (
129+ new Event ( "change" , {
130+ bubbles : true ,
131+ composed : true ,
132+ } ) ,
133+ ) ;
134+ }
100135 }
101136
102137 private setUnitValue ( unit : TimeUnit , value : Hours | Minutes | Seconds ) {
@@ -132,31 +167,31 @@ export class MaskedInput extends EventEmitter<MaskedInputEvents> {
132167 this . #isIncrementalChange = false ;
133168
134169 requestAnimationFrame ( ( ) => {
135- if ( isValidTimeString ( value ) && ! isIncremental ) {
136- this . advanceSelection ( ) ;
137- } else {
170+ if ( isIncremental ) {
138171 this . restoreSelection ( ) ;
172+ } else {
173+ this . advanceSelection ( ) ;
139174 }
140175 } ) ;
141176 }
142177 }
143178
144179 clear ( unit : TimeUnit ) {
145180 if ( this . #input) {
181+ let newValue = this . #value;
146182 if ( unit === "hours" ) {
147- this . #value = zeroTimeUnit . concat ( this . #value. slice ( 2 ) ) as TimeString ;
183+ newValue = zeroTimeUnit . concat ( this . #value. slice ( 2 ) ) as TimeString ;
148184 } else if ( unit === "minutes" ) {
149- this . #value = this . #value
185+ newValue = this . #value
150186 . slice ( 0 , 3 )
151187 . concat ( zeroTimeUnit )
152188 . concat ( this . #value. slice ( 5 ) ) as TimeString ;
153189 } else if ( unit === "seconds" ) {
154- this . #value = this . #value
155- . slice ( 0 , 6 )
156- . concat ( zeroTimeUnit ) as TimeString ;
190+ newValue = this . #value. slice ( 0 , 6 ) . concat ( zeroTimeUnit ) as TimeString ;
191+ }
192+ if ( newValue !== this . #value) {
193+ this . setValue ( newValue ) ;
157194 }
158- this . #input. value = this . #value;
159- this . emit ( "change" , this . #value as TimeString ) ;
160195 }
161196 }
162197
@@ -306,11 +341,9 @@ export class MaskedInput extends EventEmitter<MaskedInputEvents> {
306341 . slice ( 0 , cursorPos - offset )
307342 . concat ( zeroTime . slice ( cursorPos - offset , cursorPos ) )
308343 . concat ( this . #value. slice ( cursorPos ) ) as TimeString ;
309- this . #value = newValue ;
310344 this . selectionStart -= offset ;
311345 this . selectionEnd -= offset ;
312- this . #input. value = this . #value;
313- this . emit ( "change" , this . #value as TimeString ) ;
346+ this . setValue ( newValue ) ;
314347
315348 requestAnimationFrame ( ( ) => {
316349 this . #input?. setSelectionRange (
@@ -372,6 +405,7 @@ export class MaskedInput extends EventEmitter<MaskedInputEvents> {
372405
373406 click ( ) {
374407 if ( this . #input) {
408+ this . #isFocused = true ;
375409 const selection = this . getSelection ( ) ;
376410 if ( selection === NullSelection ) {
377411 this . select ( "hours" ) ;
0 commit comments