]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Port osfile.jsm usage to IOUtils
[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 ShadowRoot? and getDocument(element) instanceof 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 }
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 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