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