11import { Provider } from 'cerebral' ;
22import { KEYBINDINGS , normalizeKey } from 'common/utils/keybindings' ;
33
4+ const isIOS =
5+ typeof navigator !== 'undefined' &&
6+ ! ! navigator . platform . match ( / ( i P h o n e | i P o d | i P a d ) / i) ;
7+
48const state = {
59 keybindings : null ,
610 keydownIndex : 0 ,
@@ -19,6 +23,124 @@ function handleKeyUp() {
1923 state . keydownIndex = 0 ;
2024}
2125
26+ function hasAnyKeyModifier ( event : KeyboardEvent ) {
27+ return event . shiftKey || event . altKey || event . ctrlKey || event . metaKey ;
28+ }
29+
30+ function hasKeyModifier ( event : KeyboardEvent , modifier : string ) {
31+ return {
32+ 'Shift' : event . shiftKey ,
33+ 'Alt' : event . altKey ,
34+ 'Control' : event . ctrlKey ,
35+ 'Meta' : event . metaKey ,
36+ } [ modifier ] ;
37+ }
38+
39+ function handleIosKeyDown ( controller , event , _pressedKey ) {
40+ // the linter doesn't allow to change parameters' values.
41+ let pressedKey = _pressedKey ;
42+ // iOS shortcuts only work if there's at least one modifier key at work (shift or alt).
43+ // I'm aborting this handling right here to minimize the number of cycles needed for
44+ // every key press.
45+ if ( ! hasAnyKeyModifier ( event ) ) {
46+ return ;
47+ }
48+ // This is just here to facilitate testing on a desktop environment since on desktop we do get
49+ // pressedKey === 'Alt' (on iOS you'd get pressedKey === 'Dead').
50+ if ( pressedKey . length > 1 ) {
51+ pressedKey = '' ;
52+ }
53+
54+ const filterMatchingBindings = function filterMatchingBindings ( pendingBindings ) {
55+ return pendingBindings . map ( ( binding ) => {
56+ const bindingKeys = binding . bindings [ 0 ] ;
57+ // Sanity check
58+ if ( ! bindingKeys || bindingKeys . length === 0 ) {
59+ return null ;
60+ }
61+
62+ // Try to match the currently pressed keys with the binding keys. Generate new
63+ // keys for the bindings matched that correspond to the remaining keys needed to be
64+ // pressed in order to have a hit.
65+ let matchedKeyIndex = - 1 ;
66+ for ( let i = 0 ; i < bindingKeys . length ; i ++ ) {
67+ const keyToMatch = bindingKeys [ i ] ;
68+ if ( keyToMatch === pressedKey ) {
69+ matchedKeyIndex = i ;
70+ break ;
71+ }
72+ // We try to consume as many modifiers as possible before we find the key to match.
73+ if ( ! hasKeyModifier ( event , keyToMatch ) ) {
74+ break ;
75+ }
76+ }
77+
78+ if ( matchedKeyIndex === - 1 ) {
79+ // No key was matched so we skip this binding.
80+ return null ;
81+ }
82+
83+ return {
84+ ...binding ,
85+ bindings : [
86+ binding . bindings [ 0 ] . slice ( matchedKeyIndex + 1 ) ,
87+ binding . bindings [ 1 ] ,
88+ ]
89+ } ;
90+ } ) . filter ( Boolean )
91+ . sort ( ( a , b ) => a . bindings . length < b . bindings . length ) ;
92+ } ;
93+
94+ // We filter out any hits on full list of bindings on first key down, or just move
95+ // on filtering on existing pending bindings
96+ state . pendingPrimaryBindings = filterMatchingBindings ( state . keydownIndex === 0
97+ ? state . keybindings
98+ : state . pendingPrimaryBindings
99+ ) ;
100+
101+ state . keydownIndex ++ ;
102+
103+ const longestBinding = state . pendingPrimaryBindings [ state . pendingPrimaryBindings . length - 1 ] ;
104+ if ( ! longestBinding ) {
105+ // Nothing matched so back to the beginning!
106+ reset ( ) ;
107+ return ;
108+ }
109+
110+ // We partially matched some bindings so avoid printing that key.
111+ event . preventDefault ( ) ;
112+ event . stopPropagation ( ) ;
113+
114+ for ( let i = state . pendingPrimaryBindings . length - 1 ; i >= 0 ; i -- ) {
115+ const completedBinding = state . pendingPrimaryBindings [ i ] ;
116+ // Check if the binding has actually been completed, if it has then we have already
117+ // processed all completed bindings since they're ordered as such.
118+ if ( completedBinding . bindings [ 0 ] . length > 0 ) {
119+ break ;
120+ }
121+
122+ // This binding has been completed (not more keys needed to match) so call its payload
123+ // function or add it as a pending secondary binding.
124+ if ( completedBinding . bindings . length > 0 && completedBinding . bindings [ 1 ] ) {
125+ this . pendingSecondaryBindings . push ( completedBinding ) ;
126+ } else {
127+ const keybinding = KEYBINDINGS [ completedBinding . key ] ;
128+
129+ reset ( ) ;
130+ event . preventDefault ( ) ;
131+ event . stopPropagation ( ) ;
132+
133+ const payload =
134+ typeof keybinding . payload === 'function'
135+ ? keybinding . payload ( controller . getState ( ) )
136+ : keybinding . payload || { } ;
137+ controller . getSignal ( keybinding . signal ) ( payload ) ;
138+ // When we find a completed binding and call its payload, we're done.
139+ break ;
140+ }
141+ }
142+ }
143+
22144function handleKeyDown ( controller , e ) {
23145 if ( state . timeout ) {
24146 clearTimeout ( state . timeout ) ;
@@ -35,6 +157,10 @@ function handleKeyDown(controller, e) {
35157 return ;
36158 }
37159
160+ if ( isIOS ) {
161+ handleIosKeyDown ( controller , e , key ) ;
162+ return ;
163+ }
38164 // First we check if we have any pending secondary bindings to identify
39165 if ( state . pendingSecondaryBindings . length ) {
40166 // We filter out any hits by verifying that the current key matches the next
0 commit comments