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