Files
dees-domtools/ts/domtools.classes.domtools.ts
T

274 lines
8.2 KiB
TypeScript
Raw Permalink Normal View History

2022-03-16 13:39:50 +01:00
import * as plugins from './domtools.plugins.js';
2023-08-27 13:33:36 +02:00
import { type TViewport } from './domtools.css.breakpoints.js';
2022-03-16 13:39:50 +01:00
import { Scroller } from './domtools.classes.scroller.js';
2023-08-20 17:10:10 +02:00
import { WebSetup } from '@push.rocks/websetup';
2022-03-16 13:39:50 +01:00
import { ThemeManager } from './domtools.classes.thememanager.js';
2022-04-21 22:53:58 +02:00
import { Keyboard } from './domtools.classes.keyboard.js';
2020-06-28 17:40:03 +00:00
declare global {
var deesDomTools: DomTools | undefined;
}
2020-05-27 21:15:38 +00:00
export interface IDomToolsState {
virtualViewport: TViewport;
2022-01-14 18:20:45 +01:00
jwt: string;
2020-05-27 21:15:38 +00:00
}
2020-05-25 15:57:47 +00:00
2022-04-21 22:53:58 +02:00
export interface IDomToolsContructorOptions {
ignoreGlobal?: boolean;
}
2020-05-25 15:57:47 +00:00
export class DomTools {
2020-06-03 09:07:31 +00:00
// ======
// STATIC
// ======
private static initializationPromise: Promise<DomTools> | null = null;
2020-06-03 09:07:31 +00:00
/**
* setups domtools
*/
public static async setupDomTools(optionsArg: IDomToolsContructorOptions = {}): Promise<DomTools> {
if (optionsArg.ignoreGlobal) {
const domToolsInstance = new DomTools(optionsArg);
await domToolsInstance.initializationPromise;
return domToolsInstance;
}
if (globalThis.deesDomTools && !globalThis.deesDomTools.disposed) {
await globalThis.deesDomTools.initializationPromise;
return globalThis.deesDomTools;
}
if (!DomTools.initializationPromise) {
const domToolsInstance = new DomTools(optionsArg);
globalThis.deesDomTools = domToolsInstance;
DomTools.initializationPromise = domToolsInstance.initializationPromise
.then(() => domToolsInstance)
.catch((error) => {
if (globalThis.deesDomTools === domToolsInstance) {
globalThis.deesDomTools = undefined;
}
DomTools.initializationPromise = null;
throw error;
});
2020-05-25 15:57:47 +00:00
}
return await DomTools.initializationPromise;
2020-05-23 16:55:36 +00:00
}
2020-05-25 15:57:47 +00:00
2020-07-27 18:23:47 +00:00
/**
* if you can, use the static asysnc .setupDomTools() function instead since it is safer to use.
*/
2020-09-16 13:57:16 +00:00
public static getGlobalDomToolsSync(): DomTools {
const globalDomTools = globalThis.deesDomTools;
if (!globalDomTools || globalDomTools.disposed) {
2020-07-27 18:23:47 +00:00
throw new Error('You tried to access domtools synchronously too early');
2020-07-27 17:11:00 +00:00
}
return globalDomTools;
2020-07-27 17:11:00 +00:00
}
2020-06-03 09:07:31 +00:00
// ========
// INSTANCE
// ========
2020-06-28 15:49:46 +00:00
// elements
public elements: {
headElement: HTMLElement | null;
bodyElement: HTMLElement | null;
2020-06-28 15:49:46 +00:00
} = {
headElement: null,
bodyElement: null,
};
2020-11-05 18:08:42 +00:00
public websetup: WebSetup = new WebSetup({
metaObject: {
title: '',
2020-11-23 20:41:26 +00:00
},
2020-11-05 18:08:42 +00:00
});
2020-11-04 18:35:45 +00:00
2020-05-25 16:11:59 +00:00
public smartstate = new plugins.smartstate.Smartstate();
2020-05-27 21:59:28 +00:00
public domToolsStatePart = this.smartstate.getStatePart<IDomToolsState>('domtools', {
2020-06-03 09:07:31 +00:00
virtualViewport: 'native',
jwt: '' as string,
2020-05-27 21:59:28 +00:00
});
2020-06-03 10:34:09 +00:00
public router = new plugins.smartrouter.SmartRouter({
2020-06-28 15:49:46 +00:00
debug: false,
2020-05-27 21:59:28 +00:00
});
2020-05-25 16:11:59 +00:00
2020-07-29 17:32:28 +00:00
public convenience = {
2021-09-13 20:35:33 +02:00
typedrequest: plugins.typedrequest,
2020-09-12 13:14:17 +00:00
smartdelay: plugins.smartdelay,
2022-12-31 12:12:58 +01:00
smartjson: plugins.smartjson,
2023-01-02 17:25:17 +01:00
smarturl: plugins.smarturl,
2020-07-29 17:32:28 +00:00
};
2020-10-06 22:08:26 +00:00
public deesComms = new plugins.deesComms.DeesComms();
public scroller = new Scroller(this);
2020-11-23 20:41:26 +00:00
public themeManager = new ThemeManager(this);
public keyboard: Keyboard | null = null; // Initialized after DOM ready to avoid accessing document.body before it exists
public disposed = false;
2021-11-21 16:14:27 +01:00
2020-05-25 15:57:47 +00:00
public domToolsReady = plugins.smartpromise.defer();
public domReady = plugins.smartpromise.defer();
public globalStylesReady = plugins.smartpromise.defer();
private readonly initializationPromise: Promise<void>;
private readonly managedDomNodes: Element[] = [];
private readonly readyStateChangedFunc = () => {
this.tryMarkDomReady();
};
constructor(optionsArg: IDomToolsContructorOptions) {
this.initializationPromise = this.initialize();
}
private runOncePromiseMap = new Map<string, Promise<unknown>>();
private async initialize() {
this.tryMarkDomReady();
if (this.domReady.status === 'pending') {
document.addEventListener('readystatechange', this.readyStateChangedFunc);
}
if (this.domToolsReady.status === 'pending') {
this.domToolsReady.resolve();
}
}
private tryMarkDomReady() {
if (this.disposed || this.domReady.status !== 'pending') {
return;
}
if (document.readyState !== 'interactive' && document.readyState !== 'complete') {
return;
}
if (!document.head || !document.body) {
return;
}
this.elements.headElement = document.head;
this.elements.bodyElement = document.body;
if (!this.keyboard) {
this.keyboard = new Keyboard(document.body);
}
document.removeEventListener('readystatechange', this.readyStateChangedFunc);
this.domReady.resolve();
}
private trackManagedDomNode<T extends Element>(elementArg: T): T {
this.managedDomNodes.push(elementArg);
return elementArg;
}
2020-05-25 15:57:47 +00:00
private getHeadOrBodyElement() {
const targetElement = this.elements.headElement || document.head || this.elements.bodyElement || document.body;
if (!targetElement) {
throw new Error('DomTools could not find a DOM target element to attach resources to');
}
return targetElement;
}
2021-11-26 15:26:15 +01:00
2020-05-25 16:22:05 +00:00
/**
* run a function once and always get the Promise of the first execution
2020-06-28 15:49:46 +00:00
* @param identifierArg the indentifier arg identifies functions. functions with the same identifier are considered equal
* @param funcArg the actual func arg to run
2020-05-25 16:22:05 +00:00
*/
2020-05-25 15:57:47 +00:00
public async runOnce<T>(identifierArg: string, funcArg: () => Promise<T>) {
let runOncePromise = this.runOncePromiseMap.get(identifierArg) as Promise<T> | undefined;
if (!runOncePromise) {
runOncePromise = Promise.resolve().then(async () => {
return await funcArg();
});
this.runOncePromiseMap.set(identifierArg, runOncePromise);
2020-05-25 15:57:47 +00:00
}
return await runOncePromise;
2020-05-25 15:57:47 +00:00
}
2020-06-28 15:49:46 +00:00
// setStuff
2021-11-26 15:26:15 +01:00
/**
* allows to set global styles
* @param stylesText the css text you want to set
*/
2020-06-28 15:49:46 +00:00
public async setGlobalStyles(stylesText: string) {
await this.domReady.promise;
const styleElement = this.trackManagedDomNode(document.createElement('style'));
2020-06-28 15:49:46 +00:00
styleElement.type = 'text/css';
styleElement.appendChild(document.createTextNode(stylesText));
this.getHeadOrBodyElement().appendChild(styleElement);
2020-06-28 15:49:46 +00:00
}
2022-04-22 09:25:10 +02:00
/**
* allows to set global styles
* @param stylesText the css text you want to set
*/
2023-04-05 16:46:13 +02:00
public async setExternalScript(scriptLinkArg: string) {
2022-04-22 09:25:10 +02:00
await this.domReady.promise;
const done = plugins.smartpromise.defer<void>();
const script = this.trackManagedDomNode(document.createElement('script'));
2022-04-22 09:25:10 +02:00
script.src = scriptLinkArg;
script.addEventListener('load', () => {
2022-04-22 09:25:10 +02:00
done.resolve();
});
script.addEventListener('error', () => {
done.reject(new Error(`Failed to load external script: ${scriptLinkArg}`));
});
this.getHeadOrBodyElement().append(script);
2022-04-22 09:25:10 +02:00
await done.promise;
}
2021-11-26 15:26:15 +01:00
/**
* allows setting external css files
* @param cssLinkArg a url to an external stylesheet
*/
2020-12-01 17:31:37 +00:00
public async setExternalCss(cssLinkArg: string) {
await this.domReady.promise;
const done = plugins.smartpromise.defer<void>();
const cssTag = this.trackManagedDomNode(document.createElement('link'));
2020-12-01 17:31:37 +00:00
cssTag.rel = 'stylesheet';
cssTag.crossOrigin = 'anonymous';
cssTag.href = cssLinkArg;
cssTag.addEventListener('load', () => {
done.resolve();
});
cssTag.addEventListener('error', () => {
done.reject(new Error(`Failed to load external stylesheet: ${cssLinkArg}`));
});
this.getHeadOrBodyElement().append(cssTag);
await done.promise;
2020-12-01 17:31:37 +00:00
}
2021-11-26 15:26:15 +01:00
/**
* allows setting of website infos
* @param optionsArg the website info
*/
2020-06-28 15:49:46 +00:00
public async setWebsiteInfo(optionsArg: plugins.websetup.IWebSetupConstructorOptions) {
2020-11-05 20:08:07 +00:00
await this.websetup.setup(optionsArg);
2020-11-04 18:35:45 +00:00
await this.websetup.readyPromise;
2020-06-28 15:49:46 +00:00
}
public dispose() {
if (this.disposed) {
return;
}
this.disposed = true;
document.removeEventListener('readystatechange', this.readyStateChangedFunc);
this.keyboard?.dispose();
this.keyboard = null;
this.scroller.dispose();
this.themeManager.dispose();
for (const managedDomNode of this.managedDomNodes) {
managedDomNode.remove();
}
this.managedDomNodes.length = 0;
if (globalThis.deesDomTools === this) {
globalThis.deesDomTools = undefined;
DomTools.initializationPromise = null;
}
}
2020-05-27 21:15:38 +00:00
}