]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
improve contentAreaClick
[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
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 or the
26 # ShadowRoot.
27 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
28
29 isInShadowRoot = (element) ->
30 getDocument(element) instanceof element.ownerGlobal.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. Also,
313 # our version is only invoked from the parent process, so we can pass
314 # data.csp and data.referrerInfo without calling the E10SUtils helpers.
315 window = browser.ownerGlobal
316
317 params = {
318 charset: browser.characterSet,
319 referrerInfo: data.referrerInfo # passed unserialized
320 isContentWindowPrivate: data.isContentWindowPrivate,
321 originPrincipal: data.originPrincipal,
322 originStoragePrincipal: data.originStoragePrincipal,
323 triggeringPrincipal: data.triggeringPrincipal,
324 csp: data.csp # passed unserialized
325 frameID: data.frameID,
326 allowInheritPrincipal: true,
327 openerBrowser: browser,
328 hasValidUserGestureActivation: true,
329 triggeringRemoteType: browser.browsingContext.currentWindowGlobal
330 .domProcess?.remoteType,
331 }
332
333 if data.originAttributes.userContextId
334 params.userContextId = data.originAttributes.userContextId
335
336 try if not PrivateBrowsingUtils.isWindowPrivate(window)
337 PlacesUIUtils.markPageAsFollowedLink(data.href)
338
339 window.openLinkIn(data.href, window.whereToOpenLink(data), params)
340
341 simulateMouseEvents = (element, sequence, browserOffset) ->
342 window = element.ownerGlobal
343 rect = element.getBoundingClientRect()
344 topOffset = getTopOffset(element)
345
346 eventSequence = switch sequence
347 when 'click'
348 EVENTS_CLICK
349 when 'click-xul'
350 EVENTS_CLICK_XUL
351 when 'context'
352 EVENTS_CONTEXT
353 when 'hover-start'
354 EVENTS_HOVER_START
355 when 'hover-end'
356 EVENTS_HOVER_END
357 else
358 sequence
359
360 for type in eventSequence
361 buttonNum = switch
362 when type in EVENTS_CONTEXT
363 2
364 when type in EVENTS_CLICK
365 1
366 else
367 0
368
369 mouseEvent = new window.MouseEvent(type, {
370 # Let the event bubble in order to trigger delegated event listeners.
371 bubbles: type not in ['mouseenter', 'mouseleave']
372 # Make the event cancelable so that `<a href="#">` can be used as a
373 # JavaScript-powered button without scrolling to the top of the page.
374 cancelable: type not in ['mouseenter', 'mouseleave']
375 # These properties are just here for mimicing a real click as much as
376 # possible.
377 buttons: buttonNum
378 detail: buttonNum
379 view: window
380 # `page{X,Y}` are set automatically to the correct values when setting
381 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
382 # to set.
383 clientX: rect.left
384 clientY: rect.top + rect.height / 2
385 screenX: browserOffset.x + topOffset.x
386 screenY: browserOffset.y + topOffset.y + rect.height / 2
387 })
388
389 if type == 'mousemove'
390 # If the below technique is used for this event, the “URL popup” (shown
391 # when hovering or focusing links) does not appear.
392 element.dispatchEvent(mouseEvent)
393 else if isInShadowRoot(element)
394 # click events for links and other clickables inside the shadow DOM are
395 # caught by the callee (.click_marker_element()).
396 element.focus() if type == 'contextmenu' # for <input type=text>
397 element.dispatchEvent(mouseEvent)
398 else
399 try
400 window.windowUtils
401 .dispatchDOMEventViaPresShellForTesting(element, mouseEvent)
402 catch error
403 if error.result != Cr.NS_ERROR_UNEXPECTED
404 throw error
405
406 return
407
408 suppressEvent = (event) ->
409 event.preventDefault()
410 event.stopPropagation()
411
412
413
414 # DOM helpers
415
416 area = (element) ->
417 return element.clientWidth * element.clientHeight
418
419 checkElementOrAncestor = (element, fn) ->
420 window = element.ownerGlobal
421 while element.parentElement
422 return true if fn(element)
423 element = element.parentElement
424 return false
425
426 clearSelectionDeep = (window, {blur = true} = {}) ->
427 # The selection might be `null` in hidden frames.
428 selection = window.getSelection()
429 selection?.removeAllRanges()
430 # Note: accessing frameElement fails on oop iframes (fission); skip those.
431 for frame in window.frames when (try frame.frameElement)
432 clearSelectionDeep(frame, {blur})
433 # Allow parents to re-gain control of text selection.
434 frame.frameElement.blur() if blur
435 return
436
437 containsDeep = (parent, element) ->
438 parentWindow = parent.ownerGlobal
439 elementWindow = element.ownerGlobal
440
441 # Owner windows might be missing when opening the devtools.
442 while elementWindow and parentWindow and
443 elementWindow != parentWindow and elementWindow.top != elementWindow
444 element = elementWindow.frameElement
445 elementWindow = element.ownerGlobal
446
447 return parent.contains(element)
448
449 createBox = (document, className = '', parent = null, text = null) ->
450 box = document.createElement('box')
451 box.className = "#{className} vimfx-box"
452 box.textContent = text if text?
453 parent.appendChild(box) if parent?
454 return box
455
456 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
457 # `<body>` is considered the root element rather than `<html>`.
458 getRootElement = (document) ->
459 if document.compatMode == 'BackCompat' and document.body?
460 return document.body
461 else
462 return document.documentElement
463
464 getText = (element) ->
465 text = element.textContent or element.value or element.placeholder or ''
466 return text.trim().replace(/\s+/, ' ')
467
468 getTopOffset = (element) ->
469 window = element.ownerGlobal
470
471 {left: x, top: y} = element.getBoundingClientRect()
472 while window.frameElement
473 frame = window.frameElement
474 frameRect = frame.getBoundingClientRect()
475 x += frameRect.left
476 y += frameRect.top
477
478 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
479 if computedStyle
480 x +=
481 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
482 parseFloat(computedStyle.getPropertyValue('padding-left'))
483 y +=
484 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
485 parseFloat(computedStyle.getPropertyValue('padding-top'))
486
487 window = window.parent
488 return {x, y}
489
490 injectTemporaryPopup = (document, contents) ->
491 popup = document.createXULElement('menupopup')
492 popup.appendChild(contents)
493 document.getElementById('mainPopupSet').appendChild(popup)
494 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
495 return popup
496
497 insertText = (input, value) ->
498 {selectionStart, selectionEnd} = input
499 input.value =
500 input.value[0...selectionStart] + value + input.value[selectionEnd..]
501 input.selectionStart = input.selectionEnd = selectionStart + value.length
502
503 isDetached = (element) ->
504 return not element.ownerDocument?.documentElement?.contains?(element)
505
506 isNonEmptyTextNode = (node) ->
507 return node.nodeType == 3 and node.data.trim() != ''
508
509 querySelectorAllDeep = (window, selector) ->
510 elements = Array.from(window.document.querySelectorAll(selector))
511 for frame in window.frames
512 elements.push(querySelectorAllDeep(frame, selector)...)
513 return elements
514
515 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
516 window = element.ownerGlobal
517 selection = window.getSelection()
518 {textContent} = element
519
520 format = (string) -> if caseSensitive then string else string.toLowerCase()
521 offsets =
522 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
523 offsetsLength = offsets.length
524 return if offsetsLength == 0
525
526 textIndex = 0
527 offsetsIndex = 0
528 [currentOffset] = offsets
529 searchIndex = currentOffset.start
530 start = null
531
532 walkTextNodes(element, (textNode) ->
533 {length} = textNode.data
534 return false if length == 0
535
536 while textIndex + length > searchIndex
537 if start
538 range = window.document.createRange()
539 range.setStart(start.textNode, start.offset)
540 range.setEnd(textNode, currentOffset.end - textIndex)
541 selection.addRange(range)
542
543 offsetsIndex += 1
544 return true if offsetsIndex >= offsetsLength
545 currentOffset = offsets[offsetsIndex]
546
547 start = null
548 searchIndex = currentOffset.start
549
550 else
551 start = {textNode, offset: currentOffset.start - textIndex}
552 searchIndex = currentOffset.end - 1
553
554 textIndex += length
555 return false
556 )
557
558 selectElement = (element) ->
559 window = element.ownerGlobal
560 selection = window.getSelection()
561 range = window.document.createRange()
562 range.selectNodeContents(element)
563 selection.addRange(range)
564
565 setAttributes = (element, attributes) ->
566 for attribute, value of attributes
567 element.setAttribute(attribute, value)
568 return
569
570 walkTextNodes = (element, fn) ->
571 for node in element.childNodes then switch node.nodeType
572 when 3 # TextNode.
573 stop = fn(node)
574 return true if stop
575 when 1 # Element.
576 stop = walkTextNodes(node, fn)
577 return true if stop
578 return false
579
580
581
582 # Language helpers
583
584 class Counter
585 constructor: ({start: @value = 0, @step = 1}) ->
586 tick: -> @value += @step
587
588 class EventEmitter
589 constructor: ->
590 @listeners = {}
591
592 on: (event, listener) ->
593 (@listeners[event] ?= new Set()).add(listener)
594
595 off: (event, listener) ->
596 @listeners[event]?.delete(listener)
597
598 emit: (event, data) ->
599 @listeners[event]?.forEach((listener) ->
600 listener(data)
601 )
602
603 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
604 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
605 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
606 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
607 # max`. `[null, null]` is returned in non-sensical cases. This function is
608 # intended to be used as a faster alternative to something like this:
609 #
610 # adjacentMatchAfter = null
611 # for n in [min..max]
612 # if fn(n)
613 # adjacentMatchAfter = n
614 # break
615 bisect = (min, max, fn) ->
616 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
617
618 while max - min > 1
619 mid = min + (max - min) // 2
620 match = fn(mid)
621 if match
622 max = mid
623 else
624 min = mid
625
626 matchMin = fn(min)
627 matchMax = fn(max)
628
629 return switch
630 when matchMin and matchMax
631 [null, min]
632 when not matchMin and not matchMax
633 [max, null]
634 when not matchMin and matchMax
635 [min, max]
636 else
637 [null, null]
638
639 getAllNonOverlappingRangeOffsets = (string, substring) ->
640 {length} = substring
641 return [] if length == 0
642
643 offsets = []
644 lastOffset = {start: -Infinity, end: -Infinity}
645 index = -1
646
647 loop
648 index = string.indexOf(substring, index + 1)
649 break if index == -1
650 if index > lastOffset.end
651 lastOffset = {start: index, end: index + length}
652 offsets.push(lastOffset)
653 else
654 lastOffset.end = index + length
655
656 return offsets
657
658 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
659
660 # Check if `search` exists in `string` (case insensitively). Returns `false` if
661 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
662 includes = (string, search) ->
663 return false unless typeof string == 'string'
664 return string.toLowerCase().includes(search)
665
666 # Calls `fn` repeatedly, with at least `interval` ms between each call.
667 interval = (window, interval, fn) ->
668 stopped = false
669 currentIntervalId = null
670 next = ->
671 return if stopped
672 currentIntervalId = window.setTimeout((-> fn(next)), interval)
673 clearInterval = ->
674 stopped = true
675 window.clearTimeout(currentIntervalId)
676 next()
677 return clearInterval
678
679 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
680
681 overlaps = (rectA, rectB) ->
682 return \
683 Math.round(rectA.right) >= Math.round(rectB.left) and
684 Math.round(rectA.left) <= Math.round(rectB.right) and
685 Math.round(rectA.bottom) >= Math.round(rectB.top) and
686 Math.round(rectA.top) <= Math.round(rectB.bottom)
687
688 partition = (array, fn) ->
689 matching = []
690 nonMatching = []
691 for item, index in array
692 if fn(item, index, array)
693 matching.push(item)
694 else
695 nonMatching.push(item)
696 return [matching, nonMatching]
697
698 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
699
700 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
701
702 removeDuplicates = (array) -> Array.from(new Set(array))
703
704 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
705
706
707
708 # Misc helpers
709
710 expandPath = (path) ->
711 if path.startsWith('~/') or path.startsWith('~\\')
712 return Services.dirsvc.get('Home', Ci.nsIFile).path + path[1..]
713 else
714 return path
715
716 getCurrentLocation = ->
717 return unless window = getCurrentWindow()
718 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
719
720 # This function might return `null` on startup.
721 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
722
723 # gBrowser getFindBar() used to return the findBar directly, but in recent
724 # versions it returns a promise. This function should be removed once these old
725 # versions are no longer supported.
726 getFindBar = (gBrowser) ->
727 promiseOrFindBar = gBrowser.getFindBar()
728 if promiseOrFindBar instanceof Promise
729 promiseOrFindBar
730 else
731 Promise.resolve(promiseOrFindBar)
732
733 hasEventListeners = (element, type) ->
734 for listener in nsIEventListenerService.getListenerInfoFor(element)
735 if listener.listenerObject and listener.type == type
736 return true
737 return false
738
739 loadCss = (uriString) ->
740 uri = Services.io.newURI(uriString, null, null)
741 method = nsIStyleSheetService.AUTHOR_SHEET
742 unless nsIStyleSheetService.sheetRegistered(uri, method)
743 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
744 module.onShutdown(->
745 nsIStyleSheetService.unregisterSheet(uri, method)
746 )
747
748 observe = (topic, observer) ->
749 observer = {observe: observer} if typeof observer == 'function'
750 Services.obs.addObserver(observer, topic, false)
751 module.onShutdown(->
752 Services.obs.removeObserver(observer, topic, false)
753 )
754
755 # Try to open a button’s dropdown menu, if any.
756 openDropdown = (element) ->
757 if isXULElement(element) and
758 element.getAttribute?('type') == 'menu' and
759 element.open == false # Only change `.open` if it is already a boolean.
760 element.open = true
761
762 openPopup = (popup) ->
763 window = popup.ownerGlobal
764 # Show the popup so it gets a height and width.
765 popup.openPopupAtScreen(0, 0)
766 # Center the popup inside the window.
767 popup.moveTo(
768 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
769 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
770 )
771
772 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
773
774
775
776 module.exports = {
777 hasMarkableTextNode
778 isActivatable
779 isAdjustable
780 isContentEditable
781 isDevtoolsElement
782 isDevtoolsWindow
783 isDockedDevtoolsElement
784 isFocusable
785 isIframeEditor
786 isIgnoreModeFocusType
787 isProperLink
788 isTextInputElement
789 isTypingElement
790 isXULElement
791 isInShadowRoot
792
793 blurActiveBrowserElement
794 blurActiveElement
795 focusElement
796 getActiveElement
797 getFocusType
798
799 listen
800 listenOnce
801 onRemoved
802 contentAreaClick
803 simulateMouseEvents
804 suppressEvent
805
806 area
807 checkElementOrAncestor
808 clearSelectionDeep
809 containsDeep
810 createBox
811 getRootElement
812 getText
813 getTopOffset
814 injectTemporaryPopup
815 insertText
816 isDetached
817 isNonEmptyTextNode
818 querySelectorAllDeep
819 selectAllSubstringMatches
820 selectElement
821 setAttributes
822 walkTextNodes
823
824 Counter
825 EventEmitter
826 bisect
827 getAllNonOverlappingRangeOffsets
828 has
829 includes
830 interval
831 nextTick
832 overlaps
833 partition
834 regexEscape
835 removeDuplicateChars
836 removeDuplicates
837 sum
838
839 expandPath
840 getCurrentLocation
841 getCurrentWindow
842 getFindBar
843 hasEventListeners
844 loadCss
845 observe
846 openDropdown
847 openPopup
848 writeToClipboard
849 }
Imprint / Impressum