@js-toolkit/web-utils 1.67.0 → 1.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/EventEmitterListener.d.ts +3 -3
  2. package/EventEmitterListener.js +222 -1
  3. package/EventEmitterListener.utils.d.ts +24 -13
  4. package/EventEmitterListener.utils.js +41 -1
  5. package/EventListeners.js +58 -1
  6. package/FullscreenController.js +193 -1
  7. package/README.md +159 -20
  8. package/WakeLockController.js +76 -1
  9. package/base64ToDataUrl.js +3 -1
  10. package/blobToDataUrl.js +10 -1
  11. package/checkPermission.js +8 -1
  12. package/copyToClipboard.js +37 -1
  13. package/createLoop.js +30 -1
  14. package/createRafLoop.js +56 -1
  15. package/dataUrlToBlob.js +13 -1
  16. package/fromBase64.js +10 -1
  17. package/fullscreen.js +167 -1
  18. package/fullscreenUtils.js +37 -1
  19. package/getAspectRatio.js +8 -1
  20. package/getBrowserLanguage.js +10 -1
  21. package/getCurrentScriptUrl.js +4 -1
  22. package/getEventAwaiter.js +41 -1
  23. package/getGeoCoordinates.js +6 -1
  24. package/getGeoLocality.js +19 -1
  25. package/getInnerRect.js +8 -1
  26. package/getInnerXDimensions.js +9 -1
  27. package/getInnerYDimensions.js +9 -1
  28. package/getPinchZoomHandlers.js +134 -1
  29. package/getRandomID.js +4 -1
  30. package/getScreenSize.js +23 -1
  31. package/getSecondsCounter.js +47 -1
  32. package/iframe/getAutoConnector.js +251 -1
  33. package/iframe/getOriginFromMessage.js +3 -1
  34. package/iframe/isIframeLoaded.js +9 -1
  35. package/iframe/messages.d.ts +2 -2
  36. package/iframe/messages.js +50 -1
  37. package/iframe/utils.js +33 -1
  38. package/imageToBlob.js +20 -1
  39. package/isImageTypeSupported.js +8 -1
  40. package/isWebPSupported.js +15 -1
  41. package/loadImage.js +29 -1
  42. package/loadScript.d.ts +1 -1
  43. package/loadScript.js +67 -1
  44. package/media/Capabilities.js +44 -1
  45. package/media/MediaNotAttachedError.d.ts +1 -1
  46. package/media/MediaNotAttachedError.js +6 -1
  47. package/media/MediaStreamController.js +84 -1
  48. package/media/PipController.d.ts +2 -2
  49. package/media/PipController.js +140 -1
  50. package/media/TextTracksController/TextTracksController.d.ts +0 -3
  51. package/media/TextTracksController/TextTracksController.js +251 -1
  52. package/media/TextTracksController/index.js +1 -1
  53. package/media/TextTracksController/utils.js +147 -1
  54. package/media/getDurationTime.js +3 -1
  55. package/media/getMediaSource.js +11 -1
  56. package/media/getSourceBuffer.js +5 -1
  57. package/media/isMediaSeekable.js +4 -1
  58. package/media/parseCueText.js +224 -1
  59. package/media/resetMedia.js +17 -1
  60. package/media/timeRanges.js +9 -1
  61. package/media/toggleNativeSubtitles.js +21 -1
  62. package/metrics/ga/DataLayerProxy.js +11 -1
  63. package/metrics/ga/getHandler.js +99 -1
  64. package/metrics/ga/types.js +1 -1
  65. package/metrics/types.js +1 -1
  66. package/metrics/yandex/DataLayerProxy.js +11 -1
  67. package/metrics/yandex/getHandler.js +63 -1
  68. package/metrics/yandex/types.js +2 -1
  69. package/onDOMReady.js +12 -1
  70. package/onPageReady.js +27 -1
  71. package/package.json +16 -13
  72. package/patchConsoleLogging.js +27 -1
  73. package/performance/getNavigationTiming.js +14 -1
  74. package/platform/Semver.js +23 -1
  75. package/platform/getChromeVersion.d.ts +2 -0
  76. package/platform/getChromeVersion.js +16 -0
  77. package/platform/getIOSVersion.js +18 -1
  78. package/platform/getPlatformInfo.js +49 -1
  79. package/platform/isAirPlayAvailable.js +3 -1
  80. package/platform/isAndroid.js +7 -1
  81. package/platform/isChrome.js +7 -1
  82. package/platform/isEMESupported.js +9 -1
  83. package/platform/isIOS.js +9 -1
  84. package/platform/isMSESupported.js +16 -1
  85. package/platform/isMacOS.js +12 -1
  86. package/platform/isMediaCapabilitiesSupported.js +6 -1
  87. package/platform/isMobile.js +16 -1
  88. package/platform/isMobileSimulation.js +7 -1
  89. package/platform/isSafari.js +14 -1
  90. package/platform/isScreenHDR.js +4 -1
  91. package/platform/isStandaloneApp.js +4 -1
  92. package/platform/isTelegramWebView.js +3 -1
  93. package/platform/isTouchSupported.js +4 -1
  94. package/preventDefault.d.ts +2 -2
  95. package/preventDefault.js +5 -1
  96. package/rafCallback.js +9 -1
  97. package/responsive/MediaQuery.js +40 -1
  98. package/responsive/MediaQueryListener.js +55 -1
  99. package/responsive/ViewSize.js +82 -1
  100. package/responsive/getViewSizeQueryMap.js +7 -1
  101. package/saveFileAs.js +22 -1
  102. package/serviceWorker/ServiceWorkerInstaller.d.ts +0 -1
  103. package/serviceWorker/ServiceWorkerInstaller.js +112 -1
  104. package/serviceWorker/utils.d.ts +1 -1
  105. package/serviceWorker/utils.js +86 -1
  106. package/stopPropagation.d.ts +2 -2
  107. package/stopPropagation.js +3 -1
  108. package/takeScreenshot.js +51 -1
  109. package/toBase64.js +9 -1
  110. package/toLocalPoint.js +9 -1
  111. package/types/index.js +2 -1
  112. package/types/refs.js +1 -1
  113. package/viewableTracker.js +69 -1
  114. package/webrtc/PeerConnection.js +212 -1
  115. package/webrtc/sdputils.js +417 -1
  116. package/ws/WSController.js +148 -1
