@naturalcycles/js-lib 14.256.0 → 14.257.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 (56) hide show
  1. package/cfg/frontend/tsconfig.json +67 -0
  2. package/dist/browser/adminService.d.ts +69 -0
  3. package/dist/browser/adminService.js +98 -0
  4. package/dist/browser/analytics.util.d.ts +12 -0
  5. package/dist/browser/analytics.util.js +59 -0
  6. package/dist/browser/i18n/fetchTranslationLoader.d.ts +13 -0
  7. package/dist/browser/i18n/fetchTranslationLoader.js +17 -0
  8. package/dist/browser/i18n/translation.service.d.ts +53 -0
  9. package/dist/browser/i18n/translation.service.js +61 -0
  10. package/dist/browser/imageFitter.d.ts +60 -0
  11. package/dist/browser/imageFitter.js +69 -0
  12. package/dist/browser/script.util.d.ts +14 -0
  13. package/dist/browser/script.util.js +50 -0
  14. package/dist/browser/topbar.d.ts +23 -0
  15. package/dist/browser/topbar.js +137 -0
  16. package/dist/decorators/memo.util.d.ts +2 -1
  17. package/dist/decorators/memo.util.js +8 -6
  18. package/dist/decorators/swarmSafe.decorator.d.ts +9 -0
  19. package/dist/decorators/swarmSafe.decorator.js +42 -0
  20. package/dist/error/assert.d.ts +2 -1
  21. package/dist/error/assert.js +15 -13
  22. package/dist/error/error.util.js +9 -6
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +7 -0
  25. package/dist/zod/zod.util.d.ts +1 -1
  26. package/dist-esm/browser/adminService.js +94 -0
  27. package/dist-esm/browser/analytics.util.js +54 -0
  28. package/dist-esm/browser/i18n/fetchTranslationLoader.js +13 -0
  29. package/dist-esm/browser/i18n/translation.service.js +56 -0
  30. package/dist-esm/browser/imageFitter.js +65 -0
  31. package/dist-esm/browser/script.util.js +46 -0
  32. package/dist-esm/browser/topbar.js +134 -0
  33. package/dist-esm/decorators/memo.util.js +3 -1
  34. package/dist-esm/decorators/swarmSafe.decorator.js +38 -0
  35. package/dist-esm/error/assert.js +3 -1
  36. package/dist-esm/error/error.util.js +4 -1
  37. package/dist-esm/index.js +7 -0
  38. package/package.json +2 -1
  39. package/src/browser/adminService.ts +157 -0
  40. package/src/browser/analytics.util.ts +68 -0
  41. package/src/browser/i18n/fetchTranslationLoader.ts +16 -0
  42. package/src/browser/i18n/translation.service.ts +102 -0
  43. package/src/browser/imageFitter.ts +128 -0
  44. package/src/browser/script.util.ts +52 -0
  45. package/src/browser/topbar.ts +147 -0
  46. package/src/datetime/localDate.ts +16 -0
  47. package/src/datetime/localTime.ts +39 -0
  48. package/src/decorators/debounce.ts +1 -0
  49. package/src/decorators/memo.util.ts +4 -1
  50. package/src/decorators/swarmSafe.decorator.ts +47 -0
  51. package/src/error/assert.ts +5 -11
  52. package/src/error/error.util.ts +4 -1
  53. package/src/index.ts +7 -0
  54. package/src/json-schema/jsonSchemaBuilder.ts +20 -0
  55. package/src/semver.ts +2 -0
  56. package/src/zod/zod.util.ts +1 -1
