]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Remove misuse of isXULDocument()
[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 # The last `true` below marks the event as trusted, which some APIs
361 # require, such as `requestFullscreen()`.
362 # (`element.dispatchEvent(mouseEvent)` is not able to do this.)
363 windowUtils =
364 window.windowUtils or
365 window
366 .QueryInterface(Ci.nsIInterfaceRequestor)
367 .getInterface(Ci.nsIDOMWindowUtils) # Removed in Firefox 63.
368 windowUtils
369 .dispatchDOMEventViaPresShell(element, mouseEvent, true)
370
371 return
372
373 suppressEvent = (event) ->
374 event.preventDefault()
375 event.stopPropagation()
376
377
378
379 # DOM helpers
380
381 area = (element) ->
382 return element.clientWidth * element.clientHeight
383
384 checkElementOrAncestor = (element, fn) ->
385 window = element.ownerGlobal
386 while element.parentElement
387 return true if fn(element)
388 element = element.parentElement
389 return false
390
391 clearSelectionDeep = (window, {blur = true} = {}) ->
392 # The selection might be `null` in hidden frames.
393 selection = window.getSelection()
394 selection?.removeAllRanges()
395 for frame in window.frames
396 clearSelectionDeep(frame, {blur})
397 # Allow parents to re-gain control of text selection.
398 frame.frameElement.blur() if blur
399 return
400
401 containsDeep = (parent, element) ->
402 parentWindow = parent.ownerGlobal
403 elementWindow = element.ownerGlobal
404
405 # Owner windows might be missing when opening the devtools.
406 while elementWindow and parentWindow and
407 elementWindow != parentWindow and elementWindow.top != elementWindow
408 element = elementWindow.frameElement
409 elementWindow = element.ownerGlobal
410
411 return parent.contains(element)
412
413 createBox = (document, className = '', parent = null, text = null) ->
414 box = document.createElement('box')
415 box.className = "#{className} vimfx-box"
416 box.textContent = text if text?
417 parent.appendChild(box) if parent?
418 return box
419
420 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
421 # `<body>` is considered the root element rather than `<html>`.
422 getRootElement = (document) ->
423 if document.compatMode == 'BackCompat' and document.body?
424 return document.body
425 else
426 return document.documentElement
427
428 getText = (element) ->
429 text = element.textContent or element.value or element.placeholder or ''
430 return text.trim().replace(/\s+/, ' ')
431
432 getTopOffset = (element) ->
433 window = element.ownerGlobal
434
435 {left: x, top: y} = element.getBoundingClientRect()
436 while window.frameElement
437 frame = window.frameElement
438 frameRect = frame.getBoundingClientRect()
439 x += frameRect.left
440 y += frameRect.top
441
442 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
443 if computedStyle
444 x +=
445 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
446 parseFloat(computedStyle.getPropertyValue('padding-left'))
447 y +=
448 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
449 parseFloat(computedStyle.getPropertyValue('padding-top'))
450
451 window = window.parent
452 return {x, y}
453
454 injectTemporaryPopup = (document, contents) ->
455 popup = document.createElement('menupopup')
456 popup.appendChild(contents)
457 document.getElementById('mainPopupSet').appendChild(popup)
458 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
459 return popup
460
461 insertText = (input, value) ->
462 {selectionStart, selectionEnd} = input
463 input.value =
464 input.value[0...selectionStart] + value + input.value[selectionEnd..]
465 input.selectionStart = input.selectionEnd = selectionStart + value.length
466
467 isDetached = (element) ->
468 return not element.ownerDocument?.documentElement?.contains?(element)
469
470 isNonEmptyTextNode = (node) ->
471 return node.nodeType == 3 and node.data.trim() != ''
472
473 querySelectorAllDeep = (window, selector) ->
474 elements = Array.from(window.document.querySelectorAll(selector))
475 for frame in window.frames
476 elements.push(querySelectorAllDeep(frame, selector)...)
477 return elements
478
479 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
480 window = element.ownerGlobal
481 selection = window.getSelection()
482 {textContent} = element
483
484 format = (string) -> if caseSensitive then string else string.toLowerCase()
485 offsets =
486 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
487 offsetsLength = offsets.length
488 return if offsetsLength == 0
489
490 textIndex = 0
491 offsetsIndex = 0
492 [currentOffset] = offsets
493 searchIndex = currentOffset.start
494 start = null
495
496 walkTextNodes(element, (textNode) ->
497 {length} = textNode.data
498 return false if length == 0
499
500 while textIndex + length > searchIndex
501 if start
502 range = window.document.createRange()
503 range.setStart(start.textNode, start.offset)
504 range.setEnd(textNode, currentOffset.end - textIndex)
505 selection.addRange(range)
506
507 offsetsIndex += 1
508 return true if offsetsIndex >= offsetsLength
509 currentOffset = offsets[offsetsIndex]
510
511 start = null
512 searchIndex = currentOffset.start
513
514 else
515 start = {textNode, offset: currentOffset.start - textIndex}
516 searchIndex = currentOffset.end - 1
517
518 textIndex += length
519 return false
520 )
521
522 selectElement = (element) ->
523 window = element.ownerGlobal
524 selection = window.getSelection()
525 range = window.document.createRange()
526 range.selectNodeContents(element)
527 selection.addRange(range)
528
529 setAttributes = (element, attributes) ->
530 for attribute, value of attributes
531 element.setAttribute(attribute, value)
532 return
533
534 setHover = (element, hover) ->
535 return unless nsIDomUtils
536
537 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
538 while element.parentElement
539 nsIDomUtils[method](element, ':hover')
540 element = element.parentElement
541 return
542
543 walkTextNodes = (element, fn) ->
544 for node in element.childNodes then switch node.nodeType
545 when 3 # TextNode.
546 stop = fn(node)
547 return true if stop
548 when 1 # Element.
549 stop = walkTextNodes(node, fn)
550 return true if stop
551 return false
552
553
554
555 # Language helpers
556
557 class Counter
558 constructor: ({start: @value = 0, @step = 1}) ->
559 tick: -> @value += @step
560
561 class EventEmitter
562 constructor: ->
563 @listeners = {}
564
565 on: (event, listener) ->
566 (@listeners[event] ?= new Set()).add(listener)
567
568 off: (event, listener) ->
569 @listeners[event]?.delete(listener)
570
571 emit: (event, data) ->
572 @listeners[event]?.forEach((listener) ->
573 listener(data)
574 )
575
576 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
577 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
578 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
579 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
580 # max`. `[null, null]` is returned in non-sensical cases. This function is
581 # intended to be used as a faster alternative to something like this:
582 #
583 # adjacentMatchAfter = null
584 # for n in [min..max]
585 # if fn(n)
586 # adjacentMatchAfter = n
587 # break
588 bisect = (min, max, fn) ->
589 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
590
591 while max - min > 1
592 mid = min + (max - min) // 2
593 match = fn(mid)
594 if match
595 max = mid
596 else
597 min = mid
598
599 matchMin = fn(min)
600 matchMax = fn(max)
601
602 return switch
603 when matchMin and matchMax
604 [null, min]
605 when not matchMin and not matchMax
606 [max, null]
607 when not matchMin and matchMax
608 [min, max]
609 else
610 [null, null]
611
612 getAllNonOverlappingRangeOffsets = (string, substring) ->
613 {length} = substring
614 return [] if length == 0
615
616 offsets = []
617 lastOffset = {start: -Infinity, end: -Infinity}
618 index = -1
619
620 loop
621 index = string.indexOf(substring, index + 1)
622 break if index == -1
623 if index > lastOffset.end
624 lastOffset = {start: index, end: index + length}
625 offsets.push(lastOffset)
626 else
627 lastOffset.end = index + length
628
629 return offsets
630
631 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
632
633 # Check if `search` exists in `string` (case insensitively). Returns `false` if
634 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
635 includes = (string, search) ->
636 return false unless typeof string == 'string'
637 return string.toLowerCase().includes(search)
638
639 # Calls `fn` repeatedly, with at least `interval` ms between each call.
640 interval = (window, interval, fn) ->
641 stopped = false
642 currentIntervalId = null
643 next = ->
644 return if stopped
645 currentIntervalId = window.setTimeout((-> fn(next)), interval)
646 clearInterval = ->
647 stopped = true
648 window.clearTimeout(currentIntervalId)
649 next()
650 return clearInterval
651
652 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
653
654 overlaps = (rectA, rectB) ->
655 return \
656 Math.round(rectA.right) >= Math.round(rectB.left) and
657 Math.round(rectA.left) <= Math.round(rectB.right) and
658 Math.round(rectA.bottom) >= Math.round(rectB.top) and
659 Math.round(rectA.top) <= Math.round(rectB.bottom)
660
661 partition = (array, fn) ->
662 matching = []
663 nonMatching = []
664 for item, index in array
665 if fn(item, index, array)
666 matching.push(item)
667 else
668 nonMatching.push(item)
669 return [matching, nonMatching]
670
671 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
672
673 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
674
675 removeDuplicates = (array) -> Array.from(new Set(array))
676
677 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
678
679
680
681 # Misc helpers
682
683 expandPath = (path) ->
684 if path.startsWith('~/') or path.startsWith('~\\')
685 return OS.Constants.Path.homeDir + path[1..]
686 else
687 return path
688
689 getCurrentLocation = ->
690 return unless window = getCurrentWindow()
691 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
692
693 # This function might return `null` on startup.
694 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
695
696 # gBrowser getFindBar() used to return the findBar directly, but in recent
697 # versions it returns a promise. This function should be removed once these old
698 # versions are no longer supported.
699 getFindBar = (gBrowser) ->
700 promiseOrFindBar = gBrowser.getFindBar()
701 if promiseOrFindBar instanceof Promise
702 promiseOrFindBar
703 else
704 Promise.resolve(promiseOrFindBar)
705
706 hasEventListeners = (element, type) ->
707 for listener in nsIEventListenerService.getListenerInfoFor(element)
708 if listener.listenerObject and listener.type == type
709 return true
710 return false
711
712 loadCss = (uriString) ->
713 uri = Services.io.newURI(uriString, null, null)
714 method = nsIStyleSheetService.AUTHOR_SHEET
715 unless nsIStyleSheetService.sheetRegistered(uri, method)
716 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
717 module.onShutdown(->
718 nsIStyleSheetService.unregisterSheet(uri, method)
719 )
720
721 observe = (topic, observer) ->
722 observer = {observe: observer} if typeof observer == 'function'
723 Services.obs.addObserver(observer, topic, false)
724 module.onShutdown(->
725 Services.obs.removeObserver(observer, topic, false)
726 )
727
728 # Try to open a button’s dropdown menu, if any.
729 openDropdown = (element) ->
730 if isXULElement(element) and
731 element.getAttribute?('type') == 'menu' and
732 element.open == false # Only change `.open` if it is already a boolean.
733 element.open = true
734
735 openPopup = (popup) ->
736 window = popup.ownerGlobal
737 # Show the popup so it gets a height and width.
738 popup.openPopupAtScreen(0, 0)
739 # Center the popup inside the window.
740 popup.moveTo(
741 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
742 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
743 )
744
745 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
746
747
748
749 module.exports = {
750 hasMarkableTextNode
751 isActivatable
752 isAdjustable
753 isContentEditable
754 isDevtoolsElement
755 isDevtoolsWindow
756 isFocusable
757 isIframeEditor
758 isIgnoreModeFocusType
759 isProperLink
760 isTextInputElement
761 isTypingElement
762 isXULDocument
763 isXULElement
764 isInShadowRoot
765
766 blurActiveBrowserElement
767 blurActiveElement
768 focusElement
769 getActiveElement
770 getFocusType
771
772 listen
773 listenOnce
774 onRemoved
775 simulateMouseEvents
776 suppressEvent
777
778 area
779 checkElementOrAncestor
780 clearSelectionDeep
781 containsDeep
782 createBox
783 getRootElement
784 getText
785 getTopOffset
786 injectTemporaryPopup
787 insertText
788 isDetached
789 isNonEmptyTextNode
790 querySelectorAllDeep
791 selectAllSubstringMatches
792 selectElement
793 setAttributes
794 setHover
795 walkTextNodes
796
797 Counter
798 EventEmitter
799 bisect
800 getAllNonOverlappingRangeOffsets
801 has
802 includes
803 interval
804 nextTick
805 overlaps
806 partition
807 regexEscape
808 removeDuplicateChars
809 removeDuplicates
810 sum
811
812 expandPath
813 getCurrentLocation
814 getCurrentWindow
815 getFindBar
816 hasEventListeners
817 loadCss
818 observe
819 openDropdown
820 openPopup
821 writeToClipboard
822 }
Imprint / Impressum