@@ -1 +1,112 @@
1
- import{EventEmitter}from"@js-toolkit/utils/EventEmitter";import{ErrorCompat}from"@js-toolkit/utils/ErrorCompat";import{onPageReady}from"../onPageReady";import{isLocalhost}from"./utils";export class ServiceWorkerUnavailableError extends ErrorCompat{constructor(){super(ServiceWorkerUnavailableError,"ServiceWorker is not available",{name:"ServiceWorkerUnavailableError"})}}export class ServiceWorkerInstaller extends EventEmitter{static isAvailable(){return"serviceWorker"in navigator}registration;cancelDefferedRegister;constructor(){super()}register(r,e){if(!ServiceWorkerInstaller.isAvailable())throw new ServiceWorkerUnavailableError;const{deffered:t,...i}=e??{},o=async()=>{try{if(isLocalhost()){const e=await fetch(r);if(!e.ok)throw new Error(`No service worker found at '${e.url}'.`,{cause:`Response: ${e.status} ${e.statusText}`})}const e=await navigator.serviceWorker.register(r,i);this.registration=e,this.emit("registered",{registration:e}),e.onupdatefound=()=>{const r=e.installing;null!=r&&(r.onstatechange=()=>{"installed"===r.state&&(navigator.serviceWorker.controller?this.emit("updatePending",{registration:e}):this.emit("updated",{registration:e}))},r.onerror=r=>{const e=new Error("Error during service worker installation",{cause:r});this.emit("error",{error:e})})}}catch(r){const e=new Error("Error during service worker registration",{cause:r});this.emit("error",{error:e})}};t?this.cancelDefferedRegister=onPageReady(o,"number"==typeof t?{timeout:t}:void 0):o()}unregister(){this.cancelDefferedRegister&&this.cancelDefferedRegister(),this.registration?.unregister().catch(r=>{const e=new Error("Error during service worker unregister",{cause:r});this.emit("error",{error:e})})}destroy(){this.cancelDefferedRegister&&this.cancelDefferedRegister(),this.removeAllListeners()}[Symbol.dispose](){this.destroy()}}
1
+ /* eslint-disable @typescript-eslint/no-misused-promises */
2
+ /* eslint-disable max-classes-per-file */
3
+ /* eslint-disable @typescript-eslint/no-namespace */
4
+ import { EventEmitter } from '@js-toolkit/utils/EventEmitter';
5
+ import { ErrorCompat } from '@js-toolkit/utils/ErrorCompat';
6
+ import { onPageReady } from '../onPageReady';
7
+ import { isLocalhost } from './utils';
8
+ /*
9
+ * https://web.dev/articles/service-workers-registration?hl=ru
10
+ */
11
+ export class ServiceWorkerUnavailableError extends ErrorCompat {
12
+ constructor() {
13
+ super(ServiceWorkerUnavailableError, 'ServiceWorker is not available', {
14
+ name: 'ServiceWorkerUnavailableError',
15
+ });
16
+ }
17
+ }
18
+ export class ServiceWorkerInstaller extends EventEmitter {
19
+ static isAvailable() {
20
+ return 'serviceWorker' in navigator;
21
+ }
22
+ // private readonly logPrefix = 'SW:';
23
+ // private readonly options;
24
+ registration;
25
+ cancelDefferedRegister;
26
+ // constructor(/* options: ServiceWorkerInstaller.Options */) {
27
+ // super();
28
+ // // this.options = { ...options, logger: options.logger ?? console };
29
+ // }
30
+ register(swUrl, options) {
31
+ if (!ServiceWorkerInstaller.isAvailable()) {
32
+ throw new ServiceWorkerUnavailableError();
33
+ }
34
+ const { deffered, ...swOptions } = options ?? {};
35
+ const register = async () => {
36
+ try {
37
+ if (isLocalhost()) {
38
+ // This is running on localhost. Let's check if a service worker still exists or not.
39
+ const response = await fetch(swUrl);
40
+ if (!response.ok) {
41
+ throw new Error(`No service worker found at '${response.url}'.`, {
42
+ cause: `Response: ${response.status} ${response.statusText}`,
43
+ });
44
+ }
45
+ }
46
+ const registration = await navigator.serviceWorker.register(swUrl, swOptions);
47
+ this.registration = registration;
48
+ this.emit('registered', { registration });
49
+ // https://whatwebcando.today/articles/handling-service-worker-updates/
50
+ registration.onupdatefound = () => {
51
+ // At this point we only know the browser detected the Service Worker file change.
52
+ //
53
+ // Wait until the new instance is ready for activation (its state is installed).
54
+ const sw = registration.installing;
55
+ if (sw == null) {
56
+ return;
57
+ }
58
+ sw.onstatechange = () => {
59
+ if (sw.state === 'installed') {
60
+ if (navigator.serviceWorker.controller) {
61
+ // At this point, the updated precached content has been fetched,
62
+ // but the previous service worker will still serve the older content (until all client tabs are closed).
63
+ this.emit('updatePending', { registration });
64
+ }
65
+ else {
66
+ // It's the first install.
67
+ // At this point, everything has been precached.
68
+ // It's the perfect time to display a "Content is cached for offline use." message.
69
+ this.emit('updated', { registration });
70
+ }
71
+ }
72
+ };
73
+ sw.onerror = (error) => {
74
+ // this.options.logger.error(this.logPrefix, 'Installing worker:', error);
75
+ const nextError = new Error('Error during service worker installation', {
76
+ cause: error,
77
+ });
78
+ this.emit('error', { error: nextError });
79
+ };
80
+ };
81
+ }
82
+ catch (error) {
83
+ const nextError = new Error('Error during service worker registration', { cause: error });
84
+ // this.options.logger.error(this.logPrefix, getErrorMessage(nextError));
85
+ this.emit('error', { error: nextError });
86
+ }
87
+ };
88
+ if (typeof deffered === 'number' ? deffered > 0 : deffered) {
89
+ this.cancelDefferedRegister = onPageReady(register, typeof deffered === 'number' ? { timeout: deffered } : undefined);
90
+ }
91
+ else {
92
+ void register();
93
+ }
94
+ }
95
+ unregister() {
96
+ if (this.cancelDefferedRegister)
97
+ this.cancelDefferedRegister();
98
+ this.registration?.unregister().catch((error) => {
99
+ const nextError = new Error('Error during service worker unregister', { cause: error });
100
+ // this.options.logger.error(this.logPrefix, getErrorMessage(nextError));
101
+ this.emit('error', { error: nextError });
102
+ });
103
+ }
104
+ destroy() {
105
+ if (this.cancelDefferedRegister)
106
+ this.cancelDefferedRegister();
107
+ this.removeAllListeners();
108
+ }
109
+ [Symbol.dispose]() {
110
+ this.destroy();
111
+ }
112
+ }
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="webworker" preserve="true" />
2
2
  export declare function isLocalhost(hostname?: string): boolean;
