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