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