import { override } from '@microsoft/decorators'; import { Log } from '@microsoft/sp-core-library'; import { BaseApplicationCustomizer, PlaceholderContent, PlaceholderName } from '@microsoft/sp-application-base'; import * as strings from 'CustomBrandingApplicationCustomizerStrings'; const LOG_SOURCE: string = 'CustomBrandingApplicationCustomizer'; /** * CSS-Datei Definition */ export interface ICssFile { path: string; } /** * Definition eines HTML-Elements */ export interface IBrandingElement { type: 'div' | 'span' | 'p' | 'a' | 'button' | 'img' | 'h1' | 'h2' | 'h3' | 'strong' | 'em'; content?: string; attributes?: { [key: string]: string }; styles?: { [key: string]: string }; children?: IBrandingElement[]; } export interface IPlaceholderConfig { elements?: IBrandingElement[]; } /** * Hauptkonfiguration für das Branding */ export interface IBrandingConfig { cssfiles?: ICssFile[]; placeholdertop?: IBrandingElement[]; placeholderbottom?: IBrandingElement[]; } /** * Properties für den CustomBranding Application Customizer */ export interface ICustomBrandingApplicationCustomizerProperties { /** * Array von CSS-Dateien die geladen werden sollen */ cssfiles?: ICssFile[]; /** * Array von HTML-Elementen für den Placeholder Top */ placeholdertop?: IPlaceholderConfig; /** * Array von HTML-Elementen für den Placeholder Bottom */ placeholderbottom?: IPlaceholderConfig; } /** * CustomBranding Application Customizer * Kompiliert JSON-Konfiguration zu HTML und fügt es in den Top Placeholder ein * Lädt optional CSS-Dateien */ export default class CustomBrandingApplicationCustomizer extends BaseApplicationCustomizer { private _topPlaceholder: PlaceholderContent | undefined; private _bottomPlaceholder: PlaceholderContent | undefined; private _loadedCssFiles: string[] = []; @override public onInit(): Promise { Log.info(LOG_SOURCE, 'Initialized CustomBrandingApplicationCustomizer'); // CSS-Dateien laden this._loadCssFiles(); // Auf Placeholder-Änderungen reagieren this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders); // Initial rendern this._renderPlaceHolders(); return Promise.resolve(); } /** * Lädt CSS-Dateien aus der Konfiguration */ private _loadCssFiles(): void { if (this.properties && this.properties.cssfiles && Array.isArray(this.properties.cssfiles)) { console.log('CustomBranding: Loading CSS files...'); for (let i = 0; i < this.properties.cssfiles.length; i++) { const cssFile = this.properties.cssfiles[i]; if (cssFile && cssFile.path) { this._injectCssFile(cssFile.path); } } } } /** * Fügt eine CSS-Datei in den Head ein */ private _injectCssFile(cssPath: string): void { // Prüfen ob die Datei bereits geladen wurde for (let i = 0; i < this._loadedCssFiles.length; i++) { if (this._loadedCssFiles[i] === cssPath) { console.log('CustomBranding: CSS file already loaded: ' + cssPath); return; } } try { // Link-Element erstellen const linkElement: HTMLLinkElement = document.createElement('link'); linkElement.rel = 'stylesheet'; linkElement.type = 'text/css'; linkElement.href = cssPath; linkElement.setAttribute('data-custom-branding', 'true'); // Event-Handler für erfolgreiches Laden linkElement.onload = function () { console.log('CustomBranding: CSS loaded successfully: ' + cssPath); }; // Event-Handler für Fehler linkElement.onerror = function () { console.error('CustomBranding: Failed to load CSS: ' + cssPath); }; // In Head einfügen document.head.appendChild(linkElement); // Zur Liste hinzufügen this._loadedCssFiles.push(cssPath); console.log('CustomBranding: CSS file injected: ' + cssPath); } catch (error) { console.error('CustomBranding: Error injecting CSS file: ' + cssPath, error); } } private _renderPlaceHolders(): void { console.log('CustomBrandingApplicationCustomizer._renderPlaceHolders()'); // Prüfen ob Top Placeholder verfügbar ist if (!this._topPlaceholder && !this._bottomPlaceholder) { this._topPlaceholder = this.context.placeholderProvider.tryCreateContent( PlaceholderName.Top, { onDispose: this._onDispose } ); this._bottomPlaceholder = this.context.placeholderProvider.tryCreateContent( PlaceholderName.Bottom, { onDispose: this._onDispose } ); // Falls Placeholder nicht verfügbar, abbrechen if (!this._topPlaceholder) { console.error('CustomBranding: Top placeholder not found'); return; } // Falls Placeholder nicht verfügbar, abbrechen if (!this._bottomPlaceholder) { console.error('CustomBranding: Bottom placeholder not found'); return; } if (this._topPlaceholder.domElement && this._bottomPlaceholder.domElement) { // Container erstellen mit hoher Priorität this.renderPlaceHolder(this._topPlaceholder.domElement, this._bottomPlaceholder.domElement); console.log('CustomBranding: HTML injected successfully'); } } } private renderPlaceHolder(topcontainer: HTMLElement, bottomcontainer: HTMLElement) { if (!this.properties) { console.log('CustomBranding: No properties provided'); return; } try { // Top-Konfiguration unverändert übernehmen const topConfig: IPlaceholderConfig | undefined = this.properties.placeholdertop; // Bottom-Konfiguration klonen oder initialisieren const bottomConfig: IPlaceholderConfig = this.properties.placeholderbottom ? { ...this.properties.placeholderbottom } : { elements: [] }; // Admin-Link nur für Site Collection Admins ergänzen if (this._isSiteAdmin()) { bottomConfig.elements = bottomConfig.elements || []; bottomConfig.elements.push(this._getAdminFooterElement()); } const config: IBrandingConfig = { cssfiles: this.properties.cssfiles, placeholdertop: topConfig.elements, placeholderbottom: bottomConfig.elements }; console.log('CustomBranding: Compiling JSON to HTML...'); const compiled = this._compileToHtml(config); topcontainer.id = 'CustomHeader'; topcontainer.innerHTML = compiled.top; bottomcontainer.id = 'CustomFooter'; bottomcontainer.innerHTML = compiled.bottom; } catch (error) { console.error('CustomBranding: Error compiling configuration', error); } } /** * Kompiliert die JSON-Konfiguration zu HTML */ private _compileToHtml(config: IBrandingConfig): { top: string; bottom: string } { // Compile Top and Bottom separately let topHtml: string = ''; let bottomHtml: string = ''; if (config.placeholdertop && Array.isArray(config.placeholdertop)) { for (let i = 0; i < config.placeholdertop.length; i++) { topHtml += this._createElement(config.placeholdertop[i]); } } if (config.placeholderbottom && Array.isArray(config.placeholderbottom)) { for (let i = 0; i < config.placeholderbottom.length; i++) { bottomHtml += this._createElement(config.placeholderbottom[i]); } } return { top: topHtml, bottom: bottomHtml }; } /** * Erstellt HTML für ein einzelnes Element */ private _createElement(element: IBrandingElement): string { const tag = element.type || 'div'; let html = '<' + tag; // Attribute hinzufügen if (element.attributes) { for (const key in element.attributes) { if (element.attributes.hasOwnProperty(key)) { const value = element.attributes[key]; html += ' ' + key + '="' + this._escapeHtml(value) + '"'; } } } // Styles hinzufügen if (element.styles) { const styleArray: string[] = []; for (const key in element.styles) { if (element.styles.hasOwnProperty(key)) { const value = element.styles[key]; styleArray.push(key + ':' + value); } } if (styleArray.length > 0) { const styleString = styleArray.join(';'); html += ' style="' + styleString + '"'; } } html += '>'; // Content hinzufügen if (element.content) { html += this._escapeHtml(element.content); } // Kinder hinzufügen if (element.children && Array.isArray(element.children)) { for (let i = 0; i < element.children.length; i++) { html += this._createElement(element.children[i]); } } // Self-closing Tags behandeln const selfClosingTags = ['img', 'br', 'hr', 'input']; let isSelfClosing = false; for (let i = 0; i < selfClosingTags.length; i++) { if (selfClosingTags[i] === tag) { isSelfClosing = true; break; } } if (!isSelfClosing) { html += ''; } return html; } private _isSiteAdmin(): boolean { return this.context.pageContext.legacyPageContext.isSiteAdmin === true; } private _getAdminFooterElement(): IBrandingElement { const siteUrl = this.context.pageContext.site.absoluteUrl; return { type: 'div', styles: { 'text-align': 'right', 'padding': '8px 16px', 'border-top': '1px solid #e1e1e1', 'background-color': '#f8f8f8', 'font-size': '13px' }, children: [ { type: 'a', content: 'Einstellungen', attributes: { href: `${siteUrl}/SitePages/PortalSettings.aspx` }, styles: { 'text-decoration': 'none', 'font-weight': '600' } } ] }; } /** * Escaped HTML-Zeichen für Sicherheit */ private _escapeHtml(text: string): string { const map: { [key: string]: string } = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function (m) { return map[m]; }); } private _onDispose(): void { console.log('CustomBrandingApplicationCustomizer._onDispose()'); // CSS-Dateien beim Dispose entfernen const cssLinks = document.querySelectorAll('link[data-custom-branding="true"]'); for (let i = 0; i < cssLinks.length; i++) { const link = cssLinks[i]; if (link.parentNode) { link.parentNode.removeChild(link); } } } }