@design.estate/dees-domtools 2.5.3 → 2.5.6

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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-domtools',
6
- version: '2.5.3',
6
+ version: '2.5.6',
7
7
  description: 'A package providing tools to simplify complex CSS structures and web development tasks, featuring TypeScript support and integration with various web technologies.'
8
8
  }
@@ -5,6 +5,10 @@ import { WebSetup } from '@push.rocks/websetup';
5
5
  import { ThemeManager } from './domtools.classes.thememanager.js';
6
6
  import { Keyboard } from './domtools.classes.keyboard.js';
7
7
 
8
+ declare global {
9
+ var deesDomTools: DomTools | undefined;
10
+ }
11
+
8
12
  export interface IDomToolsState {
9
13
  virtualViewport: TViewport;
10
14
  jwt: string;
@@ -24,59 +28,43 @@ export class DomTools {
24
28
  * setups domtools
25
29
  */
26
30
  public static async setupDomTools(optionsArg: IDomToolsContructorOptions = {}): Promise<DomTools> {
27
- // If initialization is already in progress and we're not ignoring global, wait for it
28
- if (!optionsArg.ignoreGlobal && DomTools.initializationPromise) {
29
- return await DomTools.initializationPromise;
31
+ if (optionsArg.ignoreGlobal) {
32
+ const domToolsInstance = new DomTools(optionsArg);
33
+ await domToolsInstance.initializationPromise;
34
+ return domToolsInstance;
30
35
  }
31
36
 
32
- // Create initialization promise to prevent race conditions
33
- if (!optionsArg.ignoreGlobal) {
34
- DomTools.initializationPromise = (async () => {
35
- let domToolsInstance: DomTools;
36
- if (!globalThis.deesDomTools) {
37
- globalThis.deesDomTools = new DomTools(optionsArg);
38
- domToolsInstance = globalThis.deesDomTools;
39
-
40
- // lets make sure the dom is ready
41
- const readyStateChangedFunc = () => {
42
- if (document.readyState === 'interactive' || document.readyState === 'complete') {
43
- domToolsInstance.elements.headElement = document.querySelector('head');
44
- domToolsInstance.elements.bodyElement = document.querySelector('body');
45
- // Initialize keyboard now that document.body exists
46
- domToolsInstance.keyboard = new Keyboard(document.body);
47
- domToolsInstance.domReady.resolve();
48
- }
49
- };
50
- // Check current state immediately to avoid race condition
51
- if (document.readyState === 'interactive' || document.readyState === 'complete') {
52
- readyStateChangedFunc();
53
- } else {
54
- document.addEventListener('readystatechange', readyStateChangedFunc);
55
- }
56
- domToolsInstance.domToolsReady.resolve();
57
- } else {
58
- domToolsInstance = globalThis.deesDomTools;
59
- }
60
- await domToolsInstance.domToolsReady.promise;
61
- return domToolsInstance;
62
- })();
63
- return await DomTools.initializationPromise;
64
- } else {
65
- // ignoreGlobal case - create isolated instance
37
+ if (globalThis.deesDomTools && !globalThis.deesDomTools.disposed) {
38
+ await globalThis.deesDomTools.initializationPromise;
39
+ return globalThis.deesDomTools;
40
+ }
41
+
42
+ if (!DomTools.initializationPromise) {
66
43
  const domToolsInstance = new DomTools(optionsArg);
67
- return domToolsInstance;
44
+ globalThis.deesDomTools = domToolsInstance;
45
+ DomTools.initializationPromise = domToolsInstance.initializationPromise
46
+ .then(() => domToolsInstance)
47
+ .catch((error) => {
48
+ if (globalThis.deesDomTools === domToolsInstance) {
49
+ globalThis.deesDomTools = undefined;
50
+ }
51
+ DomTools.initializationPromise = null;
52
+ throw error;
53
+ });
68
54
  }
55
+
56
+ return await DomTools.initializationPromise;
69
57
  }
70
58
 
71
59
  /**
72
60
  * if you can, use the static asysnc .setupDomTools() function instead since it is safer to use.
73
61
  */
74
62
  public static getGlobalDomToolsSync(): DomTools {
75
- const globalDomTools: DomTools = globalThis.deesDomTools;
76
- if (!globalDomTools) {
63
+ const globalDomTools = globalThis.deesDomTools;
64
+ if (!globalDomTools || globalDomTools.disposed) {
77
65
  throw new Error('You tried to access domtools synchronously too early');
78
66
  }
79
- return globalThis.deesDomTools;
67
+ return globalDomTools;
80
68
  }
81
69
 
82
70
  // ========
@@ -118,16 +106,68 @@ export class DomTools {
118
106
  public scroller = new Scroller(this);
119
107
  public themeManager = new ThemeManager(this);
120
108
  public keyboard: Keyboard | null = null; // Initialized after DOM ready to avoid accessing document.body before it exists
109
+ public disposed = false;
121
110
 
122
111
  public domToolsReady = plugins.smartpromise.defer();
123
112
  public domReady = plugins.smartpromise.defer();
124
113
  public globalStylesReady = plugins.smartpromise.defer();
125
114
 
126
- constructor(optionsArg: IDomToolsContructorOptions) {}
115
+ private readonly initializationPromise: Promise<void>;
116
+ private readonly managedDomNodes: Element[] = [];
117
+ private readonly readyStateChangedFunc = () => {
118
+ this.tryMarkDomReady();
119
+ };
120
+
121
+ constructor(optionsArg: IDomToolsContructorOptions) {
122
+ this.initializationPromise = this.initialize();
123
+ }
124
+
125
+ private runOncePromiseMap = new Map<string, Promise<unknown>>();
126
+
127
+ private async initialize() {
128
+ this.tryMarkDomReady();
129
+ if (this.domReady.status === 'pending') {
130
+ document.addEventListener('readystatechange', this.readyStateChangedFunc);
131
+ }
132
+ if (this.domToolsReady.status === 'pending') {
133
+ this.domToolsReady.resolve();
134
+ }
135
+ }
136
+
137
+ private tryMarkDomReady() {
138
+ if (this.disposed || this.domReady.status !== 'pending') {
139
+ return;
140
+ }
141
+
142
+ if (document.readyState !== 'interactive' && document.readyState !== 'complete') {
143
+ return;
144
+ }
145
+
146
+ if (!document.head || !document.body) {
147
+ return;
148
+ }
149
+
150
+ this.elements.headElement = document.head;
151
+ this.elements.bodyElement = document.body;
152
+ if (!this.keyboard) {
153
+ this.keyboard = new Keyboard(document.body);
154
+ }
155
+ document.removeEventListener('readystatechange', this.readyStateChangedFunc);
156
+ this.domReady.resolve();
157
+ }
158
+
159
+ private trackManagedDomNode<T extends Element>(elementArg: T): T {
160
+ this.managedDomNodes.push(elementArg);
161
+ return elementArg;
162
+ }
127
163
 
128
- private runOnceTrackerStringMap = new plugins.lik.Stringmap();
129
- private runOnceResultMap = new plugins.lik.FastMap();
130
- private runOnceErrorMap = new plugins.lik.FastMap();
164
+ private getHeadOrBodyElement() {
165
+ const targetElement = this.elements.headElement || document.head || this.elements.bodyElement || document.body;
166
+ if (!targetElement) {
167
+ throw new Error('DomTools could not find a DOM target element to attach resources to');
168
+ }
169
+ return targetElement;
170
+ }
131
171
 
132
172
  /**
133
173
  * run a function once and always get the Promise of the first execution
@@ -135,34 +175,14 @@ export class DomTools {
135
175
  * @param funcArg the actual func arg to run
136
176
  */
137
177
  public async runOnce<T>(identifierArg: string, funcArg: () => Promise<T>) {
138
- const runningId = `${identifierArg}+runningCheck`;
139
- if (!this.runOnceTrackerStringMap.checkString(identifierArg)) {
140
- this.runOnceTrackerStringMap.addString(identifierArg);
141
- this.runOnceTrackerStringMap.addString(runningId);
142
- try {
143
- const result = await funcArg();
144
- this.runOnceResultMap.addToMap(identifierArg, result);
145
- } catch (error) {
146
- // Store error so waiting callers can receive it
147
- this.runOnceErrorMap.addToMap(identifierArg, error);
148
- } finally {
149
- // Always remove running flag to prevent permanent stuck state
150
- this.runOnceTrackerStringMap.removeString(runningId);
151
- }
178
+ let runOncePromise = this.runOncePromiseMap.get(identifierArg) as Promise<T> | undefined;
179
+ if (!runOncePromise) {
180
+ runOncePromise = Promise.resolve().then(async () => {
181
+ return await funcArg();
182
+ });
183
+ this.runOncePromiseMap.set(identifierArg, runOncePromise);
152
184
  }
153
- return await this.runOnceTrackerStringMap.registerUntilTrue(
154
- (stringMap?: string[]) => {
155
- return !stringMap?.includes(runningId);
156
- },
157
- () => {
158
- // Check if there was an error and re-throw it
159
- const error = this.runOnceErrorMap.getByKey(identifierArg);
160
- if (error) {
161
- throw error;
162
- }
163
- return this.runOnceResultMap.getByKey(identifierArg);
164
- }
165
- );
185
+ return await runOncePromise;
166
186
  }
167
187
 
168
188
  // setStuff
@@ -172,10 +192,10 @@ export class DomTools {
172
192
  */
173
193
  public async setGlobalStyles(stylesText: string) {
174
194
  await this.domReady.promise;
175
- const styleElement = document.createElement('style');
195
+ const styleElement = this.trackManagedDomNode(document.createElement('style'));
176
196
  styleElement.type = 'text/css';
177
197
  styleElement.appendChild(document.createTextNode(stylesText));
178
- this.elements.headElement!.appendChild(styleElement);
198
+ this.getHeadOrBodyElement().appendChild(styleElement);
179
199
  }
180
200
 
181
201
  /**
@@ -184,14 +204,16 @@ export class DomTools {
184
204
  */
185
205
  public async setExternalScript(scriptLinkArg: string) {
186
206
  await this.domReady.promise;
187
- const done = plugins.smartpromise.defer();
188
- const script = document.createElement('script');
207
+ const done = plugins.smartpromise.defer<void>();
208
+ const script = this.trackManagedDomNode(document.createElement('script'));
189
209
  script.src = scriptLinkArg;
190
- script.addEventListener('load', function () {
210
+ script.addEventListener('load', () => {
191
211
  done.resolve();
192
212
  });
193
- const parentNode = document.head || document.body;
194
- parentNode.append(script);
213
+ script.addEventListener('error', () => {
214
+ done.reject(new Error(`Failed to load external script: ${scriptLinkArg}`));
215
+ });
216
+ this.getHeadOrBodyElement().append(script);
195
217
  await done.promise;
196
218
  }
197
219
 
@@ -200,11 +222,20 @@ export class DomTools {
200
222
  * @param cssLinkArg a url to an external stylesheet
201
223
  */
202
224
  public async setExternalCss(cssLinkArg: string) {
203
- const cssTag = document.createElement('link');
225
+ await this.domReady.promise;
226
+ const done = plugins.smartpromise.defer<void>();
227
+ const cssTag = this.trackManagedDomNode(document.createElement('link'));
204
228
  cssTag.rel = 'stylesheet';
205
229
  cssTag.crossOrigin = 'anonymous';
206
230
  cssTag.href = cssLinkArg;
207
- document.head.append(cssTag);
231
+ cssTag.addEventListener('load', () => {
232
+ done.resolve();
233
+ });
234
+ cssTag.addEventListener('error', () => {
235
+ done.reject(new Error(`Failed to load external stylesheet: ${cssLinkArg}`));
236
+ });
237
+ this.getHeadOrBodyElement().append(cssTag);
238
+ await done.promise;
208
239
  }
209
240
 
210
241
  /**
@@ -215,4 +246,28 @@ export class DomTools {
215
246
  await this.websetup.setup(optionsArg);
216
247
  await this.websetup.readyPromise;
217
248
  }
249
+
250
+ public dispose() {
251
+ if (this.disposed) {
252
+ return;
253
+ }
254
+
255
+ this.disposed = true;
256
+ document.removeEventListener('readystatechange', this.readyStateChangedFunc);
257
+
258
+ this.keyboard?.dispose();
259
+ this.keyboard = null;
260
+ this.scroller.dispose();
261
+ this.themeManager.dispose();
262
+
263
+ for (const managedDomNode of this.managedDomNodes) {
264
+ managedDomNode.remove();
265
+ }
266
+ this.managedDomNodes.length = 0;
267
+
268
+ if (globalThis.deesDomTools === this) {
269
+ globalThis.deesDomTools = undefined;
270
+ DomTools.initializationPromise = null;
271
+ }
272
+ }
218
273
  }