3
3
  /** Delete all caches that aren't named in `caches`. */
4
- export declare function removeUnknownCaches<T extends Record<string, string>>(expectedCaches: T): Promise<unknown>;
4
+ export declare function removeUnknownCaches(expectedCaches: Record<string, string>): Promise<unknown>;
5
5
  export declare function addResourcesToCache(cacheName: string, resources: readonly RequestInfo[]): Promise<void>;
6
6
  interface CacheFirstOptions extends Pick<FetchEvent, 'request'> {
7
7
  readonly fallbackUrl?: string | undefined;
@@ -1 +1,86 @@
1
- import{getErrorMessage}from"@js-toolkit/utils/getErrorMessage";export function isLocalhost(e=window.location.hostname){return!("localhost"!==e&&"[::1]"!==e&&!e.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/))}export function removeUnknownCaches(e){return caches.keys().then(r=>{const o=new Set(Object.values(e));return Promise.all(r.map(e=>{if(!o.has(e))return caches.delete(e)}))})}export async function addResourcesToCache(e,r){const o=await caches.open(e);await o.addAll(r)}export async function cacheFirst(e,{request:r,fallbackUrl:o,saveToCache:t,logger:n=console}){const s=await caches.open(e),c=await s.match(r);if(c)return n.debug("Found response in cache:",c),c;n.debug("No response for %s found in cache. About to fetch from network...",r.url);try{const e=await fetch(r.clone());return n.debug("Response for %s from network is: %O",r.url,e),e.status<400&&(!t||t({request:r,response:e}))?(n.debug("Caching the response to",r.url),s.put(r,e.clone()).catch(e=>{n.error(getErrorMessage(new Error(`Caching error of ${r.url}`,{cause:e})))})):n.debug("Not caching the response to",r.url),e}catch(e){const r=o&&await s.match(o);if(r)return r;throw n.error("Error in fetch handler:",e),e}}
1
+ /// <reference lib="webworker" preserve="true" />
2
+ import { getErrorMessage } from '@js-toolkit/utils/getErrorMessage';
3
+ export function isLocalhost(hostname = window.location.hostname) {
4
+ return !!(hostname === 'localhost' ||
5
+ // [::1] is the IPv6 localhost address.
6
+ hostname === '[::1]' ||
7
+ // 127.0.0.1/8 is considered localhost for IPv4.
8
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/.exec(hostname));
9
+ }
10
+ /** Delete all caches that aren't named in `caches`. */
11
+ export function removeUnknownCaches(expectedCaches) {
12
+ return caches.keys().then((cacheNames) => {
13
+ const expectedCacheNamesSet = new Set(Object.values(expectedCaches));
14
+ return Promise.all(
15
+ // eslint-disable-next-line @typescript-eslint/await-thenable
16
+ cacheNames.map((cacheName) => {
17
+ if (!expectedCacheNamesSet.has(cacheName)) {
18
+ // If this cache name isn't present in the set of "expected" cache names, then delete it.
19
+ // logger.debug('Deleting out of date cache:', cacheName);
20
+ return caches.delete(cacheName);
21
+ }
22
+ return undefined;
23
+ }));
24
+ });
25
+ }
26
+ export async function addResourcesToCache(cacheName, resources) {
27
+ const cache = await caches.open(cacheName);
28
+ await cache.addAll(resources);
29
+ }
30
+ export async function cacheFirst(cacheName, { request, fallbackUrl, saveToCache, logger = console }) {
31
+ // First try to get the resource from the cache
32
+ const cache = await caches.open(cacheName);
33
+ const responseFromCache = await cache.match(request);
34
+ if (responseFromCache) {
35
+ logger.debug('Found response in cache:', responseFromCache);
36
+ return responseFromCache;
37
+ }
38
+ // Otherwise, if there is no entry in the cache for `request`, response will be undefined,
39
+ // and we need to fetch() the resource.
40
+ logger.debug('No response for %s found in cache. About to fetch from network...', request.url);
41
+ // Next try to use the preloaded response, if it's there
42
+ // NOTE: Chrome throws errors regarding preloadResponse, see:
43
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1420515
44
+ // https://github.com/mdn/dom-examples/issues/145
45
+ // To avoid those errors, remove or comment out this block of preloadResponse
46
+ // code along with enableNavigationPreload() and the "activate" listener.
47
+ // const preloadResponseValue = await preloadResponse;
48
+ // if (preloadResponseValue) {
49
+ // logger.info('using preload response', preloadResponseValue);
50
+ // putInCache(request, preloadResponseValue.clone());
51
+ // return preloadResponseValue;
52
+ // }
53
+ // Next try to get the resource from the network
54
+ // We call .clone() on the request since we might use it in a call to cache.put() later on.
55
+ // Both fetch() and cache.put() "consume" the request, so we need to make a copy.
56
+ // (see https://fetch.spec.whatwg.org/#dom-request-clone)
57
+ try {
58
+ const response = await fetch(request.clone());
59
+ logger.debug('Response for %s from network is: %O', request.url, response);
60
+ // This avoids caching responses that we know are errors (i.e. HTTP status code of 4xx or 5xx).
61
+ if (response.status < 400 && (!saveToCache || saveToCache({ request, response }))) {
62
+ // We call .clone() on the response to save a copy of it to the cache. By doing so, we get to keep
63
+ // the original response object which we will return back to the controlled page.
64
+ // (see https://fetch.spec.whatwg.org/#dom-response-clone)
65
+ logger.debug('Caching the response to', request.url);
66
+ cache.put(request, response.clone()).catch((error) => {
67
+ logger.error(getErrorMessage(new Error(`Caching error of ${request.url}`, { cause: error })));
68
+ });
69
+ }
70
+ else {
71
+ logger.debug('Not caching the response to', request.url);
72
+ }
73
+ return response;
74
+ }
75
+ catch (error) {
76
+ const fallbackResponse = fallbackUrl ? await cache.match(fallbackUrl) : undefined;
77
+ if (fallbackResponse) {
78
+ return fallbackResponse;
79
+ }
80
+ // This catch() will handle exceptions that arise from the match() or fetch() operations.
81
+ // Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
82
+ // It will return a normal response object that has the appropriate error code set.
83
+ logger.error('Error in fetch handler:', error);
84
+ throw error;
85
+ }
86
+ }
@@ -1,3 +1,3 @@
1
- export declare function stopPropagation<T extends {
1
+ export declare function stopPropagation(event: {
2
2
  stopPropagation: VoidFunction;
3
- }>(event: T): void;
3
+ }): void;
@@ -1 +1,3 @@
1
- export function stopPropagation(o){o.stopPropagation()}
1
+ export function stopPropagation(event) {
2
+ event.stopPropagation();
3
+ }
package/takeScreenshot.js CHANGED
@@ -1 +1,51 @@
1
- import{hasIn}from"@js-toolkit/utils/hasIn";export function get2dContextError(){return new Error("Failed to get canvas 2d context.")}const getDefaultWidth=t=>t instanceof HTMLVideoElement?t.videoWidth:hasIn(t,"displayWidth")?t.displayWidth:t.width instanceof SVGAnimatedLength?t.width.animVal.value:t.width,getDefaultHeight=t=>t instanceof HTMLVideoElement?t.videoHeight:hasIn(t,"displayHeight")?t.displayHeight:t.height instanceof SVGAnimatedLength?t.height.animVal.value:t.height;export function takeScreenshot(t,{width:e=getDefaultWidth(t),height:i=getDefaultHeight(t),type:n="image/jpeg",quality:a=1}={}){const o=document.createElement("canvas");o.width=e,o.height=i;const h=o.getContext("2d");if(!h)throw get2dContextError();return h.drawImage(t,0,0),o.toDataURL(n,a)}export function takeScreenshotAsync(t,{width:e=getDefaultWidth(t),height:i=getDefaultHeight(t),type:n="image/jpeg",quality:a=1}={}){return new Promise((o,h)=>{const r=document.createElement("canvas");r.width=e,r.height=i;const d=r.getContext("2d");if(!d)throw get2dContextError();d.drawImage(t,0,0),r.toBlob(t=>{null==t?h(new Error("Unable get blob")):o(t)},n,a)})}
1
+ import { hasIn } from '@js-toolkit/utils/hasIn';
2
+ export function get2dContextError() {
3
+ return new Error('Failed to get canvas 2d context.');
4
+ }
5
+ const getDefaultWidth = (element) => {
6
+ if (element instanceof HTMLVideoElement) {
7
+ return element.videoWidth;
8
+ }
9
+ if (hasIn(element, 'displayWidth')) {
10
+ return element.displayWidth;
11
+ }
12
+ return element.width instanceof SVGAnimatedLength ? element.width.animVal.value : element.width;
13
+ };
14
+ const getDefaultHeight = (element) => {
15
+ if (element instanceof HTMLVideoElement) {
16
+ return element.videoHeight;
17
+ }
18
+ if (hasIn(element, 'displayHeight')) {
19
+ return element.displayHeight;
20
+ }
21
+ return element.height instanceof SVGAnimatedLength
22
+ ? element.height.animVal.value
23
+ : element.height;
24
+ };
25
+ export function takeScreenshot(element, { width = getDefaultWidth(element), height = getDefaultHeight(element), type = 'image/jpeg', quality = 1, } = {}) {
26
+ const canvas = document.createElement('canvas');
27
+ canvas.width = width;
28
+ canvas.height = height;
29
+ const ctx = canvas.getContext('2d');
30
+ if (!ctx)
31
+ throw get2dContextError();
32
+ ctx.drawImage(element, 0, 0);
33
+ return canvas.toDataURL(type, quality);
34
+ }
35
+ export function takeScreenshotAsync(element, { width = getDefaultWidth(element), height = getDefaultHeight(element), type = 'image/jpeg', quality = 1, } = {}) {
36
+ return new Promise((resolve, reject) => {
37
+ const canvas = document.createElement('canvas');
38
+ canvas.width = width;
39
+ canvas.height = height;
40
+ const ctx = canvas.getContext('2d');
41
+ if (!ctx)
42
+ throw get2dContextError();
43
+ ctx.drawImage(element, 0, 0);
44
+ canvas.toBlob((blob) => {
45
+ if (blob == null)
46
+ reject(new Error('Unable get blob'));
47
+ else
48
+ resolve(blob);
49
+ }, type, quality);
50
+ });
51
+ }
package/toBase64.js CHANGED
@@ -1 +1,9 @@
1
- export function toBase64(o){const n=window.TextEncoder?Array.from((new TextEncoder).encode(o),o=>String.fromCodePoint(o)).join(""):unescape(encodeURIComponent(o));return window.btoa(n)}
1
+ /* eslint-disable @typescript-eslint/no-deprecated */
2
+ /* eslint-disable @typescript-eslint/strict-boolean-expressions */
3
+ /** Safe for unicode string. */
4
+ export function toBase64(str) {
5
+ const binString = window.TextEncoder
6
+ ? Array.from(new TextEncoder().encode(str), (byte) => String.fromCodePoint(byte)).join('')
7
+ : unescape(encodeURIComponent(str));
8
+ return window.btoa(binString);
9
+ }
package/toLocalPoint.js CHANGED
@@ -1 +1,9 @@
1
- export function toLocalPoint(t,n){const e="tagName"in n?n.getBoundingClientRect():n;return{x:t.clientX-e.left,y:t.clientY-e.top}}
1
+ export function toLocalPoint(coord, targetOrClientRect) {
2
+ const rect = 'tagName' in targetOrClientRect
3
+ ? targetOrClientRect.getBoundingClientRect()
4
+ : targetOrClientRect;
5
+ return {
6
+ x: coord.clientX - rect.left,
7
+ y: coord.clientY - rect.top,
8
+ };
9
+ }
package/types/index.js CHANGED
@@ -1 +1,2 @@
1
- "use strict";
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-unused-vars */
package/types/refs.js CHANGED
@@ -1 +1 @@
1
- import"@js-toolkit/utils/types";
1
+ import '@js-toolkit/utils/types';
@@ -1 +1,69 @@
1
- import throttleFn from"lodash.throttle";export function getViewableTracker(t,{visiblePart:e=.8,scrollThrottle:i=200,documentVisibility:n=!0,onChange:o}){const s=Math.min(+e,1);let r,l=0,c="visible"===document.visibilityState;const a=()=>{if(n&&"visible"!==document.visibilityState)return;const{top:e,bottom:i,height:l}=t.getBoundingClientRect(),c=l*s,a=e+c,d=window.innerHeight>=a&&i>c;r!==d&&(r=d,o(d))},d=()=>{const t="visible"===document.visibilityState;c!==t&&(c=t,t?(r=void 0,a()):r!==t&&o(t))},v=i&&i>0?throttleFn(a,i):()=>{n&&"visible"!==document.visibilityState||(l=window.requestAnimationFrame(a))};window.addEventListener("scroll",v,{capture:!1,passive:!0}),n&&document.addEventListener("visibilitychange",d);return{check:()=>{a(),n&&d()},destroy:()=>{cancelAnimationFrame(l),window.removeEventListener("scroll",v,{capture:!1}),document.removeEventListener("visibilitychange",d)},[Symbol.dispose](){this.destroy()}}}
1
+ import throttleFn from 'lodash.throttle';
2
+ export function getViewableTracker(element, { visiblePart: visiblePartOption = 0.8, scrollThrottle = 200, documentVisibility = true, onChange, }) {
3
+ const visiblePart = Math.min(+visiblePartOption, 1);
4
+ // const visiblePart = +visiblePartOption;
5
+ let raf = 0;
6
+ let lastViewable;
7
+ let lastDocumentViewable = document.visibilityState === 'visible';
8
+ const checkVisibility = () => {
9
+ if (documentVisibility && document.visibilityState !== 'visible')
10
+ return;
11
+ const { top, bottom, height } = element.getBoundingClientRect();
12
+ const visibleHeight = height * visiblePart;
13
+ const bottomPos = top + visibleHeight;
14
+ const viewable = window.innerHeight >= bottomPos && bottom > visibleHeight;
15
+ // console.log(bottomPos, top, bottom, visibleHeight, viewable);
16
+ if (lastViewable !== viewable) {
17
+ lastViewable = viewable;
18
+ onChange(viewable);
19
+ }
20
+ };
21
+ const checkVisibilityRaf = () => {
22
+ if (documentVisibility && document.visibilityState !== 'visible')
23
+ return;
24
+ raf = window.requestAnimationFrame(checkVisibility);
25
+ };
26
+ const checkDocumentVisibility = () => {
27
+ const viewable = document.visibilityState === 'visible';
28
+ if (lastDocumentViewable === viewable)
29
+ return;
30
+ lastDocumentViewable = viewable;
31
+ // Check if visible on page
32
+ if (viewable /* && visiblePart > 0 */) {
33
+ // Always re-check, the page size may be changed or something else.
34
+ lastViewable = undefined;
35
+ checkVisibility();
36
+ }
37
+ // Hidden tab
38
+ else if ( /* visiblePart <= 0 || */lastViewable !== viewable) {
39
+ onChange(viewable);
40
+ }
41
+ };
42
+ const handler = scrollThrottle > 0 ? throttleFn(checkVisibility, scrollThrottle) : checkVisibilityRaf;
43
+ // if (visiblePart > 0) {
44
+ window.addEventListener('scroll', handler, { capture: false, passive: true });
45
+ // }
46
+ if (documentVisibility) {
47
+ document.addEventListener('visibilitychange', checkDocumentVisibility);
48
+ }
49
+ const check = () => {
50
+ // if (visiblePart > 0) {
51
+ checkVisibility();
52
+ // }
53
+ if (documentVisibility) {
54
+ checkDocumentVisibility();
55
+ }
56
+ };
57
+ const destroy = () => {
58
+ cancelAnimationFrame(raf);
59
+ window.removeEventListener('scroll', handler, { capture: false });
60
+ document.removeEventListener('visibilitychange', checkDocumentVisibility);
61
+ };
62
+ return {
63
+ check,
64
+ destroy,
65
+ [Symbol.dispose]() {
66
+ this.destroy();
67
+ },
68
+ };
69
+ }
@@ -1 +1,212 @@
1
- import{EventEmitter}from"@js-toolkit/utils/EventEmitter";import{getErrorMessage}from"@js-toolkit/utils/getErrorMessage";import{hasIn}from"@js-toolkit/utils/hasIn";import log from"@js-toolkit/utils/log";import*as sdpUtils from"./sdputils";export class PeerConnection extends EventEmitter{options;get Events(){return PeerConnection.Events}logger;pc;constructor(e={}){super(),this.options=e,this.logger=e.logger??log.getLogger("PeerConnection"),this.pc=this.createPC()}clear(){this.pc.onsignalingstatechange=null,this.pc.onconnectionstatechange=null,this.pc.oniceconnectionstatechange=null,this.pc.ontrack=null,this.pc.onnegotiationneeded=null,this.pc.onicecandidate=null,this.pc.onicecandidateerror=null}createPC(){const e=new RTCPeerConnection(this.options.rtc);e.onsignalingstatechange=()=>{this.logger.debug(`Signaling state changed to: ${e.signalingState}`),"closed"===e.signalingState&&(this.emit(this.Events.Closed),this.clear())};const t=e=>{this.logger.debug(`Connection state changed to: ${e}`),"connected"===e?this.emit(this.Events.Connected):"disconnected"===e&&this.emit(this.Events.Disconnected)};return hasIn(RTCPeerConnection.prototype,"onconnectionstatechange")?e.onconnectionstatechange=()=>t(e.connectionState):e.oniceconnectionstatechange=()=>t(e.iceConnectionState),e.ontrack=({streams:e,track:t})=>{if(this.logger.debug("Remote stream received.",e.length,t.kind),0===e.length)return;const[n]=e;n.onremovetrack=()=>{this.logger.debug("onremovetrack"),this.emit(this.Events.RemoteStreamChanged,n)},n.onaddtrack=()=>{this.logger.debug("onaddtrack"),this.emit(this.Events.RemoteStreamChanged,n)},this.emit(this.Events.RemoteStreamChanged,n)},e.onnegotiationneeded=()=>{const{iceConnectionState:t}=e;"connected"!==t&&"completed"!==t||(this.logger.debug("Reinitializing connection..."),this.emit(this.Events.ReinitializingConnectionRequired))},e.onicecandidate=({candidate:e})=>{e?sdpUtils.isValidIceCandidate(e)&&this.emit(this.Events.LocalIceCandidate,e):this.emit(this.Events.EndOfIceCandidates)},e.onicecandidateerror=e=>{if(e instanceof RTCPeerConnectionIceErrorEvent){const{errorCode:t,errorText:n,hostCandidate:i,url:o}=e;this.logger.warn(`ICE candidate error: errorCode=${t}, errorText=${n}, hostCandidate=${i}, url=${o}`)}else this.logger.warn(`ICE candidate error: ${getErrorMessage(e)}`)},e}async setLocalDescription(e,t){const n=sdpUtils.prepareLocalDescription(t,this.options.codecs);return await e.setLocalDescription(n),n}async setRemoteDescription(e,t){const n=sdpUtils.prepareRemoteDescription(t,this.options.codecs);return await e.setRemoteDescription(n),n}isConnected(){return"connected"===this.pc.iceConnectionState||"completed"===this.pc.iceConnectionState}isClosed(){return"closed"===this.pc.signalingState}addIceCandidate(e){return this.pc.addIceCandidate(e)}attachStream(e){const t=this.pc.getSenders(),n=t.length?e.getTracks().filter(e=>!t.find(({track:t})=>!!t&&t.id===e.id)):e.getTracks();n.length?(n.forEach(t=>this.pc.addTrack(t,e)),this.logger.debug(`Attached ${n.length} track(s) to a peer connection.`)):this.logger.debug("No tracks to attach to a peer connection.")}reattachStream(e){const t=this.pc.iceConnectionState;if("new"!==t&&"connected"!==t&&"completed"!==t)return;const n=e?e.getTracks():[];this.pc.getSenders().forEach(e=>{const{track:t}=e;!t||"ended"!==t.readyState&&n.find(e=>e.id===t.id)||(this.pc.removeTrack(e),this.logger.debug(`'${t&&t.kind}' track is removed from a peer connection.`))}),e&&this.attachStream(e)}async createOffer(){const e=await this.pc.createOffer(this.options.offerOptions);return this.setLocalDescription(this.pc,e)}async createAnswer(e){await this.setRemoteDescription(this.pc,e);const t=await this.pc.createAnswer(this.options.offerOptions);return this.setLocalDescription(this.pc,t)}applyAnswer(e){return this.setRemoteDescription(this.pc,e)}reconnect(){this.close(),this.pc=this.createPC()}close(){this.pc.close(),this.pc.onsignalingstatechange&&(this.emit(this.Events.Closed),this.clear())}destroy(){this.close(),this.removeAllListeners()}[Symbol.dispose](){this.destroy()}}!function(e){let t;!function(e){e.LocalIceCandidate="LocalIceCandidate",e.EndOfIceCandidates="EndOfIceCandidates",e.Connected="Connected",e.Disconnected="Disconnected",e.RemoteStreamChanged="RemoteStreamChanged",e.ReinitializingConnectionRequired="ReinitializingConnectionRequired",e.Closed="Closed"}(t=e.Events||(e.Events={}))}(PeerConnection||(PeerConnection={}));
1
+ import { EventEmitter } from '@js-toolkit/utils/EventEmitter';
2
+ import { getErrorMessage } from '@js-toolkit/utils/getErrorMessage';
3
+ import { hasIn } from '@js-toolkit/utils/hasIn';
4
+ import log from '@js-toolkit/utils/log';
5
+ import * as sdpUtils from './sdputils';
6
+ export class PeerConnection extends EventEmitter {
7
+ options;
8
+ // eslint-disable-next-line class-methods-use-this
9
+ get Events() {
10
+ return PeerConnection.Events;
11
+ }
12
+ logger;
13
+ pc;
14
+ constructor(options = {}) {
15
+ super();
16
+ this.options = options;
17
+ this.logger = options.logger ?? log.getLogger('PeerConnection');
18
+ this.pc = this.createPC();
19
+ }
20
+ clear() {
21
+ this.pc.onsignalingstatechange = null;
22
+ this.pc.onconnectionstatechange = null;
23
+ this.pc.oniceconnectionstatechange = null;
24
+ this.pc.ontrack = null;
25
+ this.pc.onnegotiationneeded = null;
26
+ this.pc.onicecandidate = null;
27
+ this.pc.onicecandidateerror = null;
28
+ }
29
+ createPC() {
30
+ const pc = new RTCPeerConnection(this.options.rtc);
31
+ pc.onsignalingstatechange = () => {
32
+ this.logger.debug(`Signaling state changed to: ${pc.signalingState}`);
33
+ if (pc.signalingState === 'closed') {
34
+ this.emit(this.Events.Closed);
35
+ this.clear();
36
+ }
37
+ };
38
+ const handleConnectionStateChange = (state) => {
39
+ this.logger.debug(`Connection state changed to: ${state}`);
40
+ if (state === 'connected')
41
+ this.emit(this.Events.Connected);
42
+ else if (state === 'disconnected')
43
+ this.emit(this.Events.Disconnected);
44
+ };
45
+ if (hasIn(RTCPeerConnection.prototype, 'onconnectionstatechange')) {
46
+ pc.onconnectionstatechange = () => handleConnectionStateChange(pc.connectionState);
47
+ }
48
+ else {
49
+ pc.oniceconnectionstatechange = () => handleConnectionStateChange(pc.iceConnectionState);
50
+ }
51
+ // All tracks must be in one stream for controlling adding/removing tracks.
52
+ pc.ontrack = ({ streams, track }) => {
53
+ this.logger.debug('Remote stream received.', streams.length, track.kind);
54
+ if (streams.length === 0)
55
+ return;
56
+ const [stream] = streams;
57
+ stream.onremovetrack = () => {
58
+ this.logger.debug('onremovetrack');
59
+ this.emit(this.Events.RemoteStreamChanged, stream);
60
+ };
61
+ stream.onaddtrack = () => {
62
+ this.logger.debug('onaddtrack');
63
+ this.emit(this.Events.RemoteStreamChanged, stream);
64
+ };
65
+ // stream.oninactive = () => {
66
+ // this.logger.debug('oninactive');
67
+ // };
68
+ this.emit(this.Events.RemoteStreamChanged, stream);
69
+ };
70
+ pc.onnegotiationneeded = () => {
71
+ // When media stream is changed we should re-communicate.
72
+ const { iceConnectionState: state } = pc;
73
+ if (state === 'connected' || state === 'completed') {
74
+ this.logger.debug('Reinitializing connection...');
75
+ this.emit(this.Events.ReinitializingConnectionRequired);
76
+ }
77
+ };
78
+ pc.onicecandidate = ({ candidate }) => {
79
+ if (!candidate) {
80
+ // End of candidates.
81
+ this.emit(this.Events.EndOfIceCandidates);
82
+ return;
83
+ }
84
+ if (!sdpUtils.isValidIceCandidate(candidate))
85
+ return;
86
+ this.emit(this.Events.LocalIceCandidate, candidate);
87
+ // this.logger.debug(`New ICE candidate: ${JSON.stringify(candidate)}`);
88
+ };
89
+ pc.onicecandidateerror = (event) => {
90
+ if (event instanceof RTCPeerConnectionIceErrorEvent) {
91
+ const { errorCode, errorText, hostCandidate, url } = event;
92
+ this.logger.warn(`ICE candidate error: errorCode=${errorCode}, errorText=${errorText}, hostCandidate=${hostCandidate}, url=${url}`);
93
+ }
94
+ else {
95
+ this.logger.warn(`ICE candidate error: ${getErrorMessage(event)}`);
96
+ }
97
+ };
98
+ return pc;
99
+ }
100
+ async setLocalDescription(pc, desc) {
101
+ const newDesc = sdpUtils.prepareLocalDescription(desc, this.options.codecs);
102
+ await pc.setLocalDescription(newDesc);
103
+ return newDesc;
104
+ }
105
+ async setRemoteDescription(pc, desc) {
106
+ const newDesc = sdpUtils.prepareRemoteDescription(desc, this.options.codecs);
107
+ await pc.setRemoteDescription(newDesc);
108
+ return newDesc;
109
+ }
110
+ isConnected() {
111
+ return this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed';
112
+ }
113
+ isClosed() {
114
+ return this.pc.signalingState === 'closed';
115
+ }
116
+ /** Add icecandidate when the clients is exchanging icecandidates. */
117
+ addIceCandidate(candidate) {
118
+ return this.pc.addIceCandidate(candidate);
119
+ }
120
+ /** Add stream to the remote peer if needed. */
121
+ attachStream(stream) {
122
+ const senders = this.pc.getSenders();
123
+ // Get tracks which not existed in peer connection.
124
+ const newTracks = senders.length > 0
125
+ ? stream
126
+ .getTracks()
127
+ .filter((t) => !senders.find(({ track }) => !!track && track.id === t.id))
128
+ : stream.getTracks();
129
+ // If connection has already attached tracks then not possible attach tracks again.
130
+ if (newTracks.length === 0) {
131
+ this.logger.debug('No tracks to attach to a peer connection.');
132
+ return;
133
+ }
134
+ newTracks.forEach((track) => this.pc.addTrack(track, stream));
135
+ this.logger.debug(`Attached ${newTracks.length} track(s) to a peer connection.`);
136
+ }
137
+ /**
138
+ * Remove ended tracks and unknown tracks and invoke `attachStream`.
139
+ * If stream is not passed then all tracks will be removed.
140
+ */
141
+ reattachStream(stream) {
142
+ // Reattach only if connected or just created
143
+ const state = this.pc.iceConnectionState;
144
+ if (state !== 'new' && state !== 'connected' && state !== 'completed')
145
+ return;
146
+ // Remove diff tracks from connection
147
+ // If do not remove tracks then they are piling up on the remote client (in received stream).
148
+ // The removing track must by in `ended` readyState.
149
+ const tracks = stream ? stream.getTracks() : [];
150
+ this.pc.getSenders().forEach((s) => {
151
+ const { track } = s;
152
+ if (track && (track.readyState === 'ended' || !tracks.find((_) => _.id === track.id))) {
153
+ this.pc.removeTrack(s);
154
+ this.logger.debug(`'${track.kind}' track is removed from a peer connection.`);
155
+ }
156
+ });
157
+ if (stream)
158
+ this.attachStream(stream);
159
+ }
160
+ /** A call to establish a connection. */
161
+ async createOffer() {
162
+ const desc = await this.pc.createOffer(this.options.offerOptions);
163
+ // this.logger.debug(`Offer created with: ${JSON.stringify(options)}`);
164
+ return this.setLocalDescription(this.pc, desc);
165
+ }
166
+ /** A call from another client which received an offer. */
167
+ async createAnswer(offer) {
168
+ await this.setRemoteDescription(this.pc, offer);
169
+ const desc = await this.pc.createAnswer(this.options.offerOptions);
170
+ return this.setLocalDescription(this.pc, desc);
171
+ }
172
+ /** Apply answer from another client after calling `createOffer`. */
173
+ applyAnswer(remoteDesc) {
174
+ return this.setRemoteDescription(this.pc, remoteDesc);
175
+ }
176
+ reconnect() {
177
+ this.close();
178
+ this.pc = this.createPC();
179
+ }
180
+ /** After calling this method the connection can no longer be used unless `reconnect` will be called. */
181
+ close() {
182
+ this.pc.close();
183
+ // The event is not fired if close manually
184
+ if (this.pc.onsignalingstatechange) {
185
+ this.emit(this.Events.Closed);
186
+ this.clear();
187
+ }
188
+ }
189
+ destroy() {
190
+ this.close();
191
+ this.removeAllListeners();
192
+ }
193
+ [Symbol.dispose]() {
194
+ this.destroy();
195
+ }
196
+ }
197
+ // eslint-disable-next-line @typescript-eslint/no-namespace
198
+ (function (PeerConnection) {
199
+ let Events;
200
+ (function (Events) {
201
+ /** IceCandidate for exchanging. */
202
+ Events["LocalIceCandidate"] = "LocalIceCandidate";
203
+ Events["EndOfIceCandidates"] = "EndOfIceCandidates";
204
+ /** The connection is established. */
205
+ Events["Connected"] = "Connected";
206
+ /** The remote client closed a connection or connection is failed. */
207
+ Events["Disconnected"] = "Disconnected";
208
+ Events["RemoteStreamChanged"] = "RemoteStreamChanged";
209
+ Events["ReinitializingConnectionRequired"] = "ReinitializingConnectionRequired";
210
+ Events["Closed"] = "Closed";
211
+ })(Events = PeerConnection.Events || (PeerConnection.Events = {}));
212
+ })(PeerConnection || (PeerConnection = {}));