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
|
class KeyHandlerMode extends Mode
countPrefix: 0
keydownEvents: {}
keyState: []
constructor: (options) ->
@commandHandler = options.commandHandler ? (->)
@setKeyMapping options.commandHandler ? {}
delete options[option] for option in ["commandHandler", "keyMapping"]
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 a subsequent keypress. If we will be handling that
# event, then we suppress propagation of this keydown to prevent triggering page events.
keyChar = KeyboardUtils.getKeyChar event
if keyChar and (@mappingForKeyChar(keyChar) or @isCountKey keyChar)
DomUtils.suppressPropagation event
@keydownEvents[event.keyCode] = true
@stopBubblingAndTrue
else
@continueBubbling
onKeypress: (event) ->
keyChar = KeyboardUtils.getKeyCharString event
if keyChar and @mappingForKeyChar keyChar
@handleKeyChar event, keyChar
else if keyChar and @isCountKey keyChar
digit = parseInt keyChar
@reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
false # Suppress event.
else
@reset()
@continueBubbling
onKeyup: (event) ->
if event.keyCode of @keydownEvents
delete @keydownEvents[event.keyCode]
DomUtils.suppressPropagation event
@stopBubblingAndTrue
else
@continueBubbling
handleKeyChar: (event, keyChar) ->
bgLog "Handling key #{keyChar}, mode=#{@name}."
@advanceKeyState keyChar
commands = @keyState.filter (entry) -> entry.command
if 0 < commands.length
countPrefix = if 0 < @countPrefix then @countPrefix else 1
@reset()
bgLog "Calling mode=#{@name}, command=#{commands[0].command}, count=#{countPrefix}."
@commandHandler commands[0], countPrefix
false # Suppress event.
# This returns the first key-state entry for which keyChar is mapped. The return value is truthy if a match
# is found and falsy otherwise.
mappingForKeyChar: (keyChar) ->
(mapping for mapping in @keyState when keyChar of mapping)[0]
# This is called whenever a keyChar is matched. We keep any existing mappings matching keyChar, and append
# a new copy of the mode's global key mappings.
advanceKeyState: (keyChar) ->
newMappings = (mapping[keyChar] for mapping in @keyState when keyChar of mapping)
@keyState = [newMappings..., @keyMapping]
# Reset the state (as if no keys had been handled), but retaining the count - if one is provided.
reset: (count = 0) ->
bgLog "Clearing key queue, set count=#{count}."
@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 keyChar.length == 1
if 0 < @countPrefix
'0' <= keyChar <= '9'
else
'1' <= keyChar <= '9'
# Test whether keyChar would be the very first character of a command mapping.
isFirstKeyChar: (keyChar) ->
keyChar and @countPrefix == 0 and (@mappingForKeyChar(keyChar) == @keyMapping or @isCountKey keyChar)
root = exports ? window
root.KeyHandlerMode = KeyHandlerMode
|