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