feat: Update version to 3.0.4 and enhance CustomBranding functionality

This commit is contained in:
Torsten Brendgen
2026-08-19 21:05:17 +02:00
parent 0bf62ecadf
commit 583964a792
8 changed files with 74 additions and 30 deletions
+8 -6
View File
@@ -1,6 +1,6 @@
# CustomBranding # CustomBranding
CustomBranding 3.0.3 ist ein zentraler SPFx-1.4.1-Application-Customizer für SharePoint Server Subscription Edition. Die Solution lädt freigegebene Stylesheets und rendert eine kontrollierte Komponentenstruktur im oberen oder unteren SharePoint-Placeholder. Moderne und klassische Seiten verwenden dieselben `ClientSideComponentProperties`. CustomBranding 3.0.4 ist ein zentraler SPFx-1.4.1-Application-Customizer für SharePoint Server Subscription Edition. Die Solution lädt freigegebene Stylesheets und rendert eine kontrollierte Komponentenstruktur im oberen oder unteren SharePoint-Placeholder. Moderne und klassische Seiten verwenden dieselben `ClientSideComponentProperties`.
Die Konfiguration liegt in genau einer `SPSite.UserCustomAction` pro Site Collection. Es werden weder eine versteckte Liste noch ein Property Bag benötigt. Dadurch gilt das Branding automatisch für das Root Web, vorhandene Subwebs und später angelegte Subwebs. Die Konfiguration liegt in genau einer `SPSite.UserCustomAction` pro Site Collection. Es werden weder eine versteckte Liste noch ein Property Bag benötigt. Dadurch gilt das Branding automatisch für das Root Web, vorhandene Subwebs und später angelegte Subwebs.
@@ -16,14 +16,16 @@ Site Collection
└── klassische Seiten: optionaler SPSite ScriptLink └── klassische Seiten: optionaler SPSite ScriptLink
CustomHeader CustomHeader
├── CustomBrandingTopHost ├── Branding-Elemente mit `data-custom-branding-placement="top"`
└── MegaMenuHost (wird nicht verändert) └── CustomNavigation (wird nicht verändert)
CustomFooter CustomFooter
└── CustomBrandingBottomHost └── CustomBrandingBottomHost
``` ```
CustomBranding verändert nur seine eigenen Host-Elemente. Das MegaMenu und andere Erweiterungen im selben Placeholder bleiben bei Navigation und erneutem Rendern erhalten. CustomBranding rendert seine Top-Elemente direkt in `CustomHeader` und entfernt beim erneuten Rendern nur die über `data-custom-branding-owner` markierten eigenen Knoten. `CustomNavigation` und andere Erweiterungen im selben Placeholder bleiben erhalten.
Die zentrale Custom-Branding-Action verwendet `Sequence = 90`. MegaMenu verwendet `100` und Current Navigation `110`, sodass Custom Branding den gemeinsamen Header zuerst vorbereitet. Die DOM-Integration bleibt trotzdem unabhängig von der tatsächlichen asynchronen Fertigstellungsreihenfolge.
## Voraussetzungen und Build ## Voraussetzungen und Build
@@ -193,8 +195,8 @@ performance.getEntriesByType('resource')
({ ({
header: !!document.getElementById('CustomHeader'), header: !!document.getElementById('CustomHeader'),
branding: !!document.getElementById('CustomBrandingTopHost'), branding: document.querySelectorAll('#CustomHeader > [data-custom-branding-placement="top"]').length,
megaMenu: !!document.getElementById('MegaMenuHost') megaMenu: !!document.getElementById('CustomNavigation')
}); });
``` ```
+5 -4
View File
@@ -3,7 +3,7 @@
Analysestand: 20.07.2026 Analysestand: 20.07.2026
Umgesetzt: 20.07.2026 Umgesetzt: 20.07.2026
Ausgangsversion: 1.0.4 Ausgangsversion: 1.0.4
Zielversion: 3.0.3 Zielversion: 3.0.4
Status: Implementierung und lokale Qualitätssicherung abgeschlossen Status: Implementierung und lokale Qualitätssicherung abgeschlossen
## Zielbild ## Zielbild
@@ -67,9 +67,10 @@ CustomBranding wird pro Site Collection genau einmal über eine `SPSite.UserCust
## MegaMenu-Integration ## MegaMenu-Integration
- [x] Stabile Hosts `CustomHeader`, `CustomBrandingTopHost`, `MegaMenuHost`, `CustomFooter` und `CustomBrandingBottomHost` definiert. - [x] CustomBranding mit Sequence 90 vor MegaMenu (100) und Current Navigation (110) registriert.
- [x] Renderer verändert ausschließlich eigene Host-Inhalte. - [x] Top-Branding direkt als Geschwister von `CustomNavigation` in `CustomHeader` integriert; Bottom-Host bleibt separat.
- [x] Wiederholtes Rendern löscht den `MegaMenuHost` oder fremde DOM-Knoten nicht. - [x] Renderer verändert ausschließlich eigene, per Ownership-Attribut markierte Top-Knoten und Bottom-Host-Inhalte.
- [x] Wiederholtes Rendern löscht `CustomNavigation` oder fremde DOM-Knoten nicht.
- [x] Gemeinsamer Betrieb und Ownership in der README dokumentiert. - [x] Gemeinsamer Betrieb und Ownership in der README dokumentiert.
- [ ] Beide realen Lade-Reihenfolgen mit installiertem MegaMenu auf dem Zielserver abnehmen. - [ ] Beide realen Lade-Reihenfolgen mit installiertem MegaMenu auf dem Zielserver abnehmen.
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "custom-branding-client-side-solution", "name": "custom-branding-client-side-solution",
"id": "03a7c4de-e031-4b83-a683-5ca5c364166e", "id": "03a7c4de-e031-4b83-a683-5ca5c364166e",
"version": "3.0.3.0", "version": "3.0.4.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+1 -1
View File
@@ -187,7 +187,7 @@ try {
$action.Title = $componentTitle $action.Title = $componentTitle
$action.Description = $Description $action.Description = $Description
$action.Location = $componentLocation $action.Location = $componentLocation
$action.Sequence = 100 $action.Sequence = 90
$action.ClientSideComponentId = $componentId $action.ClientSideComponentId = $componentId
$action.ClientSideComponentProperties = [string]($configuration | ConvertTo-Json -Depth 20 -Compress) $action.ClientSideComponentProperties = [string]($configuration | ConvertTo-Json -Depth 20 -Compress)
$action.Update() $action.Update()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "custom-branding", "name": "custom-branding",
"version": "3.0.3", "version": "3.0.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "custom-branding", "name": "custom-branding",
"version": "3.0.3", "version": "3.0.4",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -25,7 +25,7 @@ export default class CustomBrandingApplicationCustomizer
private _topPlaceholder: PlaceholderContent | undefined; private _topPlaceholder: PlaceholderContent | undefined;
private _bottomPlaceholder: PlaceholderContent | undefined; private _bottomPlaceholder: PlaceholderContent | undefined;
private _topHost: HTMLElement | undefined; private _topElements: HTMLElement[] = [];
private _bottomHost: HTMLElement | undefined; private _bottomHost: HTMLElement | undefined;
private _renderer: BrandingDomRenderer = new BrandingDomRenderer(); private _renderer: BrandingDomRenderer = new BrandingDomRenderer();
private _cssLoader: BrandingCssLoader | undefined; private _cssLoader: BrandingCssLoader | undefined;
@@ -39,7 +39,7 @@ export default class CustomBrandingApplicationCustomizer
this.context.pageContext.site.absoluteUrl this.context.pageContext.site.absoluteUrl
); );
this._config = result.config; this._config = result.config;
this.debug(strings.Initialized + ' 3.0.0.', { this.debug(strings.Initialized + ' 3.0.4.', {
schemaVersion: this._config.schemaVersion, schemaVersion: this._config.schemaVersion,
cssFileCount: this._config.cssfiles.length, cssFileCount: this._config.cssfiles.length,
warningCount: result.warnings.length warningCount: result.warnings.length
@@ -78,11 +78,8 @@ export default class CustomBrandingApplicationCustomizer
if (this._topPlaceholder && this._topPlaceholder.domElement) { if (this._topPlaceholder && this._topPlaceholder.domElement) {
this._topPlaceholder.domElement.id = 'CustomHeader'; this._topPlaceholder.domElement.id = 'CustomHeader';
this._topHost = this.getOrCreateOwnedHost( this.removeLegacyTopHost(this._topPlaceholder.domElement);
this._topPlaceholder.domElement, this.renderTopSafely(this._topPlaceholder.domElement, this._config.placeholdertop.elements);
'CustomBrandingTopHost'
);
this.renderSafely(this._topHost, this._config.placeholdertop.elements);
} }
if (this._bottomPlaceholder && this._bottomPlaceholder.domElement) { if (this._bottomPlaceholder && this._bottomPlaceholder.domElement) {
this._bottomPlaceholder.domElement.id = 'CustomFooter'; this._bottomPlaceholder.domElement.id = 'CustomFooter';
@@ -106,7 +103,7 @@ export default class CustomBrandingApplicationCustomizer
host.style.fontSize = '14px'; host.style.fontSize = '14px';
host.style.color = '#323130'; host.style.color = '#323130';
host.style.borderTop = id === 'CustomBrandingBottomHost' ? '1px solid #edebe9' : ''; host.style.borderTop = id === 'CustomBrandingBottomHost' ? '1px solid #edebe9' : '';
host.style.borderBottom = id === 'CustomBrandingTopHost' ? '1px solid #edebe9' : ''; host.style.borderBottom = '';
return host; return host;
} }
@@ -124,9 +121,48 @@ export default class CustomBrandingApplicationCustomizer
} }
} }
private renderTopSafely(host: HTMLElement, elements: IBrandingElement[]): void {
this.clearTopElements();
const staging: HTMLElement = document.createElement('div');
try {
this._renderer.render(staging, elements);
} catch (error) {
this._renderer.clear(staging);
const status: HTMLElement = document.createElement('div');
status.className = 'custom-branding-status';
status.setAttribute('role', 'status');
status.textContent = strings.RenderError;
staging.appendChild(status);
this.debug('Top rendering failed.', error);
}
const insertBefore: ChildNode | null = host.firstChild;
while (staging.firstChild) {
const element: HTMLElement = staging.firstChild as HTMLElement;
element.setAttribute('data-custom-branding-owner', COMPONENT_ID);
element.setAttribute('data-custom-branding-placement', 'top');
host.insertBefore(element, insertBefore);
this._topElements.push(element);
}
}
private clearTopElements(): void {
for (let i: number = 0; i < this._topElements.length; i++) {
const element: HTMLElement = this._topElements[i];
if (element.parentNode) { element.parentNode.removeChild(element); }
}
this._topElements = [];
}
private removeLegacyTopHost(parent: HTMLElement): void {
const legacyHost: HTMLElement = parent.querySelector('#CustomBrandingTopHost') as HTMLElement;
if (legacyHost && legacyHost.parentNode === parent) {
parent.removeChild(legacyHost);
}
}
private _onTopPlaceholderDisposed = (): void => { private _onTopPlaceholderDisposed = (): void => {
this.removeHost(this._topHost); this.clearTopElements();
this._topHost = undefined;
this._topPlaceholder = undefined; this._topPlaceholder = undefined;
this.debug('Top placeholder disposed; waiting for a new placeholder.'); this.debug('Top placeholder disposed; waiting for a new placeholder.');
} }
@@ -143,12 +179,11 @@ export default class CustomBrandingApplicationCustomizer
if (this._isDisposed) { return; } if (this._isDisposed) { return; }
this._isDisposed = true; this._isDisposed = true;
this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceholders); this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceholders);
this.removeHost(this._topHost); this.clearTopElements();
this.removeHost(this._bottomHost); this.removeHost(this._bottomHost);
if (this._cssLoader) { if (this._cssLoader) {
this._cssLoader.dispose(); this._cssLoader.dispose();
} }
this._topHost = undefined;
this._bottomHost = undefined; this._bottomHost = undefined;
this._topPlaceholder = undefined; this._topPlaceholder = undefined;
this._bottomPlaceholder = undefined; this._bottomPlaceholder = undefined;
+10 -4
View File
@@ -23,9 +23,10 @@ var serveText = read('config/serve.json').toLowerCase();
var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts'); var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts');
var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts'); var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts');
var classicSource = read('classic/custom-branding-classic.js'); var classicSource = read('classic/custom-branding-classic.js');
var deploymentSource = read('deployment/add-custombranding.ps1');
assert(packageJson.version === '3.0.3', 'package.json hat nicht Version 3.0.3.'); assert(packageJson.version === '3.0.4', 'package.json hat nicht Version 3.0.4.');
assert(solution.version === '3.0.3.0', 'Solution-Version ist inkonsistent.'); assert(solution.version === '3.0.4.0', 'Solution-Version ist inkonsistent.');
assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.'); assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.');
assert(!solution.features, 'Die alte web-scoped Feature-Registrierung ist noch vorhanden.'); assert(!solution.features, 'Die alte web-scoped Feature-Registrierung ist noch vorhanden.');
assert(!fs.existsSync(path.join(root, 'sharepoint/assets/elements.xml')), 'elements.xml muss entfernt sein.'); assert(!fs.existsSync(path.join(root, 'sharepoint/assets/elements.xml')), 'elements.xml muss entfernt sein.');
@@ -38,8 +39,13 @@ assert(classicSource.indexOf('~sitecollection/SitePages/PortalSettings.aspx') >=
assert(appSource.indexOf('changedEvent.remove') >= 0, 'Lifecycle-Cleanup fuer changedEvent fehlt.'); assert(appSource.indexOf('changedEvent.remove') >= 0, 'Lifecycle-Cleanup fuer changedEvent fehlt.');
assert(appSource.indexOf('_onTopPlaceholderDisposed') >= 0 && appSource.indexOf('_onBottomPlaceholderDisposed') >= 0, assert(appSource.indexOf('_onTopPlaceholderDisposed') >= 0 && appSource.indexOf('_onBottomPlaceholderDisposed') >= 0,
'Top- und Bottom-Placeholder brauchen unabhaengige Dispose-Handler.'); 'Top- und Bottom-Placeholder brauchen unabhaengige Dispose-Handler.');
assert(appSource.indexOf("'CustomBrandingTopHost'") >= 0 && appSource.indexOf("'CustomBrandingBottomHost'") >= 0, assert(appSource.indexOf("'CustomBrandingBottomHost'") >= 0 &&
'Stabile Branding-Hosts fehlen.'); appSource.indexOf("'data-custom-branding-placement', 'top'") >= 0,
'Bottom-Host oder direkte Top-Elemente fehlen.');
assert(appSource.indexOf('renderTopSafely(this._topPlaceholder.domElement') >= 0,
'Top-Branding wird nicht direkt in CustomHeader gerendert.');
assert(deploymentSource.indexOf('$action.Sequence = 90') >= 0,
'CustomBranding muss vor MegaMenu geladen werden.');
assert(appSource.indexOf("'1px solid #edebe9'") >= 0 && appSource.indexOf('Segoe UI Web') >= 0, assert(appSource.indexOf("'1px solid #edebe9'") >= 0 && appSource.indexOf('Segoe UI Web') >= 0,
'SharePoint-Standardschrift und neutraler Host-Rahmen fehlen.'); 'SharePoint-Standardschrift und neutraler Host-Rahmen fehlen.');
assert(packageJson.scripts.package.indexOf('npm test') === 0, 'Paketierung muss mit Tests beginnen.'); assert(packageJson.scripts.package.indexOf('npm test') === 0, 'Paketierung muss mit Tests beginnen.');