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