219 lines
7.3 KiB
TypeScript
219 lines
7.3 KiB
TypeScript
import * as plugins from './domtools.plugins.js';
|
|
import { type TViewport } from './domtools.css.breakpoints.js';
|
|
import { Scroller } from './domtools.classes.scroller.js';
|
|
import { WebSetup } from '@push.rocks/websetup';
|
|
import { ThemeManager } from './domtools.classes.thememanager.js';
|
|
import { Keyboard } from './domtools.classes.keyboard.js';
|
|
|
|
export interface IDomToolsState {
|
|
virtualViewport: TViewport;
|
|
jwt: string;
|
|
}
|
|
|
|
export interface IDomToolsContructorOptions {
|
|
ignoreGlobal?: boolean;
|
|
}
|
|
|
|
export class DomTools {
|
|
// ======
|
|
// STATIC
|
|
// ======
|
|
private static initializationPromise: Promise<DomTools> | null = null;
|
|
|
|
/**
|
|
* setups domtools
|
|
*/
|
|
public static async setupDomTools(optionsArg: IDomToolsContructorOptions = {}): Promise<DomTools> {
|
|
// If initialization is already in progress and we're not ignoring global, wait for it
|
|
if (!optionsArg.ignoreGlobal && DomTools.initializationPromise) {
|
|
return await DomTools.initializationPromise;
|
|
}
|
|
|
|
// Create initialization promise to prevent race conditions
|
|
if (!optionsArg.ignoreGlobal) {
|
|
DomTools.initializationPromise = (async () => {
|
|
let domToolsInstance: DomTools;
|
|
if (!globalThis.deesDomTools) {
|
|
globalThis.deesDomTools = new DomTools(optionsArg);
|
|
domToolsInstance = globalThis.deesDomTools;
|
|
|
|
// lets make sure the dom is ready
|
|
const readyStateChangedFunc = () => {
|
|
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
|
domToolsInstance.elements.headElement = document.querySelector('head');
|
|
domToolsInstance.elements.bodyElement = document.querySelector('body');
|
|
// Initialize keyboard now that document.body exists
|
|
domToolsInstance.keyboard = new Keyboard(document.body);
|
|
domToolsInstance.domReady.resolve();
|
|
}
|
|
};
|
|
// Check current state immediately to avoid race condition
|
|
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
|
readyStateChangedFunc();
|
|
} else {
|
|
document.addEventListener('readystatechange', readyStateChangedFunc);
|
|
}
|
|
domToolsInstance.domToolsReady.resolve();
|
|
} else {
|
|
domToolsInstance = globalThis.deesDomTools;
|
|
}
|
|
await domToolsInstance.domToolsReady.promise;
|
|
return domToolsInstance;
|
|
})();
|
|
return await DomTools.initializationPromise;
|
|
} else {
|
|
// ignoreGlobal case - create isolated instance
|
|
const domToolsInstance = new DomTools(optionsArg);
|
|
return domToolsInstance;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* if you can, use the static asysnc .setupDomTools() function instead since it is safer to use.
|
|
*/
|
|
public static getGlobalDomToolsSync(): DomTools {
|
|
const globalDomTools: DomTools = globalThis.deesDomTools;
|
|
if (!globalDomTools) {
|
|
throw new Error('You tried to access domtools synchronously too early');
|
|
}
|
|
return globalThis.deesDomTools;
|
|
}
|
|
|
|
// ========
|
|
// INSTANCE
|
|
// ========
|
|
// elements
|
|
public elements: {
|
|
headElement: HTMLElement;
|
|
bodyElement: HTMLElement;
|
|
} = {
|
|
headElement: null,
|
|
bodyElement: null,
|
|
};
|
|
|
|
public websetup: WebSetup = new WebSetup({
|
|
metaObject: {
|
|
title: 'loading...',
|
|
},
|
|
});
|
|
|
|
public smartstate = new plugins.smartstate.Smartstate();
|
|
public domToolsStatePart = this.smartstate.getStatePart<IDomToolsState>('domtools', {
|
|
virtualViewport: 'native',
|
|
jwt: null,
|
|
});
|
|
|
|
public router = new plugins.smartrouter.SmartRouter({
|
|
debug: false,
|
|
});
|
|
|
|
public convenience = {
|
|
typedrequest: plugins.typedrequest,
|
|
smartdelay: plugins.smartdelay,
|
|
smartjson: plugins.smartjson,
|
|
smarturl: plugins.smarturl,
|
|
};
|
|
|
|
public deesComms = new plugins.deesComms.DeesComms();
|
|
public scroller = new Scroller(this);
|
|
public themeManager = new ThemeManager(this);
|
|
public keyboard: Keyboard = null; // Initialized after DOM ready to avoid accessing document.body before it exists
|
|
|
|
public domToolsReady = plugins.smartpromise.defer();
|
|
public domReady = plugins.smartpromise.defer();
|
|
public globalStylesReady = plugins.smartpromise.defer();
|
|
|
|
constructor(optionsArg: IDomToolsContructorOptions) {}
|
|
|
|
private runOnceTrackerStringMap = new plugins.lik.Stringmap();
|
|
private runOnceResultMap = new plugins.lik.FastMap();
|
|
private runOnceErrorMap = new plugins.lik.FastMap();
|
|
|
|
/**
|
|
* run a function once and always get the Promise of the first execution
|
|
* @param identifierArg the indentifier arg identifies functions. functions with the same identifier are considered equal
|
|
* @param funcArg the actual func arg to run
|
|
*/
|
|
public async runOnce<T>(identifierArg: string, funcArg: () => Promise<T>) {
|
|
const runningId = `${identifierArg}+runningCheck`;
|
|
if (!this.runOnceTrackerStringMap.checkString(identifierArg)) {
|
|
this.runOnceTrackerStringMap.addString(identifierArg);
|
|
this.runOnceTrackerStringMap.addString(runningId);
|
|
try {
|
|
const result = await funcArg();
|
|
this.runOnceResultMap.addToMap(identifierArg, result);
|
|
} catch (error) {
|
|
// Store error so waiting callers can receive it
|
|
this.runOnceErrorMap.addToMap(identifierArg, error);
|
|
} finally {
|
|
// Always remove running flag to prevent permanent stuck state
|
|
this.runOnceTrackerStringMap.removeString(runningId);
|
|
}
|
|
}
|
|
return await this.runOnceTrackerStringMap.registerUntilTrue(
|
|
(stringMap) => {
|
|
return !stringMap.includes(runningId);
|
|
},
|
|
() => {
|
|
// Check if there was an error and re-throw it
|
|
const error = this.runOnceErrorMap.getByKey(identifierArg);
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
return this.runOnceResultMap.getByKey(identifierArg);
|
|
}
|
|
);
|
|
}
|
|
|
|
// setStuff
|
|
/**
|
|
* allows to set global styles
|
|
* @param stylesText the css text you want to set
|
|
*/
|
|
public async setGlobalStyles(stylesText: string) {
|
|
await this.domReady.promise;
|
|
const styleElement = document.createElement('style');
|
|
styleElement.type = 'text/css';
|
|
styleElement.appendChild(document.createTextNode(stylesText));
|
|
this.elements.headElement.appendChild(styleElement);
|
|
}
|
|
|
|
/**
|
|
* allows to set global styles
|
|
* @param stylesText the css text you want to set
|
|
*/
|
|
public async setExternalScript(scriptLinkArg: string) {
|
|
await this.domReady.promise;
|
|
const done = plugins.smartpromise.defer();
|
|
const script = document.createElement('script');
|
|
script.src = scriptLinkArg;
|
|
script.addEventListener('load', function () {
|
|
done.resolve();
|
|
});
|
|
const parentNode = document.head || document.body;
|
|
parentNode.append(script);
|
|
await done.promise;
|
|
}
|
|
|
|
/**
|
|
* allows setting external css files
|
|
* @param cssLinkArg a url to an external stylesheet
|
|
*/
|
|
public async setExternalCss(cssLinkArg: string) {
|
|
const cssTag = document.createElement('link');
|
|
cssTag.rel = 'stylesheet';
|
|
cssTag.crossOrigin = 'anonymous';
|
|
cssTag.href = cssLinkArg;
|
|
document.head.append(cssTag);
|
|
}
|
|
|
|
/**
|
|
* allows setting of website infos
|
|
* @param optionsArg the website info
|
|
*/
|
|
public async setWebsiteInfo(optionsArg: plugins.websetup.IWebSetupConstructorOptions) {
|
|
await this.websetup.setup(optionsArg);
|
|
await this.websetup.readyPromise;
|
|
}
|
|
}
|