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