From 8250575422daf8a15c1b54b0ef02fba2cedb7585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 28 Nov 2023 22:04:03 +0100 Subject: [PATCH] fix loader helpers --- .../functions/load-manifest-api.function.ts | 43 +++++--- .../load-manifest-element.function.ts | 42 +++++--- .../load-manifest-plain-css.function.ts | 33 ++++-- .../load-manifest-plain-js.function.ts | 19 ++-- .../libs/extension-api/types/base.types.ts | 4 +- .../src/libs/extension-api/types/utils.ts | 101 +++++++----------- .../extension-registry/models/theme.model.ts | 2 +- .../src/packages/core/themes/theme.context.ts | 19 +++- 8 files changed, 145 insertions(+), 118 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts index ed9db89316..f153cf1e22 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts @@ -1,31 +1,44 @@ -import type { UmbApi } from "../models/api.interface.js"; -import type { ApiLoaderExports, ApiLoaderProperty, ClassConstructor, ElementAndApiLoaderProperty, ElementLoaderExports } from "../types/utils.js"; +import type { UmbApi } from '../models/api.interface.js'; +import type { + ApiLoaderExports, + ApiLoaderProperty, + ClassConstructor, + ElementAndApiLoaderProperty, + ElementLoaderExports, +} from '../types/utils.js'; -export async function loadManifestApi(property: ApiLoaderProperty | ElementAndApiLoaderProperty): Promise | undefined> { - const propType = typeof property - if(propType === 'function') { - if((property as ClassConstructor).prototype) { +export async function loadManifestApi( + property: ApiLoaderProperty | ElementAndApiLoaderProperty, +): Promise | undefined> { + const propType = typeof property; + if (propType === 'function') { + if ((property as ClassConstructor).prototype) { // Class Constructor return property as ClassConstructor; } else { // Promise function - const result = await (property as (Exclude, string>, ClassConstructor>))(); - if(typeof result === 'object' && result != null) { - const exportValue = 'api' in result ? result.api : undefined || 'default' in result ? (result as Exclude<(typeof result), ElementLoaderExports>).default : undefined; - if(exportValue && typeof exportValue === 'function') { + const result = await ( + property as Exclude, string>, ClassConstructor> + )(); + if (typeof result === 'object' && result != null) { + const exportValue = + ('api' in result ? result.api : undefined) || + ('default' in result ? (result as Exclude).default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string const result = await (import(/* @vite-ignore */ property as string) as unknown as ApiLoaderExports); - if(typeof result === 'object' && result != null) { - const exportValue = 'api' in result ? result.api : undefined || 'default' in result ? result.default : undefined; - if(exportValue && typeof exportValue === 'function') { + if (result && typeof result === 'object') { + const exportValue = + ('api' in result ? result.api : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } } return undefined; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts index b1fcd616d8..89699f7fe0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts @@ -1,28 +1,40 @@ -import type { ApiLoaderExports, ClassConstructor, ElementAndApiLoaderProperty, ElementLoaderExports, ElementLoaderProperty } from "../types/utils.js"; +import type { + ApiLoaderExports, + ClassConstructor, + ElementAndApiLoaderProperty, + ElementLoaderExports, + ElementLoaderProperty, +} from '../types/utils.js'; - -export async function loadManifestElement(property: ElementLoaderProperty | ElementAndApiLoaderProperty): Promise | undefined> { - const propType = typeof property - if(propType === 'function') { - if((property as ClassConstructor).prototype) { +export async function loadManifestElement( + property: ElementLoaderProperty | ElementAndApiLoaderProperty, +): Promise | undefined> { + const propType = typeof property; + if (propType === 'function') { + if ((property as ClassConstructor).prototype) { // Class Constructor return property as ClassConstructor; } else { // Promise function - const result = await (property as (Exclude, ClassConstructor>))(); - if(typeof result === 'object' && result !== null) { - const exportValue = 'element' in result ? result.element : undefined || 'default' in result ? (result as Exclude<(typeof result), ApiLoaderExports>).default : undefined; - if(exportValue && typeof exportValue === 'function') { + const result = await (property as Exclude, ClassConstructor>)(); + if (typeof result === 'object' && result !== null) { + const exportValue = + ('element' in result ? result.element : undefined) || + ('default' in result ? (result as Exclude).default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string - const result = await (import(/* @vite-ignore */ property as string) as unknown as ElementLoaderExports); - if(typeof result === 'object' && result != null) { - const exportValue = 'element' in result ? result.element : undefined || 'default' in result ? result.default : undefined; - if(exportValue && typeof exportValue === 'function') { + const result = await (import( + /* @vite-ignore */ property as string + ) as unknown as ElementLoaderExports); + if (result && typeof result === 'object') { + const exportValue = + ('element' in result ? result.element : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts index 01006b2350..861b220ba0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts @@ -1,18 +1,29 @@ -import type { JsLoaderProperty } from "../types/utils.js"; +import type { CssLoaderExports, CssLoaderProperty } from '../types/utils.js'; -export async function loadManifestPlainCss(property: JsLoaderProperty): Promise { +export async function loadManifestPlainCss( + property: CssLoaderProperty, +): Promise { const propType = typeof property; - if(propType === 'function') { - const result = await (property as (Exclude<(typeof property), string>))(); - if(result != null) { - return result; + if (propType === 'function') { + // Promise function + const result = await (property as Exclude)(); + if (typeof result === 'object' && result != null) { + const exportValue = + ('css' in result ? result.css : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'string') { + return exportValue as CssType; + } } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string - const result = await (import(/* @vite-ignore */ property as string) as unknown as CssType); - if(result != null) { - return result; + const result = await (import(/* @vite-ignore */ property as string) as unknown as CssLoaderExports); + if (typeof result === 'object' && result != null) { + const exportValue = + ('css' in result ? result.css : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'string') { + return exportValue; + } } } return undefined; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts index 0611d7eb25..343a5fb952 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts @@ -1,18 +1,21 @@ -import type { JsLoaderProperty } from "../types/utils.js"; +import type { JsLoaderProperty } from '../types/utils.js'; -export async function loadManifestPlainJs(property: JsLoaderProperty): Promise { +export async function loadManifestPlainJs( + property: JsLoaderProperty, +): Promise { const propType = typeof property; - if(propType === 'function') { - const result = await (property as (Exclude<(typeof property), string>))(); - if(typeof result === 'object' && result != null) { + if (propType === 'function') { + // Promise function + const result = await (property as Exclude)(); + if (typeof result === 'object' && result != null) { return result; } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string const result = await (import(/* @vite-ignore */ property as string) as unknown as JsType); - if(typeof result === 'object' && result != null) { + if (typeof result === 'object' && result != null) { return result; } } return undefined; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts index e17bc203fc..017f59aea0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts @@ -26,12 +26,12 @@ export interface ManifestElementWithElementName extends ManifestElement { elementName: string; } -export interface ManifestPlainCss extends ManifestBase { +export interface ManifestPlainCss extends ManifestBase { /** * The file location of the stylesheet file to load * @TJS-type string */ - css?: CssLoaderProperty; + css?: CssLoaderProperty; } export interface ManifestPlainJs extends ManifestBase { diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts index 9cf7996dd3..bf6ad39b25 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts @@ -1,5 +1,4 @@ -import type { UmbApi } from "../models/index.js"; - +import type { UmbApi } from '../models/index.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type HTMLElementConstructor = new (...args: any[]) => T; @@ -7,80 +6,60 @@ export type HTMLElementConstructor = new (...args: any[]) => T; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ClassConstructor = new (...args: any[]) => T; - - // Module Export Types: -export type ElementLoaderExports< - ElementType extends HTMLElement = HTMLElement -> = ({default: ClassConstructor} | {element: ClassConstructor})// | Omit, 'default'> +export type CssLoaderExports = { default: CssType } | { css: CssType }; -export type ApiLoaderExports< - ApiType extends UmbApi = UmbApi -> = ({default: ClassConstructor} | {api: ClassConstructor})//| Omit, 'default'> +export type ElementLoaderExports = + | { default: ClassConstructor } + | { element: ClassConstructor }; // | Omit, 'default'> + +export type ApiLoaderExports = + | { default: ClassConstructor } + | { api: ClassConstructor }; //| Omit, 'default'> export type ElementAndApiLoaderExports< ElementType extends HTMLElement = HTMLElement, - ApiType extends UmbApi = UmbApi -> = ({api: ClassConstructor} | {element: ClassConstructor} | {api: ClassConstructor, element: ClassConstructor})// | Omit, 'api'>, 'default'> - + ApiType extends UmbApi = UmbApi, +> = + | { api: ClassConstructor } + | { element: ClassConstructor } + | { api: ClassConstructor; element: ClassConstructor }; // | Omit, 'api'>, 'default'> // Promise Types: -export type CssLoaderPromise< - CssType = unknown -> = (() => Promise) +export type CssLoaderPromise = () => Promise>; -export type JsLoaderPromise< - JsType -> = (() => Promise) +export type JsLoaderPromise = () => Promise; -export type ElementLoaderPromise< - ElementType extends HTMLElement = HTMLElement -> = (() => Promise>) +export type ElementLoaderPromise = () => Promise< + ElementLoaderExports +>; -export type ApiLoaderPromise< - ApiType extends UmbApi = UmbApi -> = (() => Promise>) +export type ApiLoaderPromise = () => Promise>; export type ElementAndApiLoaderPromise< ElementType extends HTMLElement = HTMLElement, - ApiType extends UmbApi = UmbApi -> = (() => Promise>) - + ApiType extends UmbApi = UmbApi, +> = () => Promise>; // Property Types: -export type CssLoaderProperty = ( - string - | - CssLoaderPromise -); -export type JsLoaderProperty = ( - string - | - JsLoaderPromise -); -export type ElementLoaderProperty = ( - string - | - ElementLoaderPromise - | - ClassConstructor -); -export type ApiLoaderProperty = ( - string - | - ApiLoaderPromise - | - ClassConstructor -); -export type ElementAndApiLoaderProperty = ( - string - | - ElementAndApiLoaderPromise - | - ElementLoaderPromise - | - ApiLoaderPromise -); \ No newline at end of file +export type CssLoaderProperty = string | CssLoaderPromise; +export type JsLoaderProperty = string | JsLoaderPromise; +export type ElementLoaderProperty = + | string + | ElementLoaderPromise + | ClassConstructor; +export type ApiLoaderProperty = + | string + | ApiLoaderPromise + | ClassConstructor; +export type ElementAndApiLoaderProperty< + ElementType extends HTMLElement = HTMLElement, + ApiType extends UmbApi = UmbApi, +> = + | string + | ElementAndApiLoaderPromise + | ElementLoaderPromise + | ApiLoaderPromise; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts index 37cd90d2a9..2b003bc712 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts @@ -2,6 +2,6 @@ import type { ManifestPlainCss } from '@umbraco-cms/backoffice/extension-api'; /** * Theme manifest for styling the backoffice of Umbraco such as dark, high contrast etc */ -export interface ManifestTheme extends ManifestPlainCss { +export interface ManifestTheme extends ManifestPlainCss { type: 'theme'; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts index 333afce863..3bbbb0c81d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts @@ -42,14 +42,16 @@ export class UmbThemeContext extends UmbBaseController { if (themes.length > 0 && themes[0].css) { const activeTheme = themes[0]; if (typeof activeTheme.css === 'function') { - const styleEl = (this.#styleElement = document.createElement('style')); - styleEl.setAttribute('type', 'text/css'); - document.head.appendChild(styleEl); + this.#styleElement = document.createElement('style') as HTMLStyleElement; + // We store the current style element so we can check if it has been replaced by another theme in between. + const currentStyleEl = this.#styleElement; + currentStyleEl.setAttribute('type', 'text/css'); const result = await loadManifestPlainCss(activeTheme.css); // Checking that this is still our styleElement, it has not been replaced with another theme in between. - if (result && styleEl === this.#styleElement) { - styleEl.appendChild(document.createTextNode(result)); + if (result && currentStyleEl === this.#styleElement) { + currentStyleEl.appendChild(document.createTextNode(result)); + document.head.appendChild(currentStyleEl); } } else if (typeof activeTheme.css === 'string') { this.#styleElement = document.createElement('link'); @@ -58,16 +60,23 @@ export class UmbThemeContext extends UmbBaseController { document.head.appendChild(this.#styleElement); } } else { + console.log('remove style element', this.#styleElement); + // We could not load a theme for this alias, so we remove the theme. localStorage.removeItem(LOCAL_STORAGE_KEY); this.#styleElement?.childNodes.forEach((node) => node.remove()); this.#styleElement?.setAttribute('href', ''); + this.#styleElement = null; } }, ); } else { + // Super clean, we got a falsy value, so we remove the theme. + localStorage.removeItem(LOCAL_STORAGE_KEY); + this.#styleElement?.remove(); this.#styleElement?.childNodes.forEach((node) => node.remove()); this.#styleElement?.setAttribute('href', ''); + this.#styleElement = null; } } }