]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Consistently use `event.originalTarget`
[VimFx.git] / extension / lib / utils.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 # Copyright Wang Zhuochun 2013.
5 #
6 # This file is part of VimFx.
7 #
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
20 ###
21
22 # This file contains lots of different helper functions.
23
24 HTMLAnchorElement = Ci.nsIDOMHTMLAnchorElement
25 HTMLButtonElement = Ci.nsIDOMHTMLButtonElement
26 HTMLInputElement = Ci.nsIDOMHTMLInputElement
27 HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement
28 HTMLSelectElement = Ci.nsIDOMHTMLSelectElement
29 HTMLFrameElement = Ci.nsIDOMHTMLFrameElement
30 HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement
31 XULDocument = Ci.nsIDOMXULDocument
32 XULButtonElement = Ci.nsIDOMXULButtonElement
33 XULControlElement = Ci.nsIDOMXULControlElement
34 XULMenuListElement = Ci.nsIDOMXULMenuListElement
35 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
36
37 USE_CAPTURE = true
38
39
40
41 # Element classification helpers
42
43 isActivatable = (element) ->
44 return element instanceof HTMLAnchorElement or
45 element instanceof HTMLButtonElement or
46 (element instanceof HTMLInputElement and element.type in [
47 'button', 'submit', 'reset', 'image'
48 ]) or
49 element instanceof XULButtonElement
50
51 isAdjustable = (element) ->
52 return element instanceof HTMLInputElement and element.type in [
53 'checkbox', 'radio', 'file', 'color'
54 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
55 ] or
56 element instanceof XULControlElement or
57 # Youtube special case.
58 element.classList?.contains('html5-video-player') or
59 element.classList?.contains('ytp-button')
60
61 isContentEditable = (element) ->
62 return element.isContentEditable or
63 # `g_editable` is a non-standard attribute commonly used by Google.
64 element.getAttribute?('g_editable') == 'true' or
65 element.ownerDocument?.body?.getAttribute('g_editable') == 'true'
66
67 isProperLink = (element) ->
68 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
69 # href="">`s used as buttons on some sites.
70 return element.getAttribute('href') and
71 (element instanceof HTMLAnchorElement or
72 element.ownerDocument instanceof XULDocument) and
73 not element.href.endsWith('#') and
74 not element.href.startsWith('javascript:')
75
76 isTextInputElement = (element) ->
77 return (element instanceof HTMLInputElement and element.type in [
78 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
79 ]) or
80 element instanceof HTMLTextAreaElement or
81 # `<select>` elements can also receive text input: You may type the
82 # text of an item to select it.
83 element instanceof HTMLSelectElement or
84 element instanceof XULMenuListElement or
85 element instanceof XULTextBoxElement
86
87
88
89 # Active/focused element helpers
90
91 getActiveElement = (window) ->
92 { activeElement } = window.document
93 if activeElement instanceof HTMLFrameElement or
94 activeElement instanceof HTMLIFrameElement
95 return getActiveElement(activeElement.contentWindow)
96 else
97 return activeElement
98
99 blurActiveElement = (window) ->
100 # Only blur focusable elements, in order to interfere with the browser as
101 # little as possible.
102 activeElement = getActiveElement(window)
103 if activeElement and activeElement.tabIndex > -1
104 activeElement.blur()
105
106 blurActiveBrowserElement = (vim) ->
107 # - Some browser UI elements, such as the web console, are not marked as
108 # focusable, so we can’t check if the element is focusable as in
109 # `blurActiveElement`.
110 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
111 # reset it, for example.
112 # - Focusing the current browser afterwards allows to pass `<escape>` as well
113 # as unbound keys to the page. However, focusing the browser also triggers
114 # focus events on `document` and `window` in the current page. Many pages
115 # re-focus some text input on those events, making it impossible to blur
116 # those! Therefore we tell the frame script to suppress those events.
117 { window } = vim
118 activeElement = getActiveElement(window)
119 vim._send('browserRefocus')
120 callback = ->
121 activeElement.blur()
122 window.gBrowser.selectedBrowser.focus()
123 window.setTimeout(callback, 0)
124
125 # Focus an element and tell Firefox that the focus happened because of a user
126 # keypress (not just because some random programmatic focus).
127 focusElement = (element, options = {}) ->
128 focusManager = Cc['@mozilla.org/focus-manager;1']
129 .getService(Ci.nsIFocusManager)
130 focusManager.setFocus(element, focusManager.FLAG_BYKEY)
131 element.select?() if options.select
132
133 moveFocus = (direction) ->
134 focusManager = Cc['@mozilla.org/focus-manager;1']
135 .getService(Ci.nsIFocusManager)
136 directionFlag =
137 if direction == -1
138 focusManager.MOVEFOCUS_BACKWARD
139 else
140 focusManager.MOVEFOCUS_FORWARD
141 focusManager.moveFocus(
142 null, # Use current window.
143 null, # Move relative to the currently focused element.
144 directionFlag,
145 focusManager.FLAG_BYKEY
146 )
147
148 getFocusType = (event) ->
149 target = event.originalTarget
150 return switch
151 when isTextInputElement(target) or isContentEditable(target)
152 'editable'
153 when isActivatable(target)
154 'activatable'
155 when isAdjustable(target)
156 'adjustable'
157 else
158 null
159
160
161
162 # Event helpers
163
164 listen = (element, eventName, listener, useCapture = true) ->
165 element.addEventListener(eventName, listener, useCapture)
166 module.onShutdown(->
167 element.removeEventListener(eventName, listener, useCapture)
168 )
169
170 listenOnce = (element, eventName, listener, useCapture = true) ->
171 fn = (event) ->
172 listener(event)
173 element.removeEventListener(eventName, fn, useCapture)
174 listen(element, eventName, fn, useCapture)
175
176 suppressEvent = (event) ->
177 event.preventDefault()
178 event.stopPropagation()
179
180 # Simulate mouse click with a full chain of events. ('command' is for XUL
181 # elements.)
182 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
183 simulateClick = (element) ->
184 window = element.ownerDocument.defaultView
185 simulatedEvents = {}
186 for type in eventSequence
187 mouseEvent = new window.MouseEvent(type, {
188 # Let the event bubble in order to trigger delegated event listeners.
189 bubbles: true
190 # Make the event cancelable so that `<a href="#">` can be used as a
191 # JavaScript-powered button without scrolling to the top of the page.
192 cancelable: true
193 })
194 element.dispatchEvent(mouseEvent)
195 simulatedEvents[type] = mouseEvent
196 return simulatedEvents
197
198
199
200 # DOM helpers
201
202 area = (element) ->
203 return element.clientWidth * element.clientHeight
204
205 createBox = (document, className, parent = null, text = null) ->
206 box = document.createElement('box')
207 box.className = className
208 box.textContent = text if text?
209 parent.appendChild(box) if parent?
210 return box
211
212 insertText = (input, value) ->
213 { selectionStart, selectionEnd } = input
214 input.value =
215 input.value[0...selectionStart] + value + input.value[selectionEnd..]
216 input.selectionStart = input.selectionEnd = selectionStart + value.length
217
218 setAttributes = (element, attributes) ->
219 for attribute, value of attributes
220 element.setAttribute(attribute, value)
221 return
222
223
224
225 # Language helpers
226
227 class Counter
228 constructor: ({ start: @value = 0, @step = 1 }) ->
229 tick: -> @value += @step
230
231 class EventEmitter
232 constructor: ->
233 @listeners = {}
234
235 on: (event, listener) ->
236 (@listeners[event] ?= []).push(listener)
237
238 emit: (event, data) ->
239 for listener in @listeners[event] ? []
240 listener(data)
241 return
242
243 has = Function::call.bind(Object::hasOwnProperty)
244
245 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
246
247 removeDuplicates = (array) ->
248 # coffeelint: disable=no_backticks
249 return `[...new Set(array)]`
250 # coffeelint: enable=no_backticks
251
252 # Remove duplicate characters from string (case insensitive).
253 removeDuplicateCharacters = (str) ->
254 return removeDuplicates( str.toLowerCase().split('') ).join('')
255
256
257
258 # Misc helpers
259
260 formatError = (error) ->
261 stack = String(error.stack?.formattedStack ? error.stack ? '')
262 .split('\n')
263 .filter((line) -> line.includes('.xpi!'))
264 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
265 .join('\n')
266 return "#{ error }\n#{ stack }"
267
268 getCurrentLocation = ->
269 window = getCurrentWindow()
270 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
271
272 getCurrentWindow = ->
273 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
274 .getService(Components.interfaces.nsIWindowMediator)
275 return windowMediator.getMostRecentWindow('navigator:browser')
276
277 loadCss = (name) ->
278 sss = Cc['@mozilla.org/content/style-sheet-service;1']
279 .getService(Ci.nsIStyleSheetService)
280 uri = Services.io.newURI("chrome://vimfx/skin/#{ name }.css", null, null)
281 method = sss.AUTHOR_SHEET
282 unless sss.sheetRegistered(uri, method)
283 sss.loadAndRegisterSheet(uri, method)
284 module.onShutdown(->
285 sss.unregisterSheet(uri, method)
286 )
287
288 observe = (topic, observer) ->
289 observer = {observe: observer} if typeof observer == 'function'
290 Services.obs.addObserver(observer, topic, false)
291 module.onShutdown(->
292 Services.obs.removeObserver(observer, topic, false)
293 )
294
295 openTab = (window, url, options) ->
296 { gBrowser } = window
297 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
298 gBrowser.loadOneTab(url, options)
299
300 # Executes `fn` and measures how much time it took.
301 timeIt = (fn, name) ->
302 console.time(name)
303 result = fn()
304 console.timeEnd(name)
305 return result
306
307 writeToClipboard = (text) ->
308 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
309 .getService(Ci.nsIClipboardHelper)
310 clipboardHelper.copyString(text)
311
312
313
314 module.exports = {
315 isActivatable
316 isAdjustable
317 isContentEditable
318 isProperLink
319 isTextInputElement
320
321 getActiveElement
322 blurActiveElement
323 blurActiveBrowserElement
324 focusElement
325 moveFocus
326 getFocusType
327
328 listen
329 listenOnce
330 suppressEvent
331 simulateClick
332
333 area
334 createBox
335 insertText
336 setAttributes
337
338 Counter
339 EventEmitter
340 has
341 regexEscape
342 removeDuplicates
343 removeDuplicateCharacters
344
345 formatError
346 getCurrentLocation
347 getCurrentWindow
348 loadCss
349 observe
350 openTab
351 timeIt
352 writeToClipboard
353 }
Imprint / Impressum