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