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?
|