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