]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
getInterface(Ci.nsIDOMWindowUtils) doesn't work.
[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.windowUtils
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 # gBrowser getFindBar() used to return the findBar directly, but in recent
665 # versions it returns a promise. This function should be removed once these old
666 # versions are no longer supported.
667 getFindBar = (gBrowser) ->
668 promiseOrFindBar = gBrowser.getFindBar()
669 if promiseOrFindBar instanceof Promise
670 promiseOrFindBar
671 else
672 Promise.resolve(promiseOrFindBar)
673
674 hasEventListeners = (element, type) ->
675 for listener in nsIEventListenerService.getListenerInfoFor(element)
676 if listener.listenerObject and listener.type == type
677 return true
678 return false
679
680 loadCss = (uriString) ->
681 uri = Services.io.newURI(uriString, null, null)
682 method = nsIStyleSheetService.AUTHOR_SHEET
683 unless nsIStyleSheetService.sheetRegistered(uri, method)
684 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
685 module.onShutdown(->
686 nsIStyleSheetService.unregisterSheet(uri, method)
687 )
688
689 observe = (topic, observer) ->
690 observer = {observe: observer} if typeof observer == 'function'
691 Services.obs.addObserver(observer, topic, false)
692 module.onShutdown(->
693 Services.obs.removeObserver(observer, topic, false)
694 )
695
696 # Try to open a button’s dropdown menu, if any.
697 openDropdown = (element) ->
698 if isXULDocument(element.ownerDocument) and
699 element.getAttribute?('type') == 'menu' and
700 element.open == false # Only change `.open` if it is already a boolean.
701 element.open = true
702
703 openPopup = (popup) ->
704 window = popup.ownerGlobal
705 # Show the popup so it gets a height and width.
706 popup.openPopupAtScreen(0, 0)
707 # Center the popup inside the window.
708 popup.moveTo(
709 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
710 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
711 )
712
713 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
714
715
716
717 module.exports = {
718 hasMarkableTextNode
719 isActivatable
720 isAdjustable
721 isContentEditable
722 isDevtoolsElement
723 isDevtoolsWindow
724 isFocusable
725 isIframeEditor
726 isIgnoreModeFocusType
727 isProperLink
728 isTextInputElement
729 isTypingElement
730 isXULDocument
731
732 blurActiveBrowserElement
733 blurActiveElement
734 focusElement
735 getActiveElement
736 getFocusType
737
738 listen
739 listenOnce
740 onRemoved
741 simulateMouseEvents
742 suppressEvent
743
744 area
745 checkElementOrAncestor
746 clearSelectionDeep
747 containsDeep
748 createBox
749 getRootElement
750 getText
751 getTopOffset
752 injectTemporaryPopup
753 insertText
754 isDetached
755 isNonEmptyTextNode
756 querySelectorAllDeep
757 selectAllSubstringMatches
758 selectElement
759 setAttributes
760 setHover
761 walkTextNodes
762
763 Counter
764 EventEmitter
765 bisect
766 getAllNonOverlappingRangeOffsets
767 has
768 includes
769 interval
770 nextTick
771 overlaps
772 partition
773 regexEscape
774 removeDuplicateChars
775 removeDuplicates
776 sum
777
778 expandPath
779 getCurrentLocation
780 getCurrentWindow
781 getFindBar
782 hasEventListeners
783 loadCss
784 observe
785 openDropdown
786 openPopup
787 writeToClipboard
788 }
Imprint / Impressum