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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#
# A mode implements a number of keyboard event handlers which are pushed onto the handler stack when the mode
# is activated, and popped off when it is deactivated. The Mode class constructor takes a single argument,
# options, which can define (amongst other things):
#
# name:
# A name for this mode.
#
# badge:
# A badge (to appear on the browser popup).
# Optional. Define a badge if the badge is constant. Otherwise, do not define a badge, but override
# instead the chooseBadge method. Or, if the mode *never* shows a badge, then do neither.
#
# keydown:
# keypress:
# keyup:
# Key handlers. Optional: provide these as required. The default is to continue bubbling all key events.
#
# Further options are described in the constructor, below.
#
# Additional handlers associated with a mode can be added by using the push method. For example, if a mode
# responds to "focus" events, then push an additional handler:
# @push
# "focus": (event) => ....
# Any such handlers are removed when the mode is deactivated.
#
# To activate a mode, use:
# myMode = new MyMode()
#
# Or (usually better) just:
# new MyMode()
# It is usually not necessary to retain a reference to the mode object.
#
# To deactivate a mode, use:
# @exit() # internally triggered (more common).
# myMode.exit() # externally triggered.
#
# For debug only; to be stripped out.
count = 0
class Mode
@debug = true
# Constants; readable shortcuts for event-handler return values.
continueBubbling: true
suppressEvent: false
stopBubblingAndTrue: handlerStack.stopBubblingAndTrue
stopBubblingAndFalse: handlerStack.stopBubblingAndFalse
constructor: (options={}) ->
@options = options
@handlers = []
@exitHandlers = []
@modeIsActive = true
@badge = options.badge || ""
@name = options.name || "anonymous"
@count = ++count
console.log @count, "create:", @name if Mode.debug
@push
keydown: options.keydown || null
keypress: options.keypress || null
keyup: options.keyup || null
updateBadge: (badge) => @alwaysContinueBubbling => @chooseBadge badge
# Some modes are singletons: there may be at most one instance active at any one time. A mode is a
# singleton if options.singleton is truthy. The value of options.singleton should be the key which is
# required to be unique. See PostFindMode for an example.
# New instances deactivate existing instances as they themselves are activated.
@registerSingleton options.singleton if options.singleton
# If options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed. The
# triggering keyboard event will be passed to the mode's @exit() method.
if options.exitOnEscape
# Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes
# priority.
@push
"keydown": (event) =>
return @continueBubbling unless KeyboardUtils.isEscape event
@exit event
DomUtils.suppressKeyupAfterEscape handlerStack
@suppressEvent
# If options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element
# loses the focus.
if options.exitOnBlur
@push
"blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == options.exitOnBlur
# If options.trackState is truthy, then the mode mainatins the current state in @enabled and @passKeys,
# and calls @registerStateChange() (if defined) whenever the state changes.
if options.trackState
@enabled = false
@passKeys = ""
@push
"registerStateChange": ({enabled: enabled, passKeys: passKeys}) =>
@alwaysContinueBubbling =>
if enabled != @enabled or passKeys != @passKeys
@enabled = enabled
@passKeys = passKeys
@registerStateChange?()
# If options.trapAllKeyboardEvents is truthy, then it should be an element. All keyboard events on that
# element are suppressed *after* bubbling the event down the handler stack. This prevents such events
# from propagating to other extensions or the host page.
if options.trapAllKeyboardEvents
@unshift
keydown: (event) => @alwaysContinueBubbling ->
DomUtils.suppressPropagation event if event.srcElement == options.trapAllKeyboardEvents
keypress: (event) => @alwaysContinueBubbling ->
DomUtils.suppressEvent event if event.srcElement == options.trapAllKeyboardEvents
keyup: (event) => @alwaysContinueBubbling ->
DomUtils.suppressPropagation event if event.srcElement == options.trapAllKeyboardEvents
Mode.updateBadge() if @badge
# End of Mode.constructor().
push: (handlers) ->
@handlers.push handlerStack.push handlers
unshift: (handlers) ->
@handlers.unshift handlerStack.push handlers
onExit: (handler) ->
@exitHandlers.push handler
exit: ->
if @modeIsActive
console.log @count, "exit:", @name if Mode.debug
handler() for handler in @exitHandlers
handlerStack.remove handlerId for handlerId in @handlers
Mode.updateBadge()
@modeIsActive = false
# The badge is chosen by bubbling an "updateBadge" event down the handler stack allowing each mode the
# opportunity to choose a badge. chooseBadge, here, is the default. It is overridden in sub-classes.
chooseBadge: (badge) ->
badge.badge ||= @badge
# Shorthand for an otherwise long name. This allow us to write handlers which always yield the same value,
# without having to be concerned with the result of the handler itself.
alwaysContinueBubbling: (func) -> handlerStack.alwaysContinueBubbling func
# Static method. Used externally and internally to initiate bubbling of an updateBadge event and to send
# the resulting badge to the background page. We only update the badge if this document (hence this frame)
# has the focus.
@updateBadge: ->
if document.hasFocus()
handlerStack.bubbleEvent "updateBadge", badge = {badge: ""}
chrome.runtime.sendMessage
handler: "setBadge"
badge: badge.badge
# Temporarily install a mode to protect a function call, then exit the mode. For example, temporarily
# install an InsertModeBlocker.
@runIn: (mode, func) ->
mode = new mode()
func()
mode.exit()
registerSingleton: do ->
singletons = {} # Static.
(key) ->
singletons[key].exit() if singletons[key]
singletons[key] = @
@onExit => delete singletons[key] if singletons[key] == @
# BadgeMode is a pseudo mode for triggering badge updates on focus changes and state updates. It sits at the
# bottom of the handler stack, and so it receives state changes *after* all other modes, and can override the
# badge choices of other modes.
# Note. We also create the the one-and-only instance, here.
new class BadgeMode extends Mode
constructor: (options) ->
super
name: "badge"
trackState: true
@push
"focus": => @alwaysContinueBubbling => Mode.updateBadge()
chooseBadge: (badge) ->
# If we're not enabled, then post an empty badge.
badge.badge = "" unless @enabled
registerStateChange: ->
Mode.updateBadge()
root = exports ? window
root.Mode = Mode
|