@projektemacher/storymapjs 0.10.6 → 0.10.7

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 (148) hide show
  1. package/CHANGELOG +11 -0
  2. package/dist/assets/{demo-CVQ7kFek.js → demo-D6cTmzn6.js} +1 -1
  3. package/dist/assets/{harness-C-VZV-ou.js → harness-BPFlzutp.js} +1 -1
  4. package/dist/assets/main-Y-MYUSJ4.js +17 -0
  5. package/dist/css/fonts/font.oldstandard.css +18 -22
  6. package/dist/demo.html +2 -2
  7. package/dist/docs/migration.html +1 -1
  8. package/dist/embed/index.html +129 -4
  9. package/dist/harness.html +2 -2
  10. package/dist/index.html +17 -0
  11. package/dist/js/storymap.d.ts +126 -7
  12. package/dist/js/storymap.js +4518 -3613
  13. package/dist/js/storymap.js.map +1 -1
  14. package/docs/migration-from-knightlab.md +139 -0
  15. package/docs/storymap-as-iiif-manifest.md +510 -0
  16. package/package.json +6 -3
  17. package/src/animation/easings.ts +18 -0
  18. package/src/animation/morpheus.d.ts +41 -0
  19. package/src/core/Browser.ts +19 -0
  20. package/src/core/Load.ts +180 -0
  21. package/src/core/Util.ts +354 -0
  22. package/src/core/mixins.ts +171 -0
  23. package/src/dom/Dom.ts +31 -0
  24. package/src/dom/DomEvent.ts +61 -0
  25. package/src/globals.d.ts +16 -0
  26. package/src/language/Language.ts +107 -0
  27. package/src/language/locale/be.json +18 -0
  28. package/src/language/locale/bg.json +18 -0
  29. package/src/language/locale/cs.json +18 -0
  30. package/src/language/locale/de.json +20 -0
  31. package/src/language/locale/el.json +18 -0
  32. package/src/language/locale/en.json +28 -0
  33. package/src/language/locale/es.json +18 -0
  34. package/src/language/locale/et.json +18 -0
  35. package/src/language/locale/fr.json +18 -0
  36. package/src/language/locale/he.json +18 -0
  37. package/src/language/locale/hu.json +18 -0
  38. package/src/language/locale/is.json +18 -0
  39. package/src/language/locale/it.json +18 -0
  40. package/src/language/locale/jp.json +18 -0
  41. package/src/language/locale/ko.json +18 -0
  42. package/src/language/locale/nl.json +18 -0
  43. package/src/language/locale/nn.json +18 -0
  44. package/src/language/locale/no.json +18 -0
  45. package/src/language/locale/pl.json +18 -0
  46. package/src/language/locale/pt.json +18 -0
  47. package/src/language/locale/ru.json +18 -0
  48. package/src/language/locale/sk.json +18 -0
  49. package/src/language/locale/sr.json +17 -0
  50. package/src/language/locale/sv.json +18 -0
  51. package/src/language/locale/tr.json +18 -0
  52. package/src/language/locale/uk.json +18 -0
  53. package/src/language/locale/ur.json +18 -0
  54. package/src/language/locale/zh-cn.json +18 -0
  55. package/src/language/locale/zh-tw.json +18 -0
  56. package/src/main.ts +51 -0
  57. package/src/map/Map.ts +717 -0
  58. package/src/map/MapMarker.ts +149 -0
  59. package/src/map/openlayers/Map.OpenLayers.ts +1879 -0
  60. package/src/map/openlayers/MapMarker.OpenLayers.ts +176 -0
  61. package/src/map/openlayers/zoomifyTiles.ts +28 -0
  62. package/src/map/types.ts +17 -0
  63. package/src/media/EmbedUtil.ts +168 -0
  64. package/src/media/Media.ts +409 -0
  65. package/src/media/MediaType.ts +163 -0
  66. package/src/media/types/Audio.ts +87 -0
  67. package/src/media/types/Blockquote.ts +40 -0
  68. package/src/media/types/DailyMotion.ts +58 -0
  69. package/src/media/types/DocumentCloud.ts +41 -0
  70. package/src/media/types/Facebook.ts +41 -0
  71. package/src/media/types/Flickr.ts +115 -0
  72. package/src/media/types/GoogleDoc.ts +59 -0
  73. package/src/media/types/IFrame.ts +51 -0
  74. package/src/media/types/Image.ts +49 -0
  75. package/src/media/types/Juxtapose.ts +32 -0
  76. package/src/media/types/SoundCloud.ts +77 -0
  77. package/src/media/types/Text.ts +172 -0
  78. package/src/media/types/Twitter.ts +96 -0
  79. package/src/media/types/Video.ts +84 -0
  80. package/src/media/types/Vimeo.ts +65 -0
  81. package/src/media/types/Website.ts +36 -0
  82. package/src/media/types/Wikipedia.ts +124 -0
  83. package/src/media/types/YouTube.ts +192 -0
  84. package/src/scss/Typography.scss +237 -0
  85. package/src/scss/VCO.StoryMap.Dark.scss +138 -0
  86. package/src/scss/VCO.StoryMap.scss +164 -0
  87. package/src/scss/Variables.Dark.scss +56 -0
  88. package/src/scss/Variables.scss +47 -0
  89. package/src/scss/core/Mixins.scss +112 -0
  90. package/src/scss/core/Reset.scss +206 -0
  91. package/src/scss/fonts/_font.base.scss +113 -0
  92. package/src/scss/fonts/font.abril-droidsans.scss +75 -0
  93. package/src/scss/fonts/font.amatic-andika.scss +73 -0
  94. package/src/scss/fonts/font.bitter-raleway.scss +86 -0
  95. package/src/scss/fonts/font.clicker-garamond.scss +92 -0
  96. package/src/scss/fonts/font.dancing-ledger.scss +74 -0
  97. package/src/scss/fonts/font.default.scss +9 -0
  98. package/src/scss/fonts/font.fjalla-average.scss +80 -0
  99. package/src/scss/fonts/font.georgia-helvetica.scss +58 -0
  100. package/src/scss/fonts/font.knightlab.scss +97 -0
  101. package/src/scss/fonts/font.lustria-lato.scss +80 -0
  102. package/src/scss/fonts/font.medula-lato.scss +73 -0
  103. package/src/scss/fonts/font.oldstandard.scss +88 -0
  104. package/src/scss/fonts/font.opensans-gentiumbook.scss +80 -0
  105. package/src/scss/fonts/font.playfair-faunaone.scss +86 -0
  106. package/src/scss/fonts/font.playfair.scss +85 -0
  107. package/src/scss/fonts/font.pt.scss +90 -0
  108. package/src/scss/fonts/font.roboto-megrim.scss +73 -0
  109. package/src/scss/fonts/font.rufina-sintony.scss +73 -0
  110. package/src/scss/fonts/font.ubuntu.scss +78 -0
  111. package/src/scss/fonts/font.unicaone-vollkorn.scss +85 -0
  112. package/src/scss/icons/Icons.scss +239 -0
  113. package/src/scss/map/VCO.ImageMarker.scss +23 -0
  114. package/src/scss/map/VCO.Map.scss +21 -0
  115. package/src/scss/map/VCO.MapMarker.scss +147 -0
  116. package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +126 -0
  117. package/src/scss/media/VCO.Media.scss +222 -0
  118. package/src/scss/media/types/VCO.Media.Blockquote.scss +76 -0
  119. package/src/scss/media/types/VCO.Media.IFrame.scss +11 -0
  120. package/src/scss/media/types/VCO.Media.Image.scss +13 -0
  121. package/src/scss/media/types/VCO.Media.Instagram.scss +7 -0
  122. package/src/scss/media/types/VCO.Media.Profile.scss +5 -0
  123. package/src/scss/media/types/VCO.Media.Text.scss +90 -0
  124. package/src/scss/media/types/VCO.Media.Twitter.scss +87 -0
  125. package/src/scss/media/types/VCO.Media.Wikipedia.scss +64 -0
  126. package/src/scss/site/site.scss +748 -0
  127. package/src/scss/slider/VCO.Slide.scss +447 -0
  128. package/src/scss/slider/VCO.SlideNav.scss +338 -0
  129. package/src/scss/slider/VCO.StorySlider.scss +64 -0
  130. package/src/scss/snapmap/VCO.SnapMap.scss +103 -0
  131. package/src/scss/ui/VCO.Consent.scss +77 -0
  132. package/src/scss/ui/VCO.MenuBar.Button.scss +99 -0
  133. package/src/scss/ui/VCO.MenuBar.scss +20 -0
  134. package/src/scss/ui/VCO.Message.scss +104 -0
  135. package/src/site/site.ts +87 -0
  136. package/src/slider/Slide.ts +460 -0
  137. package/src/slider/SlideNav.ts +212 -0
  138. package/src/slider/StorySlider.ts +722 -0
  139. package/src/storymap/Consent.ts +146 -0
  140. package/src/storymap/StoryMap.ts +1108 -0
  141. package/src/storymap/iiif.ts +386 -0
  142. package/src/storymap/validate.ts +184 -0
  143. package/src/types.ts +297 -0
  144. package/src/ui/Draggable.ts +374 -0
  145. package/src/ui/MenuBar.ts +323 -0
  146. package/src/ui/Message.ts +118 -0
  147. package/src/ui/Swipable.ts +388 -0
  148. package/dist/assets/main-DwbXrrYn.js +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projektemacher/storymapjs",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "description": "StoryMapJS viewer library — unofficial TypeScript/OpenLayers rewrite of the Knight Lab StoryMapJS (viewer only)",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",
