Initial commit

This commit is contained in:
Torsten Brendgen
2026-04-13 10:24:29 +02:00
commit 0e32b831bd
24 changed files with 18584 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
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);
}
}
}
}