]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Improve "open link in new tab" interoperability
[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 #
6 # This file is part of VimFx.
7 #
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
20 ###
21
22 # This file contains lots of different helper functions.
23
24 {OS} = Components.utils.import('resource://gre/modules/osfile.jsm', {})
25
26 nsIClipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
27 .getService(Ci.nsIClipboardHelper)
28 nsIDomUtils = Cc['@mozilla.org/inspector/dom-utils;1']
29 .getService(Ci.inIDOMUtils)
30 nsIEventListenerService = Cc['@mozilla.org/eventlistenerservice;1']
31 .getService(Ci.nsIEventListenerService)
32 nsIFocusManager = Cc['@mozilla.org/focus-manager;1']
33 .getService(Ci.nsIFocusManager)
34 nsIStyleSheetService = Cc['@mozilla.org/content/style-sheet-service;1']
35 .getService(Ci.nsIStyleSheetService)
36 nsIWindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
37 .getService(Ci.nsIWindowMediator)
38
39 # For XUL, `instanceof` checks are often better than `.localName` checks,
40 # because some of the below interfaces are extended by many elements.
41 XULDocument = Ci.nsIDOMXULDocument
42 XULButtonElement = Ci.nsIDOMXULButtonElement
43 XULControlElement = Ci.nsIDOMXULControlElement
44 XULMenuListElement = Ci.nsIDOMXULMenuListElement
45 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
46
47 # Full chains of events for different mouse actions. Note: 'mouseup' is actually
48 # fired before 'click'!
49 EVENTS_CLICK = ['mousedown', 'mouseup', 'click']
50 EVENTS_CLICK_XUL = ['click', 'command']
51 EVENTS_HOVER_START = ['mouseover', 'mouseenter', 'mousemove']
52 EVENTS_HOVER_END = ['mouseout', 'mouseleave']
53
54 MINIMUM_EDGE_DISTANCE = 4
55
56
57
58 # Element classification helpers
59
60 isActivatable = (element) ->
61 return element.localName in ['a', 'button'] or
62 (element.localName == 'input' and element.type in [
63 'button', 'submit', 'reset', 'image'
64 ]) or
65 element instanceof XULButtonElement
66
67 isAdjustable = (element) ->
68 return element.localName == 'input' and element.type in [
69 'checkbox', 'radio', 'file', 'color'
70 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
71 ] or
72 element.localName in ['video', 'audio', 'embed', 'object'] or
73 element instanceof XULControlElement or
74 # Custom video players.
75 includes(element.className, 'video') or
76 includes(element.className, 'player') or
77 # Youtube special case.
78 element.classList?.contains('ytp-button') or
79 # Allow navigating object inspection trees in th devtools with the
80 # arrow keys, even if the arrow keys are used as VimFx shortcuts.
81 element.ownerGlobal?.DevTools
82
83 isContentEditable = (element) ->
84 return element.isContentEditable or
85 isIframeEditor(element) or
86 # Google.
87 element.getAttribute?('g_editable') == 'true' or
88 element.ownerDocument?.body?.getAttribute?('g_editable') == 'true' or
89 # Codeacademy terminals.
90 element.classList?.contains('real-terminal')
91
92 isFocusable = (element) ->
93 return element.tabIndex > -1 and
94 not (element.localName?.endsWith?('box') and
95 element.localName != 'checkbox') and
96 element.localName not in ['tabs', 'menuitem', 'menuseparator']
97
98 isIframeEditor = (element) ->
99 return false unless element.localName == 'body'
100 return \
101 # Etherpad.
102 element.id == 'innerdocbody' or
103 # XpressEditor.
104 (element.classList?.contains('xe_content') and
105 element.classList?.contains('editable')) or
106 # vBulletin.
107 element.classList?.contains('wysiwyg') or
108 # The wasavi extension.
109 element.hasAttribute?('data-wasavi-state')
110
111 isIgnoreModeFocusType = (element) ->
112 return \
113 # The wasavi extension.
114 element.hasAttribute?('data-wasavi-state') or
115 element.closest?('#wasavi_container') or
116 # CodeMirror in Vim mode.
117 (element.localName == 'textarea' and
118 element.closest?('.CodeMirror') and _hasVimEventListener(element))
119
120 # CodeMirror’s Vim mode is really sucky to detect. The only way seems to be to
121 # check if the there are any event listener functions with Vim-y words in them.
122 _hasVimEventListener = (element) ->
123 for listener in nsIEventListenerService.getListenerInfoFor(element)
124 if listener.listenerObject and
125 /\bvim\b|insertmode/i.test(String(listener.listenerObject))
126 return true
127 return false
128
129 isProperLink = (element) ->
130 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
131 # href="">`s used as buttons on some sites.
132 return element.getAttribute?('href') and
133 (element.localName == 'a' or
134 element.ownerDocument instanceof XULDocument) and
135 not element.href?.endsWith?('#') and
136 not element.href?.endsWith?('#?') and
137 not element.href?.startsWith?('javascript:')
138
139 isTextInputElement = (element) ->
140 return (element.localName == 'input' and element.type in [
141 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
142 ]) or
143 element.localName == 'textarea' or
144 element instanceof XULTextBoxElement or
145 isContentEditable(element)
146
147 isTypingElement = (element) ->
148 return isTextInputElement(element) or
149 # `<select>` elements can also receive text input: You may type the
150 # text of an item to select it.
151 element.localName == 'select' or
152 element instanceof XULMenuListElement
153
154
155
156 # Active/focused element helpers
157
158 # NOTE: In frame scripts, `document.activeElement` may be `null` when the page
159 # is loading. Therefore always check if anything was returned, such as:
160 #
161 # return unless activeElement = utils.getActiveElement(window)
162 getActiveElement = (window) ->
163 {activeElement} = window.document
164 return null unless activeElement
165 # If the active element is a frame, recurse into it. The easiest way to detect
166 # a frame that works both in browser UI and in web page content is to check
167 # for the presence of `.contentWindow`. However, in non-multi-process,
168 # `<browser>` (sometimes `<xul:browser>`) elements have a `.contentWindow`
169 # pointing to the web page content `window`, which we don’t want to recurse
170 # into. The problem is that there are _some_ `<browser>`s which we _want_ to
171 # recurse into, such as the sidebar (for instance the history sidebar), and
172 # dialogs in `about:preferences`. Checking the `contextmenu` attribute seems
173 # to be a reliable test, catching both the main tab `<browser>`s and bookmarks
174 # opened in the sidebar.
175 if (activeElement.localName == 'browser' and
176 activeElement.getAttribute?('contextmenu') == 'contentAreaContextMenu') or
177 not activeElement.contentWindow
178 return activeElement
179 else
180 return getActiveElement(activeElement.contentWindow)
181
182 blurActiveElement = (window) ->
183 # Blurring a frame element also blurs any active elements inside it. Recursing
184 # into the frames and blurring the “real” active element directly would give
185 # focus to the `<body>` of its containing frame, while blurring the top-most
186 # frame gives focus to the top-most `<body>`. This allows to blur fancy text
187 # editors which use an `<iframe>` as their text area.
188 window.document.activeElement?.blur()
189
190 blurActiveBrowserElement = (vim) ->
191 # - Blurring in the next tick allows to pass `<escape>` to the location bar to
192 # reset it, for example.
193 # - Focusing the current browser afterwards allows to pass `<escape>` as well
194 # as unbound keys to the page. However, focusing the browser also triggers
195 # focus events on `document` and `window` in the current page. Many pages
196 # re-focus some text input on those events, making it impossible to blur
197 # those! Therefore we tell the frame script to suppress those events.
198 {window} = vim
199 activeElement = getActiveElement(window)
200 vim._send('browserRefocus')
201 nextTick(window, ->
202 activeElement.blur()
203 window.gBrowser.selectedBrowser.focus()
204 )
205
206 # Focus an element and tell Firefox that the focus happened because of a user
207 # action (not just because some random programmatic focus). `.FLAG_BYKEY` might
208 # look more appropriate, but it unconditionally selects all text, which
209 # `.FLAG_BYMOUSE` does not.
210 focusElement = (element, options = {}) ->
211 nsIFocusManager.setFocus(element, options.flag ? 'FLAG_BYMOUSE')
212 element.select?() if options.select
213
214 getFocusType = (element) -> switch
215 when isIgnoreModeFocusType(element)
216 'ignore'
217 when isTypingElement(element)
218 if element.closest?('findbar') then 'findbar' else 'editable'
219 when isActivatable(element)
220 'activatable'
221 when isAdjustable(element)
222 'adjustable'
223 else
224 'none'
225
226
227
228 # Event helpers
229
230 listen = (element, eventName, listener, useCapture = true) ->
231 element.addEventListener(eventName, listener, useCapture)
232 module.onShutdown(->
233 element.removeEventListener(eventName, listener, useCapture)
234 )
235
236 listenOnce = (element, eventName, listener, useCapture = true) ->
237 fn = (event) ->
238 listener(event)
239 element.removeEventListener(eventName, fn, useCapture)
240 listen(element, eventName, fn, useCapture)
241
242 onRemoved = (element, fn) ->
243 window = element.ownerGlobal
244
245 disconnected = false
246 disconnect = ->
247 return if disconnected
248 disconnected = true
249 mutationObserver.disconnect() unless Cu.isDeadWrapper(mutationObserver)
250
251 mutationObserver = new window.MutationObserver((changes) ->
252 for change in changes then for removedElement in change.removedNodes
253 if removedElement.contains?(element)
254 disconnect()
255 fn()
256 return
257 )
258 mutationObserver.observe(window.document.documentElement, {
259 childList: true
260 subtree: true
261 })
262 module.onShutdown(disconnect)
263
264 return disconnect
265
266 suppressEvent = (event) ->
267 event.preventDefault()
268 event.stopPropagation()
269
270 simulateMouseEvents = (element, sequence) ->
271 window = element.ownerGlobal
272 rect = element.getBoundingClientRect()
273
274 eventSequence = switch sequence
275 when 'click'
276 EVENTS_CLICK
277 when 'click-xul'
278 EVENTS_CLICK_XUL
279 when 'hover-start'
280 EVENTS_HOVER_START
281 when 'hover-end'
282 EVENTS_HOVER_END
283 else
284 sequence
285
286 for type in eventSequence
287 buttonNum = if type in EVENTS_CLICK then 1 else 0
288 mouseEvent = new window.MouseEvent(type, {
289 # Let the event bubble in order to trigger delegated event listeners.
290 bubbles: type not in ['mouseenter', 'mouseleave']
291 # Make the event cancelable so that `<a href="#">` can be used as a
292 # JavaScript-powered button without scrolling to the top of the page.
293 cancelable: type not in ['mouseenter', 'mouseleave']
294 # These properties are just here for mimicing a real click as much as
295 # possible.
296 buttons: buttonNum
297 detail: buttonNum
298 view: window
299 # `page{X,Y}` are set automatically to the correct values when setting
300 # `client{X,Y}`. `{offset,layer,movement}{X,Y}` are not worth the trouble
301 # to set.
302 clientX: rect.left
303 clientY: rect.top
304 # To exactly calculate `screen{X,Y}` one has to to check where the web
305 # page content area is inside the browser chrome and go through all parent
306 # frames as well. This is good enough. YAGNI for now.
307 screenX: window.screenX + rect.left
308 screenY: window.screenY + rect.top
309 })
310 element.dispatchEvent(mouseEvent)
311
312 return
313
314
315
316 # DOM helpers
317
318 area = (element) ->
319 return element.clientWidth * element.clientHeight
320
321 containsDeep = (parent, element) ->
322 parentWindow = parent.ownerGlobal
323 elementWindow = element.ownerGlobal
324
325 # Owner windows might be missing when opening the devtools.
326 while elementWindow and parentWindow and
327 elementWindow != parentWindow and elementWindow.top != elementWindow
328 element = elementWindow.frameElement
329 elementWindow = element.ownerGlobal
330
331 return parent.contains(element)
332
333 createBox = (document, className = '', parent = null, text = null) ->
334 box = document.createElement('box')
335 box.className = "#{className} vimfx-box"
336 box.textContent = text if text?
337 parent.appendChild(box) if parent?
338 return box
339
340 getFrameViewport = (frame, parentViewport) ->
341 rect = frame.getBoundingClientRect()
342 return null unless isInsideViewport(rect, parentViewport)
343
344 # `.getComputedStyle()` may return `null` if the computed style isn’t availble
345 # yet. If so, consider the element not visible.
346 return null unless computedStyle = frame.ownerGlobal.getComputedStyle(frame)
347 offset = {
348 left: rect.left +
349 parseFloat(computedStyle.getPropertyValue('border-left-width')) +
350 parseFloat(computedStyle.getPropertyValue('padding-left'))
351 top: rect.top +
352 parseFloat(computedStyle.getPropertyValue('border-top-width')) +
353 parseFloat(computedStyle.getPropertyValue('padding-top'))
354 right: rect.right -
355 parseFloat(computedStyle.getPropertyValue('border-right-width')) -
356 parseFloat(computedStyle.getPropertyValue('padding-right'))
357 bottom: rect.bottom -
358 parseFloat(computedStyle.getPropertyValue('border-bottom-width')) -
359 parseFloat(computedStyle.getPropertyValue('padding-bottom'))
360 }
361
362 # Calculate the visible part of the frame, according to the parent.
363 viewport = getWindowViewport(frame.contentWindow)
364 left = viewport.left + Math.max(parentViewport.left - offset.left, 0)
365 top = viewport.top + Math.max(parentViewport.top - offset.top, 0)
366 right = viewport.right + Math.min(parentViewport.right - offset.right, 0)
367 bottom = viewport.bottom + Math.min(parentViewport.bottom - offset.bottom, 0)
368
369 return {
370 viewport: {
371 left, top, right, bottom
372 width: right - left
373 height: bottom - top
374 }
375 offset
376 }
377
378 # In quirks mode (when the page lacks a doctype), such as on Hackernews,
379 # `<body>` is considered the root element rather than `<html>`.
380 getRootElement = (document) ->
381 if document.compatMode == 'BackCompat' and document.body?
382 return document.body
383 else
384 return document.documentElement
385
386
387 # Returns the minimum of `element.clientHeight` and the height of the viewport,
388 # taking fixed headers and footers into account. Adapted from Firefox’s source
389 # code for `<space>` scrolling (which is where the arbitrary constants below
390 # come from).
391 #
392 # coffeelint: disable=max_line_length
393 # <https://hg.mozilla.org/mozilla-central/file/4d75bd6fd234/layout/generic/nsGfxScrollFrame.cpp#l3829>
394 # coffeelint: enable=max_line_length
395 getViewportCappedClientHeight = (element) ->
396 window = element.ownerGlobal
397 viewport = getWindowViewport(window)
398 headerBottom = viewport.top
399 footerTop = viewport.bottom
400 maxHeight = viewport.height / 3
401 minWidth = Math.min(viewport.width / 2, 800)
402
403 # Restricting the candidates for headers and footers to the most likely set of
404 # elements results in a noticeable performance boost.
405 candidates = window.document.querySelectorAll(
406 'div, ul, nav, header, footer, section'
407 )
408
409 for candidate in candidates
410 rect = candidate.getBoundingClientRect()
411 continue unless rect.height <= maxHeight and rect.width >= minWidth
412 # Checking for `position: fixed;` is the absolutely most expensive
413 # operation, so that is done last.
414 switch
415 when rect.top <= headerBottom and rect.bottom > headerBottom and
416 isPositionFixed(candidate)
417 headerBottom = rect.bottom
418 when rect.bottom >= footerTop and rect.top < footerTop and
419 isPositionFixed(candidate)
420 footerTop = rect.top
421
422 return Math.min(element.clientHeight, footerTop - headerBottom)
423
424 getWindowViewport = (window) ->
425 {
426 clientWidth, clientHeight # Viewport size excluding scrollbars, usually.
427 scrollWidth, scrollHeight
428 } = getRootElement(window.document)
429 {innerWidth, innerHeight} = window # Viewport size including scrollbars.
430 # We don’t want markers to cover the scrollbars, so we should use
431 # `clientWidth` and `clientHeight`. However, when there are no scrollbars
432 # those might be too small. Then we use `innerWidth` and `innerHeight`.
433 width = if scrollWidth > innerWidth then clientWidth else innerWidth
434 height = if scrollHeight > innerHeight then clientHeight else innerHeight
435 return {
436 left: 0
437 top: 0
438 right: width
439 bottom: height
440 width
441 height
442 }
443
444 injectTemporaryPopup = (document, contents) ->
445 popup = document.createElement('menupopup')
446 popup.appendChild(contents)
447 document.getElementById('mainPopupSet').appendChild(popup)
448 listenOnce(popup, 'popuphidden', popup.remove.bind(popup))
449 return popup
450
451 insertText = (input, value) ->
452 {selectionStart, selectionEnd} = input
453 input.value =
454 input.value[0...selectionStart] + value + input.value[selectionEnd..]
455 input.selectionStart = input.selectionEnd = selectionStart + value.length
456
457 isDetached = (element) ->
458 return not element.ownerDocument?.documentElement?.contains?(element)
459
460 isInsideViewport = (rect, viewport) ->
461 return \
462 rect.left <= viewport.right - MINIMUM_EDGE_DISTANCE and
463 rect.top <= viewport.bottom + MINIMUM_EDGE_DISTANCE and
464 rect.right >= viewport.left + MINIMUM_EDGE_DISTANCE and
465 rect.bottom >= viewport.top - MINIMUM_EDGE_DISTANCE
466
467 isPositionFixed = (element) ->
468 computedStyle = element.ownerGlobal.getComputedStyle(element)
469 return computedStyle?.getPropertyValue('position') == 'fixed'
470
471 querySelectorAllDeep = (window, selector) ->
472 elements = Array.from(window.document.querySelectorAll(selector))
473 for frame in window.frames
474 elements.push(querySelectorAllDeep(frame, selector)...)
475 return elements
476
477 scroll = (element, args) ->
478 {method, type, directions, amounts, properties, adjustment, smooth} = args
479 options = {}
480 for direction, index in directions
481 amount = amounts[index]
482 options[direction] = -Math.sign(amount) * adjustment + switch type
483 when 'lines'
484 amount
485 when 'pages'
486 amount *
487 if properties[index] == 'clientHeight'
488 getViewportCappedClientHeight(element)
489 else
490 element[properties[index]]
491 when 'other'
492 Math.min(amount, element[properties[index]])
493 options.behavior = 'smooth' if smooth
494 element[method](options)
495
496 setAttributes = (element, attributes) ->
497 for attribute, value of attributes
498 element.setAttribute(attribute, value)
499 return
500
501 setHover = (element, hover) ->
502 method = if hover then 'addPseudoClassLock' else 'removePseudoClassLock'
503 while element.parentElement
504 nsIDomUtils[method](element, ':hover')
505 element = element.parentElement
506 return
507
508
509
510 # Language helpers
511
512 class Counter
513 constructor: ({start: @value = 0, @step = 1}) ->
514 tick: -> @value += @step
515
516 class EventEmitter
517 constructor: ->
518 @listeners = {}
519
520 on: (event, listener) ->
521 (@listeners[event] ?= new Set()).add(listener)
522
523 off: (event, listener) ->
524 @listeners[event]?.delete(listener)
525
526 emit: (event, data) ->
527 @listeners[event]?.forEach((listener) ->
528 listener(data)
529 )
530
531 has = (obj, prop) -> Object::hasOwnProperty.call(obj, prop)
532
533 # Check if `search` exists in `string` (case insensitively). Returns `false` if
534 # `string` doesn’t exist or isn’t a string, such as `<SVG element>.className`.
535 includes = (string, search) ->
536 return false unless typeof string == 'string'
537 return string.toLowerCase().includes(search)
538
539
540 nextTick = (window, fn) -> window.setTimeout((-> fn()) , 0)
541
542 regexEscape = (s) -> s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
543
544 removeDuplicates = (array) -> Array.from(new Set(array))
545
546 # Remove duplicate characters from string (case insensitive).
547 removeDuplicateCharacters = (str) ->
548 return removeDuplicates( str.toLowerCase().split('') ).join('')
549
550 # Calls `fn` repeatedly, with at least `interval` ms between each call.
551 interval = (window, interval, fn) ->
552 stopped = false
553 currentIntervalId = null
554 next = ->
555 return if stopped
556 currentIntervalId = window.setTimeout((-> fn(next)), interval)
557 clearInterval = ->
558 stopped = true
559 window.clearTimeout(currentIntervalId)
560 next()
561 return clearInterval
562
563
564
565 # Misc helpers
566
567 expandPath = (path) ->
568 if path.startsWith('~/') or path.startsWith('~\\')
569 return OS.Constants.Path.homeDir + path[1..]
570 else
571 return path
572
573 formatError = (error) ->
574 stack = String(error.stack?.formattedStack ? error.stack ? '')
575 .split('\n')
576 .filter((line) -> line.includes('.xpi!'))
577 .map((line) -> ' ' + line.replace(/(?:\/<)*@.+\.xpi!/g, '@'))
578 .join('\n')
579 return "#{error}\n#{stack}"
580
581 getCurrentLocation = ->
582 window = getCurrentWindow()
583 return new window.URL(window.gBrowser.selectedBrowser.currentURI.spec)
584
585 getCurrentWindow = -> nsIWindowMediator.getMostRecentWindow('navigator:browser')
586
587 hasEventListeners = (element, type) ->
588 for listener in nsIEventListenerService.getListenerInfoFor(element)
589 if listener.listenerObject and listener.type == type
590 return true
591 return false
592
593 loadCss = (uriString) ->
594 uri = Services.io.newURI(uriString, null, null)
595 method = nsIStyleSheetService.AUTHOR_SHEET
596 unless nsIStyleSheetService.sheetRegistered(uri, method)
597 nsIStyleSheetService.loadAndRegisterSheet(uri, method)
598 module.onShutdown(->
599 nsIStyleSheetService.unregisterSheet(uri, method)
600 )
601
602 observe = (topic, observer) ->
603 observer = {observe: observer} if typeof observer == 'function'
604 Services.obs.addObserver(observer, topic, false)
605 module.onShutdown(->
606 Services.obs.removeObserver(observer, topic, false)
607 )
608
609 openPopup = (popup) ->
610 window = popup.ownerGlobal
611 # Show the popup so it gets a height and width.
612 popup.openPopupAtScreen(0, 0)
613 # Center the popup inside the window.
614 popup.moveTo(
615 window.screenX + window.outerWidth / 2 - popup.clientWidth / 2,
616 window.screenY + window.outerHeight / 2 - popup.clientHeight / 2
617 )
618
619 writeToClipboard = (text) -> nsIClipboardHelper.copyString(text)
620
621
622
623 module.exports = {
624 isActivatable
625 isAdjustable
626 isContentEditable
627 isFocusable
628 isIframeEditor
629 isIgnoreModeFocusType
630 isProperLink
631 isTextInputElement
632 isTypingElement
633
634 getActiveElement
635 blurActiveElement
636 blurActiveBrowserElement
637 focusElement
638 getFocusType
639
640 listen
641 listenOnce
642 onRemoved
643 suppressEvent
644 simulateMouseEvents
645
646 area
647 containsDeep
648 createBox
649 getRootElement
650 getFrameViewport
651 getViewportCappedClientHeight
652 getWindowViewport
653 injectTemporaryPopup
654 insertText
655 isDetached
656 isInsideViewport
657 isPositionFixed
658 querySelectorAllDeep
659 scroll
660 setAttributes
661 setHover
662
663 Counter
664 EventEmitter
665 has
666 includes
667 nextTick
668 regexEscape
669 removeDuplicates
670 removeDuplicateCharacters
671 interval
672
673 expandPath
674 formatError
675 getCurrentLocation
676 getCurrentWindow
677 hasEventListeners
678 loadCss
679 observe
680 openPopup
681 writeToClipboard
682 }
Imprint / Impressum