274 lines
8.2 KiB
TypeScript
274 lines
8.2 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';
|
|
|
|
declare global {
|
|
var deesDomTools: DomTools | undefined;
|
|
}
|
|
|
|
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 (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;
|
|
});
|
|
}
|
|
|
|
return await DomTools.initializationPromise;
|
|
}
|
|
|
|
/**
|
|
* if you can, use the static asysnc .setupDomTools() function instead since it is safer to use.
|
|
*/
|
|
public static getGlobalDomToolsSync(): DomTools {
|
|
const globalDomTools = globalThis.deesDomTools;
|
|
if (!globalDomTools || globalDomTools.disposed) {
|
|
throw new Error('You tried to access domtools synchronously too early');
|
|
}
|
|
return globalDomTools;
|
|
}
|
|
|
|
// ========
|
|
// INSTANCE
|
|
// ========
|
|
// elements
|
|
public elements: {
|
|
headElement: HTMLElement | null;
|
|
bodyElement: HTMLElement | null;
|
|
} = {
|
|
headElement: null,
|
|
bodyElement: null,
|
|
};
|
|
|
|
public websetup: WebSetup = new WebSetup({
|
|
metaObject: {
|
|
title: '',
|
|
},
|
|
});
|
|
|
|
public smartstate = new plugins.smartstate.Smartstate();
|
|
public domToolsStatePart = this.smartstate.getStatePart<IDomToolsState>('domtools', {
|
|
virtualViewport: 'native',
|
|
jwt: '' as string,
|
|
});
|
|
|
|
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 = null; // Initialized after DOM ready to avoid accessing document.body before it exists
|
|
public disposed = false;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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>) {
|
|
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);
|
|
}
|
|
return await runOncePromise;
|
|
}
|
|
|
|
// 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 = this.trackManagedDomNode(document.createElement('style'));
|
|
styleElement.type = 'text/css';
|
|
styleElement.appendChild(document.createTextNode(stylesText));
|
|
this.getHeadOrBodyElement().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<void>();
|
|
const script = this.trackManagedDomNode(document.createElement('script'));
|
|
script.src = scriptLinkArg;
|
|
script.addEventListener('load', () => {
|
|
done.resolve();
|
|
});
|
|
script.addEventListener('error', () => {
|
|
done.reject(new Error(`Failed to load external script: ${scriptLinkArg}`));
|
|
});
|
|
this.getHeadOrBodyElement().append(script);
|
|
await done.promise;
|
|
}
|
|
|
|
/**
|
|
* allows setting external css files
|
|
* @param cssLinkArg a url to an external stylesheet
|
|
*/
|
|
public async setExternalCss(cssLinkArg: string) {
|
|
await this.domReady.promise;
|
|
const done = plugins.smartpromise.defer<void>();
|
|
const cssTag = this.trackManagedDomNode(document.createElement('link'));
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|