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