]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Export isXULDocument from utils and fix Ci.nsIDOMElement gone (#919)
[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 isXULDocument = (doc) ->
33 doc.toString() == '[object XULDocument]'
34
35 # Full chains of events for different mouse actions. Note: 'click' is fired
36 # by Firefox automatically after 'mousedown' and 'mouseup'. Similarly,
37 # 'command' is fired automatically after 'click' on xul pages.
38 EVENTS_CLICK = ['mousedown', 'mouseup']
39 EVENTS_CLICK_XUL = ['click']
40 EVENTS_CONTEXT = ['contextmenu']
41 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
42 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
43
44
45
46 # Element classification helpers
47
48 hasMarkableTextNode = (element) ->
49 return Array.some(element.childNodes, (node) ->
50 # Ignore whitespace-only text nodes, and single-letter ones (which are
51 # common in many syntax highlighters).
52 return node.nodeType == 3 and node.data.trim().length > 1
53 )
54
55 isActivatable = (element) ->
56 return element.localName in ['a', 'button'] or
57 (element.localName == 'input' and element.type in [
58 'button', 'submit', 'reset', 'image'
59 ]) or
60 element instanceof XULButtonElement
61
62 isAdjustable = (element) ->
63 return element.localName == 'input' and element.type in [
64 'checkbox', 'radio', 'file', 'color'
65 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
66 ] or
67 element.localName in ['video', 'audio', 'embed', 'object'] or
68 element instanceof XULControlElement or
69 # Custom video players.
70 includes(element.className, 'video') or
71 includes(element.className, 'player') or
72 # Youtube special case.
73 element.classList?.contains('ytp-button') or
74 # Allow navigating object inspection trees in th devtools with the
75 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
76 isDevtoolsElement(element)
77
78 isContentEditable = (element) ->
79 return element.isContentEditable or
80 isIframeEditor(element) or
81 # Google.
82 element.getAttribute?('g_editable') == 'true' or
83 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
84 # Codeacademy terminals.
85 element.classList?.contains('real-terminal')
86
87 isDevtoolsElement = (element) ->
88 return false unless element.ownerGlobal
89 return Array.some(element.ownerGlobal.top.frames, isDevtoolsWindow)
90
91 isDevtoolsWindow = (window) ->
92 return window.location?.href in [
93 'about:devtools-toolbox'
94 'chrome://devtools/content/framework/toolbox.xul'
95 ]
96
97 isFocusable = (element) ->
98 # Focusable elements have `.tabIndex > 1` (but not necessarily a
99 # `tabindex="…"` attribute) …
100 return (element.tabIndex > -1 or
101 # … or an explicit `tabindex="-1"` attribute (which means that it is
102 # focusable, but not reachable with `<tab>`).
103 element.getAttribute?('tabindex') == '-1') and
104 not (element.localName?.endsWith?('box') and
105 element.localName != 'checkbox') and
106 not (element.localName == 'toolbarbutton' and
107 element.parentNode?.localName == 'toolbarbutton') and
108 element.localName not in ['tabs', 'menuitem', 'menuseparator']
109
110 isIframeEditor = (element) ->
111 return false unless element.localName == 'body'
112 return \
113 # Etherpad.
114 element.id == 'innerdocbody' or
115 # XpressEditor.
116 (element.classList?.contains('xe_content') and
117 element.classList?.contains('editable')) or
118 # vBulletin.
119 element.classList?.contains('wysiwyg') or
120 # TYPO3 CMS.
121 element.classList?.contains('htmlarea-content-body') or
122 # The wasavi extension.
123 element.hasAttribute?('data-wasavi-state')
124
125 isIgnoreModeFocusType = (element) ->
126 return \
127 # The wasavi extension.
128 element.hasAttribute?('data-wasavi-state') or
129 element.closest?('#wasavi_container') or
130 # CodeMirror in Vim mode.
131 (element.localName == 'textarea' and
132 element.closest?('.CodeMirror') and _hasVimEventListener(element))
133
134 # CodeMirror’s Vim mode is really sucky to detect. The only way seems to be to
135 # check if the there are any event listener functions with Vim-y words in them.
136 _hasVimEventListener = (element) ->
137 for listener in nsIEventListenerService.getListenerInfoFor(element)
138 if listener.listenerObject and
139 /\bvim\b|insertmode/i.test(String(listener.listenerObject))
140 return true
141 return false
142
143 isProperLink = (element) ->
144 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
145 # href="">`s used as buttons on some sites.
146 return element.getAttribute?('href') and
147 (element.localName == 'a' or
148 isXULDocument(element.ownerDocument)) and
149 not element.href?.endsWith?('#') and
150 not element.href?.endsWith?('#?') and
151 not element.href?.startsWith?('javascript:')
152
153 isTextInputElement = (element) ->
154 return (element.localName == 'input' and element.type in [
155 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
156 ]) or
157 element.localName in [ 'textarea', 'textbox' ] or
158 isContentEditable(element)
159
160 isTypingElement = (element) ->
161 return isTextInputElement(element) or
162 # `<select>` elements can also receive text input: You may type the
163 # text of an item to select it.
164 element.localName == 'select' or
165 element instanceof XULMenuListElement
166
167
168
169 # Active/focused element helpers
170
171 blurActiveBrowserElement = (vim) ->
172 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
173 # reset it, for example.
174 # - Focusing the current browser afterwards allows to pass `<escape>` as well
175 # as unbound keys to the page. However, focusing the browser also triggers
176 # focus events on `document` and `window` in the current page. Many pages
177 # re-focus some text input on those events, making it impossible to blur
178 # those! Therefore we tell the frame script to suppress those events.
179 {window} = vim
180 activeElement = getActiveElement(window)
181 activeElement.closest('tabmodalprompt')?.abortPrompt()
182 vim._send('browserRefocus')
183 nextTick(window, ->
184 activeElement.blur()
185 window.gBrowser.selectedBrowser.focus()
186 )
187
188 blurActiveElement = (window) ->
189 # Blurring a frame element also blurs any active elements inside it. Recursing
190 # into the frames and blurring the “real” active element directly would give
191 # focus to the `<body>` of its containing frame, while blurring the top-most
192 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
193 # editors which use an `<iframe>` as their text area.
194 window.document.activeElement?.blur()
195
196 # Focus an element and tell Firefox that the focus happened because of a user
197 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
198 # look more appropriate, but it unconditionally selects all text, which
199 # `.FLAG_BYMOUSE` does not.
200 focusElement = (element, options = {}) ->
201 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
202 element.select?() if options.select
203
204 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
205 # is loading. Therefore always check if anything was returned, such as:
206 #
207 # return unless activeElement = utils.getActiveElement(window)
208 getActiveElement = (window) ->
209 {activeElement} = window.document
210 return null unless activeElement
211 # If the active element is a frame, recurse into it. The easiest way to detect
212 # a frame that works both in browser UI and in web page content is to check
213 # for the presence of `.contentWindow`. However, in non-multi-process,
214 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
215 # pointing to the web page content `window`, which we don’t want to recurse
216 # into. The problem is that there are _some_ `<browser>`s which we _want_ to
217 # recurse into, such as the sidebar (for instance the history sidebar), and
218 # dialogs in `about:preferences`. Checking the `contextmenu` attribute seems
219 # to be a reliable test, catching both the main tab `<browser>`s and bookmarks
220 # opened in the sidebar.
221 if (activeElement.localName == 'browser' and
222 activeElement.getAttribute?('contextmenu') == 'contentAreaContextMenu') or
223 not activeElement.contentWindow
224 return activeElement
225 else
226 return getActiveElement(activeElement.contentWindow)
227
228 getFocusType = (element) -> switch
229 when isIgnoreModeFocusType(element)
230 'ignore'
231 when isTypingElement(element)
232 if element.closest?('findbar') then 'findbar' else 'editable'
233 when isActivatable(element)
234 'activatable'
235 when isAdjustable(element)
236 'adjustable'
237 else
238 'none'
239
240
241
242 # Event helpers
243
244 listen = (element, eventName, listener, useCapture = true) ->
245 element.addEventListener(eventName, listener, useCapture)
246 module.onShutdown(->
247 element.removeEventListener(eventName, listener, useCapture)
248 )
249
250 listenOnce = (element, eventName, listener, useCapture = true) ->
251 fn = (event) ->
252 listener(event)
253 element.removeEventListener(eventName, fn, useCapture)
254 listen(element, eventName, fn, useCapture)
255
256 onRemoved = (element, fn) ->
257 window = element.ownerGlobal
258
259 disconnected = false
260 disconnect = ->
261 return if disconnected
262 disconnected = true
263 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
264
265 mutationObserver = new window.MutationObserver((changes) ->
266 for change in changes then for removedElement in change.removedNodes
267 if removedElement.contains?(element)
268 disconnect()
269 fn()
270 return
271 )
272 mutationObserver.observe(window.document.documentElement, {
273 childList: true
274 subtree: true
275 })
276 module.onShutdown(disconnect)
277
278 return disconnect
279
280 simulateMouseEvents = (element, sequence, browserOffset) ->
281 window = element.ownerGlobal
282 rect = element.getBoundingClientRect()
283 topOffset = getTopOffset(element)
284
285 eventSequence = switch sequence
286 when 'click'
287 EVENTS_CLICK
288 when 'click-xul'
289 EVENTS_CLICK_XUL
290 when 'context'
291 EVENTS_CONTEXT
292 when 'hover-start'
293 EVENTS_HOVER_START
294 when 'hover-end'
295 EVENTS_HOVER_END
296 else
297 sequence
298
299 for type in eventSequence
300 buttonNum = switch
301 when type in EVENTS_CONTEXT
302 2
303 when type in EVENTS_CLICK
304 1
305 else
306 0
307
308 mouseEvent = new window.MouseEvent(type, {
309 # Let the event bubble in order to trigger delegated event listeners.
310 bubbles: type not in ['mouseenter', 'mouseleave']
311 # Make the event cancelable so that `<a href="#">` can be used as a
312 # JavaScript-powered button without scrolling to the top of the page.
313 cancelable: type not in ['mouseenter', 'mouseleave']
314 # These properties are just here for mimicing a real click as much as
315 # possible.
316 buttons: buttonNum
317 detail: buttonNum
318 view: window
319 # `page{X,Y}` are set automatically to the correct values when setting
320 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
321 # to set.
322 clientX: rect.left
323 clientY: rect.top + rect.height / 2
324 screenX: browserOffset.x + topOffset.x
325 screenY: browserOffset.y + topOffset.y + rect.height / 2
326 })
327
328 if type == 'mousemove'
329 # If the below technique is used for this event, the “URL popup” (shown
330 # when hovering or focusing links) does not appear.
331 element.dispatchEvent(mouseEvent)
332 else
333 # The last `true` below marks the event as trusted, which some APIs
334 # require, such as `requestFullscreen()`.
335 # (`element.dispatchEvent(mouseEvent)` is not able to do this.)
336 window
337 .QueryInterface(Ci.nsIInterfaceRequestor)
338 .getInterface(Ci.nsIDOMWindowUtils)
339 .dispatchDOMEventViaPresShell(element, mouseEvent, true)
340
341 return
342
343 suppressEvent = (event) ->
344 event.preventDefault()
345 event.stopPropagation()
346
347
348
349 # DOM helpers
350
351 area = (element) ->
352 return element.clientWidth * element.clientHeight
353
354 checkElementOrAncestor = (element, fn) ->
355 window = element.ownerGlobal
356 while element.parentElement
357 return true if fn(element)
358 element = element.parentElement
359 return false
360
361 clearSelectionDeep = (window, {blur = true} = {}) ->
362 # The selection might be `null` in hidden frames.
363 selection = window.getSelection()
364 selection?.removeAllRanges()
365 for frame in window.frames
366 clearSelectionDeep(frame, {blur})
367 # Allow parents to re-gain control of text selection.
368 frame.frameElement.blur() if blur
369 return
370
371 containsDeep = (parent, element) ->
372 parentWindow = parent.ownerGlobal
373 elementWindow = element.ownerGlobal
374
375 # Owner windows might be missing when opening the devtools.
376 while elementWindow and parentWindow and
377 elementWindow != parentWindow and elementWindow.top != elementWindow
378 element = elementWindow.frameElement
379 elementWindow = element.ownerGlobal
380
381 return parent.contains(element)
382
383 createBox = (document, className = '', parent = null, text = null) ->
384 box = document.createElement('box')
385 box.className = "#{className} vimfx-box"
386 box.textContent = text if text?
387 parent.appendChild(box) if parent?
388 return box
389
390 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
391 # `<body>` is considered the root element rather than `<html>`.
392 getRootElement = (document) ->
393 if document.compatMode == 'BackCompat' and document.body?
394 return document.body
395 else
396 return document.documentElement
397
398 getText = (element) ->
399 text = element.textContent or element.value or element.placeholder or ''
400 return text.trim().replace(/\s+/, ' ')
401
402 getTopOffset = (element) ->
403 window = element.ownerGlobal
404
405 {left: x, top: y} = element.getBoundingClientRect()
406 while window.frameElement
407 frame = window.frameElement
408 frameRect = frame.getBoundingClientRect()
409 x += frameRect.left
410 y += frameRect.top
411
412 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
413 if computedStyle
414 x +=
415 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
416 parseFloat(computedStyle.getPropertyValue('padding-left'))
417 y +=
418 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
419 parseFloat(computedStyle.getPropertyValue('padding-top'))
420
421 window = window.parent
422 return {x, y}
423
424 injectTemporaryPopup = (document, contents) ->
425 popup = document.createElement('menupopup')
426 popup.appendChild(contents)
427 document.getElementById('mainPopupSet').appendChild(popup)
428 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
429 return popup
430
431 insertText = (input, value) ->
432 {selectionStart, selectionEnd} = input
433 input.value =
434 input.value[0...selectionStart] + value + input.value[selectionEnd..]
435 input.selectionStart = input.selectionEnd = selectionStart + value.length
436
437 isDetached = (element) ->
438 return not element.ownerDocument?.documentElement?.contains?(element)
439
440 isNonEmptyTextNode = (node) ->
441 return node.nodeType == 3 and node.data.trim() != ''
442
443 querySelectorAllDeep = (window, selector) ->
444 elements = Array.from(window.document.querySelectorAll(selector))
445 for frame in window.frames
446 elements.push(querySelectorAllDeep(frame, selector)...)
447 return elements
448
449 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
450 window = element.ownerGlobal
451 selection = window.getSelection()
452 {textContent} = element
453
454 format = (string) -> if caseSensitive then string else string.toLowerCase()
455 offsets =
456 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
457 offsetsLength = offsets.length
458 return if offsetsLength == 0
459
460 textIndex = 0
461 offsetsIndex = 0
462 [currentOffset] = offsets
463 searchIndex = currentOffset.start
464 start = null
465
466 walkTextNodes(element, (textNode) ->
467 {length} = textNode.data
468 return false if length == 0
469
470 while textIndex + length > searchIndex
471 if start
472 range = window.document.createRange()
473 range.setStart(start.textNode, start.offset)
474 range.setEnd(textNode, currentOffset.end - textIndex)
475 selection.addRange(range)
476
477 offsetsIndex += 1
478 return true if offsetsIndex >= offsetsLength
479 currentOffset = offsets[offsetsIndex]
480
481 start = null
482 searchIndex = currentOffset.start
483
484 else
485 start = {textNode, offset: currentOffset.start - textIndex}
486 searchIndex = currentOffset.end - 1
487
488 textIndex += length
489 return false
490 )
491
492 selectElement = (element) ->
493 window = element.ownerGlobal
494 selection = window.getSelection()
495 range = window.document.createRange()
496 range.selectNodeContents(element)
497 selection.addRange(range)
498
499 setAttributes = (element, attributes) ->
500 for attribute, value of attributes
501 element.setAttribute(attribute, value)
502 return
503
504 setHover = (element, hover) ->
505 return unless nsIDomUtils
506
507 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
508 while element.parentElement
509 nsIDomUtils[method](element, ':hover')
510 element = element.parentElement
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 hasEventListeners = (element, type) ->
667 for listener in nsIEventListenerService.getListenerInfoFor(element)
668 if listener.listenerObject and listener.type == type
669 return true
670 return false
671
672 loadCss = (uriString) ->
673 uri = Services.io.newURI(uriString, null, null)
674 method = nsIStyleSheetService.AUTHOR_SHEET
675 unless nsIStyleSheetService.sheetRegistered(uri, method)
676 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
677 module.onShutdown(->
678 nsIStyleSheetService.unregisterSheet(uri, method)
679 )
680
681 observe = (topic, observer) ->
682 observer = {observe: observer} if typeof observer == 'function'
683 Services.obs.addObserver(observer, topic, false)
684 module.onShutdown(->
685 Services.obs.removeObserver(observer, topic, false)
686 )
687
688 # Try to open a button’s dropdown menu, if any.
689 openDropdown = (element) ->
690 if isXULDocument(element.ownerDocument) and
691 element.getAttribute?('type') == 'menu' and
692 element.open == false # Only change `.open` if it is already a boolean.
693 element.open = true
694
695 openPopup = (popup) ->
696 window = popup.ownerGlobal
697 # Show the popup so it gets a height and width.
698 popup.openPopupAtScreen(0, 0)
699 # Center the popup inside the window.
700 popup.moveTo(
701 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
702 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
703 )
704
705 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
706
707
708
709 module.exports = {
710 hasMarkableTextNode
711 isActivatable
712 isAdjustable
713 isContentEditable
714 isDevtoolsElement
715 isDevtoolsWindow
716 isFocusable
717 isIframeEditor
718 isIgnoreModeFocusType
719 isProperLink
720 isTextInputElement
721 isTypingElement
722 isXULDocument
723
724 blurActiveBrowserElement
725 blurActiveElement
726 focusElement
727 getActiveElement
728 getFocusType
729
730 listen
731 listenOnce
732 onRemoved
733 simulateMouseEvents
734 suppressEvent
735
736 area
737 checkElementOrAncestor
738 clearSelectionDeep
739 containsDeep
740 createBox
741 getRootElement
742 getText
743 getTopOffset
744 injectTemporaryPopup
745 insertText
746 isDetached
747 isNonEmptyTextNode
748 querySelectorAllDeep
749 selectAllSubstringMatches
750 selectElement
751 setAttributes
752 setHover
753 walkTextNodes
754
755 Counter
756 EventEmitter
757 bisect
758 getAllNonOverlappingRangeOffsets
759 has
760 includes
761 interval
762 nextTick
763 overlaps
764 partition
765 regexEscape
766 removeDuplicateChars
767 removeDuplicates
768 sum
769
770 expandPath
771 getCurrentLocation
772 getCurrentWindow
773 hasEventListeners
774 loadCss
775 observe
776 openDropdown
777 openPopup
778 writeToClipboard
779 }
Imprint / Impressum