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