]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Remove last isXULDocument() references
[VimFx.git] / extension / lib / utils.coffee
1 # This file contains lots of different helper functions.
2
3 {OS} = Components.utils.import('resource://gre/modules/osfile.jsm', {})
4
5 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
6 .getService(Ci.nsIClipboardHelper)
7 nsIEventListenerService = Cc['@mozilla.org/eventlistenerservice;1']
8 .getService(Ci.nsIEventListenerService)
9 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
10 .getService(Ci.nsIFocusManager)
11 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
12 .getService(Ci.nsIStyleSheetService)
13 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
14 .getService(Ci.nsIWindowMediator)
15
16 # This interface was removed from Firefox with no alternative. Try to use it if
17 # available but otherwise just ignore it. Code in this module handles this
18 # variable being `null`.
19 nsIDomUtils =
20 try
21 Cc['@mozilla.org/inspector/dom-utils;1']
22 .getService(Ci.inIDOMUtils)
23 catch
24 null
25
26 # For XUL, `instanceof` checks are often better than `.localName` checks,
27 # because some of the below interfaces are extended by many elements.
28 XULButtonElement = Ci.nsIDOMXULButtonElement
29 XULControlElement = Ci.nsIDOMXULControlElement
30 XULMenuListElement = Ci.nsIDOMXULMenuListElement
31
32 # Traverse the DOM upwards until we hit its containing document (most likely an
33 # HTMLDocument or (<=fx68) XULDocument) or the ShadowRoot.
34 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
35 # for shadow DOM custom elements, as they require special handling.
36 # (ShadowRoot is only available in mozilla63+)
37 isInShadowRoot = (element) ->
38 ShadowRoot? and getDocument(element) instanceof ShadowRoot
39
40 isXULElement = (element) ->
41 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
42 element.namespaceURI == XUL_NS
43
44 # Full chains of events for different mouse actions. Note: 'click' is fired
45 # by Firefox automatically after 'mousedown' and 'mouseup'. Similarly,
46 # 'command' is fired automatically after 'click' on xul pages.
47 EVENTS_CLICK = ['mousedown', 'mouseup']
48 EVENTS_CLICK_XUL = ['click']
49 EVENTS_CONTEXT = ['contextmenu']
50 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
51 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
52
53
54
55 # Element classification helpers
56
57 hasMarkableTextNode = (element) ->
58 return Array.prototype.some.call(element.childNodes, (node) ->
59 # Ignore whitespace-only text nodes, and single-letter ones (which are
60 # common in many syntax highlighters).
61 return node.nodeType == 3 and node.data.trim().length > 1
62 )
63
64 isActivatable = (element) ->
65 return element.localName in ['a', 'button'] or
66 (element.localName == 'input' and element.type in [
67 'button', 'submit', 'reset', 'image'
68 ]) or
69 element instanceof XULButtonElement
70
71 isAdjustable = (element) ->
72 return element.localName == 'input' and element.type in [
73 'checkbox', 'radio', 'file', 'color'
74 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
75 ] or
76 element.localName in ['video', 'audio', 'embed', 'object'] or
77 element instanceof XULControlElement or
78 # Custom video players.
79 includes(element.className, 'video') or
80 includes(element.className, 'player') or
81 # Youtube special case.
82 element.classList?.contains('ytp-button') or
83 # Allow navigating object inspection trees in th devtools with the
84 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
85 isDevtoolsElement(element)
86
87 isContentEditable = (element) ->
88 return element.isContentEditable or
89 isIframeEditor(element) or
90 # Google.
91 element.getAttribute?('g_editable') == 'true' or
92 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
93 # Codeacademy terminals.
94 element.classList?.contains('real-terminal')
95
96 isDevtoolsElement = (element) ->
97 return false unless element.ownerGlobal
98 return Array.prototype.some.call(
99 element.ownerGlobal.top.frames, isDevtoolsWindow
100 )
101
102 isDevtoolsWindow = (window) ->
103 return window.location?.href in [
104 'about:devtools-toolbox'
105 'chrome://devtools/content/framework/toolbox.xul'
106 ]
107
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']
120
121 isIframeEditor = (element) ->
122 return false unless element.localName == 'body'
123 return \
124 # Etherpad.
125 element.id == 'innerdocbody' or
126 # XpressEditor.
127 (element.classList?.contains('xe_content') and
128 element.classList?.contains('editable')) or
129 # vBulletin.
130 element.classList?.contains('wysiwyg') or
131 # TYPO3 CMS.
132 element.classList?.contains('htmlarea-content-body') or
133 # The wasavi extension.
134 element.hasAttribute?('data-wasavi-state')
135
136 isIgnoreModeFocusType = (element) ->
137 return \
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))
144
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))
151 return true
152 return false
153
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:')
165
166 isTextInputElement = (element) ->
167 return (element.localName == 'input' and element.type in [
168 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
169 ]) or
170 element.localName in [ 'textarea', 'textbox' ] or
171 isContentEditable(element)
172
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
179
180
181
182 # Active/focused element helpers
183
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.
192 {window} = vim
193 activeElement = getActiveElement(window)
194 activeElement.closest('tabmodalprompt')?.abortPrompt()
195 vim._send('browserRefocus')
196 nextTick(window, ->
197 activeElement.blur()
198 window.gBrowser.selectedBrowser.focus()
199 )
200
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
208 # is necessary.
209 if window.document.activeElement?.shadowRoot?
210 return getActiveElement(window)?.blur()
211 window.document.activeElement?.blur()
212
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
220
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:
223 #
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 return getActiveElement(activeElement.contentWindow)
245 else
246 return activeElement
247
248 getFocusType = (element) -> switch
249 when isIgnoreModeFocusType(element)
250 'ignore'
251 when isTypingElement(element)
252 if element.closest?('findbar') then 'findbar' else 'editable'
253 when isActivatable(element)
254 'activatable'
255 when isAdjustable(element)
256 'adjustable'
257 else
258 'none'
259
260
261
262 # Event helpers
263
264 listen = (element, eventName, listener, useCapture = true) ->
265 element.addEventListener(eventName, listener, useCapture)
266 module.onShutdown(->
267 element.removeEventListener(eventName, listener, useCapture)
268 )
269
270 listenOnce = (element, eventName, listener, useCapture = true) ->
271 fn = (event) ->
272 listener(event)
273 element.removeEventListener(eventName, fn, useCapture)
274 listen(element, eventName, fn, useCapture)
275
276 onRemoved = (element, fn) ->
277 window = element.ownerGlobal
278
279 disconnected = false
280 disconnect = ->
281 return if disconnected
282 disconnected = true
283 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
284
285 mutationObserver = new window.MutationObserver((changes) ->
286 for change in changes then for removedElement in change.removedNodes
287 if removedElement.contains?(element)
288 disconnect()
289 fn()
290 return
291 )
292 mutationObserver.observe(window.document.documentElement, {
293 childList: true
294 subtree: true
295 })
296 module.onShutdown(disconnect)
297
298 return disconnect
299
300 simulateMouseEvents = (element, sequence, browserOffset) ->
301 window = element.ownerGlobal
302 rect = element.getBoundingClientRect()
303 topOffset = getTopOffset(element)
304
305 eventSequence = switch sequence
306 when 'click'
307 EVENTS_CLICK
308 when 'click-xul'
309 EVENTS_CLICK_XUL
310 when 'context'
311 EVENTS_CONTEXT
312 when 'hover-start'
313 EVENTS_HOVER_START
314 when 'hover-end'
315 EVENTS_HOVER_END
316 else
317 sequence
318
319 for type in eventSequence
320 buttonNum = switch
321 when type in EVENTS_CONTEXT
322 2
323 when type in EVENTS_CLICK
324 1
325 else
326 0
327
328 mouseEvent = new window.MouseEvent(type, {
329 # Let the event bubble in order to trigger delegated event listeners.
330 bubbles: type not in ['mouseenter', 'mouseleave']
331 # Make the event cancelable so that `<a href="#">` can be used as a
332 # JavaScript-powered button without scrolling to the top of the page.
333 cancelable: type not in ['mouseenter', 'mouseleave']
334 # These properties are just here for mimicing a real click as much as
335 # possible.
336 buttons: buttonNum
337 detail: buttonNum
338 view: window
339 # `page{X,Y}` are set automatically to the correct values when setting
340 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
341 # to set.
342 clientX: rect.left
343 clientY: rect.top + rect.height / 2
344 screenX: browserOffset.x + topOffset.x
345 screenY: browserOffset.y + topOffset.y + rect.height / 2
346 })
347
348 if type == 'mousemove'
349 # If the below technique is used for this event, the “URL popup” (shown
350 # when hovering or focusing links) does not appear.
351 element.dispatchEvent(mouseEvent)
352 else if isInShadowRoot(element)
353 # click events for links and other clickables inside the shadow DOM are
354 # caught by the callee (.click_marker_element()).
355 element.focus() if type == 'contextmenu' # for <input type=text>
356 element.dispatchEvent(mouseEvent)
357 else
358 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
359 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
360 )(element, mouseEvent)
361
362 return
363
364 suppressEvent = (event) ->
365 event.preventDefault()
366 event.stopPropagation()
367
368
369
370 # DOM helpers
371
372 area = (element) ->
373 return element.clientWidth * element.clientHeight
374
375 checkElementOrAncestor = (element, fn) ->
376 window = element.ownerGlobal
377 while element.parentElement
378 return true if fn(element)
379 element = element.parentElement
380 return false
381
382 clearSelectionDeep = (window, {blur = true} = {}) ->
383 # The selection might be `null` in hidden frames.
384 selection = window.getSelection()
385 selection?.removeAllRanges()
386 for frame in window.frames
387 clearSelectionDeep(frame, {blur})
388 # Allow parents to re-gain control of text selection.
389 frame.frameElement.blur() if blur
390 return
391
392 containsDeep = (parent, element) ->
393 parentWindow = parent.ownerGlobal
394 elementWindow = element.ownerGlobal
395
396 # Owner windows might be missing when opening the devtools.
397 while elementWindow and parentWindow and
398 elementWindow != parentWindow and elementWindow.top != elementWindow
399 element = elementWindow.frameElement
400 elementWindow = element.ownerGlobal
401
402 return parent.contains(element)
403
404 createBox = (document, className = '', parent = null, text = null) ->
405 box = document.createElement('box')
406 box.className = "#{className} vimfx-box"
407 box.textContent = text if text?
408 parent.appendChild(box) if parent?
409 return box
410
411 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
412 # `<body>` is considered the root element rather than `<html>`.
413 getRootElement = (document) ->
414 if document.compatMode == 'BackCompat' and document.body?
415 return document.body
416 else
417 return document.documentElement
418
419 getText = (element) ->
420 text = element.textContent or element.value or element.placeholder or ''
421 return text.trim().replace(/\s+/, ' ')
422
423 getTopOffset = (element) ->
424 window = element.ownerGlobal
425
426 {left: x, top: y} = element.getBoundingClientRect()
427 while window.frameElement
428 frame = window.frameElement
429 frameRect = frame.getBoundingClientRect()
430 x += frameRect.left
431 y += frameRect.top
432
433 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
434 if computedStyle
435 x +=
436 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
437 parseFloat(computedStyle.getPropertyValue('padding-left'))
438 y +=
439 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
440 parseFloat(computedStyle.getPropertyValue('padding-top'))
441
442 window = window.parent
443 return {x, y}
444
445 injectTemporaryPopup = (document, contents) ->
446 popup = document.createElement('menupopup')
447 popup.appendChild(contents)
448 document.getElementById('mainPopupSet').appendChild(popup)
449 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
450 return popup
451
452 insertText = (input, value) ->
453 {selectionStart, selectionEnd} = input
454 input.value =
455 input.value[0...selectionStart] + value + input.value[selectionEnd..]
456 input.selectionStart = input.selectionEnd = selectionStart + value.length
457
458 isDetached = (element) ->
459 return not element.ownerDocument?.documentElement?.contains?(element)
460
461 isNonEmptyTextNode = (node) ->
462 return node.nodeType == 3 and node.data.trim() != ''
463
464 querySelectorAllDeep = (window, selector) ->
465 elements = Array.from(window.document.querySelectorAll(selector))
466 for frame in window.frames
467 elements.push(querySelectorAllDeep(frame, selector)...)
468 return elements
469
470 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
471 window = element.ownerGlobal
472 selection = window.getSelection()
473 {textContent} = element
474
475 format = (string) -> if caseSensitive then string else string.toLowerCase()
476 offsets =
477 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
478 offsetsLength = offsets.length
479 return if offsetsLength == 0
480
481 textIndex = 0
482 offsetsIndex = 0
483 [currentOffset] = offsets
484 searchIndex = currentOffset.start
485 start = null
486
487 walkTextNodes(element, (textNode) ->
488 {length} = textNode.data
489 return false if length == 0
490
491 while textIndex + length > searchIndex
492 if start
493 range = window.document.createRange()
494 range.setStart(start.textNode, start.offset)
495 range.setEnd(textNode, currentOffset.end - textIndex)
496 selection.addRange(range)
497
498 offsetsIndex += 1
499 return true if offsetsIndex >= offsetsLength
500 currentOffset = offsets[offsetsIndex]
501
502 start = null
503 searchIndex = currentOffset.start
504
505 else
506 start = {textNode, offset: currentOffset.start - textIndex}
507 searchIndex = currentOffset.end - 1
508
509 textIndex += length
510 return false
511 )
512
513 selectElement = (element) ->
514 window = element.ownerGlobal
515 selection = window.getSelection()
516 range = window.document.createRange()
517 range.selectNodeContents(element)
518 selection.addRange(range)
519
520 setAttributes = (element, attributes) ->
521 for attribute, value of attributes
522 element.setAttribute(attribute, value)
523 return
524
525 setHover = (element, hover) ->
526 return unless nsIDomUtils
527
528 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
529 while element.parentElement
530 nsIDomUtils[method](element, ':hover')
531 element = element.parentElement
532 return
533
534 walkTextNodes = (element, fn) ->
535 for node in element.childNodes then switch node.nodeType
536 when 3 # TextNode.
537 stop = fn(node)
538 return true if stop
539 when 1 # Element.
540 stop = walkTextNodes(node, fn)
541 return true if stop
542 return false
543
544
545
546 # Language helpers
547
548 class Counter
549 constructor: ({start: @value = 0, @step = 1}) ->
550 tick: -> @value += @step
551
552 class EventEmitter
553 constructor: ->
554 @listeners = {}
555
556 on: (event, listener) ->
557 (@listeners[event] ?= new Set()).add(listener)
558
559 off: (event, listener) ->
560 @listeners[event]?.delete(listener)
561
562 emit: (event, data) ->
563 @listeners[event]?.forEach((listener) ->
564 listener(data)
565 )
566
567 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
568 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
569 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
570 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
571 # max`. `[null, null]` is returned in non-sensical cases. This function is
572 # intended to be used as a faster alternative to something like this:
573 #
574 # adjacentMatchAfter = null
575 # for n in [min..max]
576 # if fn(n)
577 # adjacentMatchAfter = n
578 # break
579 bisect = (min, max, fn) ->
580 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
581
582 while max - min > 1
583 mid = min + (max - min) // 2
584 match = fn(mid)
585 if match
586 max = mid
587 else
588 min = mid
589
590 matchMin = fn(min)
591 matchMax = fn(max)
592
593 return switch
594 when matchMin and matchMax
595 [null, min]
596 when not matchMin and not matchMax
597 [max, null]
598 when not matchMin and matchMax
599 [min, max]
600 else
601 [null, null]
602
603 getAllNonOverlappingRangeOffsets = (string, substring) ->
604 {length} = substring
605 return [] if length == 0
606
607 offsets = []
608 lastOffset = {start: -Infinity, end: -Infinity}
609 index = -1
610
611 loop
612 index = string.indexOf(substring, index + 1)
613 break if index == -1
614 if index > lastOffset.end
615 lastOffset = {start: index, end: index + length}
616 offsets.push(lastOffset)
617 else
618 lastOffset.end = index + length
619
620 return offsets
621
622 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
623
624 # Check if `search` exists in `string` (case insensitively). Returns `false` if
625 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
626 includes = (string, search) ->
627 return false unless typeof string == 'string'
628 return string.toLowerCase().includes(search)
629
630 # Calls `fn` repeatedly, with at least `interval` ms between each call.
631 interval = (window, interval, fn) ->
632 stopped = false
633 currentIntervalId = null
634 next = ->
635 return if stopped
636 currentIntervalId = window.setTimeout((-> fn(next)), interval)
637 clearInterval = ->
638 stopped = true
639 window.clearTimeout(currentIntervalId)
640 next()
641 return clearInterval
642
643 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
644
645 overlaps = (rectA, rectB) ->
646 return \
647 Math.round(rectA.right) >= Math.round(rectB.left) and
648 Math.round(rectA.left) <= Math.round(rectB.right) and
649 Math.round(rectA.bottom) >= Math.round(rectB.top) and
650 Math.round(rectA.top) <= Math.round(rectB.bottom)
651
652 partition = (array, fn) ->
653 matching = []
654 nonMatching = []
655 for item, index in array
656 if fn(item, index, array)
657 matching.push(item)
658 else
659 nonMatching.push(item)
660 return [matching, nonMatching]
661
662 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
663
664 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
665
666 removeDuplicates = (array) -> Array.from(new Set(array))
667
668 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
669
670
671
672 # Misc helpers
673
674 expandPath = (path) ->
675 if path.startsWith('~/') or path.startsWith('~\\')
676 return OS.Constants.Path.homeDir + path[1..]
677 else
678 return path
679
680 getCurrentLocation = ->
681 return unless window = getCurrentWindow()
682 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
683
684 # This function might return `null` on startup.
685 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
686
687 # gBrowser getFindBar() used to return the findBar directly, but in recent
688 # versions it returns a promise. This function should be removed once these old
689 # versions are no longer supported.
690 getFindBar = (gBrowser) ->
691 promiseOrFindBar = gBrowser.getFindBar()
692 if promiseOrFindBar instanceof Promise
693 promiseOrFindBar
694 else
695 Promise.resolve(promiseOrFindBar)
696
697 hasEventListeners = (element, type) ->
698 for listener in nsIEventListenerService.getListenerInfoFor(element)
699 if listener.listenerObject and listener.type == type
700 return true
701 return false
702
703 loadCss = (uriString) ->
704 uri = Services.io.newURI(uriString, null, null)
705 method = nsIStyleSheetService.AUTHOR_SHEET
706 unless nsIStyleSheetService.sheetRegistered(uri, method)
707 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
708 module.onShutdown(->
709 nsIStyleSheetService.unregisterSheet(uri, method)
710 )
711
712 observe = (topic, observer) ->
713 observer = {observe: observer} if typeof observer == 'function'
714 Services.obs.addObserver(observer, topic, false)
715 module.onShutdown(->
716 Services.obs.removeObserver(observer, topic, false)
717 )
718
719 # Try to open a button’s dropdown menu, if any.
720 openDropdown = (element) ->
721 if isXULElement(element) and
722 element.getAttribute?('type') == 'menu' and
723 element.open == false # Only change `.open` if it is already a boolean.
724 element.open = true
725
726 openPopup = (popup) ->
727 window = popup.ownerGlobal
728 # Show the popup so it gets a height and width.
729 popup.openPopupAtScreen(0, 0)
730 # Center the popup inside the window.
731 popup.moveTo(
732 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
733 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
734 )
735
736 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
737
738
739
740 module.exports = {
741 hasMarkableTextNode
742 isActivatable
743 isAdjustable
744 isContentEditable
745 isDevtoolsElement
746 isDevtoolsWindow
747 isFocusable
748 isIframeEditor
749 isIgnoreModeFocusType
750 isProperLink
751 isTextInputElement
752 isTypingElement
753 isXULElement
754 isInShadowRoot
755
756 blurActiveBrowserElement
757 blurActiveElement
758 focusElement
759 getActiveElement
760 getFocusType
761
762 listen
763 listenOnce
764 onRemoved
765 simulateMouseEvents
766 suppressEvent
767
768 area
769 checkElementOrAncestor
770 clearSelectionDeep
771 containsDeep
772 createBox
773 getRootElement
774 getText
775 getTopOffset
776 injectTemporaryPopup
777 insertText
778 isDetached
779 isNonEmptyTextNode
780 querySelectorAllDeep
781 selectAllSubstringMatches
782 selectElement
783 setAttributes
784 setHover
785 walkTextNodes
786
787 Counter
788 EventEmitter
789 bisect
790 getAllNonOverlappingRangeOffsets
791 has
792 includes
793 interval
794 nextTick
795 overlaps
796 partition
797 regexEscape
798 removeDuplicateChars
799 removeDuplicates
800 sum
801
802 expandPath
803 getCurrentLocation
804 getCurrentWindow
805 getFindBar
806 hasEventListeners
807 loadCss
808 observe
809 openDropdown
810 openPopup
811 writeToClipboard
812 }
Imprint / Impressum