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