@naturalcycles/js-lib 14.256.0 → 14.258.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.
- package/cfg/frontend/tsconfig.json +67 -0
- package/dist/browser/adminService.d.ts +69 -0
- package/dist/browser/adminService.js +98 -0
- package/dist/browser/analytics.util.d.ts +12 -0
- package/dist/browser/analytics.util.js +59 -0
- package/dist/browser/i18n/fetchTranslationLoader.d.ts +13 -0
- package/dist/browser/i18n/fetchTranslationLoader.js +17 -0
- package/dist/browser/i18n/translation.service.d.ts +53 -0
- package/dist/browser/i18n/translation.service.js +61 -0
- package/dist/browser/imageFitter.d.ts +60 -0
- package/dist/browser/imageFitter.js +69 -0
- package/dist/browser/script.util.d.ts +14 -0
- package/dist/browser/script.util.js +50 -0
- package/dist/browser/topbar.d.ts +23 -0
- package/dist/browser/topbar.js +137 -0
- package/dist/decorators/memo.util.d.ts +2 -1
- package/dist/decorators/memo.util.js +8 -6
- package/dist/decorators/swarmSafe.decorator.d.ts +9 -0
- package/dist/decorators/swarmSafe.decorator.js +42 -0
- package/dist/deviceIdService.d.ts +65 -0
- package/dist/deviceIdService.js +109 -0
- package/dist/error/assert.d.ts +2 -1
- package/dist/error/assert.js +15 -13
- package/dist/error/error.util.js +9 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/nanoid.d.ts +7 -0
- package/dist/nanoid.js +61 -0
- package/dist/number/createDeterministicRandom.d.ts +6 -1
- package/dist/number/createDeterministicRandom.js +1 -2
- package/dist/string/hash.util.d.ts +1 -1
- package/dist/string/hash.util.js +1 -1
- package/dist/web.d.ts +6 -0
- package/dist/web.js +6 -0
- package/dist/zod/zod.util.d.ts +1 -1
- package/dist-esm/browser/adminService.js +94 -0
- package/dist-esm/browser/analytics.util.js +54 -0
- package/dist-esm/browser/i18n/fetchTranslationLoader.js +13 -0
- package/dist-esm/browser/i18n/translation.service.js +56 -0
- package/dist-esm/browser/imageFitter.js +65 -0
- package/dist-esm/browser/script.util.js +46 -0
- package/dist-esm/browser/topbar.js +134 -0
- package/dist-esm/decorators/memo.util.js +3 -1
- package/dist-esm/decorators/swarmSafe.decorator.js +38 -0
- package/dist-esm/deviceIdService.js +105 -0
- package/dist-esm/error/assert.js +3 -1
- package/dist-esm/error/error.util.js +4 -1
- package/dist-esm/index.js +9 -0
- package/dist-esm/nanoid.js +57 -0
- package/dist-esm/number/createDeterministicRandom.js +1 -2
- package/dist-esm/string/hash.util.js +1 -1
- package/dist-esm/web.js +6 -0
- package/package.json +2 -1
- package/src/browser/adminService.ts +157 -0
- package/src/browser/analytics.util.ts +68 -0
- package/src/browser/i18n/fetchTranslationLoader.ts +16 -0
- package/src/browser/i18n/translation.service.ts +102 -0
- package/src/browser/imageFitter.ts +128 -0
- package/src/browser/script.util.ts +52 -0
- package/src/browser/topbar.ts +147 -0
- package/src/datetime/localDate.ts +16 -0
- package/src/datetime/localTime.ts +39 -0
- package/src/decorators/debounce.ts +1 -0
- package/src/decorators/memo.util.ts +4 -1
- package/src/decorators/swarmSafe.decorator.ts +47 -0
- package/src/deviceIdService.ts +137 -0
- package/src/error/assert.ts +5 -11
- package/src/error/error.util.ts +4 -1
- package/src/index.ts +9 -0
- package/src/json-schema/jsonSchemaBuilder.ts +20 -0
- package/src/nanoid.ts +79 -0
- package/src/number/createDeterministicRandom.ts +7 -2
- package/src/semver.ts +2 -0
- package/src/string/hash.util.ts +1 -1
- package/src/web.ts +6 -0
- package/src/zod/zod.util.ts +1 -1
package/dist/nanoid.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Vendored from https://github.com/ai/nanoid/blob/main/index.browser.js
|
|
3
|
+
// All credit to nanoid authors: https://github.com/ai/nanoid
|
|
4
|
+
// Reason for vendoring: (still) cannot import esm, and Nanoid went ESM-only since 4.0
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.nanoidBrowser = nanoidBrowser;
|
|
7
|
+
exports.nanoidBrowserCustomAlphabet = nanoidBrowserCustomAlphabet;
|
|
8
|
+
/// <reference lib="dom" preserve="true" />
|
|
9
|
+
/* eslint-disable no-bitwise */
|
|
10
|
+
// "0-9a-zA-Z-_", same as base64url alphabet
|
|
11
|
+
const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
|
|
12
|
+
function nanoidBrowser(length = 21) {
|
|
13
|
+
let id = '';
|
|
14
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(length));
|
|
15
|
+
while (length--) {
|
|
16
|
+
// Using the bitwise AND operator to "cap" the value of
|
|
17
|
+
// the random byte from 255 to 63, in that way we can make sure
|
|
18
|
+
// that the value will be a valid index for the "chars" string.
|
|
19
|
+
id += urlAlphabet[bytes[length] & 63];
|
|
20
|
+
}
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
const defaultRandomFunction = (bytes) => globalThis.crypto.getRandomValues(new Uint8Array(bytes));
|
|
24
|
+
function nanoidBrowserCustomAlphabet(alphabet, length = 21) {
|
|
25
|
+
return customRandom(alphabet, length, defaultRandomFunction);
|
|
26
|
+
}
|
|
27
|
+
function customRandom(alphabet, defaultSize, getRandom) {
|
|
28
|
+
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
|
|
29
|
+
// values closer to the alphabet size. The bitmask calculates the closest
|
|
30
|
+
// `2^31 - 1` number, which exceeds the alphabet size.
|
|
31
|
+
// For example, the bitmask for the alphabet size 30 is 31 (00011111).
|
|
32
|
+
// `Math.clz32` is not used, because it is not available in browsers.
|
|
33
|
+
const mask = (2 << Math.log2(alphabet.length - 1)) - 1;
|
|
34
|
+
// Though, the bitmask solution is not perfect since the bytes exceeding
|
|
35
|
+
// the alphabet size are refused. Therefore, to reliably generate the ID,
|
|
36
|
+
// the random bytes redundancy has to be satisfied.
|
|
37
|
+
// Note: every hardware random generator call is performance expensive,
|
|
38
|
+
// because the system call for entropy collection takes a lot of time.
|
|
39
|
+
// So, to avoid additional system calls, extra bytes are requested in advance.
|
|
40
|
+
// Next, a step determines how many random bytes to generate.
|
|
41
|
+
// The number of random bytes gets decided upon the ID size, mask,
|
|
42
|
+
// alphabet size, and magic number 1.6 (using 1.6 peaks at performance
|
|
43
|
+
// according to benchmarks).
|
|
44
|
+
// `-~f => Math.ceil(f)` if f is a float
|
|
45
|
+
// `-~i => i + 1` if i is an integer
|
|
46
|
+
const step = -~((1.6 * mask * defaultSize) / alphabet.length);
|
|
47
|
+
return (size = defaultSize) => {
|
|
48
|
+
let id = '';
|
|
49
|
+
while (true) {
|
|
50
|
+
const bytes = getRandom(step);
|
|
51
|
+
// A compact alternative for `for (var i = 0; i < step; i++)`.
|
|
52
|
+
let j = step;
|
|
53
|
+
while (j--) {
|
|
54
|
+
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
|
|
55
|
+
id += alphabet[bytes[j] & mask] || '';
|
|
56
|
+
if (id.length === size)
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function that returns a random number between 0 and 1.
|
|
3
|
+
* Exactly same signature as Math.random function.
|
|
4
|
+
*/
|
|
5
|
+
export type RandomFunction = () => number;
|
|
1
6
|
/**
|
|
2
7
|
* Returns a "deterministic Math.random() function"
|
|
3
8
|
*
|
|
4
9
|
* Based on: https://gist.github.com/mathiasbynens/5670917
|
|
5
10
|
*/
|
|
6
|
-
export declare function _createDeterministicRandom():
|
|
11
|
+
export declare function _createDeterministicRandom(seed?: number): RandomFunction;
|
|
@@ -7,8 +7,7 @@ exports._createDeterministicRandom = _createDeterministicRandom;
|
|
|
7
7
|
*
|
|
8
8
|
* Based on: https://gist.github.com/mathiasbynens/5670917
|
|
9
9
|
*/
|
|
10
|
-
function _createDeterministicRandom() {
|
|
11
|
-
let seed = 0x2f6e2b1;
|
|
10
|
+
function _createDeterministicRandom(seed = 0x2f6e2b1) {
|
|
12
11
|
return () => {
|
|
13
12
|
// Robert Jenkins’ 32 bit integer hash function
|
|
14
13
|
seed = (seed + 0x7ed55d16 + (seed << 12)) & 0xffffffff;
|
|
@@ -6,7 +6,7 @@ import { Integer } from '../types';
|
|
|
6
6
|
*
|
|
7
7
|
* 1. Performance
|
|
8
8
|
* 2. For non-cryptographic use (where accidental collision is not the end-of-the-world)
|
|
9
|
-
* 3. Compact size (32 bits max, versus 128 in md5; presented in
|
|
9
|
+
* 3. Compact size (32 bits max, versus 128 in md5; presented in smaller number of string json-safe characters)
|
|
10
10
|
*
|
|
11
11
|
* Basically, these functions are as simple as they can be, but still "random enough" for
|
|
12
12
|
* normal non-cryptographic use cases.
|
package/dist/string/hash.util.js
CHANGED
|
@@ -14,7 +14,7 @@ const BASE64URL = BASE62 + '-_';
|
|
|
14
14
|
*
|
|
15
15
|
* 1. Performance
|
|
16
16
|
* 2. For non-cryptographic use (where accidental collision is not the end-of-the-world)
|
|
17
|
-
* 3. Compact size (32 bits max, versus 128 in md5; presented in
|
|
17
|
+
* 3. Compact size (32 bits max, versus 128 in md5; presented in smaller number of string json-safe characters)
|
|
18
18
|
*
|
|
19
19
|
* Basically, these functions are as simple as they can be, but still "random enough" for
|
|
20
20
|
* normal non-cryptographic use cases.
|
package/dist/web.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { StringMap } from './types';
|
|
|
5
5
|
* Implements WebStorage API by using in-memory storage.
|
|
6
6
|
* Can be useful in SSR environment or unit tests.
|
|
7
7
|
*
|
|
8
|
+
* This is how localStorage can be mocked in Node:
|
|
9
|
+
*
|
|
10
|
+
* Object.assign(globalThis, {
|
|
11
|
+
* localStorage: new InMemoryWebStorage(),
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
8
14
|
* @experimental
|
|
9
15
|
*/
|
|
10
16
|
export declare class InMemoryWebStorage implements Storage {
|
package/dist/web.js
CHANGED
|
@@ -7,6 +7,12 @@ exports.InMemoryWebStorage = void 0;
|
|
|
7
7
|
* Implements WebStorage API by using in-memory storage.
|
|
8
8
|
* Can be useful in SSR environment or unit tests.
|
|
9
9
|
*
|
|
10
|
+
* This is how localStorage can be mocked in Node:
|
|
11
|
+
*
|
|
12
|
+
* Object.assign(globalThis, {
|
|
13
|
+
* localStorage: new InMemoryWebStorage(),
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
10
16
|
* @experimental
|
|
11
17
|
*/
|
|
12
18
|
class InMemoryWebStorage {
|
package/dist/zod/zod.util.d.ts
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { _Memo } from '../decorators/memo.decorator';
|
|
3
|
+
import { isServerSide } from '../env';
|
|
4
|
+
import { _stringify } from '../string/stringify';
|
|
5
|
+
const RED_DOT_ID = '__red-dot__';
|
|
6
|
+
const NOOP = () => { };
|
|
7
|
+
/**
|
|
8
|
+
* @experimental
|
|
9
|
+
*
|
|
10
|
+
* Allows to listen for AdminMode keypress combination (Ctrl+Shift+L by default) to toggle AdminMode,
|
|
11
|
+
* indicated by RedDot DOM element.
|
|
12
|
+
*
|
|
13
|
+
* todo: help with Authentication
|
|
14
|
+
*/
|
|
15
|
+
export class AdminService {
|
|
16
|
+
constructor(cfg) {
|
|
17
|
+
this.adminMode = false;
|
|
18
|
+
this.listening = false;
|
|
19
|
+
this.cfg = {
|
|
20
|
+
predicate: e => e.ctrlKey && e.key === 'L',
|
|
21
|
+
persistToLocalStorage: true,
|
|
22
|
+
localStorageKey: '__adminMode__',
|
|
23
|
+
onRedDotClick: NOOP,
|
|
24
|
+
onChange: NOOP,
|
|
25
|
+
beforeEnter: () => true,
|
|
26
|
+
beforeExit: () => true,
|
|
27
|
+
...cfg,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start listening to keyboard events to toggle AdminMode when detected.
|
|
32
|
+
*/
|
|
33
|
+
startListening() {
|
|
34
|
+
if (this.listening || isServerSide())
|
|
35
|
+
return;
|
|
36
|
+
this.adminMode = !!localStorage.getItem(this.cfg.localStorageKey);
|
|
37
|
+
if (this.adminMode)
|
|
38
|
+
this.toggleRedDotVisibility();
|
|
39
|
+
document.addEventListener('keydown', this.keydownListener.bind(this), { passive: true });
|
|
40
|
+
this.listening = true;
|
|
41
|
+
}
|
|
42
|
+
stopListening() {
|
|
43
|
+
if (isServerSide())
|
|
44
|
+
return;
|
|
45
|
+
document.removeEventListener('keydown', this.keydownListener);
|
|
46
|
+
this.listening = false;
|
|
47
|
+
}
|
|
48
|
+
async keydownListener(e) {
|
|
49
|
+
// console.log(e)
|
|
50
|
+
if (!this.cfg.predicate(e))
|
|
51
|
+
return;
|
|
52
|
+
await this.toggleRedDot();
|
|
53
|
+
}
|
|
54
|
+
async toggleRedDot() {
|
|
55
|
+
try {
|
|
56
|
+
const allow = await this.cfg[this.adminMode ? 'beforeExit' : 'beforeEnter']();
|
|
57
|
+
if (!allow)
|
|
58
|
+
return; // no change
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error(err);
|
|
62
|
+
// ok to show alert to Admins, it's not user-facing
|
|
63
|
+
alert(_stringify(err));
|
|
64
|
+
return; // treat as "not allowed"
|
|
65
|
+
}
|
|
66
|
+
this.adminMode = !this.adminMode;
|
|
67
|
+
this.toggleRedDotVisibility();
|
|
68
|
+
if (this.cfg.persistToLocalStorage) {
|
|
69
|
+
const { localStorageKey } = this.cfg;
|
|
70
|
+
if (this.adminMode) {
|
|
71
|
+
localStorage.setItem(localStorageKey, '1');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
localStorage.removeItem(localStorageKey);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
this.cfg.onChange(this.adminMode);
|
|
78
|
+
}
|
|
79
|
+
toggleRedDotVisibility() {
|
|
80
|
+
this.getRedDotElement().style.display = this.adminMode ? 'block' : 'none';
|
|
81
|
+
}
|
|
82
|
+
getRedDotElement() {
|
|
83
|
+
const el = document.createElement('div');
|
|
84
|
+
el.id = RED_DOT_ID;
|
|
85
|
+
el.style.cssText =
|
|
86
|
+
'position:fixed;width:24px;height:24px;margin-top:-12px;background-color:red;opacity:0.5;top:50%;left:0;z-index:9999999;cursor:pointer;border-radius:0 3px 3px 0';
|
|
87
|
+
el.addEventListener('click', () => this.cfg.onRedDotClick());
|
|
88
|
+
document.body.append(el);
|
|
89
|
+
return el;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
__decorate([
|
|
93
|
+
_Memo()
|
|
94
|
+
], AdminService.prototype, "getRedDotElement", null);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isServerSide } from '@naturalcycles/js-lib';
|
|
2
|
+
import { loadScript } from './script.util';
|
|
3
|
+
/* eslint-disable unicorn/prefer-global-this */
|
|
4
|
+
/**
|
|
5
|
+
* Pass enabled = false to only init window.gtag, but not load actual gtag script (e.g in dev mode).
|
|
6
|
+
*/
|
|
7
|
+
export async function loadGTag(gtagId, enabled = true) {
|
|
8
|
+
if (isServerSide())
|
|
9
|
+
return;
|
|
10
|
+
window.dataLayer || (window.dataLayer = []);
|
|
11
|
+
window.gtag || (window.gtag = function gtag() {
|
|
12
|
+
// biome-ignore lint/complexity/useArrowFunction: ok
|
|
13
|
+
// biome-ignore lint/style/noArguments: ok
|
|
14
|
+
window.dataLayer.push(arguments);
|
|
15
|
+
});
|
|
16
|
+
window.gtag('js', new Date());
|
|
17
|
+
window.gtag('config', gtagId);
|
|
18
|
+
if (!enabled)
|
|
19
|
+
return;
|
|
20
|
+
await loadScript(`https://www.googletagmanager.com/gtag/js?id=${gtagId}`);
|
|
21
|
+
}
|
|
22
|
+
export async function loadGTM(gtmId, enabled = true) {
|
|
23
|
+
if (isServerSide())
|
|
24
|
+
return;
|
|
25
|
+
window.dataLayer || (window.dataLayer = []);
|
|
26
|
+
window.dataLayer.push({
|
|
27
|
+
'gtm.start': Date.now(),
|
|
28
|
+
event: 'gtm.js',
|
|
29
|
+
});
|
|
30
|
+
if (!enabled)
|
|
31
|
+
return;
|
|
32
|
+
await loadScript(`https://www.googletagmanager.com/gtm.js?id=${gtmId}`);
|
|
33
|
+
}
|
|
34
|
+
export function loadHotjar(hjid) {
|
|
35
|
+
if (isServerSide())
|
|
36
|
+
return;
|
|
37
|
+
;
|
|
38
|
+
((h, o, t, j, a, r) => {
|
|
39
|
+
h.hj =
|
|
40
|
+
h.hj ||
|
|
41
|
+
function hj() {
|
|
42
|
+
// biome-ignore lint/style/noArguments: ok
|
|
43
|
+
;
|
|
44
|
+
(h.hj.q = h.hj.q || []).push(arguments);
|
|
45
|
+
};
|
|
46
|
+
h._hjSettings = { hjid, hjsv: 6 };
|
|
47
|
+
a = o.querySelectorAll('head')[0];
|
|
48
|
+
r = o.createElement('script');
|
|
49
|
+
r.async = 1;
|
|
50
|
+
r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
|
|
51
|
+
a.append(r);
|
|
52
|
+
})(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv=');
|
|
53
|
+
/* eslint-enable */
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use `baseUrl` to prefix your language files.
|
|
3
|
+
* Example URL structure:
|
|
4
|
+
* ${baseUrl}/${locale}.json
|
|
5
|
+
*/
|
|
6
|
+
export class FetchTranslationLoader {
|
|
7
|
+
constructor(fetcher) {
|
|
8
|
+
this.fetcher = fetcher;
|
|
9
|
+
}
|
|
10
|
+
async load(locale) {
|
|
11
|
+
return await this.fetcher.get(`${locale}.json`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { pMap } from '../../promise/pMap';
|
|
2
|
+
export const defaultMissingTranslationHandler = key => {
|
|
3
|
+
console.warn(`[tr] missing: ${key}`);
|
|
4
|
+
return `[${key}]`;
|
|
5
|
+
};
|
|
6
|
+
export class TranslationService {
|
|
7
|
+
constructor(cfg, preloadedLocales = {}) {
|
|
8
|
+
this.cfg = {
|
|
9
|
+
...cfg,
|
|
10
|
+
missingTranslationHandler: defaultMissingTranslationHandler,
|
|
11
|
+
};
|
|
12
|
+
this.locales = {
|
|
13
|
+
...preloadedLocales,
|
|
14
|
+
};
|
|
15
|
+
this.currentLocale = cfg.currentLocale || cfg.defaultLocale;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Manually set locale data, bypassing the TranslationLoader.
|
|
19
|
+
*/
|
|
20
|
+
setLocale(localeName, locale) {
|
|
21
|
+
this.locales[localeName] = locale;
|
|
22
|
+
}
|
|
23
|
+
getLocale(locale) {
|
|
24
|
+
return this.locales[locale];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Loads locale(s) (if not already cached) via configured TranslationLoader.
|
|
28
|
+
* Resolves promise when done (ready to be used).
|
|
29
|
+
*/
|
|
30
|
+
async loadLocale(locale) {
|
|
31
|
+
const locales = Array.isArray(locale) ? locale : [locale];
|
|
32
|
+
await pMap(locales, async (locale) => {
|
|
33
|
+
if (this.locales[locale])
|
|
34
|
+
return; // already loaded
|
|
35
|
+
this.locales[locale] = await this.cfg.translationLoader.load(locale);
|
|
36
|
+
// console.log(`[tr] locale loaded: ${locale}`)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Will invoke `missingTranslationHandler` on missing tranlation.
|
|
41
|
+
*
|
|
42
|
+
* Does NOT do any locale loading. The locale needs to be loaded beforehand:
|
|
43
|
+
* either pre-loaded and passed to the constructor,
|
|
44
|
+
* or `await loadLocale(locale)`.
|
|
45
|
+
*/
|
|
46
|
+
translate(key, params) {
|
|
47
|
+
return this.translateIfExists(key, params) || this.cfg.missingTranslationHandler(key, params);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Does NOT invoke `missingTranslationHandler`, returns `undefined` instead.
|
|
51
|
+
*/
|
|
52
|
+
translateIfExists(key, _params) {
|
|
53
|
+
// todo: support params
|
|
54
|
+
return this.locales[this.currentLocale]?.[key] || this.locales[this.cfg.defaultLocale]?.[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the width/height of the images to fit in the layout.
|
|
3
|
+
*
|
|
4
|
+
* Currently does not mutate the cfg.images array, but DOES mutate individual images with .fitWidth, .fitHeight properties.
|
|
5
|
+
*
|
|
6
|
+
* @experimental
|
|
7
|
+
*/
|
|
8
|
+
export class ImageFitter {
|
|
9
|
+
constructor(cfg) {
|
|
10
|
+
this.containerWidth = -1;
|
|
11
|
+
this.cfg = {
|
|
12
|
+
maxHeight: 300,
|
|
13
|
+
margin: 8,
|
|
14
|
+
...cfg,
|
|
15
|
+
};
|
|
16
|
+
this.resizeObserver = new ResizeObserver(entries => this.update(entries));
|
|
17
|
+
this.resizeObserver.observe(cfg.containerElement);
|
|
18
|
+
}
|
|
19
|
+
stop() {
|
|
20
|
+
this.resizeObserver.disconnect();
|
|
21
|
+
}
|
|
22
|
+
update(entries) {
|
|
23
|
+
const width = Math.floor(entries[0].contentRect.width);
|
|
24
|
+
if (width === this.containerWidth)
|
|
25
|
+
return; // we're only interested in width changes
|
|
26
|
+
this.containerWidth = width;
|
|
27
|
+
console.log(`resize ${width}`);
|
|
28
|
+
this.doLayout(this.cfg.images);
|
|
29
|
+
this.cfg.onChange(this.cfg.images);
|
|
30
|
+
}
|
|
31
|
+
doLayout(imgs) {
|
|
32
|
+
if (imgs.length === 0)
|
|
33
|
+
return; // nothing to do
|
|
34
|
+
const { maxHeight } = this.cfg;
|
|
35
|
+
let imgNodes = imgs.slice(0);
|
|
36
|
+
w: while (imgNodes.length > 0) {
|
|
37
|
+
let slice;
|
|
38
|
+
let h;
|
|
39
|
+
for (let i = 1; i <= imgNodes.length; i++) {
|
|
40
|
+
slice = imgNodes.slice(0, i);
|
|
41
|
+
h = this.getHeigth(slice);
|
|
42
|
+
if (h < maxHeight) {
|
|
43
|
+
this.setHeight(slice, h);
|
|
44
|
+
imgNodes = imgNodes.slice(i);
|
|
45
|
+
continue w;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.setHeight(slice, Math.min(maxHeight, h));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getHeigth(images) {
|
|
53
|
+
const width = this.containerWidth - images.length * this.cfg.margin;
|
|
54
|
+
let r = 0;
|
|
55
|
+
images.forEach(img => (r += img.aspectRatio));
|
|
56
|
+
return width / r; // have to round down because Firefox will automatically roundup value with number of decimals > 3
|
|
57
|
+
}
|
|
58
|
+
// mutates/sets images' fitWidth, fitHeight properties
|
|
59
|
+
setHeight(images, height) {
|
|
60
|
+
images.forEach(img => {
|
|
61
|
+
img.fitWidth = Math.floor(height * img.aspectRatio);
|
|
62
|
+
img.fitHeight = Math.floor(height);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isServerSide } from '../env';
|
|
2
|
+
import { _objectAssign } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* opt.async defaults to `true`.
|
|
5
|
+
* No other options are set by default.
|
|
6
|
+
*/
|
|
7
|
+
export async function loadScript(src, opt) {
|
|
8
|
+
if (isServerSide())
|
|
9
|
+
return;
|
|
10
|
+
return await new Promise((resolve, reject) => {
|
|
11
|
+
const s = _objectAssign(document.createElement('script'), {
|
|
12
|
+
src,
|
|
13
|
+
async: true,
|
|
14
|
+
...opt,
|
|
15
|
+
onload: resolve,
|
|
16
|
+
onerror: (_event, _source, _lineno, _colno, err) => {
|
|
17
|
+
reject(err || new Error(`loadScript failed: ${src}`));
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
document.head.append(s);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Default options:
|
|
25
|
+
* rel: 'stylesheet'
|
|
26
|
+
*
|
|
27
|
+
* No other options are set by default.
|
|
28
|
+
*/
|
|
29
|
+
export async function loadCSS(href, opt) {
|
|
30
|
+
if (isServerSide())
|
|
31
|
+
return;
|
|
32
|
+
return await new Promise((resolve, reject) => {
|
|
33
|
+
const link = _objectAssign(document.createElement('link'), {
|
|
34
|
+
href,
|
|
35
|
+
rel: 'stylesheet',
|
|
36
|
+
// type seems to be unnecessary: https://stackoverflow.com/a/5409146/4919972
|
|
37
|
+
// type: 'text/css',
|
|
38
|
+
...opt,
|
|
39
|
+
onload: resolve,
|
|
40
|
+
onerror: (_event, _source, _lineno, _colno, err) => {
|
|
41
|
+
reject(err || new Error(`loadCSS failed: ${href}`));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
document.head.append(link);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Modified version of topbar:
|
|
2
|
+
// http://buunguyen.github.io/topbar
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
const browser = typeof window !== 'undefined';
|
|
5
|
+
let canvas;
|
|
6
|
+
let progressTimerId;
|
|
7
|
+
let fadeTimerId;
|
|
8
|
+
let currentProgress;
|
|
9
|
+
let showing;
|
|
10
|
+
const addEvent = (elem, type, handler) => {
|
|
11
|
+
if (elem.addEventListener)
|
|
12
|
+
elem.addEventListener(type, handler, false);
|
|
13
|
+
else if (elem.attachEvent)
|
|
14
|
+
elem.attachEvent('on' + type, handler);
|
|
15
|
+
else
|
|
16
|
+
elem['on' + type] = handler;
|
|
17
|
+
};
|
|
18
|
+
const options = {
|
|
19
|
+
autoRun: true,
|
|
20
|
+
barThickness: 5,
|
|
21
|
+
barColors: {
|
|
22
|
+
'0': 'rgba(26, 188, 156, .9)',
|
|
23
|
+
'.25': 'rgba(52, 152, 219, .9)',
|
|
24
|
+
'.50': 'rgba(241, 196, 15, .9)',
|
|
25
|
+
'.75': 'rgba(230, 126, 34, .9)',
|
|
26
|
+
'1.0': 'rgba(211, 84, 0, .9)',
|
|
27
|
+
},
|
|
28
|
+
shadowBlur: 10,
|
|
29
|
+
shadowColor: 'rgba(0, 0, 0, .6)',
|
|
30
|
+
};
|
|
31
|
+
const repaint = () => {
|
|
32
|
+
canvas.width = window.innerWidth;
|
|
33
|
+
canvas.height = options.barThickness * 5; // need space for shadow
|
|
34
|
+
const ctx = canvas.getContext('2d');
|
|
35
|
+
ctx.shadowBlur = options.shadowBlur;
|
|
36
|
+
ctx.shadowColor = options.shadowColor;
|
|
37
|
+
const lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
38
|
+
for (const stop in options.barColors) {
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
lineGradient.addColorStop(stop, options.barColors[stop]);
|
|
41
|
+
}
|
|
42
|
+
ctx.lineWidth = options.barThickness;
|
|
43
|
+
ctx.beginPath();
|
|
44
|
+
ctx.moveTo(0, options.barThickness / 2);
|
|
45
|
+
ctx.lineTo(Math.ceil(currentProgress * canvas.width), options.barThickness / 2);
|
|
46
|
+
ctx.strokeStyle = lineGradient;
|
|
47
|
+
ctx.stroke();
|
|
48
|
+
};
|
|
49
|
+
const createCanvas = () => {
|
|
50
|
+
canvas = document.createElement('canvas');
|
|
51
|
+
const style = canvas.style;
|
|
52
|
+
style.position = 'fixed';
|
|
53
|
+
style.top = style.left = style.right = style.margin = style.padding = 0;
|
|
54
|
+
style.zIndex = 100001;
|
|
55
|
+
style.display = 'none';
|
|
56
|
+
document.body.appendChild(canvas);
|
|
57
|
+
addEvent(window, 'resize', repaint);
|
|
58
|
+
};
|
|
59
|
+
export const topbar = {
|
|
60
|
+
config(opts) {
|
|
61
|
+
for (const key in opts) {
|
|
62
|
+
if (options.hasOwnProperty(key)) {
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
options[key] = opts[key];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
set(show, opts) {
|
|
69
|
+
if (show) {
|
|
70
|
+
topbar.show(opts);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
topbar.hide();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
show(opts) {
|
|
77
|
+
if (!browser)
|
|
78
|
+
return; // ssr protection
|
|
79
|
+
if (opts)
|
|
80
|
+
topbar.config(opts);
|
|
81
|
+
if (showing)
|
|
82
|
+
return;
|
|
83
|
+
showing = true;
|
|
84
|
+
if (fadeTimerId !== null) {
|
|
85
|
+
window.cancelAnimationFrame(fadeTimerId);
|
|
86
|
+
}
|
|
87
|
+
if (!canvas)
|
|
88
|
+
createCanvas();
|
|
89
|
+
canvas.style.opacity = 1;
|
|
90
|
+
canvas.style.display = 'block';
|
|
91
|
+
topbar.progress(0);
|
|
92
|
+
if (options.autoRun) {
|
|
93
|
+
;
|
|
94
|
+
(function loop() {
|
|
95
|
+
progressTimerId = window.requestAnimationFrame(loop);
|
|
96
|
+
topbar.progress('+' + 0.05 * (1 - Math.sqrt(currentProgress)) ** 2);
|
|
97
|
+
})();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
progress(to) {
|
|
101
|
+
if (!browser)
|
|
102
|
+
return; // ssr protection
|
|
103
|
+
if (typeof to === 'undefined') {
|
|
104
|
+
return currentProgress;
|
|
105
|
+
}
|
|
106
|
+
if (typeof to === 'string') {
|
|
107
|
+
to = (to.indexOf('+') >= 0 || to.indexOf('-') >= 0 ? currentProgress : 0) + parseFloat(to);
|
|
108
|
+
}
|
|
109
|
+
currentProgress = to > 1 ? 1 : to;
|
|
110
|
+
repaint();
|
|
111
|
+
return currentProgress;
|
|
112
|
+
},
|
|
113
|
+
hide() {
|
|
114
|
+
if (!showing || !browser)
|
|
115
|
+
return;
|
|
116
|
+
showing = false;
|
|
117
|
+
if (progressTimerId != null) {
|
|
118
|
+
window.cancelAnimationFrame(progressTimerId);
|
|
119
|
+
progressTimerId = null;
|
|
120
|
+
}
|
|
121
|
+
;
|
|
122
|
+
(function loop() {
|
|
123
|
+
if (topbar.progress('+.1') >= 1) {
|
|
124
|
+
canvas.style.opacity -= 0.05;
|
|
125
|
+
if (canvas.style.opacity <= 0.05) {
|
|
126
|
+
canvas.style.display = 'none';
|
|
127
|
+
fadeTimerId = null;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
fadeTimerId = window.requestAnimationFrame(loop);
|
|
132
|
+
})();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { _getTargetMethodSignature } from './decorator.util';
|
|
2
|
+
/**
|
|
3
|
+
* Prevents "swarm" of async calls to the same method.
|
|
4
|
+
* Allows max 1 in-flight promise to exist.
|
|
5
|
+
* If more calls appear, while Promise is not resolved yet - same Promise is returned.
|
|
6
|
+
*
|
|
7
|
+
* Does not support `cacheKey`.
|
|
8
|
+
* So, the same Promise is returned, regardless of the arguments.
|
|
9
|
+
*/
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
11
|
+
export const _SwarmSafe = () => (target, key, descriptor) => {
|
|
12
|
+
if (typeof descriptor.value !== 'function') {
|
|
13
|
+
throw new TypeError('@_SwarmSafe can be applied only to methods');
|
|
14
|
+
}
|
|
15
|
+
const originalFn = descriptor.value;
|
|
16
|
+
const keyStr = String(key);
|
|
17
|
+
const methodSignature = _getTargetMethodSignature(target, keyStr);
|
|
18
|
+
const instanceCache = new Map();
|
|
19
|
+
console.log('SwarmSafe constructor called', { key, methodSignature });
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
21
|
+
descriptor.value = function (...args) {
|
|
22
|
+
console.log('SwarmSafe method called', { key, methodSignature, args });
|
|
23
|
+
const ctx = this;
|
|
24
|
+
let inFlightPromise = instanceCache.get(ctx);
|
|
25
|
+
if (inFlightPromise) {
|
|
26
|
+
console.log(`SwarmSafe: returning in-flight promise`);
|
|
27
|
+
return inFlightPromise;
|
|
28
|
+
}
|
|
29
|
+
console.log(`SwarmSafe: first-time call, creating in-flight promise`);
|
|
30
|
+
inFlightPromise = originalFn.apply(ctx, args);
|
|
31
|
+
instanceCache.set(ctx, inFlightPromise);
|
|
32
|
+
void inFlightPromise.finally(() => {
|
|
33
|
+
console.log(`SwarmSafe: in-flight promise resolved`);
|
|
34
|
+
instanceCache.delete(ctx);
|
|
35
|
+
});
|
|
36
|
+
return inFlightPromise;
|
|
37
|
+
};
|
|
38
|
+
};
|