]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix scrolling in pages without a doctype
[VimFx.git] / extension / lib / utils.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015.
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, extraAllowedElements = null) ->
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
104 (activeElement.tabIndex > -1 or extraAllowedElements?.has(activeElement))
105 activeElement.blur()
106
107 blurActiveBrowserElement = (vim) ->
108 # - Some browser UI elements, such as the web console, are not marked as
109 # focusable, so we can’t check if the element is focusable as in
110 # `blurActiveElement`.
111 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
112 # reset it, for example.
113 # - Focusing the current browser afterwards allows to pass `<escape>` as well
114 # as unbound keys to the page. However, focusing the browser also triggers
115 # focus events on `document` and `window` in the current page. Many pages
116 # re-focus some text input on those events, making it impossible to blur
117 # those! Therefore we tell the frame script to suppress those events.
118 { window } = vim
119 activeElement = getActiveElement(window)
120 vim._send('browserRefocus')
121 nextTick(window, ->
122 activeElement.blur()
123 window.gBrowser.selectedBrowser.focus()
124 )
125
126 # Focus an element and tell Firefox that the focus happened because of a user
127 # keypress (not just because some random programmatic focus).
128 focusElement = (element, options = {}) ->
129 focusManager = Cc['@mozilla.org/focus-manager;1']
130 .getService(Ci.nsIFocusManager)
131 focusManager.setFocus(element, focusManager.FLAG_BYKEY)
132 element.select?() if options.select
133
134 moveFocus = (direction) ->
135 focusManager = Cc['@mozilla.org/focus-manager;1']
136 .getService(Ci.nsIFocusManager)
137 directionFlag =
138 if direction == -1
139 focusManager.MOVEFOCUS_BACKWARD
140 else
141 focusManager.MOVEFOCUS_FORWARD
142 focusManager.moveFocus(
143 null, # Use current window.
144 null, # Move relative to the currently focused element.
145 directionFlag,
146 focusManager.FLAG_BYKEY
147 )
148
149 getFocusType = (event) ->
150 target = event.originalTarget
151 return switch
152 when isTextInputElement(target) or isContentEditable(target)
153 'editable'
154 when isActivatable(target)
155 'activatable'
156 when isAdjustable(target)
157 'adjustable'
158 else
159 null
160
161
162
163 # Event helpers
164
165 listen = (element, eventName, listener, useCapture = true) ->
166 element.addEventListener(eventName, listener, useCapture)
167 module.onShutdown(->
168 element.removeEventListener(eventName, listener, useCapture)
169 )
170
171 listenOnce = (element, eventName, listener, useCapture = true) ->
172 fn = (event) ->
173 listener(event)
174 element.removeEventListener(eventName, fn, useCapture)
175 listen(element, eventName, fn, useCapture)
176
177 suppressEvent = (event) ->
178 event.preventDefault()
179 event.stopPropagation()
180
181 # Simulate mouse click with a full chain of events. ('command' is for XUL
182 # elements.)
183 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
184 simulateClick = (element) ->
185 window = element.ownerDocument.defaultView
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 return
196
197
198
199 # DOM helpers
200
201 area = (element) ->
202 return element.clientWidth * element.clientHeight
203
204 createBox = (document, className, parent = null, text = null) ->
205 box = document.createElement('box')
206 box.className = className
207 box.textContent = text if text?
208 parent.appendChild(box) if parent?
209 return box
210
211 insertText = (input, value) ->
212 { selectionStart, selectionEnd } = input
213 input.value =
214 input.value[0...selectionStart] + value + input.value[selectionEnd..]
215 input.selectionStart = input.selectionEnd = selectionStart + value.length
216
217 setAttributes = (element, attributes) ->
218 for attribute, value of attributes
219 element.setAttribute(attribute, value)
220 return
221
222
223
224 # Language helpers
225
226 class Counter
227 constructor: ({ start: @value = 0, @step = 1 }) ->
228 tick: -> @value += @step
229
230 class EventEmitter
231 constructor: ->
232 @listeners = {}
233
234 on: (event, listener) ->
235 (@listeners[event] ?= []).push(listener)
236
237 emit: (event, data) ->
238 for listener in @listeners[event] ? []
239 listener(data)
240 return
241
242 has = Function::call.bind(Object::hasOwnProperty)
243
244 nextTick = (window, fn) -> window.setTimeout(fn, 0)
245
246 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
247
248 removeDuplicates = (array) ->
249 # coffeelint: disable=no_backticks
250 return `[...new Set(array)]`
251 # coffeelint: enable=no_backticks
252
253 # Remove duplicate characters from string (case insensitive).
254 removeDuplicateCharacters = (str) ->
255 return removeDuplicates( str.toLowerCase().split('') ).join('')
256
257
258
259 # Misc helpers
260
261 formatError = (error) ->
262 stack = String(error.stack?.formattedStack ? error.stack ? '')
263 .split('\n')
264 .filter((line) -> line.includes('.xpi!'))
265 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
266 .join('\n')
267 return "#{ error }\n#{ stack }"
268
269 getCurrentLocation = ->
270 window = getCurrentWindow()
271 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
272
273 getCurrentWindow = ->
274 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
275 .getService(Components.interfaces.nsIWindowMediator)
276 return windowMediator.getMostRecentWindow('navigator:browser')
277
278 loadCss = (name) ->
279 sss = Cc['@mozilla.org/content/style-sheet-service;1']
280 .getService(Ci.nsIStyleSheetService)
281 uri = Services.io.newURI("chrome://vimfx/skin/#{ name }.css", null, null)
282 method = sss.AUTHOR_SHEET
283 unless sss.sheetRegistered(uri, method)
284 sss.loadAndRegisterSheet(uri, method)
285 module.onShutdown(->
286 sss.unregisterSheet(uri, method)
287 )
288
289 observe = (topic, observer) ->
290 observer = {observe: observer} if typeof observer == 'function'
291 Services.obs.addObserver(observer, topic, false)
292 module.onShutdown(->
293 Services.obs.removeObserver(observer, topic, false)
294 )
295
296 openTab = (window, url, options) ->
297 { gBrowser } = window
298 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
299 gBrowser.loadOneTab(url, options)
300
301 # Executes `fn` and measures how much time it took.
302 timeIt = (fn, name) ->
303 console.time(name)
304 result = fn()
305 console.timeEnd(name)
306 return result
307
308 writeToClipboard = (text) ->
309 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
310 .getService(Ci.nsIClipboardHelper)
311 clipboardHelper.copyString(text)
312
313
314
315 module.exports = {
316 isActivatable
317 isAdjustable
318 isContentEditable
319 isProperLink
320 isTextInputElement
321
322 getActiveElement
323 blurActiveElement
324 blurActiveBrowserElement
325 focusElement
326 moveFocus
327 getFocusType
328
329 listen
330 listenOnce
331 suppressEvent
332 simulateClick
333
334 area
335 createBox
336 insertText
337 setAttributes
338
339 Counter
340 EventEmitter
341 has
342 nextTick
343 regexEscape
344 removeDuplicates
345 removeDuplicateCharacters
346
347 formatError
348 getCurrentLocation
349 getCurrentWindow
350 loadCss
351 observe
352 openTab
353 timeIt
354 writeToClipboard
355 }
Imprint / Impressum