1 # This file contains lots of different helper functions.
3 {E10SUtils} = ChromeUtils.import('resource://gre/modules/E10SUtils.jsm')
4 {OS} = ChromeUtils.import('resource://gre/modules/osfile.jsm')
5 {PlacesUIUtils} = ChromeUtils.import('resource:///modules/PlacesUIUtils.jsm')
6 {PrivateBrowsingUtils} =
7 ChromeUtils.import('resource://gre/modules/PrivateBrowsingUtils.jsm')
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)
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
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
30 isInShadowRoot = (element) ->
31 ShadowRoot? and getDocument(element) instanceof ShadowRoot
33 isXULElement = (element) ->
34 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
35 element.namespaceURI == XUL_NS
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']
48 # Element classification helpers
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
57 isActivatable = (element) ->
58 return element.localName in ['a', 'button'] or
59 (element.localName == 'input' and element.type in [
60 'button', 'submit', 'reset', 'image'
62 element instanceof XULButtonElement
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'
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)
80 isContentEditable = (element) ->
81 return element.isContentEditable or
82 isIframeEditor(element) or
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')
89 isDevtoolsElement = (element) ->
90 return false unless element.ownerGlobal
91 return Array.prototype.some.call(
92 element.ownerGlobal.top.frames, isDevtoolsWindow
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+
105 # Note: this is possibly a bit overzealous, but Works For Now™.
106 isDockedDevtoolsElement = (element) ->
107 return element.ownerDocument.URL.startsWith('chrome://devtools/content/')
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']
122 isIframeEditor = (element) ->
123 return false unless element.localName == 'body'
126 element.id == 'innerdocbody' or
128 (element.classList?.contains('xe_content') and
129 element.classList?.contains('editable')) or
131 element.classList?.contains('wysiwyg') or
133 element.classList?.contains('htmlarea-content-body') or
134 # The wasavi extension.
135 element.hasAttribute?('data-wasavi-state')
137 isIgnoreModeFocusType = (element) ->
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))
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))
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:')
167 isTextInputElement = (element) ->
168 return (element.localName == 'input' and element.type in [
169 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
171 element.localName in [ 'textarea', 'textbox' ] or
172 isContentEditable(element)
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
183 # Active/focused element helpers
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.
194 activeElement = getActiveElement(window)
195 activeElement.closest('tabmodalprompt')?.abortPrompt()
196 vim._send('browserRefocus')
199 window.gBrowser.selectedBrowser.focus()
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
210 if window.document.activeElement?.shadowRoot?
211 return getActiveElement(window)?.blur()
212 window.document.activeElement?.blur()
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
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:
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)
249 return getActiveElement(activeElement.contentWindow)
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.
259 when isIgnoreModeFocusType(element)
261 when isTypingElement(element)
262 if element.closest?('findbar') then 'findbar' else 'editable'
263 when isActivatable(element)
265 when isAdjustable(element)
274 listen = (element, eventName, listener, useCapture = true) ->
275 element.addEventListener(eventName, listener, useCapture)
277 element.removeEventListener(eventName, listener, useCapture)
280 listenOnce = (element, eventName, listener, useCapture = true) ->
283 element.removeEventListener(eventName, fn, useCapture)
284 listen(element, eventName, fn, useCapture)
286 onRemoved = (element, fn) ->
287 window = element.ownerGlobal
291 return if disconnected
293 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
295 mutationObserver = new window.MutationObserver((changes) ->
296 for change in changes then for removedElement in change.removedNodes
297 if removedElement.contains?(element)
302 mutationObserver.observe(window.document.documentElement, {
306 module.onShutdown(disconnect)
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
318 charset: browser.characterSet,
319 referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
320 allowMixedContent: data.allowMixedContent, # <=fx88
321 isContentWindowPrivate: data.isContentWindowPrivate,
322 originPrincipal: data.originPrincipal,
323 originStoragePrincipal: data.originStoragePrincipal,
324 triggeringPrincipal: data.triggeringPrincipal,
325 csp: if data.csp then E10SUtils.deserializeCSP(data.csp) else null,
326 frameOuterWindowID: data.frameOuterWindowID, # <=fx79
327 frameID: data.frameID, # >=fx80
328 allowInheritPrincipal: true,
329 openerBrowser: browser, # >=fx98
332 if data.originAttributes.userContextId
333 params.userContextId = data.originAttributes.userContextId
335 try if not PrivateBrowsingUtils.isWindowPrivate(window)
336 PlacesUIUtils.markPageAsFollowedLink(data.href)
338 window.openLinkIn(data.href, window.whereToOpenLink(data), params)
340 simulateMouseEvents = (element, sequence, browserOffset) ->
341 window = element.ownerGlobal
342 rect = element.getBoundingClientRect()
343 topOffset = getTopOffset(element)
345 eventSequence = switch sequence
359 for type in eventSequence
361 when type in EVENTS_CONTEXT
363 when type in EVENTS_CLICK
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
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
383 clientY: rect.top + rect.height / 2
384 screenX: browserOffset.x + topOffset.x
385 screenY: browserOffset.y + topOffset.y + rect.height / 2
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)
399 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
400 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
401 )(element, mouseEvent)
403 if error.result != Cr.NS_ERROR_UNEXPECTED
408 suppressEvent = (event) ->
409 event.preventDefault()
410 event.stopPropagation()
417 return element.clientWidth * element.clientHeight
419 checkElementOrAncestor = (element, fn) ->
420 window = element.ownerGlobal
421 while element.parentElement
422 return true if fn(element)
423 element = element.parentElement
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
437 containsDeep = (parent, element) ->
438 parentWindow = parent.ownerGlobal
439 elementWindow = element.ownerGlobal
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
447 return parent.contains(element)
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?
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?
462 return document.documentElement
464 getText = (element) ->
465 text = element.textContent or element.value or element.placeholder or ''
466 return text.trim().replace(/\s+/, ' ')
468 getTopOffset = (element) ->
469 window = element.ownerGlobal
471 {left: x, top: y} = element.getBoundingClientRect()
472 while window.frameElement
473 frame = window.frameElement
474 frameRect = frame.getBoundingClientRect()
478 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
481 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
482 parseFloat(computedStyle.getPropertyValue('padding-left'))
484 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
485 parseFloat(computedStyle.getPropertyValue('padding-top'))
487 window = window.parent
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))
497 insertText = (input, value) ->
498 {selectionStart, selectionEnd} = input
500 input.value[0...selectionStart] + value + input.value[selectionEnd..]
501 input.selectionStart = input.selectionEnd = selectionStart + value.length
503 isDetached = (element) ->
504 return not element.ownerDocument?.documentElement?.contains?(element)
506 isNonEmptyTextNode = (node) ->
507 return node.nodeType == 3 and node.data.trim() != ''
509 querySelectorAllDeep = (window, selector) ->
510 elements = Array.from(window.document.querySelectorAll(selector))
511 for frame in window.frames
512 elements.push(querySelectorAllDeep(frame, selector)...)
515 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
516 window = element.ownerGlobal
517 selection = window.getSelection()
518 {textContent} = element
520 format = (string) -> if caseSensitive then string else string.toLowerCase()
522 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
523 offsetsLength = offsets.length
524 return if offsetsLength == 0
528 [currentOffset] = offsets
529 searchIndex = currentOffset.start
532 walkTextNodes(element, (textNode) ->
533 {length} = textNode.data
534 return false if length == 0
536 while textIndex + length > searchIndex
538 range = window.document.createRange()
539 range.setStart(start.textNode, start.offset)
540 range.setEnd(textNode, currentOffset.end - textIndex)
541 selection.addRange(range)
544 return true if offsetsIndex >= offsetsLength
545 currentOffset = offsets[offsetsIndex]
548 searchIndex = currentOffset.start
551 start = {textNode, offset: currentOffset.start - textIndex}
552 searchIndex = currentOffset.end - 1
558 selectElement = (element) ->
559 window = element.ownerGlobal
560 selection = window.getSelection()
561 range = window.document.createRange()
562 range.selectNodeContents(element)
563 selection.addRange(range)
565 setAttributes = (element, attributes) ->
566 for attribute, value of attributes
567 element.setAttribute(attribute, value)
570 walkTextNodes = (element, fn) ->
571 for node in element.childNodes then switch node.nodeType
576 stop = walkTextNodes(node, fn)
585 constructor: ({start: @value = 0, @step = 1}) ->
586 tick: -> @value += @step
592 on: (event, listener) ->
593 (@listeners[event] ?= new Set()).add(listener)
595 off: (event, listener) ->
596 @listeners[event]?.delete(listener)
598 emit: (event, data) ->
599 @listeners[event]?.forEach((listener) ->
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:
610 # adjacentMatchAfter = null
611 # for n in [min..max]
613 # adjacentMatchAfter = n
615 bisect = (min, max, fn) ->
616 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
619 mid = min + (max - min) // 2
630 when matchMin and matchMax
632 when not matchMin and not matchMax
634 when not matchMin and matchMax
639 getAllNonOverlappingRangeOffsets = (string, substring) ->
641 return [] if length == 0
644 lastOffset = {start: -Infinity, end: -Infinity}
648 index = string.indexOf(substring, index + 1)
650 if index > lastOffset.end
651 lastOffset = {start: index, end: index + length}
652 offsets.push(lastOffset)
654 lastOffset.end = index + length
658 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
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)
666 # Calls `fn` repeatedly, with at least `interval` ms between each call.
667 interval = (window, interval, fn) ->
669 currentIntervalId = null
672 currentIntervalId = window.setTimeout((-> fn(next)), interval)
675 window.clearTimeout(currentIntervalId)
679 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
681 overlaps = (rectA, rectB) ->
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)
688 partition = (array, fn) ->
691 for item, index in array
692 if fn(item, index, array)
695 nonMatching.push(item)
696 return [matching, nonMatching]
698 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
700 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
702 removeDuplicates = (array) -> Array.from(new Set(array))
704 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
710 expandPath = (path) ->
711 if path.startsWith('~/') or path.startsWith('~\\')
712 return OS.Constants.Path.homeDir + path[1..]
716 getCurrentLocation = ->
717 return unless window = getCurrentWindow()
718 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
720 # This function might return `null` on startup.
721 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
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
731 Promise.resolve(promiseOrFindBar)
733 hasEventListeners = (element, type) ->
734 for listener in nsIEventListenerService.getListenerInfoFor(element)
735 if listener.listenerObject and listener.type == type
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)
745 nsIStyleSheetService.unregisterSheet(uri, method)
748 observe = (topic, observer) ->
749 observer = {observe: observer} if typeof observer == 'function'
750 Services.obs.addObserver(observer, topic, false)
752 Services.obs.removeObserver(observer, topic, false)
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.
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.
768 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
769 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
772 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
783 isDockedDevtoolsElement
786 isIgnoreModeFocusType
793 blurActiveBrowserElement
807 checkElementOrAncestor
819 selectAllSubstringMatches
827 getAllNonOverlappingRangeOffsets