]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Get XPCOM interfaces once at the top of files
[VimFx.git] / extension / lib / utils.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015, 2016.
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 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
25 .getService(Ci.nsIClipboardHelper)
26 nsIDomUtils = Cc['@mozilla.org/inspector/dom-utils;1']
27 .getService(Ci.inIDOMUtils)
28 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
29 .getService(Ci.nsIFocusManager)
30 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
31 .getService(Ci.nsIStyleSheetService)
32 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
33 .getService(Ci.nsIWindowMediator)
34
35 HTMLAnchorElement = Ci.nsIDOMHTMLAnchorElement
36 HTMLButtonElement = Ci.nsIDOMHTMLButtonElement
37 HTMLInputElement = Ci.nsIDOMHTMLInputElement
38 HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement
39 HTMLSelectElement = Ci.nsIDOMHTMLSelectElement
40 HTMLBodyElement = Ci.nsIDOMHTMLBodyElement
41 XULDocument = Ci.nsIDOMXULDocument
42 XULButtonElement = Ci.nsIDOMXULButtonElement
43 XULControlElement = Ci.nsIDOMXULControlElement
44 XULMenuListElement = Ci.nsIDOMXULMenuListElement
45 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
46
47 # Full chains of events for different mouse actions. ('command' is for XUL
48 # elements.)
49 EVENTS_CLICK = ['mousedown', 'mouseup', 'click', 'command']
50 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
51 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
52
53
54
55 # Element classification helpers
56
57 isActivatable = (element) ->
58 return element instanceof HTMLAnchorElement or
59 element instanceof HTMLButtonElement or
60 (element instanceof HTMLInputElement and element.type in [
61 'button', 'submit', 'reset', 'image'
62 ]) or
63 element instanceof XULButtonElement
64
65 isAdjustable = (element) ->
66 return element instanceof HTMLInputElement and element.type in [
67 'checkbox', 'radio', 'file', 'color'
68 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
69 ] or
70 element instanceof XULControlElement or
71 # Youtube special case.
72 element.classList?.contains('html5-video-player') or
73 element.classList?.contains('ytp-button')
74
75 isContentEditable = (element) ->
76 return element.isContentEditable or
77 isIframeEditor(element) or
78 # Google.
79 element.getAttribute?('g_editable') == 'true' or
80 element.ownerDocument?.body?.getAttribute('g_editable') == 'true'
81
82 isIframeEditor = (element) ->
83 return false unless element instanceof HTMLBodyElement
84 return \
85 # Etherpad.
86 element.id == 'innerdocbody' or
87 # XpressEditor.
88 (element.classList.contains('xe_content') and
89 element.classList.contains('editable')) or
90 # vBulletin.
91 element.classList.contains('wysiwyg')
92
93 isProperLink = (element) ->
94 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
95 # href="">`s used as buttons on some sites.
96 return element.getAttribute('href') and
97 (element instanceof HTMLAnchorElement or
98 element.ownerDocument instanceof XULDocument) and
99 not element.href.endsWith('#') and
100 not element.href.endsWith('#?') and
101 not element.href.startsWith('javascript:')
102
103 isTextInputElement = (element) ->
104 return (element instanceof HTMLInputElement and element.type in [
105 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
106 ]) or
107 element instanceof HTMLTextAreaElement or
108 element instanceof XULTextBoxElement or
109 isContentEditable(element)
110
111 isTypingElement = (element) ->
112 return isTextInputElement(element) or
113 # `<select>` elements can also receive text input: You may type the
114 # text of an item to select it.
115 element instanceof HTMLSelectElement or
116 element instanceof XULMenuListElement
117
118
119
120 # Active/focused element helpers
121
122 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
123 # is loading. Therefore always check if anything was returned, such as:
124 #
125 # return unless activeElement = utils.getActiveElement(window)
126 getActiveElement = (window) ->
127 {activeElement} = window.document
128 return null unless activeElement
129 # If the active element is a frame, recurse into it. The easiest way to detect
130 # a frame that works both in browser UI and in web page content is to check
131 # for the presence of `.contentWindow`. However, in non-multi-process
132 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
133 # pointing to the web page content `window`, which we don’t want to recurse
134 # into. `.localName` is `.nodeName` without `xul:` (if it exists). This seems
135 # to be the only way to detect such elements.
136 if activeElement.localName != 'browser' and activeElement.contentWindow
137 return getActiveElement(activeElement.contentWindow)
138 else
139 return activeElement
140
141 blurActiveElement = (window) ->
142 # Blurring a frame element also blurs any active elements inside it. Recursing
143 # into the frames and blurring the “real” active element directly would give
144 # focus to the `<body>` of its containing frame, while blurring the top-most
145 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
146 # editors which use an `<iframe>` as their text area.
147 window.document.activeElement?.blur()
148
149 blurActiveBrowserElement = (vim) ->
150 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
151 # reset it, for example.
152 # - Focusing the current browser afterwards allows to pass `<escape>` as well
153 # as unbound keys to the page. However, focusing the browser also triggers
154 # focus events on `document` and `window` in the current page. Many pages
155 # re-focus some text input on those events, making it impossible to blur
156 # those! Therefore we tell the frame script to suppress those events.
157 {window} = vim
158 activeElement = getActiveElement(window)
159 vim._send('browserRefocus')
160 nextTick(window, ->
161 activeElement.blur()
162 window.gBrowser.selectedBrowser.focus()
163 )
164
165 # Focus an element and tell Firefox that the focus happened because of a user
166 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
167 # look more appropriate, but it unconditionally selects all text, which
168 # `.FLAG_BYMOUSE` does not.
169 focusElement = (element, options = {}) ->
170 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
171 element.select?() if options.select
172
173 getFocusType = (element) -> switch
174 when isTypingElement(element)
175 'editable'
176 when isActivatable(element)
177 'activatable'
178 when isAdjustable(element)
179 'adjustable'
180 else
181 null
182
183
184
185 # Event helpers
186
187 listen = (element, eventName, listener, useCapture = true) ->
188 element.addEventListener(eventName, listener, useCapture)
189 module.onShutdown(->
190 element.removeEventListener(eventName, listener, useCapture)
191 )
192
193 listenOnce = (element, eventName, listener, useCapture = true) ->
194 fn = (event) ->
195 listener(event)
196 element.removeEventListener(eventName, fn, useCapture)
197 listen(element, eventName, fn, useCapture)
198
199 onRemoved = (window, element, fn) ->
200 mutationObserver = new window.MutationObserver((changes) ->
201 for change in changes then for removedElement in change.removedNodes
202 if removedElement == element
203 mutationObserver.disconnect()
204 fn()
205 return
206 )
207 mutationObserver.observe(element.parentNode, {childList: true})
208 module.onShutdown(mutationObserver.disconnect.bind(mutationObserver))
209
210 suppressEvent = (event) ->
211 event.preventDefault()
212 event.stopPropagation()
213
214 simulateMouseEvents = (element, sequenceType) ->
215 window = element.ownerGlobal
216 rect = element.getBoundingClientRect()
217
218 eventSequence = switch sequenceType
219 when 'click' then EVENTS_CLICK
220 when 'hover-start' then EVENTS_HOVER_START
221 when 'hover-end' then EVENTS_HOVER_END
222
223 for type in eventSequence
224 mouseEvent = new window.MouseEvent(type, {
225 # Let the event bubble in order to trigger delegated event listeners.
226 bubbles: type not in ['mouseenter', 'mouseleave']
227 # Make the event cancelable so that `<a href="#">` can be used as a
228 # JavaScript-powered button without scrolling to the top of the page.
229 cancelable: type not in ['mouseenter', 'mouseleave']
230 # These properties are just here for mimicing a real click as much as
231 # possible.
232 buttons: if type in EVENTS_CLICK then 1 else 0
233 detail: if type in EVENTS_CLICK then 1 else 0
234 view: window
235 # `page{X,Y}` are set automatically to the correct values when setting
236 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
237 # to set.
238 clientX: rect.left
239 clientY: rect.top
240 # To exactly calculate `screen{X,Y}` one has to to check where the web
241 # page content area is inside the browser chrome and go through all parent
242 # frames as well. This is good enough. YAGNI for now.
243 screenX: window.screenX + rect.left
244 screenY: window.screenY + rect.top
245 })
246 element.dispatchEvent(mouseEvent)
247
248 return
249
250
251
252 # DOM helpers
253
254 area = (element) ->
255 return element.clientWidth * element.clientHeight
256
257 containsDeep = (parent, element) ->
258 parentWindow = parent.ownerGlobal
259 elementWindow = element.ownerGlobal
260
261 # Owner windows might be missing when opening the devtools.
262 while elementWindow and parentWindow and
263 elementWindow != parentWindow and elementWindow.top != elementWindow
264 element = elementWindow.frameElement
265 elementWindow = element.ownerGlobal
266
267 return parent.contains(element)
268
269 createBox = (document, className = '', parent = null, text = null) ->
270 box = document.createElement('box')
271 box.className = "#{className} vimfx-box"
272 box.textContent = text if text?
273 parent.appendChild(box) if parent?
274 return box
275
276 injectTemporaryPopup = (document, contents) ->
277 popup = document.createElement('menupopup')
278 popup.appendChild(contents)
279 document.getElementById('mainPopupSet').appendChild(popup)
280 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
281 return popup
282
283 insertText = (input, value) ->
284 {selectionStart, selectionEnd} = input
285 input.value =
286 input.value[0...selectionStart] + value + input.value[selectionEnd..]
287 input.selectionStart = input.selectionEnd = selectionStart + value.length
288
289 querySelectorAllDeep = (window, selector) ->
290 elements = Array.from(window.document.querySelectorAll(selector))
291 for frame in window.frames
292 elements.push(querySelectorAllDeep(frame, selector)...)
293 return elements
294
295 scroll = (element, args) ->
296 {method, type, directions, amounts, properties, adjustment, smooth} = args
297 options = {}
298 for direction, index in directions
299 amount = amounts[index]
300 options[direction] = -Math.sign(amount) * adjustment + switch type
301 when 'lines' then amount
302 when 'pages' then amount * element[properties[index]]
303 when 'other' then Math.min(amount, element[properties[index]])
304 options.behavior = 'smooth' if smooth
305 element[method](options)
306
307 setAttributes = (element, attributes) ->
308 for attribute, value of attributes
309 element.setAttribute(attribute, value)
310 return
311
312 setHover = (element, hover) ->
313 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
314 while element.parentElement
315 nsIDomUtils[method](element, ':hover')
316 element = element.parentElement
317 return
318
319
320
321 # Language helpers
322
323 class Counter
324 constructor: ({start: @value = 0, @step = 1}) ->
325 tick: -> @value += @step
326
327 class EventEmitter
328 constructor: ->
329 @listeners = {}
330
331 on: (event, listener) ->
332 (@listeners[event] ?= []).push(listener)
333
334 emit: (event, data) ->
335 for listener in @listeners[event] ? []
336 listener(data)
337 return
338
339 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
340
341 nextTick = (window, fn) -> window.setTimeout(fn, 0)
342
343 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
344
345 removeDuplicates = (array) -> Array.from(new Set(array))
346
347 # Remove duplicate characters from string (case insensitive).
348 removeDuplicateCharacters = (str) ->
349 return removeDuplicates( str.toLowerCase().split('') ).join('')
350
351 # Calls `fn` repeatedly, with at least `interval` ms between each call.
352 setInterval = (window, interval, fn) ->
353 stopped = false
354 currentIntervalId = null
355 next = ->
356 return if stopped
357 currentIntervalId = window.setTimeout((-> fn(next)), interval)
358 clearInterval = ->
359 stopped = true
360 window.clearTimeout(currentIntervalId)
361 next()
362 return clearInterval
363
364
365
366 # Misc helpers
367
368 formatError = (error) ->
369 stack = String(error.stack?.formattedStack ? error.stack ? '')
370 .split('\n')
371 .filter((line) -> line.includes('.xpi!'))
372 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
373 .join('\n')
374 return "#{error}\n#{stack}"
375
376 getCurrentLocation = ->
377 window = getCurrentWindow()
378 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
379
380 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
381
382 loadCss = (name) ->
383 uri = Services.io.newURI("chrome://vimfx/skin/#{name}.css", null, null)
384 method = nsIStyleSheetService.AUTHOR_SHEET
385 unless nsIStyleSheetService.sheetRegistered(uri, method)
386 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
387 module.onShutdown(->
388 nsIStyleSheetService.unregisterSheet(uri, method)
389 )
390
391 observe = (topic, observer) ->
392 observer = {observe: observer} if typeof observer == 'function'
393 Services.obs.addObserver(observer, topic, false)
394 module.onShutdown(->
395 Services.obs.removeObserver(observer, topic, false)
396 )
397
398 openPopup = (popup) ->
399 window = popup.ownerGlobal
400 # Show the popup so it gets a height and width.
401 popup.openPopupAtScreen(0, 0)
402 # Center the popup inside the window.
403 popup.moveTo(
404 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
405 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
406 )
407
408 openTab = (window, url, options) ->
409 {gBrowser} = window
410 window.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
411 gBrowser.loadOneTab(url, options)
412
413 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
414
415
416
417 module.exports = {
418 isActivatable
419 isAdjustable
420 isContentEditable
421 isProperLink
422 isTextInputElement
423 isTypingElement
424
425 getActiveElement
426 blurActiveElement
427 blurActiveBrowserElement
428 focusElement
429 getFocusType
430
431 listen
432 listenOnce
433 onRemoved
434 suppressEvent
435 simulateMouseEvents
436
437 area
438 containsDeep
439 createBox
440 injectTemporaryPopup
441 insertText
442 querySelectorAllDeep
443 scroll
444 setAttributes
445 setHover
446
447 Counter
448 EventEmitter
449 has
450 nextTick
451 regexEscape
452 removeDuplicates
453 removeDuplicateCharacters
454 setInterval
455
456 formatError
457 getCurrentLocation
458 getCurrentWindow
459 loadCss
460 observe
461 openPopup
462 openTab
463 writeToClipboard
464 }
Imprint / Impressum