@@ -0,0 +1,67 @@
1
+ //
2
+ // @naturalcycles/js-lib/cfg/frontend/tsconfig.json
3
+ //
4
+ // Shared tsconfig for Frontend applications
5
+ //
6
+ {
7
+ "compilerOptions": {
8
+ // Target/module
9
+ "target": "es2020", // es2020+ browsers, adjust to your requirements!
10
+ "lib": ["esnext", "dom", "dom.iterable"],
11
+ "module": "esnext",
12
+ "moduleResolution": "node",
13
+ "moduleDetection": "force",
14
+ // specifying these explicitly for better IDE compatibility (but they're on by default with module=nodenext)
15
+ "esModuleInterop": true,
16
+ "allowSyntheticDefaultImports": true,
17
+ // Faster compilation in general
18
+ // Support for external compilers (e.g esbuild)
19
+ // Speedup in Jest by using "isolatedModules" in 'ts-jest' config
20
+ "isolatedModules": true,
21
+
22
+ // Emit
23
+ "sourceMap": false,
24
+ "declaration": false,
25
+ // Otherwise since es2022 it defaults to true
26
+ // and starts to produce different/unexpected behavior
27
+ // https://angular.schule/blog/2022-11-use-define-for-class-fields
28
+ "useDefineForClassFields": false,
29
+ "importHelpers": true,
30
+
31
+ // Strictness
32
+ "strict": true,
33
+ "noFallthroughCasesInSwitch": true,
34
+ "forceConsistentCasingInFileNames": true,
35
+ "resolveJsonModule": true,
36
+ "suppressImplicitAnyIndexErrors": false,
37
+ "noUncheckedIndexedAccess": true,
38
+ "noPropertyAccessFromIndexSignature": true,
39
+ "noImplicitOverride": true,
40
+
41
+ // Enabled should be faster, but will catch less errors
42
+ // "skipLibCheck": true,
43
+
44
+ // Disabled because of https://github.com/Microsoft/TypeScript/issues/29172
45
+ // Need to be specified in the project tsconfig
46
+ // "outDir": "dist",
47
+ // "rootDir": "./src",
48
+ // "baseUrl": "./",
49
+ // "paths": {
50
+ // "@src/*": ["src/*"]
51
+ // },
52
+ // "typeRoots": [
53
+ // "node_modules/@types",
54
+ // "src/@types"
55
+ // ],
56
+
57
+ // Other
58
+ "jsx": "preserve",
59
+ "pretty": true,
60
+ "newLine": "lf",
61
+ "experimentalDecorators": true,
62
+ // "emitDecoratorMetadata": true // use if needed
63
+ },
64
+ // Need to be specified in the project tsconfig
65
+ // "include": ["src"],
66
+ // "exclude": ["**/__exclude", "**/@linked"]
67
+ }
@@ -0,0 +1,69 @@
1
+ import { Promisable } from '../typeFest';
2
+ export interface AdminModeCfg {
3
+ /**
4
+ * Function (predicate) to detect if needed keys are pressed.
5
+ *
6
+ * @example
7
+ * predicate: e => e.ctrlKey && e.key === 'L'
8
+ *
9
+ * @default
10
+ * Detects Ctrl+Shift+L
11
+ */
12
+ predicate?: (e: KeyboardEvent) => boolean;
13
+ /**
14
+ * Called when RedDot is clicked. Implies that AdminMode is enabled.
15
+ */
16
+ onRedDotClick?: () => any;
17
+ /**
18
+ * Called when AdminMode was changed.
19
+ */
20
+ onChange?: (adminMode: boolean) => any;
21
+ /**
22
+ * Called BEFORE entering AdminMode.
23
+ * Serves as a predicate that can cancel entering AdminMode if false is returned.
24
+ * Return true to allow.
25
+ * Function is awaited before proceeding.
26
+ */
27
+ beforeEnter?: () => Promisable<boolean>;
28
+ /**
29
+ * Called BEFORE exiting AdminMode.
30
+ * Serves as a predicate that can cancel exiting AdminMode if false is returned.
31
+ * Return true to allow.
32
+ * Function is awaited before proceeding.
33
+ */
34
+ beforeExit?: () => Promisable<boolean>;
35
+ /**
36
+ * @default true
37
+ * If true - it will "persist" the adminMode state in LocalStorage
38
+ */
39
+ persistToLocalStorage?: boolean;
40
+ /**
41
+ * The key for LocalStorage persistence.
42
+ *
43
+ * @default '__adminMode__'
44
+ */
45
+ localStorageKey?: string;
46
+ }
47
+ /**
48
+ * @experimental
49
+ *
50
+ * Allows to listen for AdminMode keypress combination (Ctrl+Shift+L by default) to toggle AdminMode,
51
+ * indicated by RedDot DOM element.
52
+ *
53
+ * todo: help with Authentication
54
+ */
55
+ export declare class AdminService {
56
+ constructor(cfg?: AdminModeCfg);
57
+ cfg: Required<AdminModeCfg>;
58
+ adminMode: boolean;
59
+ private listening;
60
+ /**
61
+ * Start listening to keyboard events to toggle AdminMode when detected.
62
+ */
63
+ startListening(): void;
64
+ stopListening(): void;
65
+ private keydownListener;
66
+ toggleRedDot(): Promise<void>;
67
+ private toggleRedDotVisibility;
68
+ private getRedDotElement;
69
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdminService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const memo_decorator_1 = require("../decorators/memo.decorator");
6
+ const env_1 = require("../env");
7
+ const stringify_1 = require("../string/stringify");
8
+ const RED_DOT_ID = '__red-dot__';
9
+ const NOOP = () => { };
10
+ /**
11
+ * @experimental
12
+ *
13
+ * Allows to listen for AdminMode keypress combination (Ctrl+Shift+L by default) to toggle AdminMode,
14
+ * indicated by RedDot DOM element.
15
+ *
16
+ * todo: help with Authentication
17
+ */
18
+ class AdminService {
19
+ constructor(cfg) {
20
+ this.adminMode = false;
21
+ this.listening = false;
22
+ this.cfg = {
23
+ predicate: e => e.ctrlKey && e.key === 'L',
24
+ persistToLocalStorage: true,
25
+ localStorageKey: '__adminMode__',
26
+ onRedDotClick: NOOP,
27
+ onChange: NOOP,
28
+ beforeEnter: () => true,
29
+ beforeExit: () => true,
30
+ ...cfg,
31
+ };
32
+ }
33
+ /**
34
+ * Start listening to keyboard events to toggle AdminMode when detected.
35
+ */
36
+ startListening() {
37
+ if (this.listening || (0, env_1.isServerSide)())
38
+ return;
39
+ this.adminMode = !!localStorage.getItem(this.cfg.localStorageKey);
40
+ if (this.adminMode)
41
+ this.toggleRedDotVisibility();
42
+ document.addEventListener('keydown', this.keydownListener.bind(this), { passive: true });
43
+ this.listening = true;
44
+ }
45
+ stopListening() {
46
+ if ((0, env_1.isServerSide)())
47
+ return;
48
+ document.removeEventListener('keydown', this.keydownListener);
49
+ this.listening = false;
50
+ }
51
+ async keydownListener(e) {
52
+ // console.log(e)
53
+ if (!this.cfg.predicate(e))
54
+ return;
55
+ await this.toggleRedDot();
56
+ }
57
+ async toggleRedDot() {
58
+ try {
59
+ const allow = await this.cfg[this.adminMode ? 'beforeExit' : 'beforeEnter']();
60
+ if (!allow)
61
+ return; // no change
62
+ }
63
+ catch (err) {
64
+ console.error(err);
65
+ // ok to show alert to Admins, it's not user-facing
66
+ alert((0, stringify_1._stringify)(err));
67
+ return; // treat as "not allowed"
68
+ }
69
+ this.adminMode = !this.adminMode;
70
+ this.toggleRedDotVisibility();
71
+ if (this.cfg.persistToLocalStorage) {
72
+ const { localStorageKey } = this.cfg;
73
+ if (this.adminMode) {
74
+ localStorage.setItem(localStorageKey, '1');
75
+ }
76
+ else {
77
+ localStorage.removeItem(localStorageKey);
78
+ }
79
+ }
80
+ this.cfg.onChange(this.adminMode);
81
+ }
82
+ toggleRedDotVisibility() {
83
+ this.getRedDotElement().style.display = this.adminMode ? 'block' : 'none';
84
+ }
85
+ getRedDotElement() {
86
+ const el = document.createElement('div');
87
+ el.id = RED_DOT_ID;
88
+ el.style.cssText =
89
+ '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';
90
+ el.addEventListener('click', () => this.cfg.onRedDotClick());
91
+ document.body.append(el);
92
+ return el;
93
+ }
94
+ }
95
+ exports.AdminService = AdminService;
96
+ tslib_1.__decorate([
97
+ (0, memo_decorator_1._Memo)()
98
+ ], AdminService.prototype, "getRedDotElement", null);
@@ -0,0 +1,12 @@
1
+ declare global {
2
+ interface Window {
3
+ dataLayer: any[];
4
+ gtag: (...args: any[]) => void;
5
+ }
6
+ }
7
+ /**
8
+ * Pass enabled = false to only init window.gtag, but not load actual gtag script (e.g in dev mode).
9
+ */
10
+ export declare function loadGTag(gtagId: string, enabled?: boolean): Promise<void>;
11
+ export declare function loadGTM(gtmId: string, enabled?: boolean): Promise<void>;
12
+ export declare function loadHotjar(hjid: number): void;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadGTag = loadGTag;
4
+ exports.loadGTM = loadGTM;
5
+ exports.loadHotjar = loadHotjar;
6
+ const js_lib_1 = require("@naturalcycles/js-lib");
7
+ const script_util_1 = require("./script.util");
8
+ /* eslint-disable unicorn/prefer-global-this */
9
+ /**
10
+ * Pass enabled = false to only init window.gtag, but not load actual gtag script (e.g in dev mode).
11
+ */
12
+ async function loadGTag(gtagId, enabled = true) {
13
+ if ((0, js_lib_1.isServerSide)())
14
+ return;
15
+ window.dataLayer ||= [];
16
+ window.gtag ||= function gtag() {
17
+ // biome-ignore lint/complexity/useArrowFunction: ok
18
+ // biome-ignore lint/style/noArguments: ok
19
+ window.dataLayer.push(arguments);
20
+ };
21
+ window.gtag('js', new Date());
22
+ window.gtag('config', gtagId);
23
+ if (!enabled)
24
+ return;
25
+ await (0, script_util_1.loadScript)(`https://www.googletagmanager.com/gtag/js?id=${gtagId}`);
26
+ }
27
+ async function loadGTM(gtmId, enabled = true) {
28
+ if ((0, js_lib_1.isServerSide)())
29
+ return;
30
+ window.dataLayer ||= [];
31
+ window.dataLayer.push({
32
+ 'gtm.start': Date.now(),
33
+ event: 'gtm.js',
34
+ });
35
+ if (!enabled)
36
+ return;
37
+ await (0, script_util_1.loadScript)(`https://www.googletagmanager.com/gtm.js?id=${gtmId}`);
38
+ }
39
+ function loadHotjar(hjid) {
40
+ if ((0, js_lib_1.isServerSide)())
41
+ return;
42
+ ;
43
+ ((h, o, t, j, a, r) => {
44
+ h.hj =
45
+ h.hj ||
46
+ function hj() {
47
+ // biome-ignore lint/style/noArguments: ok
48
+ ;
49
+ (h.hj.q = h.hj.q || []).push(arguments);
50
+ };
51
+ h._hjSettings = { hjid, hjsv: 6 };
52
+ a = o.querySelectorAll('head')[0];
53
+ r = o.createElement('script');
54
+ r.async = 1;
55
+ r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
56
+ a.append(r);
57
+ })(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv=');
58
+ /* eslint-enable */
59
+ }
@@ -0,0 +1,13 @@
1
+ import { Fetcher } from '../../http/fetcher';
2
+ import { StringMap } from '../../types';
3
+ import { TranslationLoader } from './translation.service';
4
+ /**
5
+ * Use `baseUrl` to prefix your language files.
6
+ * Example URL structure:
7
+ * ${baseUrl}/${locale}.json
8
+ */
9
+ export declare class FetchTranslationLoader implements TranslationLoader {
10
+ fetcher: Fetcher;
11
+ constructor(fetcher: Fetcher);
12
+ load(locale: string): Promise<StringMap>;
13
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchTranslationLoader = void 0;
4
+ /**
5
+ * Use `baseUrl` to prefix your language files.
6
+ * Example URL structure:
7
+ * ${baseUrl}/${locale}.json
8
+ */
9
+ class FetchTranslationLoader {
10
+ constructor(fetcher) {
11
+ this.fetcher = fetcher;
12
+ }
13
+ async load(locale) {
14
+ return await this.fetcher.get(`${locale}.json`);
15
+ }
16
+ }
17
+ exports.FetchTranslationLoader = FetchTranslationLoader;
@@ -0,0 +1,53 @@
1
+ import { StringMap } from '../../types';
2
+ export type MissingTranslationHandler = (key: string, params?: StringMap<any>) => string;
3
+ export declare const defaultMissingTranslationHandler: MissingTranslationHandler;
4
+ export interface TranslationServiceCfg {
5
+ defaultLocale: string;
6
+ supportedLocales: string[];
7
+ /**
8
+ * It is allowed to set it later. Will default to `defaultLocale` in that case.
9
+ */
10
+ currentLocale?: string;
11
+ translationLoader: TranslationLoader;
12
+ /**
13
+ * Defaults to `defaultMissingTranslationHandler` that returns `[${key}]` and emits console warning.
14
+ */
15
+ missingTranslationHandler?: MissingTranslationHandler;
16
+ }
17
+ export interface TranslationServiceCfgComplete extends TranslationServiceCfg {
18
+ missingTranslationHandler: MissingTranslationHandler;
19
+ }
20
+ export interface TranslationLoader {
21
+ load: (locale: string) => Promise<StringMap>;
22
+ }
23
+ export declare class TranslationService {
24
+ constructor(cfg: TranslationServiceCfg, preloadedLocales?: StringMap<StringMap>);
25
+ cfg: TranslationServiceCfgComplete;
26
+ /**
27
+ * Cache of loaded locales
28
+ */
29
+ locales: StringMap<StringMap>;
30
+ currentLocale: string;
31
+ /**
32
+ * Manually set locale data, bypassing the TranslationLoader.
33
+ */
34
+ setLocale(localeName: string, locale: StringMap): void;
35
+ getLocale(locale: string): StringMap | undefined;
36
+ /**
37
+ * Loads locale(s) (if not already cached) via configured TranslationLoader.
38
+ * Resolves promise when done (ready to be used).
39
+ */
40
+ loadLocale(locale: string | string[]): Promise<void>;
41
+ /**
42
+ * Will invoke `missingTranslationHandler` on missing tranlation.
43
+ *
44
+ * Does NOT do any locale loading. The locale needs to be loaded beforehand:
45
+ * either pre-loaded and passed to the constructor,
46
+ * or `await loadLocale(locale)`.
47
+ */
48
+ translate(key: string, params?: StringMap): string;
49
+ /**
50
+ * Does NOT invoke `missingTranslationHandler`, returns `undefined` instead.
51
+ */
52
+ translateIfExists(key: string, _params?: StringMap): string | undefined;
53
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TranslationService = exports.defaultMissingTranslationHandler = void 0;
4
+ const pMap_1 = require("../../promise/pMap");
5
+ const defaultMissingTranslationHandler = key => {
6
+ console.warn(`[tr] missing: ${key}`);
7
+ return `[${key}]`;
8
+ };
9
+ exports.defaultMissingTranslationHandler = defaultMissingTranslationHandler;
10
+ class TranslationService {
11
+ constructor(cfg, preloadedLocales = {}) {
12
+ this.cfg = {
13
+ ...cfg,
14
+ missingTranslationHandler: exports.defaultMissingTranslationHandler,
15
+ };
16
+ this.locales = {
17
+ ...preloadedLocales,
18
+ };
19
+ this.currentLocale = cfg.currentLocale || cfg.defaultLocale;
20
+ }
21
+ /**
22
+ * Manually set locale data, bypassing the TranslationLoader.
23
+ */
24
+ setLocale(localeName, locale) {
25
+ this.locales[localeName] = locale;
26
+ }
27
+ getLocale(locale) {
28
+ return this.locales[locale];
29
+ }
30
+ /**
31
+ * Loads locale(s) (if not already cached) via configured TranslationLoader.
32
+ * Resolves promise when done (ready to be used).
33
+ */
34
+ async loadLocale(locale) {
35
+ const locales = Array.isArray(locale) ? locale : [locale];
36
+ await (0, pMap_1.pMap)(locales, async (locale) => {
37
+ if (this.locales[locale])
38
+ return; // already loaded
39
+ this.locales[locale] = await this.cfg.translationLoader.load(locale);
40
+ // console.log(`[tr] locale loaded: ${locale}`)
41
+ });
42
+ }
43
+ /**
44
+ * Will invoke `missingTranslationHandler` on missing tranlation.
45
+ *
46
+ * Does NOT do any locale loading. The locale needs to be loaded beforehand:
47
+ * either pre-loaded and passed to the constructor,
48
+ * or `await loadLocale(locale)`.
49
+ */
50
+ translate(key, params) {
51
+ return this.translateIfExists(key, params) || this.cfg.missingTranslationHandler(key, params);
52
+ }
53
+ /**
54
+ * Does NOT invoke `missingTranslationHandler`, returns `undefined` instead.
55
+ */
56
+ translateIfExists(key, _params) {
57
+ // todo: support params
58
+ return this.locales[this.currentLocale]?.[key] || this.locales[this.cfg.defaultLocale]?.[key];
59
+ }
60
+ }
61
+ exports.TranslationService = TranslationService;
@@ -0,0 +1,60 @@
1
+ export interface FitImagesCfg {
2
+ /**
3
+ * Container of the images
4
+ */
5
+ containerElement: HTMLElement;
6
+ /**
7
+ * Array of image metadatas (most notably: aspectRatio).
8
+ */
9
+ images: FitImage[];
10
+ /**
11
+ * Will be called on each layout change.
12
+ * Should be listened to to update the width/height of the images in your DOM.
13
+ */
14
+ onChange: (images: FitImage[]) => any;
15
+ /**
16
+ * Max image height in pixels.
17
+ *
18
+ * @default 300
19
+ */
20
+ maxHeight?: number;
21
+ /**
22
+ * Margin between images.
23
+ *
24
+ * @default 8
25
+ */
26
+ margin?: number;
27
+ }
28
+ export interface FitImage {
29
+ src: string;
30
+ /**
31
+ * width divided by height
32
+ */
33
+ aspectRatio: number;
34
+ /**
35
+ * Calculated image width to fit the layout.
36
+ */
37
+ fitWidth?: number;
38
+ /**
39
+ * Calculated image height to fit the layout.
40
+ */
41
+ fitHeight?: number;
42
+ }
43
+ /**
44
+ * Calculates the width/height of the images to fit in the layout.
45
+ *
46
+ * Currently does not mutate the cfg.images array, but DOES mutate individual images with .fitWidth, .fitHeight properties.
47
+ *
48
+ * @experimental
49
+ */
50
+ export declare class ImageFitter {
51
+ constructor(cfg: FitImagesCfg);
52
+ cfg: Required<FitImagesCfg>;
53
+ resizeObserver: ResizeObserver;
54
+ containerWidth: number;
55
+ stop(): void;
56
+ private update;
57
+ private doLayout;
58
+ private getHeigth;
59
+ private setHeight;
60
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ImageFitter = void 0;
4
+ /**
5
+ * Calculates the width/height of the images to fit in the layout.
6
+ *
7
+ * Currently does not mutate the cfg.images array, but DOES mutate individual images with .fitWidth, .fitHeight properties.
8
+ *
9
+ * @experimental
10
+ */
11
+ class ImageFitter {
12
+ constructor(cfg) {
13
+ this.containerWidth = -1;
14
+ this.cfg = {
15
+ maxHeight: 300,
16
+ margin: 8,
17
+ ...cfg,
18
+ };
19
+ this.resizeObserver = new ResizeObserver(entries => this.update(entries));
20
+ this.resizeObserver.observe(cfg.containerElement);
21
+ }
22
+ stop() {
23
+ this.resizeObserver.disconnect();
24
+ }
25
+ update(entries) {
26
+ const width = Math.floor(entries[0].contentRect.width);
27
+ if (width === this.containerWidth)
28
+ return; // we're only interested in width changes
29
+ this.containerWidth = width;
30
+ console.log(`resize ${width}`);
31
+ this.doLayout(this.cfg.images);
32
+ this.cfg.onChange(this.cfg.images);
33
+ }
34
+ doLayout(imgs) {
35
+ if (imgs.length === 0)
36
+ return; // nothing to do
37
+ const { maxHeight } = this.cfg;
38
+ let imgNodes = imgs.slice(0);
39
+ w: while (imgNodes.length > 0) {
40
+ let slice;
41
+ let h;
42
+ for (let i = 1; i <= imgNodes.length; i++) {
43
+ slice = imgNodes.slice(0, i);
44
+ h = this.getHeigth(slice);
45
+ if (h < maxHeight) {
46
+ this.setHeight(slice, h);
47
+ imgNodes = imgNodes.slice(i);
48
+ continue w;
49
+ }
50
+ }
51
+ this.setHeight(slice, Math.min(maxHeight, h));
52
+ break;
53
+ }
54
+ }
55
+ getHeigth(images) {
56
+ const width = this.containerWidth - images.length * this.cfg.margin;
57
+ let r = 0;
58
+ images.forEach(img => (r += img.aspectRatio));
59
+ return width / r; // have to round down because Firefox will automatically roundup value with number of decimals > 3
60
+ }
61
+ // mutates/sets images' fitWidth, fitHeight properties
62
+ setHeight(images, height) {
63
+ images.forEach(img => {
64
+ img.fitWidth = Math.floor(height * img.aspectRatio);
65
+ img.fitHeight = Math.floor(height);
66
+ });
67
+ }
68
+ }
69
+ exports.ImageFitter = ImageFitter;
@@ -0,0 +1,14 @@
1
+ export type LoadScriptOptions = Partial<HTMLScriptElement>;
2
+ export type LoadCSSOptions = Partial<HTMLLinkElement>;
3
+ /**
4
+ * opt.async defaults to `true`.
5
+ * No other options are set by default.
6
+ */
7
+ export declare function loadScript(src: string, opt?: LoadScriptOptions): Promise<void>;
8
+ /**
9
+ * Default options:
10
+ * rel: 'stylesheet'
11
+ *
12
+ * No other options are set by default.
13
+ */
14
+ export declare function loadCSS(href: string, opt?: LoadCSSOptions): Promise<void>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadScript = loadScript;
4
+ exports.loadCSS = loadCSS;
5
+ const env_1 = require("../env");
6
+ const types_1 = require("../types");
7
+ /**
8
+ * opt.async defaults to `true`.
9
+ * No other options are set by default.
10
+ */
11
+ async function loadScript(src, opt) {
12
+ if ((0, env_1.isServerSide)())
13
+ return;
14
+ return await new Promise((resolve, reject) => {
15
+ const s = (0, types_1._objectAssign)(document.createElement('script'), {
16
+ src,
17
+ async: true,
18
+ ...opt,
19
+ onload: resolve,
20
+ onerror: (_event, _source, _lineno, _colno, err) => {
21
+ reject(err || new Error(`loadScript failed: ${src}`));
22
+ },
23
+ });
24
+ document.head.append(s);
25
+ });
26
+ }
27
+ /**
28
+ * Default options:
29
+ * rel: 'stylesheet'
30
+ *
31
+ * No other options are set by default.
32
+ */
33
+ async function loadCSS(href, opt) {
34
+ if ((0, env_1.isServerSide)())
35
+ return;
36
+ return await new Promise((resolve, reject) => {
37
+ const link = (0, types_1._objectAssign)(document.createElement('link'), {
38
+ href,
39
+ rel: 'stylesheet',
40
+ // type seems to be unnecessary: https://stackoverflow.com/a/5409146/4919972
41
+ // type: 'text/css',
42
+ ...opt,
43
+ onload: resolve,
44
+ onerror: (_event, _source, _lineno, _colno, err) => {
45
+ reject(err || new Error(`loadCSS failed: ${href}`));
46
+ },
47
+ });
48
+ document.head.append(link);
49
+ });
50
+ }
@@ -0,0 +1,23 @@
1
+ export interface TopBarOptions {
2
+ /**
3
+ * @default true
4
+ */
5
+ autoRun?: boolean;
6
+ /**
7
+ * @default 5
8
+ */
9
+ barThickness?: number;
10
+ barColors?: any;
11
+ shadowColor?: any;
12
+ /**
13
+ * @default 10
14
+ */
15
+ shadowBlur?: number;
16
+ }
17
+ export declare const topbar: {
18
+ config(opts: TopBarOptions): void;
19
+ set(show: boolean, opts?: TopBarOptions): void;
20
+ show(opts?: TopBarOptions): void;
21
+ progress(to: number | string): any;
22
+ hide(): void;
23
+ };