aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/mode_key_handler.coffee
blob: 9b044923c48b75fbb020d3d967ff36a8228e369c (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
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