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