From 583964a7925c8ac8db11965996deb99246438c0a Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Wed, 19 Aug 2026 21:05:17 +0200 Subject: [PATCH] feat: Update version to 3.0.4 and enhance CustomBranding functionality --- README.md | 14 +++-- ToDo.md | 9 +-- config/package-solution.json | 2 +- deployment/add-custombranding.ps1 | 2 +- package-lock.json | 2 +- package.json | 2 +- .../CustomBrandingApplicationCustomizer.ts | 59 +++++++++++++++---- tests/validate-static-assets.js | 14 +++-- 8 files changed, 74 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index b2d67b0..1958beb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. @@ -16,14 +16,16 @@ Site Collection └── klassische Seiten: optionaler SPSite ScriptLink CustomHeader -├── CustomBrandingTopHost -└── MegaMenuHost (wird nicht verändert) +├── Branding-Elemente mit `data-custom-branding-placement="top"` +└── CustomNavigation (wird nicht verändert) CustomFooter └── 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 @@ -193,8 +195,8 @@ performance.getEntriesByType('resource') ({ header: !!document.getElementById('CustomHeader'), - branding: !!document.getElementById('CustomBrandingTopHost'), - megaMenu: !!document.getElementById('MegaMenuHost') + branding: document.querySelectorAll('#CustomHeader > [data-custom-branding-placement="top"]').length, + megaMenu: !!document.getElementById('CustomNavigation') }); ``` diff --git a/ToDo.md b/ToDo.md index acf3af2..ec7a7b9 100644 --- a/ToDo.md +++ b/ToDo.md @@ -3,7 +3,7 @@ Analysestand: 20.07.2026 Umgesetzt: 20.07.2026 Ausgangsversion: 1.0.4 -Zielversion: 3.0.3 +Zielversion: 3.0.4 Status: Implementierung und lokale Qualitätssicherung abgeschlossen ## Zielbild @@ -67,9 +67,10 @@ CustomBranding wird pro Site Collection genau einmal über eine `SPSite.UserCust ## MegaMenu-Integration -- [x] Stabile Hosts `CustomHeader`, `CustomBrandingTopHost`, `MegaMenuHost`, `CustomFooter` und `CustomBrandingBottomHost` definiert. -- [x] Renderer verändert ausschließlich eigene Host-Inhalte. -- [x] Wiederholtes Rendern löscht den `MegaMenuHost` oder fremde DOM-Knoten nicht. +- [x] CustomBranding mit Sequence 90 vor MegaMenu (100) und Current Navigation (110) registriert. +- [x] Top-Branding direkt als Geschwister von `CustomNavigation` in `CustomHeader` integriert; Bottom-Host bleibt separat. +- [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. - [ ] Beide realen Lade-Reihenfolgen mit installiertem MegaMenu auf dem Zielserver abnehmen. diff --git a/config/package-solution.json b/config/package-solution.json index 3f6f65c..7b252fc 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "custom-branding-client-side-solution", "id": "03a7c4de-e031-4b83-a683-5ca5c364166e", - "version": "3.0.3.0", + "version": "3.0.4.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/deployment/add-custombranding.ps1 b/deployment/add-custombranding.ps1 index eb8faa1..d2e09f5 100644 --- a/deployment/add-custombranding.ps1 +++ b/deployment/add-custombranding.ps1 @@ -187,7 +187,7 @@ try { $action.Title = $componentTitle $action.Description = $Description $action.Location = $componentLocation - $action.Sequence = 100 + $action.Sequence = 90 $action.ClientSideComponentId = $componentId $action.ClientSideComponentProperties = [string]($configuration | ConvertTo-Json -Depth 20 -Compress) $action.Update() diff --git a/package-lock.json b/package-lock.json index a1abbb3..f1d35db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "custom-branding", - "version": "3.0.3", + "version": "3.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index abe83b4..bae486b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "custom-branding", - "version": "3.0.3", + "version": "3.0.4", "private": true, "main": "lib/index.js", "engines": { diff --git a/src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts b/src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts index e20b5ca..799f36e 100644 --- a/src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts +++ b/src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts @@ -25,7 +25,7 @@ export default class CustomBrandingApplicationCustomizer private _topPlaceholder: PlaceholderContent | undefined; private _bottomPlaceholder: PlaceholderContent | undefined; - private _topHost: HTMLElement | undefined; + private _topElements: HTMLElement[] = []; private _bottomHost: HTMLElement | undefined; private _renderer: BrandingDomRenderer = new BrandingDomRenderer(); private _cssLoader: BrandingCssLoader | undefined; @@ -39,7 +39,7 @@ export default class CustomBrandingApplicationCustomizer this.context.pageContext.site.absoluteUrl ); this._config = result.config; - this.debug(strings.Initialized + ' 3.0.0.', { + this.debug(strings.Initialized + ' 3.0.4.', { schemaVersion: this._config.schemaVersion, cssFileCount: this._config.cssfiles.length, warningCount: result.warnings.length @@ -78,11 +78,8 @@ export default class CustomBrandingApplicationCustomizer if (this._topPlaceholder && this._topPlaceholder.domElement) { this._topPlaceholder.domElement.id = 'CustomHeader'; - this._topHost = this.getOrCreateOwnedHost( - this._topPlaceholder.domElement, - 'CustomBrandingTopHost' - ); - this.renderSafely(this._topHost, this._config.placeholdertop.elements); + this.removeLegacyTopHost(this._topPlaceholder.domElement); + this.renderTopSafely(this._topPlaceholder.domElement, this._config.placeholdertop.elements); } if (this._bottomPlaceholder && this._bottomPlaceholder.domElement) { this._bottomPlaceholder.domElement.id = 'CustomFooter'; @@ -106,7 +103,7 @@ export default class CustomBrandingApplicationCustomizer host.style.fontSize = '14px'; host.style.color = '#323130'; host.style.borderTop = id === 'CustomBrandingBottomHost' ? '1px solid #edebe9' : ''; - host.style.borderBottom = id === 'CustomBrandingTopHost' ? '1px solid #edebe9' : ''; + host.style.borderBottom = ''; 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 => { - this.removeHost(this._topHost); - this._topHost = undefined; + this.clearTopElements(); this._topPlaceholder = undefined; this.debug('Top placeholder disposed; waiting for a new placeholder.'); } @@ -143,12 +179,11 @@ export default class CustomBrandingApplicationCustomizer if (this._isDisposed) { return; } this._isDisposed = true; this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceholders); - this.removeHost(this._topHost); + this.clearTopElements(); this.removeHost(this._bottomHost); if (this._cssLoader) { this._cssLoader.dispose(); } - this._topHost = undefined; this._bottomHost = undefined; this._topPlaceholder = undefined; this._bottomPlaceholder = undefined; diff --git a/tests/validate-static-assets.js b/tests/validate-static-assets.js index 289cb67..7a7c043 100644 --- a/tests/validate-static-assets.js +++ b/tests/validate-static-assets.js @@ -23,9 +23,10 @@ var serveText = read('config/serve.json').toLowerCase(); var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts'); var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts'); 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(solution.version === '3.0.3.0', 'Solution-Version ist inkonsistent.'); +assert(packageJson.version === '3.0.4', 'package.json hat nicht Version 3.0.4.'); +assert(solution.version === '3.0.4.0', 'Solution-Version ist inkonsistent.'); assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.'); 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.'); @@ -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('_onTopPlaceholderDisposed') >= 0 && appSource.indexOf('_onBottomPlaceholderDisposed') >= 0, 'Top- und Bottom-Placeholder brauchen unabhaengige Dispose-Handler.'); -assert(appSource.indexOf("'CustomBrandingTopHost'") >= 0 && appSource.indexOf("'CustomBrandingBottomHost'") >= 0, - 'Stabile Branding-Hosts fehlen.'); +assert(appSource.indexOf("'CustomBrandingBottomHost'") >= 0 && + 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, 'SharePoint-Standardschrift und neutraler Host-Rahmen fehlen.'); assert(packageJson.scripts.package.indexOf('npm test') === 0, 'Paketierung muss mit Tests beginnen.');