1 # This file contains lots of different helper functions.
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')
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)
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
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
29 isInShadowRoot = (element) ->
30 getDocument(element) instanceof element.ownerGlobal.ShadowRoot
32 isXULElement = (element) ->
33 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
34 element.namespaceURI == XUL_NS
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']
47 # Element classification helpers
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
56 isActivatable = (element) ->
57 return element.localName in ['a', 'button'] or
58 (element.localName == 'input' and element.type in [
59 'button', 'submit', 'reset', 'image'
61 element instanceof XULButtonElement
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'
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)
79 isContentEditable = (element) ->
80 return element.isContentEditable or
81 isIframeEditor(element) or
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')
88 isDevtoolsElement = (element) ->
89 return false unless element.ownerGlobal
90 return Array.prototype.some.call(
91 element.ownerGlobal.top.frames, isDevtoolsWindow
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+
104 # Note: this is possibly a bit overzealous, but Works For Now™.
105 isDockedDevtoolsElement = (element) ->
106 return element.ownerDocument.URL.startsWith('chrome://devtools/content/')
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']
121 isIframeEditor = (element) ->
122 return false unless element.localName == 'body'
125 element.id == 'innerdocbody' or
127 (element.classList?.contains('xe_content') and
128 element.classList?.contains('editable')) or
130 element.classList?.contains('wysiwyg') or
132 element.classList?.contains('htmlarea-content-body') or
133 # The wasavi extension.
134 element.hasAttribute?('data-wasavi-state')
136 isIgnoreModeFocusType = (element) ->
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))
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))
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:')
166 isTextInputElement = (element) ->
167 return (element.localName == 'input' and element.type in [
168 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
170 element.localName in [ 'textarea', 'textbox' ] or
171 isContentEditable(element)
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
182 # Active/focused element helpers
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.
193 activeElement = getActiveElement(window)
194 activeElement.closest('tabmodalprompt')?.abortPrompt()
195 vim._send('browserRefocus')
198 window.gBrowser.selectedBrowser.focus()
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
209 if window.document.activeElement?.shadowRoot?
210 return getActiveElement(window)?.blur()
211 window.document.activeElement?.blur()
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
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:
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)
248 return getActiveElement(activeElement.contentWindow)
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.
258 when isIgnoreModeFocusType(element)
260 when isTypingElement(element)
261 if element.closest?('findbar') then 'findbar' else 'editable'
262 when isActivatable(element)
264 when isAdjustable(element)
273 listen = (element, eventName, listener, useCapture = true) ->
274 element.addEventListener(eventName, listener, useCapture)
276 element.removeEventListener(eventName, listener, useCapture)
279 listenOnce = (element, eventName, listener, useCapture = true) ->
282 element.removeEventListener(eventName, fn, useCapture)
283 listen(element, eventName, fn, useCapture)
285 onRemoved = (element, fn) ->
286 window = element.ownerGlobal
290 return if disconnected
292 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
294 mutationObserver = new window.MutationObserver((changes) ->
295 for change in changes then for removedElement in change.removedNodes
296 if removedElement.contains?(element)
301 mutationObserver.observe(window.document.documentElement, {
305 module.onShutdown(disconnect)
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
317 charset: browser.characterSet,
318 referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
319 allowMixedContent: data.allowMixedContent, # <=fx88
320 isContentWindowPrivate: data.isContentWindowPrivate,
321 originPrincipal: data.originPrincipal,
322 originStoragePrincipal: data.originStoragePrincipal,
323 triggeringPrincipal: data.triggeringPrincipal,
324 csp: if data.csp then E10SUtils.deserializeCSP(data.csp) else null,
325 frameOuterWindowID: data.frameOuterWindowID, # <=fx79
326 frameID: data.frameID, # >=fx80
327 allowInheritPrincipal: true,
328 openerBrowser: browser, # >=fx98
329 hasValidUserGestureActivation: true, # >=fx103
330 triggeringRemoteType: browser.browsingContext.currentWindowGlobal
331 .domProcess?.remoteType, # >=fx109
334 if data.originAttributes.userContextId
335 params.userContextId = data.originAttributes.userContextId
337 try if not PrivateBrowsingUtils.isWindowPrivate(window)
338 PlacesUIUtils.markPageAsFollowedLink(data.href)
340 window.openLinkIn(data.href, window.whereToOpenLink(data), params)
342 simulateMouseEvents = (element, sequence, browserOffset) ->
343 window = element.ownerGlobal
344 rect = element.getBoundingClientRect()
345 topOffset = getTopOffset(element)
347 eventSequence = switch sequence
361 for type in eventSequence
363 when type in EVENTS_CONTEXT
365 when type in EVENTS_CLICK
370 mouseEvent = new window.MouseEvent(type, {
371 # Let the event bubble in order to trigger delegated event listeners.
372 bubbles: type not in ['mouseenter', 'mouseleave']
373 # Make the event cancelable so that `<a href="#">` can be used as a
374 # JavaScript-powered button without scrolling to the top of the page.
375 cancelable: type not in ['mouseenter', 'mouseleave']
376 # These properties are just here for mimicing a real click as much as
381 # `page{X,Y}` are set automatically to the correct values when setting
382 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
385 clientY: rect.top + rect.height / 2
386 screenX: browserOffset.x + topOffset.x
387 screenY: browserOffset.y + topOffset.y + rect.height / 2
390 if type == 'mousemove'
391 # If the below technique is used for this event, the “URL popup” (shown
392 # when hovering or focusing links) does not appear.
393 element.dispatchEvent(mouseEvent)
394 else if isInShadowRoot(element)
395 # click events for links and other clickables inside the shadow DOM are
396 # caught by the callee (.click_marker_element()).
397 element.focus() if type == 'contextmenu' # for <input type=text>
398 element.dispatchEvent(mouseEvent)
401 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
402 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
403 )(element, mouseEvent)
405 if error.result != Cr.NS_ERROR_UNEXPECTED
410 suppressEvent = (event) ->
411 event.preventDefault()
412 event.stopPropagation()
419 return element.clientWidth * element.clientHeight
421 checkElementOrAncestor = (element, fn) ->
422 window = element.ownerGlobal
423 while element.parentElement
424 return true if fn(element)
425 element = element.parentElement
428 clearSelectionDeep = (window, {blur = true} = {}) ->
429 # The selection might be `null` in hidden frames.
430 selection = window.getSelection()
431 selection?.removeAllRanges()
432 # Note: accessing frameElement fails on oop iframes (fission); skip those.
433 for frame in window.frames when (try frame.frameElement)
434 clearSelectionDeep(frame, {blur})
435 # Allow parents to re-gain control of text selection.
436 frame.frameElement.blur() if blur
439 containsDeep = (parent, element) ->
440 parentWindow = parent.ownerGlobal
441 elementWindow = element.ownerGlobal
443 # Owner windows might be missing when opening the devtools.
444 while elementWindow and parentWindow and
445 elementWindow != parentWindow and elementWindow.top != elementWindow
446 element = elementWindow.frameElement
447 elementWindow = element.ownerGlobal
449 return parent.contains(element)
451 createBox = (document, className = '', parent = null, text = null) ->
452 box = document.createElement('box')
453 box.className = "#{className} vimfx-box"
454 box.textContent = text if text?
455 parent.appendChild(box) if parent?
458 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
459 # `<body>` is considered the root element rather than `<html>`.
460 getRootElement = (document) ->
461 if document.compatMode == 'BackCompat' and document.body?
464 return document.documentElement
466 getText = (element) ->
467 text = element.textContent or element.value or element.placeholder or ''
468 return text.trim().replace(/\s+/, ' ')
470 getTopOffset = (element) ->
471 window = element.ownerGlobal
473 {left: x, top: y} = element.getBoundingClientRect()
474 while window.frameElement
475 frame = window.frameElement
476 frameRect = frame.getBoundingClientRect()
480 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
483 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
484 parseFloat(computedStyle.getPropertyValue('padding-left'))
486 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
487 parseFloat(computedStyle.getPropertyValue('padding-top'))
489 window = window.parent
492 injectTemporaryPopup = (document, contents) ->
493 popup = document.createXULElement('menupopup')
494 popup.appendChild(contents)
495 document.getElementById('mainPopupSet').appendChild(popup)
496 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
499 insertText = (input, value) ->
500 {selectionStart, selectionEnd} = input
502 input.value[0...selectionStart] + value + input.value[selectionEnd..]
503 input.selectionStart = input.selectionEnd = selectionStart + value.length
505 isDetached = (element) ->
506 return not element.ownerDocument?.documentElement?.contains?(element)
508 isNonEmptyTextNode = (node) ->
509 return node.nodeType == 3 and node.data.trim() != ''
511 querySelectorAllDeep = (window, selector) ->
512 elements = Array.from(window.document.querySelectorAll(selector))
513 for frame in window.frames
514 elements.push(querySelectorAllDeep(frame, selector)...)
517 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
518 window = element.ownerGlobal
519 selection = window.getSelection()
520 {textContent} = element
522 format = (string) -> if caseSensitive then string else string.toLowerCase()
524 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
525 offsetsLength = offsets.length
526 return if offsetsLength == 0
530 [currentOffset] = offsets
531 searchIndex = currentOffset.start
534 walkTextNodes(element, (textNode) ->
535 {length} = textNode.data
536 return false if length == 0
538 while textIndex + length > searchIndex
540 range = window.document.createRange()
541 range.setStart(start.textNode, start.offset)
542 range.setEnd(textNode, currentOffset.end - textIndex)
543 selection.addRange(range)
546 return true if offsetsIndex >= offsetsLength
547 currentOffset = offsets[offsetsIndex]
550 searchIndex = currentOffset.start
553 start = {textNode, offset: currentOffset.start - textIndex}
554 searchIndex = currentOffset.end - 1
560 selectElement = (element) ->
561 window = element.ownerGlobal
562 selection = window.getSelection()
563 range = window.document.createRange()
564 range.selectNodeContents(element)
565 selection.addRange(range)
567 setAttributes = (element, attributes) ->
568 for attribute, value of attributes
569 element.setAttribute(attribute, value)
572 walkTextNodes = (element, fn) ->
573 for node in element.childNodes then switch node.nodeType
578 stop = walkTextNodes(node, fn)
587 constructor: ({start: @value = 0, @step = 1}) ->
588 tick: -> @value += @step
594 on: (event, listener) ->
595 (@listeners[event] ?= new Set()).add(listener)
597 off: (event, listener) ->
598 @listeners[event]?.delete(listener)
600 emit: (event, data) ->
601 @listeners[event]?.forEach((listener) ->
605 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
606 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
607 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
608 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
609 # max`. `[null, null]` is returned in non-sensical cases. This function is
610 # intended to be used as a faster alternative to something like this:
612 # adjacentMatchAfter = null
613 # for n in [min..max]
615 # adjacentMatchAfter = n
617 bisect = (min, max, fn) ->
618 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
621 mid = min + (max - min) // 2
632 when matchMin and matchMax
634 when not matchMin and not matchMax
636 when not matchMin and matchMax
641 getAllNonOverlappingRangeOffsets = (string, substring) ->
643 return [] if length == 0
646 lastOffset = {start: -Infinity, end: -Infinity}
650 index = string.indexOf(substring, index + 1)
652 if index > lastOffset.end
653 lastOffset = {start: index, end: index + length}
654 offsets.push(lastOffset)
656 lastOffset.end = index + length
660 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
662 # Check if `search` exists in `string` (case insensitively). Returns `false` if
663 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
664 includes = (string, search) ->
665 return false unless typeof string == 'string'
666 return string.toLowerCase().includes(search)
668 # Calls `fn` repeatedly, with at least `interval` ms between each call.
669 interval = (window, interval, fn) ->
671 currentIntervalId = null
674 currentIntervalId = window.setTimeout((-> fn(next)), interval)
677 window.clearTimeout(currentIntervalId)
681 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
683 overlaps = (rectA, rectB) ->
685 Math.round(rectA.right) >= Math.round(rectB.left) and
686 Math.round(rectA.left) <= Math.round(rectB.right) and
687 Math.round(rectA.bottom) >= Math.round(rectB.top) and
688 Math.round(rectA.top) <= Math.round(rectB.bottom)
690 partition = (array, fn) ->
693 for item, index in array
694 if fn(item, index, array)
697 nonMatching.push(item)
698 return [matching, nonMatching]
700 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
702 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
704 removeDuplicates = (array) -> Array.from(new Set(array))
706 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
712 expandPath = (path) ->
713 if path.startsWith('~/') or path.startsWith('~\\')
714 return Services.dirsvc.get('Home', Ci.nsIFile).path + path[1..]
718 getCurrentLocation = ->
719 return unless window = getCurrentWindow()
720 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
722 # This function might return `null` on startup.
723 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
725 # gBrowser getFindBar() used to return the findBar directly, but in recent
726 # versions it returns a promise. This function should be removed once these old
727 # versions are no longer supported.
728 getFindBar = (gBrowser) ->
729 promiseOrFindBar = gBrowser.getFindBar()
730 if promiseOrFindBar instanceof Promise
733 Promise.resolve(promiseOrFindBar)
735 hasEventListeners = (element, type) ->
736 for listener in nsIEventListenerService.getListenerInfoFor(element)
737 if listener.listenerObject and listener.type == type
741 loadCss = (uriString) ->
742 uri = Services.io.newURI(uriString, null, null)
743 method = nsIStyleSheetService.AUTHOR_SHEET
744 unless nsIStyleSheetService.sheetRegistered(uri, method)
745 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
747 nsIStyleSheetService.unregisterSheet(uri, method)
750 observe = (topic, observer) ->
751 observer = {observe: observer} if typeof observer == 'function'
752 Services.obs.addObserver(observer, topic, false)
754 Services.obs.removeObserver(observer, topic, false)
757 # Try to open a button’s dropdown menu, if any.
758 openDropdown = (element) ->
759 if isXULElement(element) and
760 element.getAttribute?('type') == 'menu' and
761 element.open == false # Only change `.open` if it is already a boolean.
764 openPopup = (popup) ->
765 window = popup.ownerGlobal
766 # Show the popup so it gets a height and width.
767 popup.openPopupAtScreen(0, 0)
768 # Center the popup inside the window.
770 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
771 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
774 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
785 isDockedDevtoolsElement
788 isIgnoreModeFocusType
795 blurActiveBrowserElement
809 checkElementOrAncestor
821 selectAllSubstringMatches
829 getAllNonOverlappingRangeOffsets