]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
move from deprecated Cu.import to ChromeUtils.import
[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 {OS} = ChromeUtils.import('resource://gre/modules/osfile.jsm')
5 {PlacesUIUtils} = ChromeUtils.import('resource:///modules/PlacesUIUtils.jsm')
6 {PrivateBrowsingUtils} =
7 ChromeUtils.import('resource://gre/modules/PrivateBrowsingUtils.jsm')
8
9 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
10 .getService(Ci.nsIClipboardHelper)
11 nsIEventListenerService = Cc['@mozilla.org/eventlistenerservice;1']
12 .getService(Ci.nsIEventListenerService)
13 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
14 .getService(Ci.nsIFocusManager)
15 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
16 .getService(Ci.nsIStyleSheetService)
17 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
18 .getService(Ci.nsIWindowMediator)
19
20 # For XUL, `instanceof` checks are often better than `.localName` checks,
21 # because some of the below interfaces are extended by many elements.
22 XULButtonElement = Ci.nsIDOMXULButtonElement
23 XULControlElement = Ci.nsIDOMXULControlElement
24 XULMenuListElement = Ci.nsIDOMXULMenuListElement
25
26 # Traverse the DOM upwards until we hit its containing document (most likely an
27 # HTMLDocument or (<=fx68) XULDocument) or the ShadowRoot.
28 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
29
30 isInShadowRoot = (element) ->
31 ShadowRoot? and getDocument(element) instanceof ShadowRoot
32
33 isXULElement = (element) ->
34 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
35 element.namespaceURI == XUL_NS
36
37 # Full chains of events for different mouse actions. Note: 'click' is fired
38 # by Firefox automatically after 'mousedown' and 'mouseup'. Similarly,
39 # 'command' is fired automatically after 'click' on xul pages.
40 EVENTS_CLICK = ['mousedown', 'mouseup']
41 EVENTS_CLICK_XUL = ['click']
42 EVENTS_CONTEXT = ['contextmenu']
43 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
44 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
45
46
47
48 # Element classification helpers
49
50 hasMarkableTextNode = (element) ->
51 return Array.prototype.some.call(element.childNodes, (node) ->
52 # Ignore whitespace-only text nodes, and single-letter ones (which are
53 # common in many syntax highlighters).
54 return node.nodeType == 3 and node.data.trim().length > 1
55 )
56
57 isActivatable = (element) ->
58 return element.localName in ['a', 'button'] or
59 (element.localName == 'input' and element.type in [
60 'button', 'submit', 'reset', 'image'
61 ]) or
62 element instanceof XULButtonElement
63
64 isAdjustable = (element) ->
65 return element.localName == 'input' and element.type in [
66 'checkbox', 'radio', 'file', 'color'
67 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
68 ] or
69 element.localName in ['video', 'audio', 'embed', 'object'] or
70 element instanceof XULControlElement or
71 # Custom video players.
72 includes(element.className, 'video') or
73 includes(element.className, 'player') or
74 # Youtube special case.
75 element.classList?.contains('ytp-button') or
76 # Allow navigating object inspection trees in th devtools with the
77 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
78 isDevtoolsElement(element)
79
80 isContentEditable = (element) ->
81 return element.isContentEditable or
82 isIframeEditor(element) or
83 # Google.
84 element.getAttribute?('g_editable') == 'true' or
85 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
86 # Codeacademy terminals.
87 element.classList?.contains('real-terminal')
88
89 isDevtoolsElement = (element) ->
90 return false unless element.ownerGlobal
91 return Array.prototype.some.call(
92 element.ownerGlobal.top.frames, isDevtoolsWindow
93 )
94
95 isDevtoolsWindow = (window) ->
96 # Note: this function is called for each frame by isDevtoolsElement. When
97 # called on an out-of-process iframe, accessing .href will fail with
98 # SecurityError; the `try` around it makes it `undefined` in such a case.
99 return (try window.location?.href) in [
100 'about:devtools-toolbox'
101 'chrome://devtools/content/framework/toolbox.xul'
102 'chrome://devtools/content/framework/toolbox.xhtml' # fx72+
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.jsm. 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.
315 window = browser.ownerGlobal
316
317 params = {
318 charset: browser.characterSet,
319 referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
320 allowMixedContent: data.allowMixedContent, # <=fx88
321 isContentWindowPrivate: data.isContentWindowPrivate,
322 originPrincipal: data.originPrincipal,
323 originStoragePrincipal: data.originStoragePrincipal,
324 triggeringPrincipal: data.triggeringPrincipal,
325 csp: if data.csp then E10SUtils.deserializeCSP(data.csp) else null,
326 frameOuterWindowID: data.frameOuterWindowID, # <=fx79
327 frameID: data.frameID, # >=fx80
328 allowInheritPrincipal: true,
329 openerBrowser: browser, # >=fx98
330 }
331
332 if data.originAttributes.userContextId
333 params.userContextId = data.originAttributes.userContextId
334
335 try if not PrivateBrowsingUtils.isWindowPrivate(window)
336 PlacesUIUtils.markPageAsFollowedLink(data.href)
337
338 window.openLinkIn(data.href, window.whereToOpenLink(data), params)
339
340 simulateMouseEvents = (element, sequence, browserOffset) ->
341 window = element.ownerGlobal
342 rect = element.getBoundingClientRect()
343 topOffset = getTopOffset(element)
344
345 eventSequence = switch sequence
346 when 'click'
347 EVENTS_CLICK
348 when 'click-xul'
349 EVENTS_CLICK_XUL
350 when 'context'
351 EVENTS_CONTEXT
352 when 'hover-start'
353 EVENTS_HOVER_START
354 when 'hover-end'
355 EVENTS_HOVER_END
356 else
357 sequence
358
359 for type in eventSequence
360 buttonNum = switch
361 when type in EVENTS_CONTEXT
362 2
363 when type in EVENTS_CLICK
364 1
365 else
366 0
367
368 mouseEvent = new window.MouseEvent(type, {
369 # Let the event bubble in order to trigger delegated event listeners.
370 bubbles: type not in ['mouseenter', 'mouseleave']
371 # Make the event cancelable so that `<a href="#">` can be used as a
372 # JavaScript-powered button without scrolling to the top of the page.
373 cancelable: type not in ['mouseenter', 'mouseleave']
374 # These properties are just here for mimicing a real click as much as
375 # possible.
376 buttons: buttonNum
377 detail: buttonNum
378 view: window
379 # `page{X,Y}` are set automatically to the correct values when setting
380 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
381 # to set.
382 clientX: rect.left
383 clientY: rect.top + rect.height / 2
384 screenX: browserOffset.x + topOffset.x
385 screenY: browserOffset.y + topOffset.y + rect.height / 2
386 })
387
388 if type == 'mousemove'
389 # If the below technique is used for this event, the “URL popup” (shown
390 # when hovering or focusing links) does not appear.
391 element.dispatchEvent(mouseEvent)
392 else if isInShadowRoot(element)
393 # click events for links and other clickables inside the shadow DOM are
394 # caught by the callee (.click_marker_element()).
395 element.focus() if type == 'contextmenu' # for <input type=text>
396 element.dispatchEvent(mouseEvent)
397 else
398 try
399 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
400 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
401 )(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 OS.Constants.Path.homeDir + 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