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