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