1 # This file contains lots of different helper functions.
3 {E10SUtils} = ChromeUtils
4 .importESModule('resource://gre/modules/E10SUtils.sys.mjs')
5 {PlacesUIUtils} = ChromeUtils
6 .importESModule('resource:///modules/PlacesUIUtils.sys.mjs')
7 {PrivateBrowsingUtils} = ChromeUtils
8 .importESModule('resource://gre/modules/PrivateBrowsingUtils.sys.mjs')
10 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
11 .getService(Ci.nsIClipboardHelper)
12 nsIEventListenerService = Cc['@mozilla.org/eventlistenerservice;1']
13 .getService(Ci.nsIEventListenerService)
14 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
15 .getService(Ci.nsIFocusManager)
16 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
17 .getService(Ci.nsIStyleSheetService)
18 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
19 .getService(Ci.nsIWindowMediator)
21 # For XUL, `instanceof` checks are often better than `.localName` checks,
22 # because some of the below interfaces are extended by many elements.
23 XULButtonElement = Ci.nsIDOMXULButtonElement
24 XULControlElement = Ci.nsIDOMXULControlElement
25 XULMenuListElement = Ci.nsIDOMXULMenuListElement
27 # Traverse the DOM upwards until we hit its containing document (most likely an
28 # HTMLDocument or (<=fx68) XULDocument) or the ShadowRoot.
29 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
31 isInShadowRoot = (element) ->
32 getDocument(element) instanceof element.ownerGlobal.ShadowRoot
34 isXULElement = (element) ->
35 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
36 element.namespaceURI == XUL_NS
38 # Full chains of events for different mouse actions. Note: 'click' is fired
39 # by Firefox automatically after 'mousedown' and 'mouseup'. Similarly,
40 # 'command' is fired automatically after 'click' on xul pages.
41 EVENTS_CLICK = ['mousedown', 'mouseup']
42 EVENTS_CLICK_XUL = ['click']
43 EVENTS_CONTEXT = ['contextmenu']
44 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
45 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
49 # Element classification helpers
51 hasMarkableTextNode = (element) ->
52 return Array.prototype.some.call(element.childNodes, (node) ->
53 # Ignore whitespace-only text nodes, and single-letter ones (which are
54 # common in many syntax highlighters).
55 return node.nodeType == 3 and node.data.trim().length > 1
58 isActivatable = (element) ->
59 return element.localName in ['a', 'button'] or
60 (element.localName == 'input' and element.type in [
61 'button', 'submit', 'reset', 'image'
63 element instanceof XULButtonElement
65 isAdjustable = (element) ->
66 return element.localName == 'input' and element.type in [
67 'checkbox', 'radio', 'file', 'color'
68 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
70 element.localName in ['video', 'audio', 'embed', 'object'] or
71 element instanceof XULControlElement or
72 # Custom video players.
73 includes(element.className, 'video') or
74 includes(element.className, 'player') or
75 # Youtube special case.
76 element.classList?.contains('ytp-button') or
77 # Allow navigating object inspection trees in th devtools with the
78 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
79 isDevtoolsElement(element)
81 isContentEditable = (element) ->
82 return element.isContentEditable or
83 isIframeEditor(element) or
85 element.getAttribute?('g_editable') == 'true' or
86 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
87 # Codeacademy terminals.
88 element.classList?.contains('real-terminal')
90 isDevtoolsElement = (element) ->
91 return false unless element.ownerGlobal
92 return Array.prototype.some.call(
93 element.ownerGlobal.top.frames, isDevtoolsWindow
96 isDevtoolsWindow = (window) ->
97 # Note: this function is called for each frame by isDevtoolsElement. When
98 # called on an out-of-process iframe, accessing .href will fail with
99 # SecurityError; the `try` around it makes it `undefined` in such a case.
100 return (try window.location?.href) in [
101 'about:devtools-toolbox'
102 'chrome://devtools/content/framework/toolbox.xul'
103 'chrome://devtools/content/framework/toolbox.xhtml' # fx72+
106 # Note: this is possibly a bit overzealous, but Works For Now™.
107 isDockedDevtoolsElement = (element) ->
108 return element.ownerDocument.URL.startsWith('chrome://devtools/content/')
110 isFocusable = (element) ->
111 # Focusable elements have `.tabIndex > 1` (but not necessarily a
112 # `tabindex="…"` attribute) …
113 return (element.tabIndex > -1 or
114 # … or an explicit `tabindex="-1"` attribute (which means that it is
115 # focusable, but not reachable with `<tab>`).
116 element.getAttribute?('tabindex') == '-1') and
117 not (element.localName?.endsWith?('box') and
118 element.localName != 'checkbox') and
119 not (element.localName == 'toolbarbutton' and
120 element.parentNode?.localName == 'toolbarbutton') and
121 element.localName not in ['tabs', 'menuitem', 'menuseparator']
123 isIframeEditor = (element) ->
124 return false unless element.localName == 'body'
127 element.id == 'innerdocbody' or
129 (element.classList?.contains('xe_content') and
130 element.classList?.contains('editable')) or
132 element.classList?.contains('wysiwyg') or
134 element.classList?.contains('htmlarea-content-body') or
135 # The wasavi extension.
136 element.hasAttribute?('data-wasavi-state')
138 isIgnoreModeFocusType = (element) ->
140 # The wasavi extension.
141 element.hasAttribute?('data-wasavi-state') or
142 element.closest?('#wasavi_container') or
143 # CodeMirror in Vim mode.
144 (element.localName == 'textarea' and
145 element.closest?('.CodeMirror') and _hasVimEventListener(element))
147 # CodeMirror’s Vim mode is really sucky to detect. The only way seems to be to
148 # check if the there are any event listener functions with Vim-y words in them.
149 _hasVimEventListener = (element) ->
150 for listener in nsIEventListenerService.getListenerInfoFor(element)
151 if listener.listenerObject and
152 /\bvim\b|insertmode/i.test(String(listener.listenerObject))
156 isProperLink = (element) ->
157 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
158 # href="">`s used as buttons on some sites.
159 return element.getAttribute?('href') and
160 (element.localName == 'a' or
161 (isXULElement(element) and
162 element.localName == 'label' and
163 element.getAttribute('is') == 'text-link')) and
164 not element.href?.endsWith?('#') and
165 not element.href?.endsWith?('#?') and
166 not element.href?.startsWith?('javascript:')
168 isTextInputElement = (element) ->
169 return (element.localName == 'input' and element.type in [
170 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
172 element.localName in [ 'textarea', 'textbox' ] or
173 isContentEditable(element)
175 isTypingElement = (element) ->
176 return isTextInputElement(element) or
177 # `<select>` elements can also receive text input: You may type the
178 # text of an item to select it.
179 element.localName == 'select' or
180 element instanceof XULMenuListElement
184 # Active/focused element helpers
186 blurActiveBrowserElement = (vim) ->
187 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
188 # reset it, for example.
189 # - Focusing the current browser afterwards allows to pass `<escape>` as well
190 # as unbound keys to the page. However, focusing the browser also triggers
191 # focus events on `document` and `window` in the current page. Many pages
192 # re-focus some text input on those events, making it impossible to blur
193 # those! Therefore we tell the frame script to suppress those events.
195 activeElement = getActiveElement(window)
196 activeElement.closest('tabmodalprompt')?.abortPrompt()
197 vim._send('browserRefocus')
200 window.gBrowser.selectedBrowser.focus()
203 blurActiveElement = (window) ->
204 # Blurring a frame element also blurs any active elements inside it. Recursing
205 # into the frames and blurring the “real” active element directly would give
206 # focus to the `<body>` of its containing frame, while blurring the top-most
207 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
208 # editors which use an `<iframe>` as their text area.
209 # Note that this trick does not work with Web Components; for them, recursing
211 if window.document.activeElement?.shadowRoot?
212 return getActiveElement(window)?.blur()
213 window.document.activeElement?.blur()
215 # Focus an element and tell Firefox that the focus happened because of a user
216 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
217 # look more appropriate, but it unconditionally selects all text, which
218 # `.FLAG_BYMOUSE` does not.
219 focusElement = (element, options = {}) ->
220 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
221 element.select?() if options.select
223 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
224 # is loading. Therefore always check if anything was returned, such as:
226 # return unless activeElement = utils.getActiveElement(window)
227 getActiveElement = (window) ->
228 {activeElement} = window.shadowRoot or window.document
229 return null unless activeElement
230 # If the active element is a frame, recurse into it. The easiest way to detect
231 # a frame that works both in browser UI and in web page content is to check
232 # for the presence of `.contentWindow`. However, in non-multi-process,
233 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
234 # pointing to the web page content `window`, which we don’t want to recurse
235 # into. The problem is that there are _some_ `<browser>`s which we _want_ to
236 # recurse into, such as the sidebar (for instance the history sidebar), and
237 # dialogs in `about:preferences`. Checking the `contextmenu` attribute seems
238 # to be a reliable test, catching both the main tab `<browser>`s and bookmarks
239 # opened in the sidebar.
240 # We also want to recurse into the (open) shadow DOM of custom elements.
241 if activeElement.shadowRoot?
242 return getActiveElement(activeElement)
243 else if activeElement.contentWindow and
244 not (activeElement.localName == 'browser' and
245 activeElement.getAttribute?('contextmenu') == 'contentAreaContextMenu')
246 # with Fission enabled, the iframe might be located in a different process
247 # (oop). Then, recursing into it isn't possible (throws SecurityError).
248 return activeElement unless (try activeElement.contentWindow.document)
250 return getActiveElement(activeElement.contentWindow)
254 getFocusType = (element) -> switch
255 when element.tagName in ['FRAME', 'IFRAME'] and
256 not (try element.contentWindow.document)
257 # Encountered an out-of-process iframe, which we can't inspect. We fall
258 # back to insert mode, so any text inputs it may contain are still usable.
260 when isIgnoreModeFocusType(element)
262 when isTypingElement(element)
263 if element.closest?('findbar') then 'findbar' else 'editable'
264 when isActivatable(element)
266 when isAdjustable(element)
275 listen = (element, eventName, listener, useCapture = true) ->
276 element.addEventListener(eventName, listener, useCapture)
278 element.removeEventListener(eventName, listener, useCapture)
281 listenOnce = (element, eventName, listener, useCapture = true) ->
284 element.removeEventListener(eventName, fn, useCapture)
285 listen(element, eventName, fn, useCapture)
287 onRemoved = (element, fn) ->
288 window = element.ownerGlobal
292 return if disconnected
294 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
296 mutationObserver = new window.MutationObserver((changes) ->
297 for change in changes then for removedElement in change.removedNodes
298 if removedElement.contains?(element)
303 mutationObserver.observe(window.document.documentElement, {
307 module.onShutdown(disconnect)
311 contentAreaClick = (data, browser) ->
312 # This function is adapted from the same-named one currently in
313 # mozilla-central/browser/actors/ClickHandlerParent.jsm. Keep in sync!
314 # Note: Our version is shortened substantially and unlike Mozilla, we pass in
315 # the browser object instead of extracting it from the browsingContext.
316 window = browser.ownerGlobal
319 charset: browser.characterSet,
320 referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
321 allowMixedContent: data.allowMixedContent, # <=fx88
322 isContentWindowPrivate: data.isContentWindowPrivate,
323 originPrincipal: data.originPrincipal,
324 originStoragePrincipal: data.originStoragePrincipal,
325 triggeringPrincipal: data.triggeringPrincipal,
326 csp: if data.csp then E10SUtils.deserializeCSP(data.csp) else null,
327 frameOuterWindowID: data.frameOuterWindowID, # <=fx79
328 frameID: data.frameID, # >=fx80
329 allowInheritPrincipal: true,
330 openerBrowser: browser, # >=fx98
331 hasValidUserGestureActivation: true, # >=fx103
332 triggeringRemoteType: browser.browsingContext.currentWindowGlobal
333 .domProcess?.remoteType, # >=fx109
336 if data.originAttributes.userContextId
337 params.userContextId = data.originAttributes.userContextId
339 try if not PrivateBrowsingUtils.isWindowPrivate(window)
340 PlacesUIUtils.markPageAsFollowedLink(data.href)
342 window.openLinkIn(data.href, window.whereToOpenLink(data), params)
344 simulateMouseEvents = (element, sequence, browserOffset) ->
345 window = element.ownerGlobal
346 rect = element.getBoundingClientRect()
347 topOffset = getTopOffset(element)
349 eventSequence = switch sequence
363 for type in eventSequence
365 when type in EVENTS_CONTEXT
367 when type in EVENTS_CLICK
372 mouseEvent = new window.MouseEvent(type, {
373 # Let the event bubble in order to trigger delegated event listeners.
374 bubbles: type not in ['mouseenter', 'mouseleave']
375 # Make the event cancelable so that `<a href="#">` can be used as a
376 # JavaScript-powered button without scrolling to the top of the page.
377 cancelable: type not in ['mouseenter', 'mouseleave']
378 # These properties are just here for mimicing a real click as much as
383 # `page{X,Y}` are set automatically to the correct values when setting
384 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
387 clientY: rect.top + rect.height / 2
388 screenX: browserOffset.x + topOffset.x
389 screenY: browserOffset.y + topOffset.y + rect.height / 2
392 if type == 'mousemove'
393 # If the below technique is used for this event, the “URL popup” (shown
394 # when hovering or focusing links) does not appear.
395 element.dispatchEvent(mouseEvent)
396 else if isInShadowRoot(element)
397 # click events for links and other clickables inside the shadow DOM are
398 # caught by the callee (.click_marker_element()).
399 element.focus() if type == 'contextmenu' # for <input type=text>
400 element.dispatchEvent(mouseEvent)
403 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
404 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
405 )(element, mouseEvent)
407 if error.result != Cr.NS_ERROR_UNEXPECTED
412 suppressEvent = (event) ->
413 event.preventDefault()
414 event.stopPropagation()
421 return element.clientWidth * element.clientHeight
423 checkElementOrAncestor = (element, fn) ->
424 window = element.ownerGlobal
425 while element.parentElement
426 return true if fn(element)
427 element = element.parentElement
430 clearSelectionDeep = (window, {blur = true} = {}) ->
431 # The selection might be `null` in hidden frames.
432 selection = window.getSelection()
433 selection?.removeAllRanges()
434 # Note: accessing frameElement fails on oop iframes (fission); skip those.
435 for frame in window.frames when (try frame.frameElement)
436 clearSelectionDeep(frame, {blur})
437 # Allow parents to re-gain control of text selection.
438 frame.frameElement.blur() if blur
441 containsDeep = (parent, element) ->
442 parentWindow = parent.ownerGlobal
443 elementWindow = element.ownerGlobal
445 # Owner windows might be missing when opening the devtools.
446 while elementWindow and parentWindow and
447 elementWindow != parentWindow and elementWindow.top != elementWindow
448 element = elementWindow.frameElement
449 elementWindow = element.ownerGlobal
451 return parent.contains(element)
453 createBox = (document, className = '', parent = null, text = null) ->
454 box = document.createElement('box')
455 box.className = "#{className} vimfx-box"
456 box.textContent = text if text?
457 parent.appendChild(box) if parent?
460 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
461 # `<body>` is considered the root element rather than `<html>`.
462 getRootElement = (document) ->
463 if document.compatMode == 'BackCompat' and document.body?
466 return document.documentElement
468 getText = (element) ->
469 text = element.textContent or element.value or element.placeholder or ''
470 return text.trim().replace(/\s+/, ' ')
472 getTopOffset = (element) ->
473 window = element.ownerGlobal
475 {left: x, top: y} = element.getBoundingClientRect()
476 while window.frameElement
477 frame = window.frameElement
478 frameRect = frame.getBoundingClientRect()
482 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
485 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
486 parseFloat(computedStyle.getPropertyValue('padding-left'))
488 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
489 parseFloat(computedStyle.getPropertyValue('padding-top'))
491 window = window.parent
494 injectTemporaryPopup = (document, contents) ->
495 popup = document.createXULElement('menupopup')
496 popup.appendChild(contents)
497 document.getElementById('mainPopupSet').appendChild(popup)
498 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
501 insertText = (input, value) ->
502 {selectionStart, selectionEnd} = input
504 input.value[0...selectionStart] + value + input.value[selectionEnd..]
505 input.selectionStart = input.selectionEnd = selectionStart + value.length
507 isDetached = (element) ->
508 return not element.ownerDocument?.documentElement?.contains?(element)
510 isNonEmptyTextNode = (node) ->
511 return node.nodeType == 3 and node.data.trim() != ''
513 querySelectorAllDeep = (window, selector) ->
514 elements = Array.from(window.document.querySelectorAll(selector))
515 for frame in window.frames
516 elements.push(querySelectorAllDeep(frame, selector)...)
519 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
520 window = element.ownerGlobal
521 selection = window.getSelection()
522 {textContent} = element
524 format = (string) -> if caseSensitive then string else string.toLowerCase()
526 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
527 offsetsLength = offsets.length
528 return if offsetsLength == 0
532 [currentOffset] = offsets
533 searchIndex = currentOffset.start
536 walkTextNodes(element, (textNode) ->
537 {length} = textNode.data
538 return false if length == 0
540 while textIndex + length > searchIndex
542 range = window.document.createRange()
543 range.setStart(start.textNode, start.offset)
544 range.setEnd(textNode, currentOffset.end - textIndex)
545 selection.addRange(range)
548 return true if offsetsIndex >= offsetsLength
549 currentOffset = offsets[offsetsIndex]
552 searchIndex = currentOffset.start
555 start = {textNode, offset: currentOffset.start - textIndex}
556 searchIndex = currentOffset.end - 1
562 selectElement = (element) ->
563 window = element.ownerGlobal
564 selection = window.getSelection()
565 range = window.document.createRange()
566 range.selectNodeContents(element)
567 selection.addRange(range)
569 setAttributes = (element, attributes) ->
570 for attribute, value of attributes
571 element.setAttribute(attribute, value)
574 walkTextNodes = (element, fn) ->
575 for node in element.childNodes then switch node.nodeType
580 stop = walkTextNodes(node, fn)
589 constructor: ({start: @value = 0, @step = 1}) ->
590 tick: -> @value += @step
596 on: (event, listener) ->
597 (@listeners[event] ?= new Set()).add(listener)
599 off: (event, listener) ->
600 @listeners[event]?.delete(listener)
602 emit: (event, data) ->
603 @listeners[event]?.forEach((listener) ->
607 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
608 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
609 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
610 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
611 # max`. `[null, null]` is returned in non-sensical cases. This function is
612 # intended to be used as a faster alternative to something like this:
614 # adjacentMatchAfter = null
615 # for n in [min..max]
617 # adjacentMatchAfter = n
619 bisect = (min, max, fn) ->
620 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
623 mid = min + (max - min) // 2
634 when matchMin and matchMax
636 when not matchMin and not matchMax
638 when not matchMin and matchMax
643 getAllNonOverlappingRangeOffsets = (string, substring) ->
645 return [] if length == 0
648 lastOffset = {start: -Infinity, end: -Infinity}
652 index = string.indexOf(substring, index + 1)
654 if index > lastOffset.end
655 lastOffset = {start: index, end: index + length}
656 offsets.push(lastOffset)
658 lastOffset.end = index + length
662 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
664 # Check if `search` exists in `string` (case insensitively). Returns `false` if
665 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
666 includes = (string, search) ->
667 return false unless typeof string == 'string'
668 return string.toLowerCase().includes(search)
670 # Calls `fn` repeatedly, with at least `interval` ms between each call.
671 interval = (window, interval, fn) ->
673 currentIntervalId = null
676 currentIntervalId = window.setTimeout((-> fn(next)), interval)
679 window.clearTimeout(currentIntervalId)
683 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
685 overlaps = (rectA, rectB) ->
687 Math.round(rectA.right) >= Math.round(rectB.left) and
688 Math.round(rectA.left) <= Math.round(rectB.right) and
689 Math.round(rectA.bottom) >= Math.round(rectB.top) and
690 Math.round(rectA.top) <= Math.round(rectB.bottom)
692 partition = (array, fn) ->
695 for item, index in array
696 if fn(item, index, array)
699 nonMatching.push(item)
700 return [matching, nonMatching]
702 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
704 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
706 removeDuplicates = (array) -> Array.from(new Set(array))
708 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
714 expandPath = (path) ->
715 if path.startsWith('~/') or path.startsWith('~\\')
716 return Services.dirsvc.get('Home', Ci.nsIFile).path + path[1..]
720 getCurrentLocation = ->
721 return unless window = getCurrentWindow()
722 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
724 # This function might return `null` on startup.
725 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
727 # gBrowser getFindBar() used to return the findBar directly, but in recent
728 # versions it returns a promise. This function should be removed once these old
729 # versions are no longer supported.
730 getFindBar = (gBrowser) ->
731 promiseOrFindBar = gBrowser.getFindBar()
732 if promiseOrFindBar instanceof Promise
735 Promise.resolve(promiseOrFindBar)
737 hasEventListeners = (element, type) ->
738 for listener in nsIEventListenerService.getListenerInfoFor(element)
739 if listener.listenerObject and listener.type == type
743 loadCss = (uriString) ->
744 uri = Services.io.newURI(uriString, null, null)
745 method = nsIStyleSheetService.AUTHOR_SHEET
746 unless nsIStyleSheetService.sheetRegistered(uri, method)
747 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
749 nsIStyleSheetService.unregisterSheet(uri, method)
752 observe = (topic, observer) ->
753 observer = {observe: observer} if typeof observer == 'function'
754 Services.obs.addObserver(observer, topic, false)
756 Services.obs.removeObserver(observer, topic, false)
759 # Try to open a button’s dropdown menu, if any.
760 openDropdown = (element) ->
761 if isXULElement(element) and
762 element.getAttribute?('type') == 'menu' and
763 element.open == false # Only change `.open` if it is already a boolean.
766 openPopup = (popup) ->
767 window = popup.ownerGlobal
768 # Show the popup so it gets a height and width.
769 popup.openPopupAtScreen(0, 0)
770 # Center the popup inside the window.
772 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
773 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
776 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
787 isDockedDevtoolsElement
790 isIgnoreModeFocusType
797 blurActiveBrowserElement
811 checkElementOrAncestor
823 selectAllSubstringMatches
831 getAllNonOverlappingRangeOffsets