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