]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Make it possible to scroll browser elements
[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 XULDocument = Ci.nsIDOMXULDocument
30 XULButtonElement = Ci.nsIDOMXULButtonElement
31 XULControlElement = Ci.nsIDOMXULControlElement
32 XULMenuListElement = Ci.nsIDOMXULMenuListElement
33 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
34
35 USE_CAPTURE = true
36
37
38
39 # Element classification helpers
40
41 isActivatable = (element) ->
42 return element instanceof HTMLAnchorElement or
43 element instanceof HTMLButtonElement or
44 (element instanceof HTMLInputElement and element.type in [
45 'button', 'submit', 'reset', 'image'
46 ]) or
47 element instanceof XULButtonElement
48
49 isAdjustable = (element) ->
50 return element instanceof HTMLInputElement and element.type in [
51 'checkbox', 'radio', 'file', 'color'
52 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
53 ] or
54 element instanceof XULControlElement or
55 # Youtube special case.
56 element.classList?.contains('html5-video-player') or
57 element.classList?.contains('ytp-button')
58
59 isContentEditable = (element) ->
60 return element.isContentEditable or
61 # `g_editable` is a non-standard attribute commonly used by Google.
62 element.getAttribute?('g_editable') == 'true' or
63 element.ownerDocument?.body?.getAttribute('g_editable') == 'true'
64
65 isProperLink = (element) ->
66 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
67 # href="">`s used as buttons on some sites.
68 return element.getAttribute('href') and
69 (element instanceof HTMLAnchorElement or
70 element.ownerDocument instanceof XULDocument) and
71 not element.href.endsWith('#') and
72 not element.href.endsWith('#?') and
73 not element.href.startsWith('javascript:')
74
75 isTextInputElement = (element) ->
76 return (element instanceof HTMLInputElement and element.type in [
77 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
78 ]) or
79 element instanceof HTMLTextAreaElement or
80 element instanceof XULTextBoxElement
81
82 isTypingElement = (element) ->
83 return isTextInputElement(element) or
84 # `<select>` elements can also receive text input: You may type the
85 # text of an item to select it.
86 element instanceof HTMLSelectElement or
87 element instanceof XULMenuListElement
88
89
90
91 # Active/focused element helpers
92
93 getActiveElement = (window) ->
94 {activeElement} = window.document
95 # If the active element is a frame, recurse into it. The easiest way to detect
96 # a frame that works both in browser UI and in web page content is to check
97 # for the presence of `.contentWindow`. However, in non-multi-process
98 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
99 # pointing to the web page content `window`, which we don’t want to recurse
100 # into. `.localName` is `.nodeName` without `xul:` (if it exists). This seems
101 # to be the only way to detect such elements.
102 if activeElement.localName != 'browser' and activeElement.contentWindow
103 return getActiveElement(activeElement.contentWindow)
104 else
105 return activeElement
106
107 blurActiveElement = (window) ->
108 return unless activeElement = getActiveElement(window)
109 activeElement.blur()
110
111 blurActiveBrowserElement = (vim) ->
112 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
113 # reset it, for example.
114 # - Focusing the current browser afterwards allows to pass `<escape>` as well
115 # as unbound keys to the page. However, focusing the browser also triggers
116 # focus events on `document` and `window` in the current page. Many pages
117 # re-focus some text input on those events, making it impossible to blur
118 # those! Therefore we tell the frame script to suppress those events.
119 {window} = vim
120 activeElement = getActiveElement(window)
121 vim._send('browserRefocus')
122 nextTick(window, ->
123 activeElement.blur()
124 window.gBrowser.selectedBrowser.focus()
125 )
126
127 # Focus an element and tell Firefox that the focus happened because of a user
128 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
129 # look more appropriate, but it unconditionally selects all text, which
130 # `.FLAG_BYMOUSE` does not.
131 focusElement = (element, options = {}) ->
132 focusManager = Cc['@mozilla.org/focus-manager;1']
133 .getService(Ci.nsIFocusManager)
134 focusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
135 element.select?() if options.select
136
137 getFocusType = (element) -> switch
138 when isTypingElement(element) or isContentEditable(element)
139 'editable'
140 when isActivatable(element)
141 'activatable'
142 when isAdjustable(element)
143 'adjustable'
144 else
145 null
146
147
148
149 # Event helpers
150
151 listen = (element, eventName, listener, useCapture = true) ->
152 element.addEventListener(eventName, listener, useCapture)
153 module.onShutdown(->
154 element.removeEventListener(eventName, listener, useCapture)
155 )
156
157 listenOnce = (element, eventName, listener, useCapture = true) ->
158 fn = (event) ->
159 listener(event)
160 element.removeEventListener(eventName, fn, useCapture)
161 listen(element, eventName, fn, useCapture)
162
163 onRemoved = (window, element, fn) ->
164 mutationObserver = new window.MutationObserver((changes) ->
165 for change in changes then for removedElement in change.removedNodes
166 if removedElement == element
167 mutationObserver.disconnect()
168 fn()
169 return
170 )
171 mutationObserver.observe(element.parentNode, {childList: true})
172 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
173
174 suppressEvent = (event) ->
175 event.preventDefault()
176 event.stopPropagation()
177
178 # Simulate mouse click with a full chain of events. ('command' is for XUL
179 # elements.)
180 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
181 simulateClick = (element) ->
182 window = element.ownerGlobal
183 for type in eventSequence
184 mouseEvent = new window.MouseEvent(type, {
185 # Let the event bubble in order to trigger delegated event listeners.
186 bubbles: true
187 # Make the event cancelable so that `<a href="#">` can be used as a
188 # JavaScript-powered button without scrolling to the top of the page.
189 cancelable: true
190 })
191 element.dispatchEvent(mouseEvent)
192 return
193
194
195
196 # DOM helpers
197
198 area = (element) ->
199 return element.clientWidth * element.clientHeight
200
201 containsDeep = (parent, element) ->
202 parentWindow = parent.ownerGlobal
203 elementWindow = element.ownerGlobal
204
205 while elementWindow != parentWindow and elementWindow.top != elementWindow
206 element = elementWindow.frameElement
207 elementWindow = element.ownerGlobal
208
209 return parent.contains(element)
210
211 createBox = (document, className = '', parent = null, text = null) ->
212 box = document.createElement('box')
213 box.className = "#{className} vimfx-box"
214 box.textContent = text if text?
215 parent.appendChild(box) if parent?
216 return box
217
218 injectTemporaryPopup = (document, contents) ->
219 popup = document.createElement('menupopup')
220 popup.appendChild(contents)
221 document.getElementById('mainPopupSet').appendChild(popup)
222 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
223 return popup
224
225 insertText = (input, value) ->
226 {selectionStart, selectionEnd} = input
227 input.value =
228 input.value[0...selectionStart] + value + input.value[selectionEnd..]
229 input.selectionStart = input.selectionEnd = selectionStart + value.length
230
231 querySelectorAllDeep = (window, selector) ->
232 elements = Array.from(window.document.querySelectorAll(selector))
233 for frame in window.frames
234 elements.push(querySelectorAllDeep(frame, selector)...)
235 return elements
236
237 scroll = (element, args) ->
238 {method, type, directions, amounts, properties, adjustment, smooth} = args
239 options = {}
240 for direction, index in directions
241 amount = amounts[index]
242 options[direction] = -Math.sign(amount) * adjustment + switch type
243 when 'lines' then amount
244 when 'pages' then amount * element[properties[index]]
245 when 'other' then Math.min(amount, element[properties[index]])
246 options.behavior = 'smooth' if smooth
247 element[method](options)
248
249 setAttributes = (element, attributes) ->
250 for attribute, value of attributes
251 element.setAttribute(attribute, value)
252 return
253
254
255
256 # Language helpers
257
258 class Counter
259 constructor: ({start: @value = 0, @step = 1}) ->
260 tick: -> @value += @step
261
262 class EventEmitter
263 constructor: ->
264 @listeners = {}
265
266 on: (event, listener) ->
267 (@listeners[event] ?= []).push(listener)
268
269 emit: (event, data) ->
270 for listener in @listeners[event] ? []
271 listener(data)
272 return
273
274 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
275
276 nextTick = (window, fn) -> window.setTimeout(fn, 0)
277
278 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
279
280 removeDuplicates = (array) -> Array.from(new Set(array))
281
282 # Remove duplicate characters from string (case insensitive).
283 removeDuplicateCharacters = (str) ->
284 return removeDuplicates( str.toLowerCase().split('') ).join('')
285
286
287
288 # Misc helpers
289
290 formatError = (error) ->
291 stack = String(error.stack?.formattedStack ? error.stack ? '')
292 .split('\n')
293 .filter((line) -> line.includes('.xpi!'))
294 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
295 .join('\n')
296 return "#{error}\n#{stack}"
297
298 getCurrentLocation = ->
299 window = getCurrentWindow()
300 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
301
302 getCurrentWindow = ->
303 windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
304 .getService(Components.interfaces.nsIWindowMediator)
305 return windowMediator.getMostRecentWindow('navigator:browser')
306
307 loadCss = (name) ->
308 sss = Cc['@mozilla.org/content/style-sheet-service;1']
309 .getService(Ci.nsIStyleSheetService)
310 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
311 method = sss.AUTHOR_SHEET
312 unless sss.sheetRegistered(uri, method)
313 sss.loadAndRegisterSheet(uri, method)
314 module.onShutdown(->
315 sss.unregisterSheet(uri, method)
316 )
317
318 observe = (topic, observer) ->
319 observer = {observe: observer} if typeof observer == 'function'
320 Services.obs.addObserver(observer, topic, false)
321 module.onShutdown(->
322 Services.obs.removeObserver(observer, topic, false)
323 )
324
325 openPopup = (popup) ->
326 window = popup.ownerGlobal
327 # Show the popup so it gets a height and width.
328 popup.openPopupAtScreen(0, 0)
329 # Center the popup inside the window.
330 popup.moveTo(
331 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
332 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
333 )
334
335 openTab = (window, url, options) ->
336 {gBrowser} = window
337 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
338 gBrowser.loadOneTab(url, options)
339
340 writeToClipboard = (text) ->
341 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
342 .getService(Ci.nsIClipboardHelper)
343 clipboardHelper.copyString(text)
344
345
346
347 module.exports = {
348 isActivatable
349 isAdjustable
350 isContentEditable
351 isProperLink
352 isTextInputElement
353 isTypingElement
354
355 getActiveElement
356 blurActiveElement
357 blurActiveBrowserElement
358 focusElement
359 getFocusType
360
361 listen
362 listenOnce
363 onRemoved
364 suppressEvent
365 simulateClick
366
367 area
368 containsDeep
369 createBox
370 injectTemporaryPopup
371 insertText
372 querySelectorAllDeep
373 scroll
374 setAttributes
375
376 Counter
377 EventEmitter
378 has
379 nextTick
380 regexEscape
381 removeDuplicates
382 removeDuplicateCharacters
383
384 formatError
385 getCurrentLocation
386 getCurrentWindow
387 loadCss
388 observe
389 openPopup
390 openTab
391 writeToClipboard
392 }
Imprint / Impressum