- Introduced custom branding CSS styles in `custom-branding.css`. - Created example JSON configuration for custom branding in `custom-branding.example.json`. - Implemented branding configuration logic in `BrandingConfig.ts` to normalize and validate branding settings. - Developed CSS loader to manage loading and unloading of custom stylesheets in `BrandingCssLoader.ts`. - Added DOM rendering capabilities for branding elements in `BrandingDomRenderer.ts`. - Defined types and interfaces for branding elements and configurations in `BrandingTypes.ts`. - Included localization support for German in `de-de.js`. - Added unit tests for branding configuration, CSS loader, and DOM renderer. - Validated project structure and static assets with new validation scripts.
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import { ICssFile } from './BrandingTypes';
|
|
|
|
interface ISharedCssEntry {
|
|
element: HTMLLinkElement;
|
|
references: number;
|
|
owned: boolean;
|
|
}
|
|
|
|
export class BrandingCssLoader {
|
|
private static _registry: { [url: string]: ISharedCssEntry } = {};
|
|
private _loadedKeys: string[] = [];
|
|
|
|
constructor(private ownerId: string, private debugLog: (message: string, data?: any) => void) { } // tslint:disable-line:no-any
|
|
|
|
public load(files: ICssFile[]): void {
|
|
for (let i: number = 0; i < files.length; i++) {
|
|
this.loadOne(files[i]);
|
|
}
|
|
}
|
|
|
|
public dispose(): void {
|
|
for (let i: number = 0; i < this._loadedKeys.length; i++) {
|
|
const key: string = this._loadedKeys[i];
|
|
const entry: ISharedCssEntry = BrandingCssLoader._registry[key];
|
|
if (!entry) { continue; }
|
|
entry.references--;
|
|
if (entry.references <= 0) {
|
|
if (entry.owned && entry.element.parentNode) {
|
|
entry.element.parentNode.removeChild(entry.element);
|
|
}
|
|
delete BrandingCssLoader._registry[key];
|
|
}
|
|
}
|
|
this._loadedKeys = [];
|
|
}
|
|
|
|
private loadOne(file: ICssFile): void {
|
|
const absoluteUrl: string = this.toAbsoluteUrl(file.path);
|
|
const key: string = absoluteUrl.toLowerCase();
|
|
if (this._loadedKeys.indexOf(key) >= 0) { return; }
|
|
|
|
let entry: ISharedCssEntry = BrandingCssLoader._registry[key];
|
|
if (entry) {
|
|
entry.references++;
|
|
this._loadedKeys.push(key);
|
|
return;
|
|
}
|
|
|
|
const existing: HTMLLinkElement = this.findExistingStylesheet(key);
|
|
if (existing) {
|
|
BrandingCssLoader._registry[key] = { element: existing, references: 1, owned: false };
|
|
this._loadedKeys.push(key);
|
|
return;
|
|
}
|
|
|
|
const link: HTMLLinkElement = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.type = 'text/css';
|
|
link.href = absoluteUrl;
|
|
link.media = file.media || 'all';
|
|
link.setAttribute('data-custom-branding-owner', this.ownerId);
|
|
link.setAttribute('data-custom-branding-url', key);
|
|
const timeout: number = window.setTimeout((): void => {
|
|
this.debugLog('Stylesheet load timed out.', { path: file.path });
|
|
}, 10000);
|
|
link.onload = (): void => {
|
|
window.clearTimeout(timeout);
|
|
this.debugLog('Stylesheet loaded.', { path: file.path });
|
|
};
|
|
link.onerror = (): void => {
|
|
window.clearTimeout(timeout);
|
|
this.debugLog('Stylesheet failed to load.', { path: file.path });
|
|
};
|
|
document.head.appendChild(link);
|
|
|
|
entry = { element: link, references: 1, owned: true };
|
|
BrandingCssLoader._registry[key] = entry;
|
|
this._loadedKeys.push(key);
|
|
}
|
|
|
|
private toAbsoluteUrl(path: string): string {
|
|
const anchor: HTMLAnchorElement = document.createElement('a');
|
|
anchor.href = path;
|
|
return anchor.href;
|
|
}
|
|
|
|
private findExistingStylesheet(key: string): HTMLLinkElement | undefined {
|
|
const links: NodeListOf<HTMLLinkElement> = document.getElementsByTagName('link');
|
|
for (let i: number = 0; i < links.length; i++) {
|
|
if (String(links[i].rel || '').toLowerCase() === 'stylesheet'
|
|
&& String(links[i].href || '').toLowerCase() === key) {
|
|
return links[i];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|