1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
class KeyHandlerMode extends Mode
useCount: true
countPrefix: 0
keydownEvents: {}
keyState: []
constructor: (options) ->
# A function accepting a command name and a count; required.
@commandHandler = options.commandHandler ? (->)
@useCount = false if options.noCount
@reset()
# We don't pass these options on to super().
options = Utils.copyObjectOmittingProperties options, "commandHandler", "keyMapping", "noCount"
super extend options,
keydown: @onKeydown.bind this
keypress: @onKeypress.bind this
keyup: @onKeyup.bind this
# We cannot track matching keydown/keyup events if we lose the focus.
blur: (event) => @alwaysContinueBubbling =>
@keydownEvents = {} if event.target == window
setKeyMapping: (@keyMapping) -> @reset()
onKeydown: (event) ->
keyChar = KeyboardUtils.getKeyCharString event
if KeyboardUtils.isEscape event
if @isInResetState()
@continueBubbling
else
@reset()
DomUtils.suppressKeyupAfterEscape handlerStack
false # Suppress event.
else if keyChar and @mappingForKeyChar keyChar
@handleKeyChar event, keyChar
else
# We did not handle the event, but we might handle the subsequent keypress event. If we *will* be
# handling that event, then we need to suppress propagation of this keydown event to prevent triggering
# page features like Google instant search.
keyChar = KeyboardUtils.getKeyChar event
if keyChar and (@mappingForKeyChar(keyChar) or @isCountKey keyChar)
DomUtils.suppressPropagation event
@keydownEvents[@getEventCode event] = true
@stopBubblingAndTrue
else
@countPrefix = 0 if keyChar
@continueBubbling
onKeypress: (event) ->
keyChar = KeyboardUtils.getKeyCharString event
if keyChar and @mappingForKeyChar keyChar
@handleKeyChar event, keyChar
else if keyChar and @isCountKey keyChar
@reset @countPrefix * 10 + parseInt keyChar
false # Suppress event.
else
@continueBubbling
onKeyup: (event) ->
eventCode = @getEventCode event
if eventCode of @keydownEvents
delete @keydownEvents[eventCode]
DomUtils.suppressPropagation event
@stopBubblingAndTrue
else
@continueBubbling
handleKeyChar: (event, keyChar) ->
@advanceKeyState keyChar
commands = @keyState.filter (entry) -> entry.command
@invokeCommand commands[0] if 0 < commands.length
false # Suppress event.
# This returns the first mapping for which keyChar is mapped. The return value is truthy if a match is found
# and falsy otherwise.
mappingForKeyChar: (keyChar) ->
for mapping in @keyState
return mapping if keyChar of mapping
null
# This is called whenever a keyChar is matched. We keep any existing entries matching keyChar, and append a
# new copy of the global key mappings.
advanceKeyState: (keyChar) ->
newKeyState =
for mapping in @keyState
continue unless keyChar of mapping
mapping[keyChar]
@keyState = [newKeyState..., @keyMapping]
# This is called to invoke a command and reset the key state.
invokeCommand: (command) ->
countPrefix = if 0 < @countPrefix then @countPrefix else 1
@reset()
@commandHandler command, countPrefix
# Reset the state (as if no keys had been handled), but retaining the count - if one is provided.
reset: (count = 0) ->
@countPrefix = count
@keyState = [@keyMapping]
# This tests whether we are in the reset state. It is used to check whether we should be using escape to
# reset the key state, or passing it to the page.
isInResetState: ->
@countPrefix == 0 and @keyState.length == 1
# This tests whether keyChar should be treated as a count key.
isCountKey: (keyChar) ->
return false unless @useCount and keyChar.length == 1
if 0 < @countPrefix
'0' <= keyChar <= '9'
else
'1' <= keyChar <= '9'
# True if keyChar would be the first character of a command mapping. This is used by passKeys to decide
# whether keyChar is a continuation of a command which the user has already begin entering.
isFirstKeyChar: (keyChar) ->
@countPrefix == 0 and @keyMapping == @mappingForKeyChar keyChar
getEventCode: (event) -> event.keyCode
root = exports ? window
root.KeyHandlerMode = KeyHandlerMode
|