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