]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix element focusing with `f` commands
[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, {skipBlurring = false} = {}) ->
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)
381 # Allow parents to re-gain control of text selection.
382 frame.frameElement.blur() unless skipBlurring
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 getFirstNonEmptyTextNodeBoxQuads = (element) ->
405 for node in element.childNodes then switch node.nodeType
406 when 3 # TextNode.
407 unless node.data.trim() == ''
408 boxQuads = node.getBoxQuads()
409 return boxQuads if boxQuads?.length > 0
410 when 1 # Element.
411 result = getFirstNonEmptyTextNodeBoxQuads(node)
412 return result if result
413 return null
414
415 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
416 # `<body>` is considered the root element rather than `<html>`.
417 getRootElement = (document) ->
418 if document.compatMode == 'BackCompat' and document.body?
419 return document.body
420 else
421 return document.documentElement
422
423 getText = (element) ->
424 text = element.textContent or element.value or element.placeholder or ''
425 return text.trim().replace(/\s+/, ' ')
426
427 getTopOffset = (element) ->
428 window = element.ownerGlobal
429
430 {left: x, top: y} = element.getBoundingClientRect()
431 while window.frameElement
432 frame = window.frameElement
433 frameRect = frame.getBoundingClientRect()
434 x += frameRect.left
435 y += frameRect.top
436
437 computedStyle = frame.ownerGlobal.getComputedStyle(frame)
438 if computedStyle
439 x +=
440 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
441 parseFloat(computedStyle.getPropertyValue('padding-left'))
442 y +=
443 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
444 parseFloat(computedStyle.getPropertyValue('padding-top'))
445
446 window = window.parent
447 return {x, y}
448
449 injectTemporaryPopup = (document, contents) ->
450 popup = document.createElement('menupopup')
451 popup.appendChild(contents)
452 document.getElementById('mainPopupSet').appendChild(popup)
453 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
454 return popup
455
456 insertText = (input, value) ->
457 {selectionStart, selectionEnd} = input
458 input.value =
459 input.value[0...selectionStart] + value + input.value[selectionEnd..]
460 input.selectionStart = input.selectionEnd = selectionStart + value.length
461
462 isDetached = (element) ->
463 return not element.ownerDocument?.documentElement?.contains?(element)
464
465 isNonEmptyTextNode = (node) ->
466 return node.nodeType == 3 and node.data.trim() != ''
467
468 isPositionFixed = (element) ->
469 computedStyle = element.ownerGlobal.getComputedStyle(element)
470 return computedStyle?.getPropertyValue('position') == 'fixed'
471
472 querySelectorAllDeep = (window, selector) ->
473 elements = Array.from(window.document.querySelectorAll(selector))
474 for frame in window.frames
475 elements.push(querySelectorAllDeep(frame, selector)...)
476 return elements
477
478 selectAllSubstringMatches = (element, substring, {caseSensitive = true} = {}) ->
479 window = element.ownerGlobal
480 selection = window.getSelection()
481 {textContent} = element
482
483 format = (string) -> if caseSensitive then string else string.toLowerCase()
484 offsets =
485 getAllNonOverlappingRangeOffsets(format(textContent), format(substring))
486 offsetsLength = offsets.length
487 return if offsetsLength == 0
488
489 textIndex = 0
490 offsetsIndex = 0
491 [currentOffset] = offsets
492 searchIndex = currentOffset.start
493 start = null
494
495 walkTextNodes(element, (textNode) ->
496 {length} = textNode.data
497 return false if length == 0
498
499 while textIndex + length > searchIndex
500 if start
501 range = window.document.createRange()
502 range.setStart(start.textNode, start.offset)
503 range.setEnd(textNode, currentOffset.end - textIndex)
504 selection.addRange(range)
505
506 offsetsIndex += 1
507 return true if offsetsIndex >= offsetsLength
508 currentOffset = offsets[offsetsIndex]
509
510 start = null
511 searchIndex = currentOffset.start
512
513 else
514 start = {textNode, offset: currentOffset.start - textIndex}
515 searchIndex = currentOffset.end - 1
516
517 textIndex += length
518 return false
519 )
520
521 selectElement = (element) ->
522 window = element.ownerGlobal
523 selection = window.getSelection()
524 range = window.document.createRange()
525 range.selectNodeContents(element)
526 selection.addRange(range)
527
528 setAttributes = (element, attributes) ->
529 for attribute, value of attributes
530 element.setAttribute(attribute, value)
531 return
532
533 setHover = (element, hover) ->
534 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
535 while element.parentElement
536 nsIDomUtils[method](element, ':hover')
537 element = element.parentElement
538 return
539
540 walkTextNodes = (element, fn) ->
541 for node in element.childNodes then switch node.nodeType
542 when 3 # TextNode.
543 stop = fn(node)
544 return true if stop
545 when 1 # Element.
546 stop = walkTextNodes(node, fn)
547 return true if stop
548 return false
549
550
551
552 # Language helpers
553
554 class Counter
555 constructor: ({start: @value = 0, @step = 1}) ->
556 tick: -> @value += @step
557
558 class EventEmitter
559 constructor: ->
560 @listeners = {}
561
562 on: (event, listener) ->
563 (@listeners[event] ?= new Set()).add(listener)
564
565 off: (event, listener) ->
566 @listeners[event]?.delete(listener)
567
568 emit: (event, data) ->
569 @listeners[event]?.forEach((listener) ->
570 listener(data)
571 )
572
573 # Returns `[nonMatch, adjacentMatchAfter]`, where `adjacentMatchAfter - nonMatch
574 # == 1`. `fn(n)` is supposed to return `false` for `n <= nonMatch` and `true`
575 # for `n >= adjacentMatchAfter`. Both `nonMatch` and `adjacentMatchAfter` may be
576 # `null` if they cannot be found. Otherwise they’re in the range `min <= n <=
577 # max`. `[null, null]` is returned in non-sensical cases. This function is
578 # intended to be used as a faster alternative to something like this:
579 #
580 # adjacentMatchAfter = null
581 # for n in [min..max]
582 # if fn(n)
583 # adjacentMatchAfter = n
584 # break
585 bisect = (min, max, fn) ->
586 return [null, null] unless max - min >= 0 and min % 1 == 0 and max % 1 == 0
587
588 while max - min > 1
589 mid = min + (max - min) // 2
590 match = fn(mid)
591 if match
592 max = mid
593 else
594 min = mid
595
596 matchMin = fn(min)
597 matchMax = fn(max)
598
599 return switch
600 when matchMin and matchMax
601 [null, min]
602 when not matchMin and not matchMax
603 [max, null]
604 when not matchMin and matchMax
605 [min, max]
606 else
607 [null, null]
608
609 getAllNonOverlappingRangeOffsets = (string, substring) ->
610 {length} = substring
611 return [] if length == 0
612
613 offsets = []
614 lastOffset = {start: -Infinity, end: -Infinity}
615 index = -1
616
617 loop
618 index = string.indexOf(substring, index + 1)
619 break if index == -1
620 if index > lastOffset.end
621 lastOffset = {start: index, end: index + length}
622 offsets.push(lastOffset)
623 else
624 lastOffset.end = index + length
625
626 return offsets
627
628 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
629
630 # Check if `search` exists in `string` (case insensitively). Returns `false` if
631 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
632 includes = (string, search) ->
633 return false unless typeof string == 'string'
634 return string.toLowerCase().includes(search)
635
636 # Calls `fn` repeatedly, with at least `interval` ms between each call.
637 interval = (window, interval, fn) ->
638 stopped = false
639 currentIntervalId = null
640 next = ->
641 return if stopped
642 currentIntervalId = window.setTimeout((-> fn(next)), interval)
643 clearInterval = ->
644 stopped = true
645 window.clearTimeout(currentIntervalId)
646 next()
647 return clearInterval
648
649 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
650
651 partition = (array, fn) ->
652 matching = []
653 nonMatching = []
654 for item, index in array
655 if fn(item, index, array)
656 matching.push(item)
657 else
658 nonMatching.push(item)
659 return [matching, nonMatching]
660
661 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
662
663 removeDuplicateChars = (string) -> removeDuplicates(string.split('')).join('')
664
665 removeDuplicates = (array) -> Array.from(new Set(array))
666
667 sum = (numbers) -> numbers.reduce(((sum, number) -> sum + number), 0)
668
669
670
671 # Misc helpers
672
673 expandPath = (path) ->
674 if path.startsWith('~/') or path.startsWith('~\\')
675 return OS.Constants.Path.homeDir + path[1..]
676 else
677 return path
678
679 formatError = (error) ->
680 stack = String(error.stack?.formattedStack ? error.stack ? '')
681 .split('\n')
682 .filter((line) -> line.includes('.xpi!'))
683 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
684 .join('\n')
685 return "#{error}\n#{stack}"
686
687 getCurrentLocation = ->
688 return unless window = getCurrentWindow()
689 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
690
691 # This function might return `null` on startup.
692 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
693
694 hasEventListeners = (element, type) ->
695 for listener in nsIEventListenerService.getListenerInfoFor(element)
696 if listener.listenerObject and listener.type == type
697 return true
698 return false
699
700 loadCss = (uriString) ->
701 uri = Services.io.newURI(uriString, null, null)
702 method = nsIStyleSheetService.AUTHOR_SHEET
703 unless nsIStyleSheetService.sheetRegistered(uri, method)
704 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
705 module.onShutdown(->
706 nsIStyleSheetService.unregisterSheet(uri, method)
707 )
708
709 observe = (topic, observer) ->
710 observer = {observe: observer} if typeof observer == 'function'
711 Services.obs.addObserver(observer, topic, false)
712 module.onShutdown(->
713 Services.obs.removeObserver(observer, topic, false)
714 )
715
716 # Try to open a button’s dropdown menu, if any.
717 openDropdown = (element) ->
718 if element.ownerDocument instanceof XULDocument and
719 element.getAttribute?('type') == 'menu' and
720 element.open == false # Only change `.open` if it is already a boolean.
721 element.open = true
722
723 openPopup = (popup) ->
724 window = popup.ownerGlobal
725 # Show the popup so it gets a height and width.
726 popup.openPopupAtScreen(0, 0)
727 # Center the popup inside the window.
728 popup.moveTo(
729 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
730 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
731 )
732
733 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
734
735
736
737 module.exports = {
738 hasMarkableTextNode
739 isActivatable
740 isAdjustable
741 isContentEditable
742 isDevtoolsElement
743 isDevtoolsWindow
744 isFocusable
745 isIframeEditor
746 isIgnoreModeFocusType
747 isProperLink
748 isTextInputElement
749 isTypingElement
750
751 blurActiveBrowserElement
752 blurActiveElement
753 focusElement
754 getActiveElement
755 getFocusType
756
757 listen
758 listenOnce
759 onRemoved
760 simulateMouseEvents
761 suppressEvent
762
763 area
764 checkElementOrAncestor
765 clearSelectionDeep
766 containsDeep
767 createBox
768 getFirstNonEmptyTextNodeBoxQuads
769 getRootElement
770 getText
771 getTopOffset
772 injectTemporaryPopup
773 insertText
774 isDetached
775 isNonEmptyTextNode
776 isPositionFixed
777 querySelectorAllDeep
778 selectAllSubstringMatches
779 selectElement
780 setAttributes
781 setHover
782 walkTextNodes
783
784 Counter
785 EventEmitter
786 bisect
787 getAllNonOverlappingRangeOffsets
788 has
789 includes
790 interval
791 nextTick
792 partition
793 regexEscape
794 removeDuplicateChars
795 removeDuplicates
796 sum
797
798 expandPath
799 formatError
800 getCurrentLocation
801 getCurrentWindow
802 hasEventListeners
803 loadCss
804 observe
805 openDropdown
806 openPopup
807 writeToClipboard
808 }
Imprint / Impressum