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
# Check for a pass-next-key key.
if KeyboardUtils.getKeyCharString(event) in Settings.get "passNextKeyKeys"
new PassNextKeyMode
return false
return @stopBubblingAndTrue unless event.type == 'keydown' and KeyboardUtils.isEscape event
DomUtils.suppressKeyupAfterEscape handlerStack
target = event.srcElement
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.srcElement
@suppressEvent
defaults =
name: "insert"
indicator: if @permanent then null else "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.
# We don't exit if we're running under edit mode. Edit mode itself will handles that case.
@exit event, target if @insertModeLock and target == @insertModeLock and not @options.parentMode
"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) ->
(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 = @ 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: =>
@stopBubblingAndTrue
keydown: =>
seenKeyDown = true
keyDownCount += 1
@stopBubblingAndTrue
keyup: =>
if seenKeyDown
unless 0 < --keyDownCount
unless 0 < --count
@exit()
@stopBubblingAndTrue
root = exports ? window
root.InsertMode = InsertMode
root.PassNextKeyMode = PassNextKeyMode
|