aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/mode_insert.coffee
blob: a43a129f16ecbc7d341ee42aaea82e710eba5c53 (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
128
129
130
131
132
133
134
135
class InsertMode extends Mode
  constructor: (options = {}) ->
    # There is one permanently-installed instance of InsertMode.  It tracks focus changes and
    # activates/deactivates itself (by setting @insertModeLock) accordingly.
    @permanent = options.permanent

    # If truthy, then we were activated by the user (with "i").
    @global = options.global

    handleKeyEvent = (event) =>
      return @continueBubbling unless @isActive event
      return @passEventToPage if @insertModeLock is document.body

      # Check for a pass-next-key key.
      if KeyboardUtils.getKeyCharString(event) in Settings.get "passNextKeyKeys"
        new PassNextKeyMode
        return @suppressEvent

      return @passEventToPage unless event.type == 'keydown' and KeyboardUtils.isEscape event
      target = event.target
      if target and DomUtils.isFocusable target
        # Remove the focus, so the user can't just get back into insert mode by typing in the same input box.
        target.blur()
      else if target?.shadowRoot and @insertModeLock
        # An editable element in a shadow DOM is focused; blur it.
        @insertModeLock.blur()
      @exit event, event.target
      DomUtils.consumeKeyup event

    defaults =
      name: "insert"
      indicator: if not @permanent and not Settings.get "hideHud"  then "Insert mode"
      keypress: handleKeyEvent
      keyup: handleKeyEvent
      keydown: handleKeyEvent

    super extend defaults, options

    @insertModeLock =
      if options.targetElement and DomUtils.isEditable options.targetElement
        # The caller has told us which element to activate on.
        options.targetElement
      else if document.activeElement and DomUtils.isEditable document.activeElement
        # An input element is already active, so use it.
        document.activeElement
      else
        null

    @push
      _name: "mode-#{@id}-focus"
      "blur": (event) => @alwaysContinueBubbling =>
        target = event.target
        # We can't rely on focus and blur events arriving in the expected order.  When the active element
        # changes, we might get "focus" before "blur".  We track the active element in @insertModeLock, and
        # exit only when that element blurs.
        @exit event, target if @insertModeLock and target == @insertModeLock
      "focus": (event) => @alwaysContinueBubbling =>
        if @insertModeLock != event.target and DomUtils.isFocusable event.target
          @activateOnElement event.target
        else if event.target.shadowRoot
          # A focusable element inside the shadow DOM might have been selected. If so, we can catch the focus
          # event inside the shadow DOM. This fixes #853.
          shadowRoot = event.target.shadowRoot
          eventListeners = {}
          for type in [ "focus", "blur" ]
            eventListeners[type] = do (type) ->
              forTrusted (event) -> handlerStack.bubbleEvent type, event
            shadowRoot.addEventListener type, eventListeners[type], true

          handlerStack.push
            _name: "shadow-DOM-input-mode"
            blur: (event) ->
              if event.target.shadowRoot == shadowRoot
                handlerStack.remove()
                for own type, listener of eventListeners
                  shadowRoot.removeEventListener type, listener, true

    # Only for tests.  This gives us a hook to test the status of the permanently-installed instance.
    InsertMode.permanentInstance = this if @permanent

  isActive: (event) ->
    return false if event == InsertMode.suppressedEvent
    return true if @insertModeLock or @global
    # Some sites (e.g. inbox.google.com) change the contentEditable property on the fly (see #1245); and
    # unfortunately, the focus event fires *before* the change.  Therefore, we need to re-check whether the
    # active element is contentEditable.
    @activateOnElement document.activeElement if document.activeElement?.isContentEditable
    @insertModeLock != null

  activateOnElement: (element) ->
    @log "#{@id}: activating (permanent)" if @debug and @permanent
    @insertModeLock = element

  exit: (_, target)  ->
    if (target and target == @insertModeLock) or @global or target == undefined
      @log "#{@id}: deactivating (permanent)" if @debug and @permanent and @insertModeLock
      @insertModeLock = null
      # Exit, but only if this isn't the permanently-installed instance.
      super() unless @permanent

  # Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance.
  @suppressedEvent: null
  @suppressEvent: (event) -> @suppressedEvent = event

# This implements the pasNexKey command.
class PassNextKeyMode extends Mode
  constructor: (count = 1) ->
    seenKeyDown = false
    keyDownCount = 0

    super
      name: "pass-next-key"
      indicator: "Pass next key."
      # We exit on blur because, once we lose the focus, we can no longer track key events.
      exitOnBlur: window
      keypress: =>
        @passEventToPage

      keydown: =>
        seenKeyDown = true
        keyDownCount += 1
        @passEventToPage

      keyup: =>
        if seenKeyDown
          unless 0 < --keyDownCount
            unless 0 < --count
              @exit()
        @passEventToPage

root = exports ? (window.root ?= {})
root.InsertMode = InsertMode
root.PassNextKeyMode = PassNextKeyMode
extend window, root unless exports?