Initial commit
This commit is contained in:
@@ -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 } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user