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