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