]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix scrolling issues on Gmail
[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.endsWith('#?') and
75 not element.href.startsWith('javascript:')
76
77 isTextInputElement = (element) ->
78 return (element instanceof HTMLInputElement and element.type in [
79 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
80 ]) or
81 element instanceof HTMLTextAreaElement or
82 element instanceof XULTextBoxElement
83
84 isTypingElement = (element) ->
85 return isTextInputElement(element) or
86 # `<select>` elements can also receive text input: You may type the
87 # text of an item to select it.
88 element instanceof HTMLSelectElement or
89 element instanceof XULMenuListElement
90
91
92
93 # Active/focused element helpers
94
95 getActiveElement = (window) ->
96 {activeElement} = window.document
97 if activeElement instanceof HTMLFrameElement or
98 activeElement instanceof HTMLIFrameElement
99 return getActiveElement(activeElement.contentWindow)
100 else
101 return activeElement
102
103 blurActiveElement = (window) ->
104 return unless activeElement = getActiveElement(window)
105 activeElement.blur()
106
107 blurActiveBrowserElement = (vim) ->
108 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
109 # reset it, for example.
110 # - Focusing the current browser afterwards allows to pass `<escape>` as well
111 # as unbound keys to the page. However, focusing the browser also triggers
112 # focus events on `document` and `window` in the current page. Many pages
113 # re-focus some text input on those events, making it impossible to blur
114 # those! Therefore we tell the frame script to suppress those events.
115 {window} = vim
116 activeElement = getActiveElement(window)
117 vim._send('browserRefocus')
118 nextTick(window, ->
119 activeElement.blur()
120 window.gBrowser.selectedBrowser.focus()
121 )
122
123 # Focus an element and tell Firefox that the focus happened because of a user
124 # keypress (not just because some random programmatic focus).
125 focusElement = (element, options = {}) ->
126 focusManager = Cc['@mozilla.org/focus-manager;1']
127 .getService(Ci.nsIFocusManager)
128 focusManager.setFocus(element, focusManager.FLAG_BYKEY)
129 element.select?() if options.select
130
131 moveFocus = (direction) ->
132 focusManager = Cc['@mozilla.org/focus-manager;1']
133 .getService(Ci.nsIFocusManager)
134 directionFlag =
135 if direction == -1
136 focusManager.MOVEFOCUS_BACKWARD
137 else
138 focusManager.MOVEFOCUS_FORWARD
139 focusManager.moveFocus(
140 null, # Use current window.
141 null, # Move relative to the currently focused element.
142 directionFlag,
143 focusManager.FLAG_BYKEY
144 )
145
146 getFocusType = (event) ->
147 target = event.originalTarget
148 return switch
149 when isTypingElement(target) or isContentEditable(target)
150 'editable'
151 when isActivatable(target)
152 'activatable'
153 when isAdjustable(target)
154 'adjustable'
155 else
156 null
157
158
159
160 # Event helpers
161
162 listen = (element, eventName, listener, useCapture = true) ->
163 element.addEventListener(eventName, listener, useCapture)
164 module.onShutdown(->
165 element.removeEventListener(eventName, listener, useCapture)
166 )
167
168 listenOnce = (element, eventName, listener, useCapture = true) ->
169 fn = (event) ->
170 listener(event)
171 element.removeEventListener(eventName, fn, useCapture)
172 listen(element, eventName, fn, useCapture)
173
174 onRemoved = (window, element, fn) ->
175 mutationObserver = new window.MutationObserver((changes) ->
176 for change in changes then for removedElement in change.removedNodes
177 if removedElement == element
178 mutationObserver.disconnect()
179 fn()
180 return
181 )
182 mutationObserver.observe(element.parentNode, {childList: true})
183 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
184
185 suppressEvent = (event) ->
186 event.preventDefault()
187 event.stopPropagation()
188
189 # Simulate mouse click with a full chain of events. ('command' is for XUL
190 # elements.)
191 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
192 simulateClick = (element) ->
193 window = element.ownerDocument.defaultView
194 for type in eventSequence
195 mouseEvent = new window.MouseEvent(type, {
196 # Let the event bubble in order to trigger delegated event listeners.
197 bubbles: true
198 # Make the event cancelable so that `<a href="#">` can be used as a
199 # JavaScript-powered button without scrolling to the top of the page.
200 cancelable: true
201 })
202 element.dispatchEvent(mouseEvent)
203 return
204
205
206
207 # DOM helpers
208
209 area = (element) ->
210 return element.clientWidth * element.clientHeight
211
212 createBox = (document, className, parent = null, text = null) ->
213 box = document.createElement('box')
214 box.className = className
215 box.textContent = text if text?
216 parent.appendChild(box) if parent?
217 return box
218
219 insertText = (input, value) ->
220 {selectionStart, selectionEnd} = input
221 input.value =
222 input.value[0...selectionStart] + value + input.value[selectionEnd..]
223 input.selectionStart = input.selectionEnd = selectionStart + value.length
224
225 querySelectorAllDeep = (window, selector) ->
226 elements = Array.from(window.document.querySelectorAll(selector))
227 for frame in window.frames
228 elements.push(querySelectorAllDeep(frame, selector)...)
229 return elements
230
231 setAttributes = (element, attributes) ->
232 for attribute, value of attributes
233 element.setAttribute(attribute, value)
234 return
235
236
237
238 # Language helpers
239
240 class Counter
241 constructor: ({start: @value = 0, @step = 1}) ->
242 tick: -> @value += @step
243
244 class EventEmitter
245 constructor: ->
246 @listeners = {}
247
248 on: (event, listener) ->
249 (@listeners[event] ?= []).push(listener)
250
251 emit: (event, data) ->
252 for listener in @listeners[event] ? []
253 listener(data)
254 return
255
256 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
257
258 nextTick = (window, fn) -> window.setTimeout(fn, 0)
259
260 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
261
262 removeDuplicates = (array) -> Array.from(new Set(array))
263
264 # Remove duplicate characters from string (case insensitive).
265 removeDuplicateCharacters = (str) ->
266 return removeDuplicates( str.toLowerCase().split('') ).join('')
267
268
269
270 # Misc helpers
271
272 formatError = (error) ->
273 stack = String(error.stack?.formattedStack ? error.stack ? '')
274 .split('\n')
275 .filter((line) -> line.includes('.xpi!'))
276 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
277 .join('\n')
278 return "#{error}\n#{stack}"
279
280 getCurrentLocation = ->
281 window = getCurrentWindow()
282 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
283
284 getCurrentWindow = ->
285 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
286 .getService(Components.interfaces.nsIWindowMediator)
287 return windowMediator.getMostRecentWindow('navigator:browser')
288
289 loadCss = (name) ->
290 sss = Cc['@mozilla.org/content/style-sheet-service;1']
291 .getService(Ci.nsIStyleSheetService)
292 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
293 method = sss.AUTHOR_SHEET
294 unless sss.sheetRegistered(uri, method)
295 sss.loadAndRegisterSheet(uri, method)
296 module.onShutdown(->
297 sss.unregisterSheet(uri, method)
298 )
299
300 observe = (topic, observer) ->
301 observer = {observe: observer} if typeof observer == 'function'
302 Services.obs.addObserver(observer, topic, false)
303 module.onShutdown(->
304 Services.obs.removeObserver(observer, topic, false)
305 )
306
307 openTab = (window, url, options) ->
308 {gBrowser} = window
309 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
310 gBrowser.loadOneTab(url, options)
311
312 writeToClipboard = (text) ->
313 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
314 .getService(Ci.nsIClipboardHelper)
315 clipboardHelper.copyString(text)
316
317
318
319 module.exports = {
320 isActivatable
321 isAdjustable
322 isContentEditable
323 isProperLink
324 isTextInputElement
325 isTypingElement
326
327 getActiveElement
328 blurActiveElement
329 blurActiveBrowserElement
330 focusElement
331 moveFocus
332 getFocusType
333
334 listen
335 listenOnce
336 onRemoved
337 suppressEvent
338 simulateClick
339
340 area
341 createBox
342 insertText
343 querySelectorAllDeep
344 setAttributes
345
346 Counter
347 EventEmitter
348 has
349 nextTick
350 regexEscape
351 removeDuplicates
352 removeDuplicateCharacters
353
354 formatError
355 getCurrentLocation
356 getCurrentWindow
357 loadCss
358 observe
359 openTab
360 writeToClipboard
361 }
Imprint / Impressum