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