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 the ShadowRoot.
27 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
29 isInShadowRoot = (element) ->
30 ShadowRoot? and getDocument(element) instanceof 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.xhtml'
103 # Note: this is possibly a bit overzealous, but Works For Now™.
104 isDockedDevtoolsElement = (element) ->
105 return element.ownerDocument.URL.startsWith('chrome://devtools/content/')
107 isFocusable = (element) ->
108 # Focusable elements have `.tabIndex > 1` (but not necessarily a
109 # `tabindex="…"` attribute) …
110 return (element.tabIndex > -1 or
111 # … or an explicit `tabindex="-1"` attribute (which means that it is
112 # focusable, but not reachable with `<tab>`).
113 element.getAttribute?('tabindex') == '-1') and
114 not (element.localName?.endsWith?('box') and
115 element.localName != 'checkbox') and
116 not (element.localName == 'toolbarbutton' and
117 element.parentNode?.localName == 'toolbarbutton') and
118 element.localName not in ['tabs', 'menuitem', 'menuseparator']
120 isIframeEditor = (element) ->
121 return false unless element.localName == 'body'
124 element.id == 'innerdocbody' or
126 (element.classList?.contains('xe_content') and
127 element.classList?.contains('editable')) or
129 element.classList?.contains('wysiwyg') or
131 element.classList?.contains('htmlarea-content-body') or
132 # The wasavi extension.
133 element.hasAttribute?('data-wasavi-state')
135 isIgnoreModeFocusType = (element) ->
137 # The wasavi extension.
138 element.hasAttribute?('data-wasavi-state') or
139 element.closest?('#wasavi_container') or
140 # CodeMirror in Vim mode.
141 (element.localName == 'textarea' and
142 element.closest?('.CodeMirror') and _hasVimEventListener(element))
144 # CodeMirror’s Vim mode is really sucky to detect. The only way seems to be to
145 # check if the there are any event listener functions with Vim-y words in them.
146 _hasVimEventListener = (element) ->
147 for listener in nsIEventListenerService.getListenerInfoFor(element)
148 if listener.listenerObject and
149 /\bvim\b|insertmode/i.test(String(listener.listenerObject))
153 isProperLink = (element) ->
154 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
155 # href="">`s used as buttons on some sites.
156 return element.getAttribute?('href') and
157 (element.localName == 'a' or
158 (isXULElement(element) and
159 element.localName == 'label' and
160 element.getAttribute('is') == 'text-link')) and
161 not element.href?.endsWith?('#') and
162 not element.href?.endsWith?('#?') and
163 not element.href?.startsWith?('javascript:')
165 isTextInputElement = (element) ->
166 return (element.localName == 'input' and element.type in [
167 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
169 element.localName in [ 'textarea', 'textbox' ] or
170 isContentEditable(element)
172 isTypingElement = (element) ->
173 return isTextInputElement(element) or
174 # `<select>` elements can also receive text input: You may type the
175 # text of an item to select it.
176 element.localName == 'select' or
177 element instanceof XULMenuListElement
181 # Active/focused element helpers
183 blurActiveBrowserElement = (vim) ->
184 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
185 # reset it, for example.
186 # - Focusing the current browser afterwards allows to pass `<escape>` as well
187 # as unbound keys to the page. However, focusing the browser also triggers
188 # focus events on `document` and `window` in the current page. Many pages
189 # re-focus some text input on those events, making it impossible to blur
190 # those! Therefore we tell the frame script to suppress those events.
192 activeElement = getActiveElement(window)
193 activeElement.closest('tabmodalprompt')?.abortPrompt()
194 vim._send('browserRefocus')
197 window.gBrowser.selectedBrowser.focus()
200 blurActiveElement = (window) ->
201 # Blurring a frame element also blurs any active elements inside it. Recursing
202 # into the frames and blurring the “real” active element directly would give
203 # focus to the `<body>` of its containing frame, while blurring the top-most
204 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
205 # editors which use an `<iframe>` as their text area.
206 # Note that this trick does not work with Web Components; for them, recursing
208 if window.document.activeElement?.shadowRoot?
209 return getActiveElement(window)?.blur()
210 window.document.activeElement?.blur()
212 # Focus an element and tell Firefox that the focus happened because of a user
213 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
214 # look more appropriate, but it unconditionally selects all text, which
215 # `.FLAG_BYMOUSE` does not.
216 focusElement = (element, options = {}) ->
217 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
218 element.select?() if options.select
220 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
221 # is loading. Therefore always check if anything was returned, such as:
223 # return unless activeElement = utils.getActiveElement(window)
224 getActiveElement = (window) ->
225 {activeElement} = window.shadowRoot or window.document
226 return null unless activeElement
227 # If the active element is a frame, recurse into it. The easiest way to detect
228 # a frame that works both in browser UI and in web page content is to check
229 # for the presence of `.contentWindow`. However, in non-multi-process,
230 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
231 # pointing to the web page content `window`, which we don’t want to recurse
232 # into. The problem is that there are _some_ `<browser>`s which we _want_ to
233 # recurse into, such as the sidebar (for instance the history sidebar), and
234 # dialogs in `about:preferences`. Checking the `contextmenu` attribute seems
235 # to be a reliable test, catching both the main tab `<browser>`s and bookmarks
236 # opened in the sidebar.
237 # We also want to recurse into the (open) shadow DOM of custom elements.
238 if activeElement.shadowRoot?
239 return getActiveElement(activeElement)
240 else if activeElement.contentWindow and
241 not (activeElement.localName == 'browser' and
242 activeElement.getAttribute?('contextmenu') == 'contentAreaContextMenu')
243 # with Fission enabled, the iframe might be located in a different process
244 # (oop). Then, recursing into it isn't possible (throws SecurityError).
245 return activeElement unless (try activeElement.contentWindow.document)
247 return getActiveElement(activeElement.contentWindow)
251 getFocusType = (element) -> switch
252 when element.tagName in ['FRAME', 'IFRAME'] and
253 not (try element.contentWindow.document)
254 # Encountered an out-of-process iframe, which we can't inspect. We fall
255 # back to insert mode, so any text inputs it may contain are still usable.
257 when isIgnoreModeFocusType(element)
259 when isTypingElement(element)
260 if element.closest?('findbar') then 'findbar' else 'editable'
261 when isActivatable(element)
263 when isAdjustable(element)
272 listen = (element, eventName, listener, useCapture = true) ->
273 element.addEventListener(eventName, listener, useCapture)
275 element.removeEventListener(eventName, listener, useCapture)
278 listenOnce = (element, eventName, listener, useCapture = true) ->
281 element.removeEventListener(eventName, fn, useCapture)
282 listen(element, eventName, fn, useCapture)
284 onRemoved = (element, fn) ->
285 window = element.ownerGlobal
289 return if disconnected
291 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
293 mutationObserver = new window.MutationObserver((changes) ->
294 for change in changes then for removedElement in change.removedNodes
295 if removedElement.contains?(element)
300 mutationObserver.observe(window.document.documentElement, {
304 module.onShutdown(disconnect)
308 contentAreaClick = (data, browser) ->
309 # This function is adapted from the same-named one currently in
310 # mozilla-central/browser/actors/ClickHandlerParent.jsm. Keep in sync!
311 # Note: Our version is shortened substantially and unlike Mozilla, we pass in
312 # the browser object instead of extracting it from the browsingContext.
313 window = browser.ownerGlobal
316 charset: browser.characterSet,
317 referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
318 allowMixedContent: data.allowMixedContent, # <=fx88
319 isContentWindowPrivate: data.isContentWindowPrivate,
320 originPrincipal: data.originPrincipal,
321 originStoragePrincipal: data.originStoragePrincipal,
322 triggeringPrincipal: data.triggeringPrincipal,
323 csp: if data.csp then E10SUtils.deserializeCSP(data.csp) else null,
324 frameID: data.frameID,
325 allowInheritPrincipal: true,
326 openerBrowser: browser, # >=fx98
327 hasValidUserGestureActivation: true, # >=fx103
330 if data.originAttributes.userContextId
331 params.userContextId = data.originAttributes.userContextId
333 try if not PrivateBrowsingUtils.isWindowPrivate(window)
334 PlacesUIUtils.markPageAsFollowedLink(data.href)
336 window.openLinkIn(data.href, window.whereToOpenLink(data), params)
338 simulateMouseEvents = (element, sequence, browserOffset) ->
339 window = element.ownerGlobal
340 rect = element.getBoundingClientRect()
341 topOffset = getTopOffset(element)
343 eventSequence = switch sequence
357 for type in eventSequence
359 when type in EVENTS_CONTEXT
361 when type in EVENTS_CLICK
366 mouseEvent = new window.MouseEvent(type, {
367 # Let the event bubble in order to trigger delegated event listeners.
368 bubbles: type not in ['mouseenter', 'mouseleave']
369 # Make the event cancelable so that `<a href="#">` can be used as a
370 # JavaScript-powered button without scrolling to the top of the page.
371 cancelable: type not in ['mouseenter', 'mouseleave']
372 # These properties are just here for mimicing a real click as much as
377 # `page{X,Y}` are set automatically to the correct values when setting
378 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
381 clientY: rect.top + rect.height / 2
382 screenX: browserOffset.x + topOffset.x
383 screenY: browserOffset.y + topOffset.y + rect.height / 2
386 if type == 'mousemove'
387 # If the below technique is used for this event, the “URL popup” (shown
388 # when hovering or focusing links) does not appear.
389 element.dispatchEvent(mouseEvent)
390 else if isInShadowRoot(element)
391 # click events for links and other clickables inside the shadow DOM are
392 # caught by the callee (.click_marker_element()).
393 element.focus() if type == 'contextmenu' # for <input type=text>
394 element.dispatchEvent(mouseEvent)
397 window.windowUtils.dispatchDOMEventViaPresShellForTesting(
401 if error.result != Cr.NS_ERROR_UNEXPECTED
406 suppressEvent = (event) ->
407 event.preventDefault()
408 event.stopPropagation()
415 return element.clientWidth * element.clientHeight
417 checkElementOrAncestor = (element, fn) ->
418 window = element.ownerGlobal
419 while element.parentElement
420 return true if fn(element)
421 element = element.parentElement
424 clearSelectionDeep = (window, {blur = true} = {}) ->
425 # The selection might be `null` in hidden frames.
426 selection = window.getSelection()
427 selection?.removeAllRanges()
428 # Note: accessing frameElement fails on oop iframes (fission); skip those.
429 for frame in window.frames when (try frame.frameElement)
430 clearSelectionDeep(frame, {blur})
431 # Allow parents to re-gain control of text selection.
432 frame.frameElement.blur() if blur
435 containsDeep = (parent, element) ->
436 parentWindow = parent.ownerGlobal
437 elementWindow = element.ownerGlobal
439 # Owner windows might be missing when opening the devtools.
440 while elementWindow and parentWindow and
441 elementWindow != parentWindow and elementWindow.top != elementWindow
442 element = elementWindow.frameElement
443 elementWindow = element.ownerGlobal
445 return parent.contains(element)
447 createBox = (document, className = '', parent = null, text = null) ->
448 box = document.createElement('box')
449 box.className = "#{className} vimfx-box"
450 box.textContent = text if text?
451 parent.appendChild(box) if parent?
454 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
455 # `<body>` is considered the root element rather than `<html>`.
456 getRootElement = (document) ->
457 if document.compatMode == 'BackCompat' and document.body?
460 return document.documentElement
462 getText = (element) ->
463 text = element.textContent or element.value or element.placeholder or ''
464 return text.trim().replace(/\s+/, ' ')
466 getTopOffset = (element) ->
467 window = element.ownerGlobal
469 {left: x, top: y} = element.getBoundingClientRect()
470 while window.frameElement
471 frame = window.frameElement
472 frameRect = frame.getBoundingClientRect()
476 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
479 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
480 parseFloat(computedStyle.getPropertyValue('padding-left'))
482 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
483 parseFloat(computedStyle.getPropertyValue('padding-top'))
485 window = window.parent
488 injectTemporaryPopup = (document, contents) ->
489 popup = document.createXULElement('menupopup')
490 popup.appendChild(contents)
491 document.getElementById('mainPopupSet').appendChild(popup)
492 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
495 insertText = (input, value) ->
496 {selectionStart, selectionEnd} = input
498 input.value[0...selectionStart] + value + input.value[selectionEnd..]
499 input.selectionStart = input.selectionEnd = selectionStart + value.length
501 isDetached = (element) ->
502 return not element.ownerDocument?.documentElement?.contains?(element)
504 isNonEmptyTextNode = (node) ->
505 return node.nodeType == 3 and node.data.trim() != ''
507 querySelectorAllDeep = (window, selector) ->
508 elements = Array.from(window.document.querySelectorAll(selector))
509 for frame in window.frames
510 elements.push(querySelectorAllDeep(frame, selector)...)
513 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
514 window = element.ownerGlobal
515 selection = window.getSelection()
516 {textContent} = element
518 format = (string) -> if caseSensitive then string else string.toLowerCase()
520 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
521 offsetsLength = offsets.length
522 return if offsetsLength == 0
526 [currentOffset] = offsets
527 searchIndex = currentOffset.start
530 walkTextNodes(element, (textNode) ->
531 {length} = textNode.data
532 return false if length == 0
534 while textIndex + length > searchIndex
536 range = window.document.createRange()
537 range.setStart(start.textNode, start.offset)
538 range.setEnd(textNode, currentOffset.end - textIndex)
539 selection.addRange(range)
542 return true if offsetsIndex >= offsetsLength
543 currentOffset = offsets[offsetsIndex]
546 searchIndex = currentOffset.start
549 start = {textNode, offset: currentOffset.start - textIndex}
550 searchIndex = currentOffset.end - 1
556 selectElement = (element) ->
557 window = element.ownerGlobal
558 selection = window.getSelection()
559 range = window.document.createRange()
560 range.selectNodeContents(element)
561 selection.addRange(range)
563 setAttributes = (element, attributes) ->
564 for attribute, value of attributes
565 element.setAttribute(attribute, value)
568 walkTextNodes = (element, fn) ->
569 for node in element.childNodes then switch node.nodeType
574 stop = walkTextNodes(node, fn)
583 constructor: ({start: @value = 0, @step = 1}) ->
584 tick: -> @value += @step
590 on: (event, listener) ->
591 (@listeners[event] ?= new Set()).add(listener)
593 off: (event, listener) ->
594 @listeners[event]?.delete(listener)
596 emit: (event, data) ->
597 @listeners[event]?.forEach((listener) ->
601 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
602 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
603 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
604 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
605 # max`. `[null, null]` is returned in non-sensical cases. This function is
606 # intended to be used as a faster alternative to something like this:
608 # adjacentMatchAfter = null
609 # for n in [min..max]
611 # adjacentMatchAfter = n
613 bisect = (min, max, fn) ->
614 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
617 mid = min + (max - min) // 2
628 when matchMin and matchMax
630 when not matchMin and not matchMax
632 when not matchMin and matchMax
637 getAllNonOverlappingRangeOffsets = (string, substring) ->
639 return [] if length == 0
642 lastOffset = {start: -Infinity, end: -Infinity}
646 index = string.indexOf(substring, index + 1)
648 if index > lastOffset.end
649 lastOffset = {start: index, end: index + length}
650 offsets.push(lastOffset)
652 lastOffset.end = index + length
656 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
658 # Check if `search` exists in `string` (case insensitively). Returns `false` if
659 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
660 includes = (string, search) ->
661 return false unless typeof string == 'string'
662 return string.toLowerCase().includes(search)
664 # Calls `fn` repeatedly, with at least `interval` ms between each call.
665 interval = (window, interval, fn) ->
667 currentIntervalId = null
670 currentIntervalId = window.setTimeout((-> fn(next)), interval)
673 window.clearTimeout(currentIntervalId)
677 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
679 overlaps = (rectA, rectB) ->
681 Math.round(rectA.right) >= Math.round(rectB.left) and
682 Math.round(rectA.left) <= Math.round(rectB.right) and
683 Math.round(rectA.bottom) >= Math.round(rectB.top) and
684 Math.round(rectA.top) <= Math.round(rectB.bottom)
686 partition = (array, fn) ->
689 for item, index in array
690 if fn(item, index, array)
693 nonMatching.push(item)
694 return [matching, nonMatching]
696 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
698 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
700 removeDuplicates = (array) -> Array.from(new Set(array))
702 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
708 expandPath = (path) ->
709 if path.startsWith('~/') or path.startsWith('~\\')
710 return Services.dirsvc.get('Home', Ci.nsIFile).path + path[1..]
714 getCurrentLocation = ->
715 return unless window = getCurrentWindow()
716 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
718 # This function might return `null` on startup.
719 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
721 # gBrowser getFindBar() used to return the findBar directly, but in recent
722 # versions it returns a promise. This function should be removed once these old
723 # versions are no longer supported.
724 getFindBar = (gBrowser) ->
725 promiseOrFindBar = gBrowser.getFindBar()
726 if promiseOrFindBar instanceof Promise
729 Promise.resolve(promiseOrFindBar)
731 hasEventListeners = (element, type) ->
732 for listener in nsIEventListenerService.getListenerInfoFor(element)
733 if listener.listenerObject and listener.type == type
737 loadCss = (uriString) ->
738 uri = Services.io.newURI(uriString, null, null)
739 method = nsIStyleSheetService.AUTHOR_SHEET
740 unless nsIStyleSheetService.sheetRegistered(uri, method)
741 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
743 nsIStyleSheetService.unregisterSheet(uri, method)
746 observe = (topic, observer) ->
747 observer = {observe: observer} if typeof observer == 'function'
748 Services.obs.addObserver(observer, topic, false)
750 Services.obs.removeObserver(observer, topic, false)
753 # Try to open a button’s dropdown menu, if any.
754 openDropdown = (element) ->
755 if isXULElement(element) and
756 element.getAttribute?('type') == 'menu' and
757 element.open == false # Only change `.open` if it is already a boolean.
760 openPopup = (popup) ->
761 window = popup.ownerGlobal
762 # Show the popup so it gets a height and width.
763 popup.openPopupAtScreen(0, 0)
764 # Center the popup inside the window.
766 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
767 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
770 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
781 isDockedDevtoolsElement
784 isIgnoreModeFocusType
791 blurActiveBrowserElement
805 checkElementOrAncestor
817 selectAllSubstringMatches
825 getAllNonOverlappingRangeOffsets