Files
CustomBranding/src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts
Torsten Brendgen 0e32b831bd Initial commit
2026-04-13 10:24:29 +02:00

373 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ICustomBrandingApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
private _bottomPlaceholder: PlaceholderContent | undefined;
private _loadedCssFiles: string[] = [];
@override
public onInit(): Promise<void> {
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 += '</' + tag + '>';
}
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 } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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);
}
}
}
}