]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Don't crash on pages with out-of-process iframes when fission is enabled
[VimFx.git] / extension / lib / utils.coffee
1 # This file contains lots of different helper functions.
2
3 {OS} = Components.utils.import('resource://gre/modules/osfile.jsm', {})
4
5 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
6 .getService(Ci.nsIClipboardHelper)
7 nsIEventListenerService = Cc['@mozilla.org/eventlistenerservice;1']
8 .getService(Ci.nsIEventListenerService)
9 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
10 .getService(Ci.nsIFocusManager)
11 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
12 .getService(Ci.nsIStyleSheetService)
13 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
14 .getService(Ci.nsIWindowMediator)
15
16 # For XUL, `instanceof` checks are often better than `.localName` checks,
17 # because some of the below interfaces are extended by many elements.
18 XULButtonElement = Ci.nsIDOMXULButtonElement
19 XULControlElement = Ci.nsIDOMXULControlElement
20 XULMenuListElement = Ci.nsIDOMXULMenuListElement
21
22 # Traverse the DOM upwards until we hit its containing document (most likely an
23 # HTMLDocument or (<=fx68) XULDocument) or the ShadowRoot.
24 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
25
26 isInShadowRoot = (element) ->
27 ShadowRoot? and getDocument(element) instanceof ShadowRoot
28
29 isXULElement = (element) ->
30 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
31 element.namespaceURI == XUL_NS
32
33 # Full chains of events for different mouse actions. Note: 'click' is fired
34 # by Firefox automatically after 'mousedown' and 'mouseup'. Similarly,
35 # 'command' is fired automatically after 'click' on xul pages.
36 EVENTS_CLICK = ['mousedown', 'mouseup']
37 EVENTS_CLICK_XUL = ['click']
38 EVENTS_CONTEXT = ['contextmenu']
39 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
40 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
41
42
43
44 # Element classification helpers
45
46 hasMarkableTextNode = (element) ->
47 return Array.prototype.some.call(element.childNodes, (node) ->
48 # Ignore whitespace-only text nodes, and single-letter ones (which are
49 # common in many syntax highlighters).
50 return node.nodeType == 3 and node.data.trim().length > 1
51 )
52
53 isActivatable = (element) ->
54 return element.localName in ['a', 'button'] or
55 (element.localName == 'input' and element.type in [
56 'button', 'submit', 'reset', 'image'
57 ]) or
58 element instanceof XULButtonElement
59
60 isAdjustable = (element) ->
61 return element.localName == 'input' and element.type in [
62 'checkbox', 'radio', 'file', 'color'
63 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
64 ] or
65 element.localName in ['video', 'audio', 'embed', 'object'] or
66 element instanceof XULControlElement or
67 # Custom video players.
68 includes(element.className, 'video') or
69 includes(element.className, 'player') or
70 # Youtube special case.
71 element.classList?.contains('ytp-button') or
72 # Allow navigating object inspection trees in th devtools with the
73 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
74 isDevtoolsElement(element)
75
76 isContentEditable = (element) ->
77 return element.isContentEditable or
78 isIframeEditor(element) or
79 # Google.
80 element.getAttribute?('g_editable') == 'true' or
81 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
82 # Codeacademy terminals.
83 element.classList?.contains('real-terminal')
84
85 isDevtoolsElement = (element) ->
86 return false unless element.ownerGlobal
87 return Array.prototype.some.call(
88 element.ownerGlobal.top.frames, isDevtoolsWindow
89 )
90
91 isDevtoolsWindow = (window) ->
92 # Note: this function is called for each frame by isDevtoolsElement. When
93 # called on an out-of-process iframe, accessing .href will fail with
94 # SecurityError; the `try` around it makes it `undefined` in such a case.
95 return (try window.location?.href) in [
96 'about:devtools-toolbox'
97 'chrome://devtools/content/framework/toolbox.xul'
98 'chrome://devtools/content/framework/toolbox.xhtml' # fx72+
99 ]
100
101 isDockedDevtoolsElement = (element) ->
102 return element.ownerDocument.URL.startsWith('chrome://devtools/content/')
103
104 isFocusable = (element) ->
105 # Focusable elements have `.tabIndex > 1` (but not necessarily a
106 # `tabindex="…"` attribute) …
107 return (element.tabIndex > -1 or
108 # … or an explicit `tabindex="-1"` attribute (which means that it is
109 # focusable, but not reachable with `<tab>`).
110 element.getAttribute?('tabindex') == '-1') and
111 not (element.localName?.endsWith?('box') and
112 element.localName != 'checkbox') and
113 not (element.localName == 'toolbarbutton' and
114 element.parentNode?.localName == 'toolbarbutton') and
115 element.localName not in ['tabs', 'menuitem', 'menuseparator']
116
117 isIframeEditor = (element) ->
118 return false unless element.localName == 'body'
119 return \
120 # Etherpad.
121 element.id == 'innerdocbody' or
122 # XpressEditor.
123 (element.classList?.contains('xe_content') and
124 element.classList?.contains('editable')) or
125 # vBulletin.
126 element.classList?.contains('wysiwyg') or
127 # TYPO3 CMS.
128 element.classList?.contains('htmlarea-content-body') or
129 # The wasavi extension.
130 element.hasAttribute?('data-wasavi-state')
131
132 isIgnoreModeFocusType = (element) ->
133 return \
134 # The wasavi extension.
135 element.hasAttribute?('data-wasavi-state') or
136 element.closest?('#wasavi_container') or
137 # CodeMirror in Vim mode.
138 (element.localName == 'textarea' and
139 element.closest?('.CodeMirror') and _hasVimEventListener(element))
140
141 # CodeMirror’s Vim mode is really sucky to detect. The only way seems to be to
142 # check if the there are any event listener functions with Vim-y words in them.
143 _hasVimEventListener = (element) ->
144 for listener in nsIEventListenerService.getListenerInfoFor(element)
145 if listener.listenerObject and
146 /\bvim\b|insertmode/i.test(String(listener.listenerObject))
147 return true
148 return false
149
150 isProperLink = (element) ->
151 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
152 # href="">`s used as buttons on some sites.
153 return element.getAttribute?('href') and
154 (element.localName == 'a' or
155 (isXULElement(element) and
156 element.localName == 'label' and
157 element.getAttribute('is') == 'text-link')) and
158 not element.href?.endsWith?('#') and
159 not element.href?.endsWith?('#?') and
160 not element.href?.startsWith?('javascript:')
161
162 isTextInputElement = (element) ->
163 return (element.localName == 'input' and element.type in [
164 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
165 ]) or
166 element.localName in [ 'textarea', 'textbox' ] or
167 isContentEditable(element)
168
169 isTypingElement = (element) ->
170 return isTextInputElement(element) or
171 # `<select>` elements can also receive text input: You may type the
172 # text of an item to select it.
173 element.localName == 'select' or
174 element instanceof XULMenuListElement
175
176
177
178 # Active/focused element helpers
179
180 blurActiveBrowserElement = (vim) ->
181 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
182 # reset it, for example.
183 # - Focusing the current browser afterwards allows to pass `<escape>` as well
184 # as unbound keys to the page. However, focusing the browser also triggers
185 # focus events on `document` and `window` in the current page. Many pages
186 # re-focus some text input on those events, making it impossible to blur
187 # those! Therefore we tell the frame script to suppress those events.
188 {window} = vim
189 activeElement = getActiveElement(window)
190 activeElement.closest('tabmodalprompt')?.abortPrompt()
191 vim._send('browserRefocus')
192 nextTick(window, ->
193 activeElement.blur()
194 window.gBrowser.selectedBrowser.focus()
195 )
196
197 blurActiveElement = (window) ->
198 # Blurring a frame element also blurs any active elements inside it. Recursing
199 # into the frames and blurring the “real” active element directly would give
200 # focus to the `<body>` of its containing frame, while blurring the top-most
201 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
202 # editors which use an `<iframe>` as their text area.
203 # Note that this trick does not work with Web Components; for them, recursing
204 # is necessary.
205 if window.document.activeElement?.shadowRoot?
206 return getActiveElement(window)?.blur()
207 window.document.activeElement?.blur()
208
209 # Focus an element and tell Firefox that the focus happened because of a user
210 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
211 # look more appropriate, but it unconditionally selects all text, which
212 # `.FLAG_BYMOUSE` does not.
213 focusElement = (element, options = {}) ->
214 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
215 element.select?() if options.select
216
217 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
218 # is loading. Therefore always check if anything was returned, such as:
219 #
220 # return unless activeElement = utils.getActiveElement(window)
221 getActiveElement = (window) ->
222 {activeElement} = window.shadowRoot or window.document
223 return null unless activeElement
224 # If the active element is a frame, recurse into it. The easiest way to detect
225 # a frame that works both in browser UI and in web page content is to check
226 # for the presence of `.contentWindow`. However, in non-multi-process,
227 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
228 # pointing to the web page content `window`, which we don’t want to recurse
229 # into. The problem is that there are _some_ `<browser>`s which we _want_ to
230 # recurse into, such as the sidebar (for instance the history sidebar), and
231 # dialogs in `about:preferences`. Checking the `contextmenu` attribute seems
232 # to be a reliable test, catching both the main tab `<browser>`s and bookmarks
233 # opened in the sidebar.
234 # We also want to recurse into the (open) shadow DOM of custom elements.
235 if activeElement.shadowRoot?
236 return getActiveElement(activeElement)
237 else if activeElement.contentWindow and
238 not (activeElement.localName == 'browser' and
239 activeElement.getAttribute?('contextmenu') == 'contentAreaContextMenu')
240 # with Fission enabled, the iframe might be located in a different process
241 # (oop). Then, recursing into it isn't possible (throws SecurityError).
242 return activeElement unless (try activeElement.contentWindow.document)
243
244 return getActiveElement(activeElement.contentWindow)
245 else
246 return activeElement
247
248 getFocusType = (element) -> switch
249 when element.tagName in ['FRAME', 'IFRAME'] and
250 not (try element.contentWindow.document)
251 # Encountered an out-of-process iframe, which we can't inspect. We fall
252 # back to insert mode, so any text inputs it may contain are still usable.
253 'editable'
254 when isIgnoreModeFocusType(element)
255 'ignore'
256 when isTypingElement(element)
257 if element.closest?('findbar') then 'findbar' else 'editable'
258 when isActivatable(element)
259 'activatable'
260 when isAdjustable(element)
261 'adjustable'
262 else
263 'none'
264
265
266
267 # Event helpers
268
269 listen = (element, eventName, listener, useCapture = true) ->
270 element.addEventListener(eventName, listener, useCapture)
271 module.onShutdown(->
272 element.removeEventListener(eventName, listener, useCapture)
273 )
274
275 listenOnce = (element, eventName, listener, useCapture = true) ->
276 fn = (event) ->
277 listener(event)
278 element.removeEventListener(eventName, fn, useCapture)
279 listen(element, eventName, fn, useCapture)
280
281 onRemoved = (element, fn) ->
282 window = element.ownerGlobal
283
284 disconnected = false
285 disconnect = ->
286 return if disconnected
287 disconnected = true
288 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
289
290 mutationObserver = new window.MutationObserver((changes) ->
291 for change in changes then for removedElement in change.removedNodes
292 if removedElement.contains?(element)
293 disconnect()
294 fn()
295 return
296 )
297 mutationObserver.observe(window.document.documentElement, {
298 childList: true
299 subtree: true
300 })
301 module.onShutdown(disconnect)
302
303 return disconnect
304
305 simulateMouseEvents = (element, sequence, browserOffset) ->
306 window = element.ownerGlobal
307 rect = element.getBoundingClientRect()
308 topOffset = getTopOffset(element)
309
310 eventSequence = switch sequence
311 when 'click'
312 EVENTS_CLICK
313 when 'click-xul'
314 EVENTS_CLICK_XUL
315 when 'context'
316 EVENTS_CONTEXT
317 when 'hover-start'
318 EVENTS_HOVER_START
319 when 'hover-end'
320 EVENTS_HOVER_END
321 else
322 sequence
323
324 for type in eventSequence
325 buttonNum = switch
326 when type in EVENTS_CONTEXT
327 2
328 when type in EVENTS_CLICK
329 1
330 else
331 0
332
333 mouseEvent = new window.MouseEvent(type, {
334 # Let the event bubble in order to trigger delegated event listeners.
335 bubbles: type not in ['mouseenter', 'mouseleave']
336 # Make the event cancelable so that `<a href="#">` can be used as a
337 # JavaScript-powered button without scrolling to the top of the page.
338 cancelable: type not in ['mouseenter', 'mouseleave']
339 # These properties are just here for mimicing a real click as much as
340 # possible.
341 buttons: buttonNum
342 detail: buttonNum
343 view: window
344 # `page{X,Y}` are set automatically to the correct values when setting
345 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
346 # to set.
347 clientX: rect.left
348 clientY: rect.top + rect.height / 2
349 screenX: browserOffset.x + topOffset.x
350 screenY: browserOffset.y + topOffset.y + rect.height / 2
351 })
352
353 if type == 'mousemove'
354 # If the below technique is used for this event, the “URL popup” (shown
355 # when hovering or focusing links) does not appear.
356 element.dispatchEvent(mouseEvent)
357 else if isInShadowRoot(element)
358 # click events for links and other clickables inside the shadow DOM are
359 # caught by the callee (.click_marker_element()).
360 element.focus() if type == 'contextmenu' # for <input type=text>
361 element.dispatchEvent(mouseEvent)
362 else
363 try
364 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
365 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
366 )(element, mouseEvent)
367 catch error
368 if error.result != Cr.NS_ERROR_UNEXPECTED
369 throw error
370
371 return
372
373 suppressEvent = (event) ->
374 event.preventDefault()
375 event.stopPropagation()
376
377
378
379 # DOM helpers
380
381 area = (element) ->
382 return element.clientWidth * element.clientHeight
383
384 checkElementOrAncestor = (element, fn) ->
385 window = element.ownerGlobal
386 while element.parentElement
387 return true if fn(element)
388 element = element.parentElement
389 return false
390
391 clearSelectionDeep = (window, {blur = true} = {}) ->
392 # The selection might be `null` in hidden frames.
393 selection = window.getSelection()
394 selection?.removeAllRanges()
395 # Note: accessing frameElement fails on oop iframes (fission); skip those.
396 for frame in window.frames when (try frame.frameElement)
397 clearSelectionDeep(frame, {blur})
398 # Allow parents to re-gain control of text selection.
399 frame.frameElement.blur() if blur
400 return
401
402 containsDeep = (parent, element) ->
403 parentWindow = parent.ownerGlobal
404 elementWindow = element.ownerGlobal
405
406 # Owner windows might be missing when opening the devtools.
407 while elementWindow and parentWindow and
408 elementWindow != parentWindow and elementWindow.top != elementWindow
409 element = elementWindow.frameElement
410 elementWindow = element.ownerGlobal
411
412 return parent.contains(element)
413
414 createBox = (document, className = '', parent = null, text = null) ->
415 box = document.createElement('box')
416 box.className = "#{className} vimfx-box"
417 box.textContent = text if text?
418 parent.appendChild(box) if parent?
419 return box
420
421 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
422 # `<body>` is considered the root element rather than `<html>`.
423 getRootElement = (document) ->
424 if document.compatMode == 'BackCompat' and document.body?
425 return document.body
426 else
427 return document.documentElement
428
429 getText = (element) ->
430 text = element.textContent or element.value or element.placeholder or ''
431 return text.trim().replace(/\s+/, ' ')
432
433 getTopOffset = (element) ->
434 window = element.ownerGlobal
435
436 {left: x, top: y} = element.getBoundingClientRect()
437 while window.frameElement
438 frame = window.frameElement
439 frameRect = frame.getBoundingClientRect()
440 x += frameRect.left
441 y += frameRect.top
442
443 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
444 if computedStyle
445 x +=
446 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
447 parseFloat(computedStyle.getPropertyValue('padding-left'))
448 y +=
449 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
450 parseFloat(computedStyle.getPropertyValue('padding-top'))
451
452 window = window.parent
453 return {x, y}
454
455 injectTemporaryPopup = (document, contents) ->
456 popup = document.createElement('menupopup')
457 popup.appendChild(contents)
458 document.getElementById('mainPopupSet').appendChild(popup)
459 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
460 return popup
461
462 insertText = (input, value) ->
463 {selectionStart, selectionEnd} = input
464 input.value =
465 input.value[0...selectionStart] + value + input.value[selectionEnd..]
466 input.selectionStart = input.selectionEnd = selectionStart + value.length
467
468 isDetached = (element) ->
469 return not element.ownerDocument?.documentElement?.contains?(element)
470
471 isNonEmptyTextNode = (node) ->
472 return node.nodeType == 3 and node.data.trim() != ''
473
474 querySelectorAllDeep = (window, selector) ->
475 elements = Array.from(window.document.querySelectorAll(selector))
476 for frame in window.frames
477 elements.push(querySelectorAllDeep(frame, selector)...)
478 return elements
479
480 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
481 window = element.ownerGlobal
482 selection = window.getSelection()
483 {textContent} = element
484
485 format = (string) -> if caseSensitive then string else string.toLowerCase()
486 offsets =
487 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
488 offsetsLength = offsets.length
489 return if offsetsLength == 0
490
491 textIndex = 0
492 offsetsIndex = 0
493 [currentOffset] = offsets
494 searchIndex = currentOffset.start
495 start = null
496
497 walkTextNodes(element, (textNode) ->
498 {length} = textNode.data
499 return false if length == 0
500
501 while textIndex + length > searchIndex
502 if start
503 range = window.document.createRange()
504 range.setStart(start.textNode, start.offset)
505 range.setEnd(textNode, currentOffset.end - textIndex)
506 selection.addRange(range)
507
508 offsetsIndex += 1
509 return true if offsetsIndex >= offsetsLength
510 currentOffset = offsets[offsetsIndex]
511
512 start = null
513 searchIndex = currentOffset.start
514
515 else
516 start = {textNode, offset: currentOffset.start - textIndex}
517 searchIndex = currentOffset.end - 1
518
519 textIndex += length
520 return false
521 )
522
523 selectElement = (element) ->
524 window = element.ownerGlobal
525 selection = window.getSelection()
526 range = window.document.createRange()
527 range.selectNodeContents(element)
528 selection.addRange(range)
529
530 setAttributes = (element, attributes) ->
531 for attribute, value of attributes
532 element.setAttribute(attribute, value)
533 return
534
535 walkTextNodes = (element, fn) ->
536 for node in element.childNodes then switch node.nodeType
537 when 3 # TextNode.
538 stop = fn(node)
539 return true if stop
540 when 1 # Element.
541 stop = walkTextNodes(node, fn)
542 return true if stop
543 return false
544
545
546
547 # Language helpers
548
549 class Counter
550 constructor: ({start: @value = 0, @step = 1}) ->
551 tick: -> @value += @step
552
553 class EventEmitter
554 constructor: ->
555 @listeners = {}
556
557 on: (event, listener) ->
558 (@listeners[event] ?= new Set()).add(listener)
559
560 off: (event, listener) ->
561 @listeners[event]?.delete(listener)
562
563 emit: (event, data) ->
564 @listeners[event]?.forEach((listener) ->
565 listener(data)
566 )
567
568 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
569 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
570 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
571 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
572 # max`. `[null, null]` is returned in non-sensical cases. This function is
573 # intended to be used as a faster alternative to something like this:
574 #
575 # adjacentMatchAfter = null
576 # for n in [min..max]
577 # if fn(n)
578 # adjacentMatchAfter = n
579 # break
580 bisect = (min, max, fn) ->
581 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
582
583 while max - min > 1
584 mid = min + (max - min) // 2
585 match = fn(mid)
586 if match
587 max = mid
588 else
589 min = mid
590
591 matchMin = fn(min)
592 matchMax = fn(max)
593
594 return switch
595 when matchMin and matchMax
596 [null, min]
597 when not matchMin and not matchMax
598 [max, null]
599 when not matchMin and matchMax
600 [min, max]
601 else
602 [null, null]
603
604 getAllNonOverlappingRangeOffsets = (string, substring) ->
605 {length} = substring
606 return [] if length == 0
607
608 offsets = []
609 lastOffset = {start: -Infinity, end: -Infinity}
610 index = -1
611
612 loop
613 index = string.indexOf(substring, index + 1)
614 break if index == -1
615 if index > lastOffset.end
616 lastOffset = {start: index, end: index + length}
617 offsets.push(lastOffset)
618 else
619 lastOffset.end = index + length
620
621 return offsets
622
623 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
624
625 # Check if `search` exists in `string` (case insensitively). Returns `false` if
626 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
627 includes = (string, search) ->
628 return false unless typeof string == 'string'
629 return string.toLowerCase().includes(search)
630
631 # Calls `fn` repeatedly, with at least `interval` ms between each call.
632 interval = (window, interval, fn) ->
633 stopped = false
634 currentIntervalId = null
635 next = ->
636 return if stopped
637 currentIntervalId = window.setTimeout((-> fn(next)), interval)
638 clearInterval = ->
639 stopped = true
640 window.clearTimeout(currentIntervalId)
641 next()
642 return clearInterval
643
644 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
645
646 overlaps = (rectA, rectB) ->
647 return \
648 Math.round(rectA.right) >= Math.round(rectB.left) and
649 Math.round(rectA.left) <= Math.round(rectB.right) and
650 Math.round(rectA.bottom) >= Math.round(rectB.top) and
651 Math.round(rectA.top) <= Math.round(rectB.bottom)
652
653 partition = (array, fn) ->
654 matching = []
655 nonMatching = []
656 for item, index in array
657 if fn(item, index, array)
658 matching.push(item)
659 else
660 nonMatching.push(item)
661 return [matching, nonMatching]
662
663 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
664
665 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
666
667 removeDuplicates = (array) -> Array.from(new Set(array))
668
669 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
670
671
672
673 # Misc helpers
674
675 expandPath = (path) ->
676 if path.startsWith('~/') or path.startsWith('~\\')
677 return OS.Constants.Path.homeDir + path[1..]
678 else
679 return path
680
681 getCurrentLocation = ->
682 return unless window = getCurrentWindow()
683 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
684
685 # This function might return `null` on startup.
686 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
687
688 # gBrowser getFindBar() used to return the findBar directly, but in recent
689 # versions it returns a promise. This function should be removed once these old
690 # versions are no longer supported.
691 getFindBar = (gBrowser) ->
692 promiseOrFindBar = gBrowser.getFindBar()
693 if promiseOrFindBar instanceof Promise
694 promiseOrFindBar
695 else
696 Promise.resolve(promiseOrFindBar)
697
698 hasEventListeners = (element, type) ->
699 for listener in nsIEventListenerService.getListenerInfoFor(element)
700 if listener.listenerObject and listener.type == type
701 return true
702 return false
703
704 loadCss = (uriString) ->
705 uri = Services.io.newURI(uriString, null, null)
706 method = nsIStyleSheetService.AUTHOR_SHEET
707 unless nsIStyleSheetService.sheetRegistered(uri, method)
708 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
709 module.onShutdown(->
710 nsIStyleSheetService.unregisterSheet(uri, method)
711 )
712
713 observe = (topic, observer) ->
714 observer = {observe: observer} if typeof observer == 'function'
715 Services.obs.addObserver(observer, topic, false)
716 module.onShutdown(->
717 Services.obs.removeObserver(observer, topic, false)
718 )
719
720 # Try to open a button’s dropdown menu, if any.
721 openDropdown = (element) ->
722 if isXULElement(element) and
723 element.getAttribute?('type') == 'menu' and
724 element.open == false # Only change `.open` if it is already a boolean.
725 element.open = true
726
727 openPopup = (popup) ->
728 window = popup.ownerGlobal
729 # Show the popup so it gets a height and width.
730 popup.openPopupAtScreen(0, 0)
731 # Center the popup inside the window.
732 popup.moveTo(
733 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
734 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
735 )
736
737 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
738
739
740
741 module.exports = {
742 hasMarkableTextNode
743 isActivatable
744 isAdjustable
745 isContentEditable
746 isDevtoolsElement
747 isDevtoolsWindow
748 isDockedDevtoolsElement
749 isFocusable
750 isIframeEditor
751 isIgnoreModeFocusType
752 isProperLink
753 isTextInputElement
754 isTypingElement
755 isXULElement
756 isInShadowRoot
757
758 blurActiveBrowserElement
759 blurActiveElement
760 focusElement
761 getActiveElement
762 getFocusType
763
764 listen
765 listenOnce
766 onRemoved
767 simulateMouseEvents
768 suppressEvent
769
770 area
771 checkElementOrAncestor
772 clearSelectionDeep
773 containsDeep
774 createBox
775 getRootElement
776 getText
777 getTopOffset
778 injectTemporaryPopup
779 insertText
780 isDetached
781 isNonEmptyTextNode
782 querySelectorAllDeep
783 selectAllSubstringMatches
784 selectElement
785 setAttributes
786 walkTextNodes
787
788 Counter
789 EventEmitter
790 bisect
791 getAllNonOverlappingRangeOffsets
792 has
793 includes
794 interval
795 nextTick
796 overlaps
797 partition
798 regexEscape
799 removeDuplicateChars
800 removeDuplicates
801 sum
802
803 expandPath
804 getCurrentLocation
805 getCurrentWindow
806 getFindBar
807 hasEventListeners
808 loadCss
809 observe
810 openDropdown
811 openPopup
812 writeToClipboard
813 }
Imprint / Impressum