aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/mode_key_handler.coffee
blob: c9cab2444112f4e7825e0ead64bab567e362d57a (plain)
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