@@ -8,7 +8,10 @@
8
8
  "author": "Christian Mahnke <194820+cmahnke@users.noreply.github.com>",
9
9
  "files": [
10
10
  "dist",
11
+ "src",
11
12
  "schema",
13
+ "docs/migration-from-knightlab.md",
14
+ "docs/storymap-as-iiif-manifest.md",
12
15
  "README.md",
13
16
  "CHANGELOG"
14
17
  ],
@@ -87,8 +90,8 @@
87
90
  "typescript": "^5.9.3",
88
91
  "typescript-eslint": "^8.70.1",
89
92
  "unplugin-dts": "^1.1.1",
90
- "vite": "^8.3.0",
91
- "vitest": "^5.0.1"
93
+ "vite": "^8.3.1",
94
+ "vitest": "^5.0.2"
92
95
  },
93
96
  "scripts": {
94
97
  "dev": "vite",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Easing functions used across the viewer, sourced from maintained
3
+ * dependencies instead of the previously vendored Penner/KeySpline code:
4
+ *
5
+ * - d3-ease (BSD-3): polynomial and exponential easings
6
+ * - bezier-easing (MIT, by the KeySpline author): cubic-bezier sampling
7
+ */
8
+ import { easeExpOut, easePoly } from "d3-ease";
9
+ import bezierEasing from "bezier-easing";
10
+
11
+ /** Acceleration until halfway, then deceleration (quintic). */
12
+ export const easeInOutQuint = easePoly.exponent(5);
13
+
14
+ /** Strong exponential ease-out. */
15
+ export const easeOutStrong = easeExpOut;
16
+
17
+ /** CSS `ease-in` cubic-bezier(0.42, 0, 1.0, 1.0), sampled. */
18
+ export const easeInSpline = bezierEasing(0.42, 0, 1.0, 1.0);
@@ -0,0 +1,41 @@
1
+ // Type declarations for the morpheus animation library (https://github.com/ded/morpheus)
2
+ declare module "morpheus" {
3
+ interface MorpheusOptions extends Record<string, unknown> {
4
+ duration?: number;
5
+ easing?: unknown;
6
+ complete?: unknown;
7
+ bezier?: number[][];
8
+ left?: string | number;
9
+ top?: string | number;
10
+ }
11
+
12
+ interface MorpheusHandle {
13
+ stop: (jump?: boolean) => void;
14
+ }
15
+
16
+ interface Morpheus {
17
+ (
18
+ elements: HTMLElement | HTMLElement[] | NodeListOf<HTMLElement>,
19
+ options: MorpheusOptions,
20
+ ): MorpheusHandle;
21
+ tween(
22
+ this: unknown,
23
+ duration: number,
24
+ fn: (pos: number) => void,
25
+ done?: () => void,
26
+ ease?: ((t: number) => number) | string,
27
+ from?: number,
28
+ to?: number,
29
+ ): MorpheusHandle;
30
+ getStyle(el: HTMLElement, property: string): unknown;
31
+ bezier(points: number[][], pos: number): number[];
32
+ transform: string;
33
+ parseTransform(style: string, base?: Record<string, number>): Record<string, number>;
34
+ formatTransform(values: Record<string, number>): string;
35
+ animationFrame(callback: (timestamp: number) => void): number;
36
+ easings: Record<string, (t: number) => number>;
37
+ }
38
+
39
+ const morpheus: Morpheus;
40
+ export = morpheus;
41
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ Browser - the only feature detection StoryMapJS still needs.
3
+ 2022+ browsers only: no vendor sniffing, no legacy engine flags.
4
+ */
5
+
6
+ export const touch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
7
+
8
+ /** Coarse-pointer devices (phones, tablets) - drives the mobile/skinny layouts. */
9
+ export const mobile = window.matchMedia?.("(pointer: coarse)").matches ?? false;
10
+
11
+ export const Browser = {
12
+ touch: touch,
13
+ mobile: mobile,
14
+ orientation: function (): "landscape" | "portrait" {
15
+ const w = window.innerWidth;
16
+ const h = window.innerHeight;
17
+ return w > h ? "landscape" : "portrait";
18
+ },
19
+ };
@@ -0,0 +1,180 @@
1
+ /* Load
2
+ Loads external Javascript and CSS using plain DOM element injection.
3
+ Modern browsers preserve execution order for dynamically inserted
4
+ scripts with async=false, so no queueing machinery is needed.
5
+ ================================================== */
6
+
7
+ export interface LoadOptions {
8
+ /** Abort the load (removes the element, rejects the promise). */
9
+ signal?: AbortSignal;
10
+ }
11
+
12
+ function loadElement(
13
+ tag: "script" | "link",
14
+ attrs: Record<string, string>,
15
+ options?: LoadOptions,
16
+ ): Promise<void> {
17
+ return new Promise((resolve, reject) => {
18
+ if (options?.signal?.aborted) {
19
+ reject(new DOMException("Load aborted", "AbortError"));
20
+ return;
21
+ }
22
+ const node = document.createElement(tag);
23
+ for (const attr in attrs) {
24
+ if (Object.hasOwn(attrs, attr)) {
25
+ node.setAttribute(attr, attrs[attr]);
26
+ }
27
+ }
28
+ if (tag === "script") {
29
+ (node as HTMLScriptElement).async = false;
30
+ }
31
+ const cleanup = () => {
32
+ node.onload = null;
33
+ node.onerror = null;
34
+ options?.signal?.removeEventListener("abort", onAbort);
35
+ };
36
+ const onAbort = () => {
37
+ cleanup();
38
+ node.remove();
39
+ reject(new DOMException("Load aborted", "AbortError"));
40
+ };
41
+ node.onload = () => {
42
+ cleanup();
43
+ resolve();
44
+ };
45
+ node.onerror = () => {
46
+ cleanup();
47
+ reject(new Error(`Failed to load ${attrs.src ?? attrs.href}`));
48
+ };
49
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
50
+ document.head.appendChild(node);
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Append a script to the document head.
56
+ *
57
+ * @param url - The script URL.
58
+ * @param options - Optional AbortSignal to cancel an in-flight load.
59
+ * @returns Resolves when the script has loaded, rejects on error or abort.
60
+ */
61
+ function loadJS(url: string, options?: LoadOptions): Promise<void> {
62
+ return loadElement("script", { src: url }, options);
63
+ }
64
+
65
+ /**
66
+ * Append one stylesheet to the document head.
67
+ *
68
+ * @param url - The stylesheet URL.
69
+ * @param options - Optional AbortSignal to cancel an in-flight load.
70
+ * @returns Resolves when the stylesheet has loaded, rejects on error or abort.
71
+ */
72
+ function loadCSS(url: string, options?: LoadOptions): Promise<void> {
73
+ return loadElement("link", { href: url, rel: "stylesheet" }, options);
74
+ }
75
+
76
+ export interface JSONPOptions extends LoadOptions {
77
+ /** Milliseconds to wait for the callback before rejecting (default 15000). */
78
+ timeout?: number;
79
+ }
80
+
81
+ let globalNameCounter = 0;
82
+
83
+ /**
84
+ * Claim a free global callback slot: names derived from content (e.g. a
85
+ * Wikipedia article title) are not unique per request, and concurrent loads
86
+ * sharing a name would clobber each other's handler — delivering payloads to
87
+ * the wrong promise and leaving the loser's script calling a deleted global
88
+ * (Uncaught ReferenceError).
89
+ *
90
+ * @param base - The preferred global function name.
91
+ * @returns `base` when free, otherwise `base` with a numeric suffix.
92
+ */
93
+ function uniqueGlobalName(base: string): string {
94
+ const globals = window as unknown as Record<string, unknown>;
95
+ if (!Object.hasOwn(globals, base)) {
96
+ return base;
97
+ }
98
+ let candidate: string;
99
+ do {
100
+ globalNameCounter += 1;
101
+ candidate = `${base}_${globalNameCounter}`;
102
+ } while (Object.hasOwn(globals, candidate));
103
+ return candidate;
104
+ }
105
+
106
+ /**
107
+ * Load a JSONP endpoint: injects a script that calls a global function with
108
+ * its payload, and resolves with that payload. The global is removed and the
109
+ * script element detached once settled.
110
+ *
111
+ * @param url - The full JSONP URL (must include the callback parameter).
112
+ * @param callbackName - The global function name the endpoint will invoke.
113
+ * Must be claimed via uniqueGlobalName when concurrent loads may share it.
114
+ * @param options - Optional AbortSignal and timeout.
115
+ * @returns Resolves with the JSONP payload, rejects on error, abort or timeout.
116
+ */
117
+ function loadJSONP<T>(url: string, callbackName: string, options?: JSONPOptions): Promise<T> {
118
+ return new Promise((resolve, reject) => {
119
+ if (options?.signal?.aborted) {
120
+ reject(new DOMException("Load aborted", "AbortError"));
121
+ return;
122
+ }
123
+ const globals = window as unknown as Record<string, unknown>;
124
+ const script = document.createElement("script");
125
+ let settled = false;
126
+ const cleanup = () => {
127
+ settled = true;
128
+ clearTimeout(timer);
129
+ script.remove();
130
+ if (globals[callbackName] === onCallback) {
131
+ delete globals[callbackName];
132
+ }
133
+ options?.signal?.removeEventListener("abort", onAbort);
134
+ };
135
+ // Abandon without deleting: an already-fetched script may still
136
+ // execute afterwards, and calling a deleted global throws an
137
+ // Uncaught ReferenceError. The self-deleting stub swallows it.
138
+ const abandon = () => {
139
+ settled = true;
140
+ clearTimeout(timer);
141
+ script.remove();
142
+ if (globals[callbackName] === onCallback) {
143
+ globals[callbackName] = () => {
144
+ delete globals[callbackName];
145
+ };
146
+ }
147
+ options?.signal?.removeEventListener("abort", onAbort);
148
+ };
149
+ const onAbort = () => {
150
+ if (!settled) {
151
+ abandon();
152
+ reject(new DOMException("Load aborted", "AbortError"));
153
+ }
154
+ };
155
+ const onCallback = (data: T) => {
156
+ if (!settled) {
157
+ cleanup();
158
+ resolve(data);
159
+ }
160
+ };
161
+ const timer = setTimeout(() => {
162
+ if (!settled) {
163
+ abandon();
164
+ reject(new Error(`JSONP request timed out: ${url}`));
165
+ }
166
+ }, options?.timeout ?? 15000);
167
+ globals[callbackName] = onCallback;
168
+ script.onerror = () => {
169
+ if (!settled) {
170
+ abandon();
171
+ reject(new Error(`Failed to load ${url}`));
172
+ }
173
+ };
174
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
175
+ script.src = url;
176
+ document.body.appendChild(script);
177
+ });
178
+ }
179
+
180
+ export { loadJS, loadCSS, loadJSONP, uniqueGlobalName };
@@ -0,0 +1,354 @@
1
+ // const debug = true;
2
+
3
+ export function extend<T extends Record<string, unknown>>(
4
+ dest: T,
5
+ ...sources: (Record<string, unknown> | null | undefined)[]
6
+ ): T {
7
+ // merge src properties into dest
8
+ sources = sources.filter(Boolean);
9
+ for (let j = 0, len = sources.length, src; j < len; j++) {
10
+ src = sources[j] || {};
11
+ for (const i in src) {
12
+ if (Object.hasOwn(src, i)) {
13
+ (dest as Record<string, unknown>)[i] = src[i];
14
+ }
15
+ }
16
+ }
17
+ return dest;
18
+ }
19
+
20
+ export function convertUnixTime(str: string): string {
21
+ // created for Instagram. It's ISO8601-ish
22
+ // 2013-12-09 01:56:28
23
+ const pattern = /^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2}):(\d{2})/;
24
+ const date_parts = str.match(pattern)?.slice(1);
25
+ if (!date_parts) {
26
+ return str;
27
+ }
28
+ const date_array = [];
29
+ for (let i = 0; i < date_parts.length; i++) {
30
+ let val = parseInt(date_parts[i]);
31
+ if (i === 1) {
32
+ val = val - 1;
33
+ } // stupid javascript months
34
+ date_array.push(val);
35
+ }
36
+ const date = new Date(
37
+ date_array[0],
38
+ date_array[1],
39
+ date_array[2],
40
+ date_array[3],
41
+ date_array[4],
42
+ date_array[5],
43
+ );
44
+ const months = [
45
+ "Jan",
46
+ "Feb",
47
+ "Mar",
48
+ "Apr",
49
+ "May",
50
+ "Jun",
51
+ "Jul",
52
+ "Aug",
53
+ "Sep",
54
+ "Oct",
55
+ "Nov",
56
+ "Dec",
57
+ ];
58
+ const year = date.getFullYear();
59
+ const month = months[date.getMonth()];
60
+ const day = date.getDate();
61
+ const time = month + ", " + day + " " + year;
62
+ return time;
63
+ }
64
+
65
+ export function setData(obj: { data?: unknown }, data: unknown): void {
66
+ obj.data = extend(
67
+ {} as Record<string, unknown>,
68
+ obj.data as Record<string, unknown> | undefined,
69
+ data as Record<string, unknown> | undefined,
70
+ );
71
+ const stored = obj.data as Record<string, unknown>;
72
+ if (stored.uniqueid === "") {
73
+ stored.uniqueid = unique_ID(6);
74
+ }
75
+ }
76
+
77
+ export function mergeData<T extends object>(data_main: T, data_to_merge: object): T {
78
+ const target = data_main as Record<string, unknown>;
79
+ const source = data_to_merge as Record<string, unknown>;
80
+ let x;
81
+ for (x in source) {
82
+ if (Object.prototype.hasOwnProperty.call(source, x)) {
83
+ target[x] = source[x];
84
+ }
85
+ }
86
+ return data_main;
87
+ }
88
+
89
+ /**
90
+ * Like mergeData, except will only try to copy data that already exists
91
+ * in data_main
92
+ */
93
+ export function updateData<T extends object>(data_main: T, data_to_merge: object): T {
94
+ const target = data_main as Record<string, unknown>;
95
+ const source = data_to_merge as Record<string, unknown>;
96
+ let x;
97
+ for (x in data_main) {
98
+ if (Object.prototype.hasOwnProperty.call(source, x)) {
99
+ target[x] = source[x];
100
+ }
101
+ }
102
+ return data_main;
103
+ }
104
+
105
+ let _lastStampId = 0;
106
+ const _stampKey = "_vco_id";
107
+
108
+ /** Stamp an object (or function) with a unique id and return it. */
109
+ export function stamp(obj: object): number {
110
+ const target = obj as Record<string, unknown>;
111
+ target[_stampKey] = target[_stampKey] || ++_lastStampId;
112
+ return target[_stampKey] as number;
113
+ }
114
+
115
+ export function findArrayNumberByUniqueID(
116
+ id: unknown,
117
+ array: { data: Record<string, unknown> }[],
118
+ prop: string,
119
+ ): number {
120
+ let _n = 0;
121
+ for (let i = 0; i < array.length; i++) {
122
+ if (array[i].data[prop] === id) {
123
+ _n = i;
124
+ }
125
+ }
126
+ return _n;
127
+ }
128
+
129
+ export function unique_ID(size: number, prefix?: string): string {
130
+ function getRandomNumber(range: number): number {
131
+ return Math.floor(Math.random() * range);
132
+ }
133
+ function getRandomChar(): string {
134
+ const chars = "abcdefghijklmnopqurstuvwxyz";
135
+ return chars.charAt(getRandomNumber(chars.length));
136
+ }
137
+ function randomID(size: number): string {
138
+ let str = "";
139
+ for (let i = 0; i < size; i++) {
140
+ str += getRandomChar();
141
+ }
142
+ return str;
143
+ }
144
+ if (prefix) {
145
+ return prefix + "-" + randomID(size);
146
+ } else {
147
+ return "vco-" + randomID(size);
148
+ }
149
+ }
150
+
151
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
152
+ // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
153
+ const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
154
+ hex = hex.replace(shorthandRegex, function (m: string, r: string, g: string, b: string) {
155
+ return r + r + g + g + b + b;
156
+ });
157
+
158
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
159
+ return result
160
+ ? {
161
+ r: parseInt(result[1], 16),
162
+ g: parseInt(result[2], 16),
163
+ b: parseInt(result[3], 16),
164
+ }
165
+ : null;
166
+ }
167
+
168
+ // CSS named colors (Colors Level 4) for parseCssColor below.
169
+ const NAMED_COLORS: Record<string, [number, number, number]> = {
170
+ aliceblue: [240, 248, 255], antiquewhite: [250, 235, 215], aqua: [0, 255, 255],
171
+ aquamarine: [127, 255, 212], azure: [240, 255, 255], beige: [245, 245, 220],
172
+ bisque: [255, 228, 196], black: [0, 0, 0], blanchedalmond: [255, 235, 205],
173
+ blue: [0, 0, 255], blueviolet: [138, 43, 226], brown: [165, 42, 42],
174
+ burlywood: [222, 184, 135], cadetblue: [95, 158, 160], chartreuse: [127, 255, 0],
175
+ chocolate: [210, 105, 30], coral: [255, 127, 80], cornflowerblue: [100, 149, 237],
176
+ cornsilk: [255, 248, 220], crimson: [220, 20, 60], cyan: [0, 255, 255],
177
+ darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgoldenrod: [184, 134, 11],
178
+ darkgray: [169, 169, 169], darkgreen: [0, 100, 0], darkgrey: [169, 169, 169],
179
+ darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47],
180
+ darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0],
181
+ darksalmon: [233, 150, 122], darkseagreen: [143, 188, 143], darkslateblue: [72, 61, 139],
182
+ darkslategray: [47, 79, 79], darkslategrey: [47, 79, 79], darkturquoise: [0, 206, 209],
183
+ darkviolet: [148, 0, 211], deeppink: [255, 20, 147], deepskyblue: [0, 191, 255],
184
+ dimgray: [105, 105, 105], dimgrey: [105, 105, 105], dodgerblue: [30, 144, 255],
185
+ firebrick: [178, 34, 34], floralwhite: [255, 250, 240], forestgreen: [34, 139, 34],
186
+ fuchsia: [255, 0, 255], gainsboro: [220, 220, 220], ghostwhite: [248, 248, 255],
187
+ gold: [255, 215, 0], goldenrod: [218, 165, 32], gray: [128, 128, 128],
188
+ green: [0, 128, 0], greenyellow: [173, 255, 47], grey: [128, 128, 128],
189
+ honeydew: [240, 255, 240], hotpink: [255, 105, 180], indianred: [205, 92, 92],
190
+ indigo: [75, 0, 130], ivory: [255, 255, 240], khaki: [240, 230, 140],
191
+ lavender: [230, 230, 250], lavenderblush: [255, 240, 245], lawngreen: [124, 252, 0],
192
+ lemonchiffon: [255, 250, 205], lightblue: [173, 216, 230], lightcoral: [240, 128, 128],
193
+ lightcyan: [224, 255, 255], lightgoldenrodyellow: [250, 250, 210], lightgray: [211, 211, 211],
194
+ lightgreen: [144, 238, 144], lightgrey: [211, 211, 211], lightpink: [255, 182, 193],
195
+ lightsalmon: [255, 160, 122], lightseagreen: [32, 178, 170], lightskyblue: [135, 206, 250],
196
+ lightslategray: [119, 136, 153], lightslategrey: [119, 136, 153], lightsteelblue: [176, 196, 222],
197
+ lightyellow: [255, 255, 224], lime: [0, 255, 0], limegreen: [50, 205, 50],
198
+ linen: [250, 240, 230], magenta: [255, 0, 255], maroon: [128, 0, 0],
199
+ mediumaquamarine: [102, 205, 170], mediumblue: [0, 0, 205], mediumorchid: [186, 85, 211],
200
+ mediumpurple: [147, 112, 219], mediumseagreen: [60, 179, 113], mediumslateblue: [123, 104, 238],
201
+ mediumspringgreen: [0, 250, 154], mediumturquoise: [72, 209, 204], mediumvioletred: [199, 21, 133],
202
+ midnightblue: [25, 25, 112], mintcream: [245, 255, 250], mistyrose: [255, 228, 225],
203
+ moccasin: [255, 228, 181], navajowhite: [255, 222, 173], navy: [0, 0, 128],
204
+ oldlace: [253, 245, 230], olive: [128, 128, 0], olivedrab: [107, 142, 35],
205
+ orange: [255, 165, 0], orangered: [255, 69, 0], orchid: [218, 112, 214],
206
+ palegoldenrod: [238, 232, 170], palegreen: [152, 251, 152], paleturquoise: [175, 238, 238],
207
+ palevioletred: [219, 112, 147], papayawhip: [255, 239, 213], peachpuff: [255, 218, 185],
208
+ peru: [205, 133, 63], pink: [255, 192, 203], plum: [221, 160, 221],
209
+ powderblue: [176, 224, 230], purple: [128, 0, 128], rebeccapurple: [102, 51, 153],
210
+ red: [255, 0, 0], rosybrown: [188, 143, 143], royalblue: [65, 105, 225],
211
+ saddlebrown: [139, 69, 19], salmon: [250, 128, 114], sandybrown: [244, 164, 96],
212
+ seagreen: [46, 139, 87], seashell: [255, 245, 238], sienna: [160, 82, 45],
213
+ silver: [192, 192, 192], skyblue: [135, 206, 235], slateblue: [106, 90, 205],
214
+ slategray: [112, 128, 144], slategrey: [112, 128, 144], snow: [255, 250, 250],
215
+ springgreen: [0, 255, 127], steelblue: [70, 130, 180], tan: [210, 180, 140],
216
+ teal: [0, 128, 128], thistle: [216, 191, 216], tomato: [255, 99, 71],
217
+ turquoise: [64, 224, 208], violet: [238, 130, 238], wheat: [245, 222, 179],
218
+ white: [255, 255, 255], whitesmoke: [245, 245, 245], yellow: [255, 255, 0],
219
+ yellowgreen: [154, 205, 50],
220
+ };
221
+
222
+ function parseRgbComponent(part: string, max: number): number | null {
223
+ const percent = part.endsWith("%");
224
+ const value = parseFloat(part);
225
+ if (!isFinite(value)) {
226
+ return null;
227
+ }
228
+ const scaled = percent ? (value / 100) * max : value;
229
+ return Math.min(max, Math.max(0, Math.round(scaled)));
230
+ }
231
+
232
+ /**
233
+ * Parse any CSS color (hex, rgb()/rgba(), named colors) into {r, g, b}.
234
+ * Unlike hexToRgb this never throws on valid CSS input; returns null only
235
+ * for unparseable values so callers can fall back to a default.
236
+ */
237
+ export function parseCssColor(value: string): { r: number; g: number; b: number } | null {
238
+ const input = value.trim().toLowerCase();
239
+ const hex = hexToRgb(input);
240
+ if (hex) {
241
+ return hex;
242
+ }
243
+ const rgb = /^rgba?\(\s*([^,)]+)\s*,\s*([^,)]+)\s*,\s*([^,)]+)\s*(?:,\s*[^)]+\s*)?\)$/.exec(
244
+ input,
245
+ );
246
+ if (rgb) {
247
+ const r = parseRgbComponent(rgb[1], 255);
248
+ const g = parseRgbComponent(rgb[2], 255);
249
+ const b = parseRgbComponent(rgb[3], 255);
250
+ return r === null || g === null || b === null ? null : { r, g, b };
251
+ }
252
+ const named = NAMED_COLORS[input];
253
+ return named ? { r: named[0], g: named[1], b: named[2] } : null;
254
+ }
255
+
256
+ /**
257
+ * Glide duration for a slide jump, scaled with the distance so out-of-order
258
+ * navigation glides instead of flicking. Shared by the slider and the map
259
+ * so both animations stay in sync, and exposed for transition events.
260
+ */
261
+ export function slideTransitionDuration(from: number, to: number): number {
262
+ const steps = Math.abs(to - from);
263
+ return Math.max(600, Math.min(1000 + steps * 120, 2000));
264
+ }
265
+
266
+ export function htmlify(str: string): string {
267
+ //if (str.match(/<\s*p[^>]*>([^<]*)<\s*\/\s*p\s*>/)) {
268
+ if (str.match(/<p>[\s\S]*?<\/p>/)) {
269
+ return str;
270
+ } else {
271
+ return "<p>" + str + "</p>";
272
+ }
273
+ }
274
+
275
+ export function getUrlVars(string: string): string[] & Record<string, string> {
276
+ let str: string, hash: string[];
277
+ const vars: string[] = [];
278
+ str = string.toString();
279
+ if (str.match("&#038;")) {
280
+ str = str.replace("&#038;", "&");
281
+ } else if (str.match("&#38;")) {
282
+ str = str.replace("&#38;", "&");
283
+ } else if (str.match("&amp;")) {
284
+ str = str.replace("&amp;", "&");
285
+ }
286
+ const hashes = str.slice(str.indexOf("?") + 1).split("&");
287
+ for (let i = 0; i < hashes.length; i++) {
288
+ hash = hashes[i].split("=");
289
+ vars.push(hash[0]);
290
+ (vars as unknown as Record<string, string>)[hash[0]] = hash[1];
291
+ }
292
+ return vars as string[] & Record<string, string>;
293
+ }
294
+
295
+ export const ratio = {
296
+ r16_9: function (size: { w?: number; h?: number }): number {
297
+ if (size.w !== null && (size.w as unknown as string) !== "") {
298
+ return Math.round((size.w / 16) * 9);
299
+ } else if (size.h !== null && (size.h as unknown as string) !== "") {
300
+ return Math.round((size.h / 9) * 16);
301
+ } else {
302
+ return 0;
303
+ }
304
+ },
305
+ };
306
+
307
+ export function urljoin(base_url: string, path: string): string {
308
+ if (base_url.length && base_url.at(-1) === "/") {
309
+ base_url = base_url.substring(0, base_url.length - 1);
310
+ }
311
+ if (path.length && path[0] === "/") {
312
+ path = path.substring(1);
313
+ }
314
+ const url1 = base_url.split("/");
315
+ const url2 = path.split("/");
316
+ const url3 = [];
317
+ for (let i = 0, l = url1.length; i < l; i++) {
318
+ if (url1[i] === "..") {
319
+ url3.pop();
320
+ } else if (url1[i] === ".") {
321
+ continue;
322
+ } else {
323
+ url3.push(url1[i]);
324
+ }
325
+ }
326
+ for (let i = 0, l = url2.length; i < l; i++) {
327
+ if (url2[i] === "..") {
328
+ url3.pop();
329
+ } else if (url2[i] === ".") {
330
+ continue;
331
+ } else {
332
+ url3.push(url2[i]);
333
+ }
334
+ }
335
+ return url3.join("/");
336
+ }
337
+
338
+ export function getObjectAttributeByIndex(
339
+ obj: Record<string, unknown> | undefined,
340
+ index: number,
341
+ ): unknown {
342
+ if (typeof obj != "undefined") {
343
+ let i = 0;
344
+ for (const attr in obj) {
345
+ if (index === i) {
346
+ return obj[attr];
347
+ }
348
+ i++;
349
+ }
350
+ return "";
351
+ } else {
352
+ return "";
353
+ }
354
+ }