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