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