@@ -42,7 +42,10 @@ type SimulatedDefault = (
4242 options : typeOptions
4343) => void
4444
45+ type KeyInfo = KeyDetails | ShortcutDetails
46+
4547interface KeyDetails {
48+ type : 'key'
4649 key : string
4750 text : string | null
4851 code : string
@@ -59,6 +62,13 @@ interface KeyDetails {
5962 }
6063}
6164
65+ interface ShortcutDetails {
66+ type : 'shortcut'
67+ modifiers : KeyDetails [ ]
68+ key : KeyDetails
69+ originalSequence : string
70+ }
71+
6272const dateRe = / ^ \d { 4 } - \d { 2 } - \d { 2 } /
6373const monthRe = / ^ \d { 4 } - ( 0 \d | 1 [ 0 - 2 ] ) /
6474const weekRe = / ^ \d { 4 } - W ( 0 [ 1 - 9 ] | [ 1 - 4 ] \d | 5 [ 0 - 3 ] ) /
@@ -138,20 +148,24 @@ const modifiersToString = (modifiers: KeyboardModifiers) => {
138148 } ) ) . join ( ', ' )
139149}
140150
141- const joinKeyArrayToString = ( keyArr : KeyDetails [ ] ) => {
142- return _ . map ( keyArr , ( keyDetails ) => {
143- if ( keyDetails . text ) return keyDetails . key
151+ const joinKeyArrayToString = ( keyArr : KeyInfo [ ] ) => {
152+ return _ . map ( keyArr , ( key ) => {
153+ if ( key . type === 'key' ) {
154+ if ( key . text ) return key . key
144155
145- return `{${ keyDetails . key } }`
156+ return `{${ key . key } }`
157+ }
158+
159+ return `{${ key . originalSequence } }`
146160 } ) . join ( '' )
147161}
148162
149163type modifierKeyDetails = KeyDetails & {
150164 key : keyof typeof keyToModifierMap
151165}
152166
153- const isModifier = ( details : KeyDetails ) : details is modifierKeyDetails => {
154- return ! ! keyToModifierMap [ details . key ]
167+ const isModifier = ( details : KeyInfo ) : details is modifierKeyDetails => {
168+ return details . type === 'key' && ! ! keyToModifierMap [ details . key ]
155169}
156170
157171const getFormattedKeyString = ( details : KeyDetails ) => {
@@ -170,7 +184,7 @@ const getFormattedKeyString = (details: KeyDetails) => {
170184 return details . originalSequence
171185}
172186
173- const countNumIndividualKeyStrokes = ( keys : KeyDetails [ ] ) => {
187+ const countNumIndividualKeyStrokes = ( keys : KeyInfo [ ] ) => {
174188 return _ . countBy ( keys , isModifier ) [ 'false' ]
175189}
176190
@@ -188,7 +202,7 @@ const findKeyDetailsOrLowercase = (key: string): KeyDetailsPartial => {
188202const getTextLength = ( str ) => _ . toArray ( str ) . length
189203
190204const getKeyDetails = ( onKeyNotFound ) => {
191- return ( key : string ) : KeyDetails => {
205+ return ( key : string ) : KeyDetails | ShortcutDetails => {
192206 let foundKey : KeyDetailsPartial
193207
194208 if ( getTextLength ( key ) === 1 ) {
@@ -199,6 +213,7 @@ const getKeyDetails = (onKeyNotFound) => {
199213
200214 if ( foundKey ) {
201215 const details = _ . defaults ( { } , foundKey , {
216+ type : 'key' ,
202217 key : '' ,
203218 keyCode : 0 ,
204219 code : '' ,
@@ -211,11 +226,67 @@ const getKeyDetails = (onKeyNotFound) => {
211226 details . text = details . key
212227 }
213228
229+ details . type = 'key'
214230 details . originalSequence = key
215231
216232 return details
217233 }
218234
235+ if ( key . includes ( '+' ) ) {
236+ if ( key . endsWith ( '++' ) ) {
237+ key = key . replace ( '++' , '+plus' )
238+ }
239+
240+ const keys = key . split ( '+' )
241+ let lastKey = _ . last ( keys )
242+
243+ if ( lastKey === 'plus' ) {
244+ keys [ keys . length - 1 ] = '+'
245+ lastKey = '+'
246+ }
247+
248+ if ( ! lastKey ) {
249+ return onKeyNotFound ( key , _ . keys ( getKeymap ( ) ) . join ( ', ' ) )
250+ }
251+
252+ const keyWithModifiers = getKeyDetails ( onKeyNotFound ) ( lastKey ) as KeyDetails
253+
254+ let hasModifierBesidesShift = false
255+
256+ const modifiers = keys . slice ( 0 , - 1 )
257+ . map ( ( m ) => {
258+ if ( ! Object . keys ( modifierChars ) . includes ( m ) ) {
259+ $errUtils . throwErrByPath ( 'type.not_a_modifier' , {
260+ args : {
261+ key : m ,
262+ } ,
263+ } )
264+ }
265+
266+ if ( m !== 'shift' ) {
267+ hasModifierBesidesShift = true
268+ }
269+
270+ return getKeyDetails ( onKeyNotFound ) ( m )
271+ } ) as KeyDetails [ ]
272+
273+ const details : ShortcutDetails = {
274+ type : 'shortcut' ,
275+ modifiers,
276+ key : keyWithModifiers ,
277+ originalSequence : key ,
278+ }
279+
280+ // if we are going to type {ctrl+b}, the 'b' shouldn't be input as text
281+ // normally we don't bypass text input but for shortcuts it's definitely what the user wants
282+ // since the modifiers only apply to this single key.
283+ if ( hasModifierBesidesShift ) {
284+ details . key . text = null
285+ }
286+
287+ return details
288+ }
289+
219290 onKeyNotFound ( key , _ . keys ( getKeymap ( ) ) . join ( ', ' ) )
220291
221292 throw new Error ( 'this can never happen' )
@@ -311,7 +382,7 @@ const getKeymap = () => {
311382}
312383const validateTyping = (
313384 el : HTMLElement ,
314- keys : KeyDetails [ ] ,
385+ keys : KeyInfo [ ] ,
315386 currentIndex : number ,
316387 onFail : Function ,
317388 skipCheckUntilIndex : number | undefined ,
@@ -684,12 +755,18 @@ export class Keyboard {
684755
685756 const typeKeyFns = _ . map (
686757 keyDetailsArr ,
687- ( key : KeyDetails , currentKeyIndex : number ) => {
758+ ( key : KeyInfo , currentKeyIndex : number ) => {
688759 return ( ) => {
689- debug ( 'typing key:' , key . key )
690-
691760 const activeEl = getActiveEl ( doc )
692761
762+ if ( key . type === 'shortcut' ) {
763+ this . simulateShortcut ( activeEl , key , options )
764+
765+ return null
766+ }
767+
768+ debug ( 'typing key:' , key . key )
769+
693770 _skipCheckUntilIndex = _skipCheckUntilIndex && _skipCheckUntilIndex - 1
694771
695772 if ( ! _skipCheckUntilIndex ) {
@@ -717,21 +794,27 @@ export class Keyboard {
717794 // singleValueChange inputs must have their value set once at the end
718795 // performing the simulatedDefault for a key would try to insert text on each character
719796 // we still send all the events as normal, however
720- key . simulatedDefault = _ . noop
797+ if ( key . type === 'key' ) {
798+ key . simulatedDefault = _ . noop
799+ }
721800 } )
722801
723- _ . last ( keysToType ) ! . simulatedDefault = ( ) => {
724- options . onValueChange ( originalText , activeEl )
802+ const lastKeyToType = _ . last ( keysToType ) !
725803
726- const valToSet = isClearChars ? '' : joinKeyArrayToString ( keysToType )
804+ if ( lastKeyToType . type === 'key' ) {
805+ lastKeyToType . simulatedDefault = ( ) => {
806+ options . onValueChange ( originalText , activeEl )
727807
728- debug ( 'setting element value' , valToSet , activeEl )
808+ const valToSet = isClearChars ? '' : joinKeyArrayToString ( keysToType )
729809
730- return $elements . setNativeProp (
731- activeEl as $elements . HTMLTextLikeInputElement ,
732- 'value' ,
733- valToSet ,
734- )
810+ debug ( 'setting element value' , valToSet , activeEl )
811+
812+ return $elements . setNativeProp (
813+ activeEl as $elements . HTMLTextLikeInputElement ,
814+ 'value' ,
815+ valToSet ,
816+ )
817+ }
735818 }
736819 }
737820 } else {
@@ -1126,6 +1209,25 @@ export class Keyboard {
11261209 this . simulatedKeyup ( elToKeyup , key , options )
11271210 }
11281211
1212+ simulateShortcut ( el : HTMLElement , key : ShortcutDetails , options ) {
1213+ key . modifiers . forEach ( ( key ) => {
1214+ this . simulatedKeydown ( el , key , options )
1215+ } )
1216+
1217+ this . simulatedKeydown ( el , key . key , options )
1218+ this . simulatedKeyup ( el , key . key , options )
1219+
1220+ options . id = _ . uniqueId ( 'char' )
1221+
1222+ const elToKeyup = this . getActiveEl ( options )
1223+
1224+ key . modifiers . reverse ( ) . forEach ( ( key ) => {
1225+ delete key . events . keyup
1226+ options . id = _ . uniqueId ( 'char' )
1227+ this . simulatedKeyup ( elToKeyup , key , options )
1228+ } )
1229+ }
1230+
11291231 simulatedKeyup ( el : HTMLElement , _key : KeyDetails , options : typeOptions ) {
11301232 if ( shouldIgnoreEvent ( 'keyup' , _key . events ) ) {
11311233 debug ( 'simulatedKeyup: ignoring event' )
0 commit comments