]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
fix 'gX' menupopup
[VimFx.git] / extension / lib / utils.coffee
1 # This file contains lots of different helper functions.
2
3 {OS} = Components.utils.import('resource://gre/modules/osfile.jsm', {})
4
5 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
6 .getService(Ci.nsIClipboardHelper)
7 nsIEventListenerService = Cc['@mozilla.org/eventlistenerservice;1']
8 .getService(Ci.nsIEventListenerService)
9 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
10 .getService(Ci.nsIFocusManager)
11 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
12 .getService(Ci.nsIStyleSheetService)
13 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
14 .getService(Ci.nsIWindowMediator)
15
16 # For XUL, `instanceof` checks are often better than `.localName` checks,
17 # because some of the below interfaces are extended by many elements.
18 XULButtonElement = Ci.nsIDOMXULButtonElement
19 XULControlElement = Ci.nsIDOMXULControlElement
20 XULMenuListElement = Ci.nsIDOMXULMenuListElement
21
22 # Traverse the DOM upwards until we hit its containing document (most likely an
23 # HTMLDocument or (<=fx68) XULDocument) or the ShadowRoot.
24 getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e
25
26 isInShadowRoot = (element) ->
27 ShadowRoot? and getDocument(element) instanceof ShadowRoot
28
29 isXULElement = (element) ->
30 XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
31 element.namespaceURI == XUL_NS
32
33 # Full chains of events for different mouse actions. Note: 'click' is fired
34 # by Firefox automatically after 'mousedown' and 'mouseup'. Similarly,
35 # 'command' is fired automatically after 'click' on xul pages.
36 EVENTS_CLICK = ['mousedown', 'mouseup']
37 EVENTS_CLICK_XUL = ['click']
38 EVENTS_CONTEXT = ['contextmenu']
39 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
40 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
41
42
43
44 # Element classification helpers
45
46 hasMarkableTextNode = (element) ->
47 return Array.prototype.some.call(element.childNodes, (node) ->
48 # Ignore whitespace-only text nodes, and single-letter ones (which are
49 # common in many syntax highlighters).
50 return node.nodeType == 3 and node.data.trim().length > 1
51 )
52
53 isActivatable = (element) ->
54 return element.localName in ['a', 'button'] or
55 (element.localName == 'input' and element.type in [
56 'button', 'submit', 'reset', 'image'
57 ]) or
58 element instanceof XULButtonElement
59
60 isAdjustable = (element) ->
61 return element.localName == 'input' and element.type in [
62 'checkbox', 'radio', 'file', 'color'
63 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
64 ] or
65 element.localName in ['video', 'audio', 'embed', 'object'] or
66 element instanceof XULControlElement or
67 # Custom video players.
68 includes(element.className, 'video') or
69 includes(element.className, 'player') or
70 # Youtube special case.
71 element.classList?.contains('ytp-button') or
72 # Allow navigating object inspection trees in th devtools with the
73 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
74 isDevtoolsElement(element)
75
76 isContentEditable = (element) ->
77 return element.isContentEditable or
78 isIframeEditor(element) or
79 # Google.
80 element.getAttribute?('g_editable') == 'true' or
81 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
82 # Codeacademy terminals.
83 element.classList?.contains('real-terminal')
84
85 isDevtoolsElement = (element) ->
86 return false unless element.ownerGlobal
87 return Array.prototype.some.call(
88 element.ownerGlobal.top.frames, isDevtoolsWindow
89 )
90
91 isDevtoolsWindow = (window) ->
92 # Note: this function is called for each frame by isDevtoolsElement. When
93 # called on an out-of-process iframe, accessing .href will fail with
94 # SecurityError; the `try` around it makes it `undefined` in such a case.
95 return (try window.location?.href) in [
96 'about:devtools-toolbox'
97 'chrome://devtools/content/framework/toolbox.xul'
98 'chrome://devtools/content/framework/toolbox.xhtml' # fx72+
99 ]
100
101 # Note: this is possibly a bit overzealous, but Works For Now™.
102 isDockedDevtoolsElement = (element) ->
103 return element.ownerDocument.URL.startsWith('chrome://devtools/content/')
104
105 isFocusable = (element) ->
106 # Focusable elements have `.tabIndex > 1` (but not necessarily a
107 # `tabindex="…"` attribute) …
108 return (element.tabIndex > -1 or
109 # … or an explicit `tabindex="-1"` attribute (which means that it is
110 # focusable, but not reachable with `<tab>`).
111 element.getAttribute?('tabindex') == '-1') and
112 not (element.localName?.endsWith?('box') and
113 element.localName != 'checkbox') and
114 not (element.localName == 'toolbarbutton' and
115 element.parentNode?.localName == 'toolbarbutton') and
116 element.localName not in ['tabs', 'menuitem', 'menuseparator']
117
118 isIframeEditor = (element) ->
119 return false unless element.localName == 'body'
120 return \
121 # Etherpad.
122 element.id == 'innerdocbody' or
123 # XpressEditor.
124 (element.classList?.contains('xe_content') and
125 element.classList?.contains('editable')) or
126 # vBulletin.
127 element.classList?.contains('wysiwyg') or
128 # TYPO3 CMS.
129 element.classList?.contains('htmlarea-content-body') or
130 # The wasavi extension.
131 element.hasAttribute?('data-wasavi-state')
132
133 isIgnoreModeFocusType = (element) ->
134 return \
135 # The wasavi extension.
136 element.hasAttribute?('data-wasavi-state') or
137 element.closest?('#wasavi_container') or
138 # CodeMirror in Vim mode.
139 (element.localName == 'textarea' and
140 element.closest?('.CodeMirror') and _hasVimEventListener(element))
141
142 # CodeMirror’s Vim mode is really sucky to detect. The only way seems to be to
143 # check if the there are any event listener functions with Vim-y words in them.
144 _hasVimEventListener = (element) ->
145 for listener in nsIEventListenerService.getListenerInfoFor(element)
146 if listener.listenerObject and
147 /\bvim\b|insertmode/i.test(String(listener.listenerObject))
148 return true
149 return false
150
151 isProperLink = (element) ->
152 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
153 # href="">`s used as buttons on some sites.
154 return element.getAttribute?('href') and
155 (element.localName == 'a' or
156 (isXULElement(element) and
157 element.localName == 'label' and
158 element.getAttribute('is') == 'text-link')) and
159 not element.href?.endsWith?('#') and
160 not element.href?.endsWith?('#?') and
161 not element.href?.startsWith?('javascript:')
162
163 isTextInputElement = (element) ->
164 return (element.localName == 'input' and element.type in [
165 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
166 ]) or
167 element.localName in [ 'textarea', 'textbox' ] or
168 isContentEditable(element)
169
170 isTypingElement = (element) ->
171 return isTextInputElement(element) or
172 # `<select>` elements can also receive text input: You may type the
173 # text of an item to select it.
174 element.localName == 'select' or
175 element instanceof XULMenuListElement
176
177
178
179 # Active/focused element helpers
180
181 blurActiveBrowserElement = (vim) ->
182 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
183 # reset it, for example.
184 # - Focusing the current browser afterwards allows to pass `<escape>` as well
185 # as unbound keys to the page. However, focusing the browser also triggers
186 # focus events on `document` and `window` in the current page. Many pages
187 # re-focus some text input on those events, making it impossible to blur
188 # those! Therefore we tell the frame script to suppress those events.
189 {window} = vim
190 activeElement = getActiveElement(window)
191 activeElement.closest('tabmodalprompt')?.abortPrompt()
192 vim._send('browserRefocus')
193 nextTick(window, ->
194 activeElement.blur()
195 window.gBrowser.selectedBrowser.focus()
196 )
197
198 blurActiveElement = (window) ->
199 # Blurring a frame element also blurs any active elements inside it. Recursing
200 # into the frames and blurring the “real” active element directly would give
201 # focus to the `<body>` of its containing frame, while blurring the top-most
202 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
203 # editors which use an `<iframe>` as their text area.
204 # Note that this trick does not work with Web Components; for them, recursing
205 # is necessary.
206 if window.document.activeElement?.shadowRoot?
207 return getActiveElement(window)?.blur()
208 window.document.activeElement?.blur()
209
210 # Focus an element and tell Firefox that the focus happened because of a user
211 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
212 # look more appropriate, but it unconditionally selects all text, which
213 # `.FLAG_BYMOUSE` does not.
214 focusElement = (element, options = {}) ->
215 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
216 element.select?() if options.select
217
218 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
219 # is loading. Therefore always check if anything was returned, such as:
220 #
221 # return unless activeElement = utils.getActiveElement(window)
222 getActiveElement = (window) ->
223 {activeElement} = window.shadowRoot or window.document
224 return null unless activeElement
225 # If the active element is a frame, recurse into it. The easiest way to detect
226 # a frame that works both in browser UI and in web page content is to check
227 # for the presence of `.contentWindow`. However, in non-multi-process,
228 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
229 # pointing to the web page content `window`, which we don’t want to recurse
230 # into. The problem is that there are _some_ `<browser>`s which we _want_ to
231 # recurse into, such as the sidebar (for instance the history sidebar), and
232 # dialogs in `about:preferences`. Checking the `contextmenu` attribute seems
233 # to be a reliable test, catching both the main tab `<browser>`s and bookmarks
234 # opened in the sidebar.
235 # We also want to recurse into the (open) shadow DOM of custom elements.
236 if activeElement.shadowRoot?
237 return getActiveElement(activeElement)
238 else if activeElement.contentWindow and
239 not (activeElement.localName == 'browser' and
240 activeElement.getAttribute?('contextmenu') == 'contentAreaContextMenu')
241 # with Fission enabled, the iframe might be located in a different process
242 # (oop). Then, recursing into it isn't possible (throws SecurityError).
243 return activeElement unless (try activeElement.contentWindow.document)
244
245 return getActiveElement(activeElement.contentWindow)
246 else
247 return activeElement
248
249 getFocusType = (element) -> switch
250 when element.tagName in ['FRAME', 'IFRAME'] and
251 not (try element.contentWindow.document)
252 # Encountered an out-of-process iframe, which we can't inspect. We fall
253 # back to insert mode, so any text inputs it may contain are still usable.
254 'editable'
255 when isIgnoreModeFocusType(element)
256 'ignore'
257 when isTypingElement(element)
258 if element.closest?('findbar') then 'findbar' else 'editable'
259 when isActivatable(element)
260 'activatable'
261 when isAdjustable(element)
262 'adjustable'
263 else
264 'none'
265
266
267
268 # Event helpers
269
270 listen = (element, eventName, listener, useCapture = true) ->
271 element.addEventListener(eventName, listener, useCapture)
272 module.onShutdown(->
273 element.removeEventListener(eventName, listener, useCapture)
274 )
275
276 listenOnce = (element, eventName, listener, useCapture = true) ->
277 fn = (event) ->
278 listener(event)
279 element.removeEventListener(eventName, fn, useCapture)
280 listen(element, eventName, fn, useCapture)
281
282 onRemoved = (element, fn) ->
283 window = element.ownerGlobal
284
285 disconnected = false
286 disconnect = ->
287 return if disconnected
288 disconnected = true
289 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
290
291 mutationObserver = new window.MutationObserver((changes) ->
292 for change in changes then for removedElement in change.removedNodes
293 if removedElement.contains?(element)
294 disconnect()
295 fn()
296 return
297 )
298 mutationObserver.observe(window.document.documentElement, {
299 childList: true
300 subtree: true
301 })
302 module.onShutdown(disconnect)
303
304 return disconnect
305
306 simulateMouseEvents = (element, sequence, browserOffset) ->
307 window = element.ownerGlobal
308 rect = element.getBoundingClientRect()
309 topOffset = getTopOffset(element)
310
311 eventSequence = switch sequence
312 when 'click'
313 EVENTS_CLICK
314 when 'click-xul'
315 EVENTS_CLICK_XUL
316 when 'context'
317 EVENTS_CONTEXT
318 when 'hover-start'
319 EVENTS_HOVER_START
320 when 'hover-end'
321 EVENTS_HOVER_END
322 else
323 sequence
324
325 for type in eventSequence
326 buttonNum = switch
327 when type in EVENTS_CONTEXT
328 2
329 when type in EVENTS_CLICK
330 1
331 else
332 0
333
334 mouseEvent = new window.MouseEvent(type, {
335 # Let the event bubble in order to trigger delegated event listeners.
336 bubbles: type not in ['mouseenter', 'mouseleave']
337 # Make the event cancelable so that `<a href="#">` can be used as a
338 # JavaScript-powered button without scrolling to the top of the page.
339 cancelable: type not in ['mouseenter', 'mouseleave']
340 # These properties are just here for mimicing a real click as much as
341 # possible.
342 buttons: buttonNum
343 detail: buttonNum
344 view: window
345 # `page{X,Y}` are set automatically to the correct values when setting
346 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
347 # to set.
348 clientX: rect.left
349 clientY: rect.top + rect.height / 2
350 screenX: browserOffset.x + topOffset.x
351 screenY: browserOffset.y + topOffset.y + rect.height / 2
352 })
353
354 if type == 'mousemove'
355 # If the below technique is used for this event, the “URL popup” (shown
356 # when hovering or focusing links) does not appear.
357 element.dispatchEvent(mouseEvent)
358 else if isInShadowRoot(element)
359 # click events for links and other clickables inside the shadow DOM are
360 # caught by the callee (.click_marker_element()).
361 element.focus() if type == 'contextmenu' # for <input type=text>
362 element.dispatchEvent(mouseEvent)
363 else
364 try
365 (window.windowUtils.dispatchDOMEventViaPresShellForTesting or
366 window.windowUtils.dispatchDOMEventViaPresShell # < fx73
367 )(element, mouseEvent)
368 catch error
369 if error.result != Cr.NS_ERROR_UNEXPECTED
370 throw error
371
372 return
373
374 suppressEvent = (event) ->
375 event.preventDefault()
376 event.stopPropagation()
377
378
379
380 # DOM helpers
381
382 area = (element) ->
383 return element.clientWidth * element.clientHeight
384
385 checkElementOrAncestor = (element, fn) ->
386 window = element.ownerGlobal
387 while element.parentElement
388 return true if fn(element)
389 element = element.parentElement
390 return false
391
392 clearSelectionDeep = (window, {blur = true} = {}) ->
393 # The selection might be `null` in hidden frames.
394 selection = window.getSelection()
395 selection?.removeAllRanges()
396 # Note: accessing frameElement fails on oop iframes (fission); skip those.
397 for frame in window.frames when (try frame.frameElement)
398 clearSelectionDeep(frame, {blur})
399 # Allow parents to re-gain control of text selection.
400 frame.frameElement.blur() if blur
401 return
402
403 containsDeep = (parent, element) ->
404 parentWindow = parent.ownerGlobal
405 elementWindow = element.ownerGlobal
406
407 # Owner windows might be missing when opening the devtools.
408 while elementWindow and parentWindow and
409 elementWindow != parentWindow and elementWindow.top != elementWindow
410 element = elementWindow.frameElement
411 elementWindow = element.ownerGlobal
412
413 return parent.contains(element)
414
415 createBox = (document, className = '', parent = null, text = null) ->
416 box = document.createElement('box')
417 box.className = "#{className} vimfx-box"
418 box.textContent = text if text?
419 parent.appendChild(box) if parent?
420 return box
421
422 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
423 # `<body>` is considered the root element rather than `<html>`.
424 getRootElement = (document) ->
425 if document.compatMode == 'BackCompat' and document.body?
426 return document.body
427 else
428 return document.documentElement
429
430 getText = (element) ->
431 text = element.textContent or element.value or element.placeholder or ''
432 return text.trim().replace(/\s+/, ' ')
433
434 getTopOffset = (element) ->
435 window = element.ownerGlobal
436
437 {left: x, top: y} = element.getBoundingClientRect()
438 while window.frameElement
439 frame = window.frameElement
440 frameRect = frame.getBoundingClientRect()
441 x += frameRect.left
442 y += frameRect.top
443
444 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
445 if computedStyle
446 x +=
447 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
448 parseFloat(computedStyle.getPropertyValue('padding-left'))
449 y +=
450 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
451 parseFloat(computedStyle.getPropertyValue('padding-top'))
452
453 window = window.parent
454 return {x, y}
455
456 injectTemporaryPopup = (document, contents) ->
457 popup = document.createXULElement('menupopup')
458 popup.appendChild(contents)
459 document.getElementById('mainPopupSet').appendChild(popup)
460 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
461 return popup
462
463 insertText = (input, value) ->
464 {selectionStart, selectionEnd} = input
465 input.value =
466 input.value[0...selectionStart] + value + input.value[selectionEnd..]
467 input.selectionStart = input.selectionEnd = selectionStart + value.length
468
469 isDetached = (element) ->
470 return not element.ownerDocument?.documentElement?.contains?(element)
471
472 isNonEmptyTextNode = (node) ->
473 return node.nodeType == 3 and node.data.trim() != ''
474
475 querySelectorAllDeep = (window, selector) ->
476 elements = Array.from(window.document.querySelectorAll(selector))
477 for frame in window.frames
478 elements.push(querySelectorAllDeep(frame, selector)...)
479 return elements
480
481 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
482 window = element.ownerGlobal
483 selection = window.getSelection()
484 {textContent} = element
485
486 format = (string) -> if caseSensitive then string else string.toLowerCase()
487 offsets =
488 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
489 offsetsLength = offsets.length
490 return if offsetsLength == 0
491
492 textIndex = 0
493 offsetsIndex = 0
494 [currentOffset] = offsets
495 searchIndex = currentOffset.start
496 start = null
497
498 walkTextNodes(element, (textNode) ->
499 {length} = textNode.data
500 return false if length == 0
501
502 while textIndex + length > searchIndex
503 if start
504 range = window.document.createRange()
505 range.setStart(start.textNode, start.offset)
506 range.setEnd(textNode, currentOffset.end - textIndex)
507 selection.addRange(range)
508
509 offsetsIndex += 1
510 return true if offsetsIndex >= offsetsLength
511 currentOffset = offsets[offsetsIndex]
512
513 start = null
514 searchIndex = currentOffset.start
515
516 else
517 start = {textNode, offset: currentOffset.start - textIndex}
518 searchIndex = currentOffset.end - 1
519
520 textIndex += length
521 return false
522 )
523
524 selectElement = (element) ->
525 window = element.ownerGlobal
526 selection = window.getSelection()
527 range = window.document.createRange()
528 range.selectNodeContents(element)
529 selection.addRange(range)
530
531 setAttributes = (element, attributes) ->
532 for attribute, value of attributes
533 element.setAttribute(attribute, value)
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 isDockedDevtoolsElement
750 isFocusable
751 isIframeEditor
752 isIgnoreModeFocusType
753 isProperLink
754 isTextInputElement
755 isTypingElement
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 walkTextNodes
788
789 Counter
790 EventEmitter
791 bisect
792 getAllNonOverlappingRangeOffsets
793 has
794 includes
795 interval
796 nextTick
797 overlaps
798 partition
799 regexEscape
800 removeDuplicateChars
801 removeDuplicates
802 sum
803
804 expandPath
805 getCurrentLocation
806 getCurrentWindow
807 getFindBar
808 hasEventListeners
809 loadCss
810 observe
811 openDropdown
812 openPopup
813 writeToClipboard
814 }
Imprint / Impressum