]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Improve Hints mode in various ways
[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
425 {left: x, top: y} = element.getBoundingClientRect()
426 while window.frameElement
427 frame = window.frameElement
428 frameRect = frame.getBoundingClientRect()
429 x += frameRect.left
430 y += frameRect.top
431
432 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
433 if computedStyle
434 x +=
435 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
436 parseFloat(computedStyle.getPropertyValue('padding-left'))
437 y +=
438 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
439 parseFloat(computedStyle.getPropertyValue('padding-top'))
440
441 window = window.parent
442 return {x, y}
443
444 injectTemporaryPopup = (document, contents) ->
445 popup = document.createElement('menupopup')
446 popup.appendChild(contents)
447 document.getElementById('mainPopupSet').appendChild(popup)
448 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
449 return popup
450
451 insertText = (input, value) ->
452 {selectionStart, selectionEnd} = input
453 input.value =
454 input.value[0...selectionStart] + value + input.value[selectionEnd..]
455 input.selectionStart = input.selectionEnd = selectionStart + value.length
456
457 isDetached = (element) ->
458 return not element.ownerDocument?.documentElement?.contains?(element)
459
460 isNonEmptyTextNode = (node) ->
461 return node.nodeType == 3 and node.data.trim() != ''
462
463 isPositionFixed = (element) ->
464 computedStyle = element.ownerGlobal.getComputedStyle(element)
465 return computedStyle?.getPropertyValue('position') == 'fixed'
466
467 querySelectorAllDeep = (window, selector) ->
468 elements = Array.from(window.document.querySelectorAll(selector))
469 for frame in window.frames
470 elements.push(querySelectorAllDeep(frame, selector)...)
471 return elements
472
473 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
474 window = element.ownerGlobal
475 selection = window.getSelection()
476 {textContent} = element
477
478 format = (string) -> if caseSensitive then string else string.toLowerCase()
479 offsets =
480 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
481 offsetsLength = offsets.length
482 return if offsetsLength == 0
483
484 textIndex = 0
485 offsetsIndex = 0
486 [currentOffset] = offsets
487 searchIndex = currentOffset.start
488 start = null
489
490 walkTextNodes(element, (textNode) ->
491 {length} = textNode.data
492 return false if length == 0
493
494 while textIndex + length > searchIndex
495 if start
496 range = window.document.createRange()
497 range.setStart(start.textNode, start.offset)
498 range.setEnd(textNode, currentOffset.end - textIndex)
499 selection.addRange(range)
500
501 offsetsIndex += 1
502 return true if offsetsIndex >= offsetsLength
503 currentOffset = offsets[offsetsIndex]
504
505 start = null
506 searchIndex = currentOffset.start
507
508 else
509 start = {textNode, offset: currentOffset.start - textIndex}
510 searchIndex = currentOffset.end - 1
511
512 textIndex += length
513 return false
514 )
515
516 selectElement = (element) ->
517 window = element.ownerGlobal
518 selection = window.getSelection()
519 range = window.document.createRange()
520 range.selectNodeContents(element)
521 selection.addRange(range)
522
523 setAttributes = (element, attributes) ->
524 for attribute, value of attributes
525 element.setAttribute(attribute, value)
526 return
527
528 setHover = (element, hover) ->
529 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
530 while element.parentElement
531 nsIDomUtils[method](element, ':hover')
532 element = element.parentElement
533 return
534
535 walkTextNodes = (element, fn) ->
536 for node in element.childNodes then switch node.nodeType
537 when 3 # TextNode.
538 stop = fn(node)
539 return true if stop
540 when 1 # Element.
541 stop = walkTextNodes(node, fn)
542 return true if stop
543 return false
544
545
546
547 # Language helpers
548
549 class Counter
550 constructor: ({start: @value = 0, @step = 1}) ->
551 tick: -> @value += @step
552
553 class EventEmitter
554 constructor: ->
555 @listeners = {}
556
557 on: (event, listener) ->
558 (@listeners[event] ?= new Set()).add(listener)
559
560 off: (event, listener) ->
561 @listeners[event]?.delete(listener)
562
563 emit: (event, data) ->
564 @listeners[event]?.forEach((listener) ->
565 listener(data)
566 )
567
568 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
569 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
570 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
571 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
572 # max`. `[null, null]` is returned in non-sensical cases. This function is
573 # intended to be used as a faster alternative to something like this:
574 #
575 # adjacentMatchAfter = null
576 # for n in [min..max]
577 # if fn(n)
578 # adjacentMatchAfter = n
579 # break
580 bisect = (min, max, fn) ->
581 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
582
583 while max - min > 1
584 mid = min + (max - min) // 2
585 match = fn(mid)
586 if match
587 max = mid
588 else
589 min = mid
590
591 matchMin = fn(min)
592 matchMax = fn(max)
593
594 return switch
595 when matchMin and matchMax
596 [null, min]
597 when not matchMin and not matchMax
598 [max, null]
599 when not matchMin and matchMax
600 [min, max]
601 else
602 [null, null]
603
604 getAllNonOverlappingRangeOffsets = (string, substring) ->
605 {length} = substring
606 return [] if length == 0
607
608 offsets = []
609 lastOffset = {start: -Infinity, end: -Infinity}
610 index = -1
611
612 loop
613 index = string.indexOf(substring, index + 1)
614 break if index == -1
615 if index > lastOffset.end
616 lastOffset = {start: index, end: index + length}
617 offsets.push(lastOffset)
618 else
619 lastOffset.end = index + length
620
621 return offsets
622
623 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
624
625 # Check if `search` exists in `string` (case insensitively). Returns `false` if
626 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
627 includes = (string, search) ->
628 return false unless typeof string == 'string'
629 return string.toLowerCase().includes(search)
630
631 # Calls `fn` repeatedly, with at least `interval` ms between each call.
632 interval = (window, interval, fn) ->
633 stopped = false
634 currentIntervalId = null
635 next = ->
636 return if stopped
637 currentIntervalId = window.setTimeout((-> fn(next)), interval)
638 clearInterval = ->
639 stopped = true
640 window.clearTimeout(currentIntervalId)
641 next()
642 return clearInterval
643
644 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
645
646 partition = (array, fn) ->
647 matching = []
648 nonMatching = []
649 for item, index in array
650 if fn(item, index, array)
651 matching.push(item)
652 else
653 nonMatching.push(item)
654 return [matching, nonMatching]
655
656 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
657
658 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
659
660 removeDuplicates = (array) -> Array.from(new Set(array))
661
662 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
663
664
665
666 # Misc helpers
667
668 expandPath = (path) ->
669 if path.startsWith('~/') or path.startsWith('~\\')
670 return OS.Constants.Path.homeDir + path[1..]
671 else
672 return path
673
674 formatError = (error) ->
675 stack = String(error.stack?.formattedStack ? error.stack ? '')
676 .split('\n')
677 .filter((line) -> line.includes('.xpi!'))
678 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
679 .join('\n')
680 return "#{error}\n#{stack}"
681
682 getCurrentLocation = ->
683 return unless window = getCurrentWindow()
684 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
685
686 # This function might return `null` on startup.
687 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
688
689 hasEventListeners = (element, type) ->
690 for listener in nsIEventListenerService.getListenerInfoFor(element)
691 if listener.listenerObject and listener.type == type
692 return true
693 return false
694
695 loadCss = (uriString) ->
696 uri = Services.io.newURI(uriString, null, null)
697 method = nsIStyleSheetService.AUTHOR_SHEET
698 unless nsIStyleSheetService.sheetRegistered(uri, method)
699 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
700 module.onShutdown(->
701 nsIStyleSheetService.unregisterSheet(uri, method)
702 )
703
704 observe = (topic, observer) ->
705 observer = {observe: observer} if typeof observer == 'function'
706 Services.obs.addObserver(observer, topic, false)
707 module.onShutdown(->
708 Services.obs.removeObserver(observer, topic, false)
709 )
710
711 # Try to open a button’s dropdown menu, if any.
712 openDropdown = (element) ->
713 if element.ownerDocument instanceof XULDocument and
714 element.getAttribute?('type') == 'menu' and
715 element.open == false # Only change `.open` if it is already a boolean.
716 element.open = true
717
718 openPopup = (popup) ->
719 window = popup.ownerGlobal
720 # Show the popup so it gets a height and width.
721 popup.openPopupAtScreen(0, 0)
722 # Center the popup inside the window.
723 popup.moveTo(
724 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
725 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
726 )
727
728 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
729
730
731
732 module.exports = {
733 hasMarkableTextNode
734 isActivatable
735 isAdjustable
736 isContentEditable
737 isDevtoolsElement
738 isDevtoolsWindow
739 isFocusable
740 isIframeEditor
741 isIgnoreModeFocusType
742 isProperLink
743 isTextInputElement
744 isTypingElement
745
746 blurActiveBrowserElement
747 blurActiveElement
748 focusElement
749 getActiveElement
750 getFocusType
751
752 listen
753 listenOnce
754 onRemoved
755 simulateMouseEvents
756 suppressEvent
757
758 area
759 checkElementOrAncestor
760 clearSelectionDeep
761 containsDeep
762 createBox
763 getFirstNonEmptyTextNodeBoxQuads
764 getRootElement
765 getText
766 getTopOffset
767 injectTemporaryPopup
768 insertText
769 isDetached
770 isNonEmptyTextNode
771 isPositionFixed
772 querySelectorAllDeep
773 selectAllSubstringMatches
774 selectElement
775 setAttributes
776 setHover
777 walkTextNodes
778
779 Counter
780 EventEmitter
781 bisect
782 getAllNonOverlappingRangeOffsets
783 has
784 includes
785 interval
786 nextTick
787 partition
788 regexEscape
789 removeDuplicateChars
790 removeDuplicates
791 sum
792
793 expandPath
794 formatError
795 getCurrentLocation
796 getCurrentWindow
797 hasEventListeners
798 loadCss
799 observe
800 openDropdown
801 openPopup
802 writeToClipboard
803 }
Imprint / Impressum