Compare commits
4
Commits
0bf62ecadf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e98e88a7b | ||
|
|
281dfa68de | ||
|
|
36bcef54e7 | ||
|
|
583964a792 |
@@ -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.5 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
|
||||||
|
|
||||||
@@ -119,6 +121,10 @@ Das aktuelle Konfigurationsschema hat die Version 2:
|
|||||||
|
|
||||||
Bestehende 1.x-Konfigurationen mit einem Root-Array `elements` werden weiterhin als Top-Inhalt gelesen. Unbekannte Properties ignoriert die Runtime. Das vollständige Beispiel liegt unter `examples/custom-branding.example.json`.
|
Bestehende 1.x-Konfigurationen mit einem Root-Array `elements` werden weiterhin als Top-Inhalt gelesen. Unbekannte Properties ignoriert die Runtime. Das vollständige Beispiel liegt unter `examples/custom-branding.example.json`.
|
||||||
|
|
||||||
|
Eine eigene Suche kann deklarativ als `form` mit einem `input type="search"` und einem Submit-Button unter
|
||||||
|
`placeholdertop.elements` konfiguriert werden. Formulare werden ausschließlich als GET-Formulare akzeptiert;
|
||||||
|
unsichere oder protokollrelative Action-URLs werden verworfen. Das vollständige Beispiel enthält eine solche Suche.
|
||||||
|
|
||||||
Ist `placeholderbottom.elements` leer oder fehlt die Eigenschaft, rendert CustomBranding automatisch den bisherigen
|
Ist `placeholderbottom.elements` leer oder fehlt die Eigenschaft, rendert CustomBranding automatisch den bisherigen
|
||||||
**Portal Settings**-Link auf `~sitecollection/SitePages/PortalSettings.aspx`. Sobald eigene Footer-Elemente konfiguriert
|
**Portal Settings**-Link auf `~sitecollection/SitePages/PortalSettings.aspx`. Sobald eigene Footer-Elemente konfiguriert
|
||||||
sind, ersetzen sie diesen Standard.
|
sind, ersetzen sie diesen Standard.
|
||||||
@@ -193,8 +199,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')
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.5
|
||||||
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,4 +1,4 @@
|
|||||||
/* CustomBranding 2.0.0 - safe Classic SharePoint runtime */
|
/* CustomBranding 3.0.5 - safe Classic SharePoint runtime */
|
||||||
(function (global) {
|
(function (global) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
var OWNER = 'CustomBranding.Classic';
|
var OWNER = 'CustomBranding.Classic';
|
||||||
var MAX_DEPTH = 8;
|
var MAX_DEPTH = 8;
|
||||||
var MAX_ELEMENTS = 200;
|
var MAX_ELEMENTS = 200;
|
||||||
var allowedTags = ['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
|
var allowedTags = ['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
|
||||||
var allowedStyles = ('align-items background background-color border border-bottom border-color border-left border-radius border-right border-style border-top border-width box-sizing color display flex flex-basis flex-direction flex-grow flex-shrink flex-wrap font-family font-size font-style font-weight gap grid-template-columns height justify-content line-height margin margin-bottom margin-left margin-right margin-top max-height max-width min-height min-width opacity overflow padding padding-bottom padding-left padding-right padding-top text-align text-decoration text-transform white-space width').split(' ');
|
var allowedStyles = ('align-items background background-color border border-bottom border-color border-left border-radius border-right border-style border-top border-width box-sizing color display flex flex-basis flex-direction flex-grow flex-shrink flex-wrap font-family font-size font-style font-weight gap grid-template-columns height justify-content line-height margin margin-bottom margin-left margin-right margin-top max-height max-width min-height min-width opacity overflow padding padding-bottom padding-left padding-right padding-top text-align text-decoration text-transform white-space width').split(' ');
|
||||||
var state = { debug: false, hosts: [], css: [] };
|
var state = { debug: false, hosts: [], css: [] };
|
||||||
|
|
||||||
@@ -78,6 +78,8 @@
|
|||||||
if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; }
|
if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; }
|
||||||
if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; }
|
if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; }
|
||||||
if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; }
|
if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; }
|
||||||
|
if (tag === 'form') { return ['action', 'method'].indexOf(name) >= 0; }
|
||||||
|
if (tag === 'input') { return ['type', 'name', 'placeholder', 'autocomplete', 'maxlength'].indexOf(name) >= 0; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +96,23 @@
|
|||||||
var name = String(rawName).toLowerCase();
|
var name = String(rawName).toLowerCase();
|
||||||
var value = String(attributes[rawName] === undefined ? '' : attributes[rawName]).substring(0, 2048);
|
var value = String(attributes[rawName] === undefined ? '' : attributes[rawName]).substring(0, 2048);
|
||||||
if (name.indexOf('on') === 0 || !allowedAttribute(tag, name)) { continue; }
|
if (name.indexOf('on') === 0 || !allowedAttribute(tag, name)) { continue; }
|
||||||
if (name === 'href' || name === 'src') {
|
if (name === 'href' || name === 'src' || name === 'action') {
|
||||||
var safeUrl = sanitizeUrl(value, name === 'href');
|
var safeUrl = sanitizeUrl(value, name === 'href');
|
||||||
if (safeUrl) { element.setAttribute(name, safeUrl); }
|
if (safeUrl && safeUrl.indexOf('//') !== 0) { element.setAttribute(name, safeUrl); }
|
||||||
} else if (name === 'target') {
|
} else if (name === 'target') {
|
||||||
if (value === '_blank' || value === '_self') { element.setAttribute(name, value); }
|
if (value === '_blank' || value === '_self') { element.setAttribute(name, value); }
|
||||||
|
} else if (name === 'type' && tag === 'button') {
|
||||||
|
element.setAttribute('type', value.toLowerCase() === 'submit' ? 'submit' : 'button');
|
||||||
|
} else if (name === 'type' && tag === 'input') {
|
||||||
|
element.setAttribute('type', 'search');
|
||||||
|
} else if (name === 'method' && tag === 'form') {
|
||||||
|
element.setAttribute('method', 'get');
|
||||||
|
} else if (name === 'name' && tag === 'input') {
|
||||||
|
if (/^[a-z0-9_-]{1,64}$/i.test(value)) { element.setAttribute('name', value); }
|
||||||
|
} else if (name === 'autocomplete' && tag === 'input') {
|
||||||
|
if (value === 'on' || value === 'off') { element.setAttribute('autocomplete', value); }
|
||||||
|
} else if (name === 'maxlength' && tag === 'input') {
|
||||||
|
if (/^\d{1,4}$/.test(value)) { element.setAttribute('maxlength', value); }
|
||||||
} else if ((name === 'id' || name === 'class') && !/^[a-z0-9 _-]{1,256}$/i.test(value)) {
|
} else if ((name === 'id' || name === 'class') && !/^[a-z0-9 _-]{1,256}$/i.test(value)) {
|
||||||
continue;
|
continue;
|
||||||
} else if ((name === 'width' || name === 'height') && !/^\d{1,4}$/.test(value)) {
|
} else if ((name === 'width' || name === 'height') && !/^\d{1,4}$/.test(value)) {
|
||||||
@@ -109,7 +123,10 @@
|
|||||||
if (name === 'alt') { hasAlt = true; }
|
if (name === 'alt') { hasAlt = true; }
|
||||||
}
|
}
|
||||||
if (tag === 'img' && !hasAlt) { return null; }
|
if (tag === 'img' && !hasAlt) { return null; }
|
||||||
if (tag === 'button') { element.setAttribute('type', 'button'); }
|
if (tag === 'form' && !element.getAttribute('action')) { return null; }
|
||||||
|
if (tag === 'form') { element.setAttribute('method', 'get'); }
|
||||||
|
if (tag === 'input') { element.setAttribute('type', 'search'); element.setAttribute('name', element.getAttribute('name') || 'q'); }
|
||||||
|
if (tag === 'button' && element.getAttribute('type') !== 'submit') { element.setAttribute('type', 'button'); }
|
||||||
if (tag === 'a' && element.getAttribute('target') === '_blank') { element.setAttribute('rel', 'noopener noreferrer'); }
|
if (tag === 'a' && element.getAttribute('target') === '_blank') { element.setAttribute('rel', 'noopener noreferrer'); }
|
||||||
|
|
||||||
var styles = config.styles && typeof config.styles === 'object' ? config.styles : {};
|
var styles = config.styles && typeof config.styles === 'object' ? config.styles : {};
|
||||||
@@ -121,7 +138,7 @@
|
|||||||
if (config.content !== undefined && config.content !== null) {
|
if (config.content !== undefined && config.content !== null) {
|
||||||
element.appendChild(document.createTextNode(String(config.content).substring(0, 4000)));
|
element.appendChild(document.createTextNode(String(config.content).substring(0, 4000)));
|
||||||
}
|
}
|
||||||
if (tag !== 'img' && Array.isArray(config.children)) {
|
if (tag !== 'img' && tag !== 'input' && Array.isArray(config.children)) {
|
||||||
for (var i = 0; i < config.children.length; i++) {
|
for (var i = 0; i < config.children.length; i++) {
|
||||||
var child = createElement(config.children[i], depth + 1, counter);
|
var child = createElement(config.children[i], depth + 1, counter);
|
||||||
if (child) { element.appendChild(child); }
|
if (child) { element.appendChild(child); }
|
||||||
|
|||||||
@@ -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.5.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": true
|
"skipFeatureDeployment": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* Custom Branding und Mega Menu sind direkte Geschwister in #CustomHeader:
|
||||||
|
*
|
||||||
|
* #CustomHeader
|
||||||
|
* .system-ndzk-bar
|
||||||
|
* .portal-branding | #CustomNavigation
|
||||||
|
*/
|
||||||
|
|
||||||
|
#CustomHeader {
|
||||||
|
align-items: stretch;
|
||||||
|
background: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
font-family: "Segoe UI", "Segoe UI Web (West European)", sans-serif;
|
||||||
|
grid-template-areas:
|
||||||
|
"system system"
|
||||||
|
"branding navigation";
|
||||||
|
grid-template-columns: 240px minmax(0, 1fr);
|
||||||
|
grid-template-rows: 28px 309px;
|
||||||
|
gap: 0;
|
||||||
|
height: 250px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > .system-ndzk-bar {
|
||||||
|
align-items: center;
|
||||||
|
background: #c50f1f;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
grid-area: system;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > .portal-branding {
|
||||||
|
grid-area: branding;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > #CustomNavigation {
|
||||||
|
align-items: stretch;
|
||||||
|
align-self: start;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
border-top: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
grid-area: navigation;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
--megaMenuBarHeight: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > #CustomNavigation > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-main {
|
||||||
|
background: #003d79;
|
||||||
|
border-top: 0 !important;
|
||||||
|
color: #ffffff;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-top-level,
|
||||||
|
#CustomHeader > #CustomNavigation .menu-item-link,
|
||||||
|
#CustomHeader > #CustomNavigation .menu-item-text,
|
||||||
|
#CustomHeader > #CustomNavigation .menu-item-toggle {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-top-item:hover,
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-top-item:focus-within,
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-top-item:active,
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-top-item.is-open,
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-top-item.is-active {
|
||||||
|
background: #0f548c;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Das geöffnete Mega Menu nutzt auch die 240-px-Fläche unterhalb des Logos. */
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu {
|
||||||
|
left: -240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#CustomHeader > #CustomNavigation .mega-menu-grid {
|
||||||
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eigene Suche im freien Bereich rechts neben dem Logo. */
|
||||||
|
#CustomHeader > #CustomBrandingSearch {
|
||||||
|
align-self: start;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
justify-self: end;
|
||||||
|
margin: 64px 2px 0 0;
|
||||||
|
width: 272px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-form {
|
||||||
|
align-items: stretch;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #8a8886;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-form:focus-within {
|
||||||
|
border-color: #605e5c;
|
||||||
|
box-shadow: inset 0 0 0 1px #605e5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-input {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #323130;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
outline: 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-input::placeholder {
|
||||||
|
color: #605e5c;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-button {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #005a9e;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-button:hover,
|
||||||
|
.custom-branding-search-button:focus {
|
||||||
|
background: #f3f2f1;
|
||||||
|
color: #004578;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-branding-search-button:focus-visible {
|
||||||
|
box-shadow: inset 0 0 0 2px #605e5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-compositeHeader .ms-compositeHeader-searchBoxContainer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-branding {
|
||||||
|
align-items: flex-start;
|
||||||
|
border: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: 220px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-branding-home {
|
||||||
|
align-items: flex-start;
|
||||||
|
color: #323130;
|
||||||
|
display: block;
|
||||||
|
width: 240px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-branding-home:hover {
|
||||||
|
color: #201f1e;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-branding-home:focus-visible {
|
||||||
|
outline: 2px solid #605e5c;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal-branding-logo {
|
||||||
|
display: block;
|
||||||
|
height: 220px;
|
||||||
|
object-fit: contain;
|
||||||
|
width: 240px;
|
||||||
|
background-image: url(http://portal.contoso.local/Branding/Images/logo-background.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-compositeHeader {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
#CustomHeader,
|
||||||
|
.portal-branding,
|
||||||
|
.system-ndzk-bar {
|
||||||
|
border-color: CanvasText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"portalSettings": {
|
||||||
|
"providerKey": "custombranding",
|
||||||
|
"contractVersion": 1,
|
||||||
|
"minimumPortalSettingsVersion": "3.0.0"
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"debug": false,
|
||||||
|
"allowedCssHosts": [],
|
||||||
|
"cssfiles": [
|
||||||
|
{
|
||||||
|
"path": "http://portal.contoso.local/SiteAssets/branding/custom-branding.css",
|
||||||
|
"media": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"placeholdertop": {
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "div",
|
||||||
|
"content": "System NDzK",
|
||||||
|
"attributes": {
|
||||||
|
"class": "system-ndzk-bar",
|
||||||
|
"role": "note"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "div",
|
||||||
|
"attributes": {
|
||||||
|
"class": "portal-branding",
|
||||||
|
"role": "banner",
|
||||||
|
"aria-label": "Portal-Kopfbereich"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "a",
|
||||||
|
"attributes": {
|
||||||
|
"class": "portal-branding-home",
|
||||||
|
"href": "http://portal.contoso.local/SitePages/Home.aspx",
|
||||||
|
"title": "Zur Portal-Startseite"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "img",
|
||||||
|
"attributes": {
|
||||||
|
"class": "portal-branding-logo",
|
||||||
|
"src": "http://portal.contoso.local/Branding/Images/logo.png",
|
||||||
|
"alt": "Zentrale Abbildende Aufklärung"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"attributes": {
|
||||||
|
"id": "CustomBrandingSearch",
|
||||||
|
"class": "custom-branding-search-form",
|
||||||
|
"role": "search",
|
||||||
|
"aria-label": "Diese Website durchsuchen",
|
||||||
|
"action": "~sitecollection/_layouts/15/search.aspx/siteall",
|
||||||
|
"method": "get"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "input",
|
||||||
|
"attributes": {
|
||||||
|
"class": "custom-branding-search-input",
|
||||||
|
"type": "search",
|
||||||
|
"name": "q",
|
||||||
|
"placeholder": "Diese Website durchsuchen",
|
||||||
|
"autocomplete": "off",
|
||||||
|
"maxlength": "500",
|
||||||
|
"aria-label": "Diese Website durchsuchen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"attributes": {
|
||||||
|
"class": "custom-branding-search-button",
|
||||||
|
"type": "submit",
|
||||||
|
"title": "Suchen",
|
||||||
|
"aria-label": "Suchen"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "span",
|
||||||
|
"attributes": {
|
||||||
|
"class": "ms-Icon ms-Icon--Search",
|
||||||
|
"aria-hidden": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"placeholderbottom": {
|
||||||
|
"elements": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
.custom-branding-banner {
|
|
||||||
align-items: center;
|
|
||||||
background: #f3f2f1;
|
|
||||||
border-bottom: 1px solid #edebe9;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #323130;
|
|
||||||
display: flex;
|
|
||||||
font-family: "Segoe UI", "Segoe UI Web (West European)", sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
gap: 16px;
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 8px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-branding-link {
|
|
||||||
color: #005a9e;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-branding-link:hover {
|
|
||||||
color: #004578;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-branding-link:focus-visible,
|
|
||||||
.custom-branding-banner button:focus-visible {
|
|
||||||
outline: 2px solid #005a9e;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.custom-branding-banner {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
|
||||||
.custom-branding-banner {
|
|
||||||
border-bottom: 1px solid CanvasText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 2,
|
|
||||||
"portalSettings": {
|
|
||||||
"providerKey": "custombranding",
|
|
||||||
"contractVersion": 1,
|
|
||||||
"minimumPortalSettingsVersion": "3.0.0"
|
|
||||||
},
|
|
||||||
"enabled": true,
|
|
||||||
"debug": false,
|
|
||||||
"allowedCssHosts": [],
|
|
||||||
"cssfiles": [
|
|
||||||
{
|
|
||||||
"path": "~sitecollection/SiteAssets/branding/custom-branding.css",
|
|
||||||
"media": "all"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"placeholdertop": {
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "section",
|
|
||||||
"attributes": {
|
|
||||||
"class": "custom-branding-banner",
|
|
||||||
"role": "region",
|
|
||||||
"aria-label": "Portalhinweis"
|
|
||||||
},
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "strong",
|
|
||||||
"content": "Willkommen im Portal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "a",
|
|
||||||
"content": "Zur Startseite",
|
|
||||||
"attributes": {
|
|
||||||
"href": "~sitecollection/SitePages/Home.aspx",
|
|
||||||
"class": "custom-branding-link"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"placeholderbottom": {
|
|
||||||
"elements": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "custom-branding",
|
"name": "custom-branding",
|
||||||
"version": "3.0.3",
|
"version": "3.0.5",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "custom-branding",
|
"name": "custom-branding",
|
||||||
"version": "3.0.3",
|
"version": "3.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const MaxBrandingDepth: number = 8;
|
|||||||
export const MaxBrandingElements: number = 200;
|
export const MaxBrandingElements: number = 200;
|
||||||
export const MaxBrandingConfigurationLength: number = 100000;
|
export const MaxBrandingConfigurationLength: number = 100000;
|
||||||
|
|
||||||
const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
|
const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
|
||||||
const GlobalAttributes: string[] = ['id', 'class', 'title', 'role', 'aria-label', 'aria-hidden', 'aria-current', 'aria-live'];
|
const GlobalAttributes: string[] = ['id', 'class', 'title', 'role', 'aria-label', 'aria-hidden', 'aria-current', 'aria-live'];
|
||||||
const AllowedStyles: string[] = [
|
const AllowedStyles: string[] = [
|
||||||
'align-items', 'background', 'background-color', 'border', 'border-bottom', 'border-color', 'border-left',
|
'align-items', 'background', 'background-color', 'border', 'border-bottom', 'border-color', 'border-left',
|
||||||
@@ -254,6 +254,10 @@ function normalizeElement(value: any, depth: number, state: INormalizationState)
|
|||||||
state.warnings.push('Image without alt attribute was rejected.');
|
state.warnings.push('Image without alt attribute was rejected.');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (tag === 'form' && !attributes.action) {
|
||||||
|
state.warnings.push('Form without a safe action URL was rejected.');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const styles: { [key: string]: string } = {};
|
const styles: { [key: string]: string } = {};
|
||||||
if (value.styles && typeof value.styles === 'object') {
|
if (value.styles && typeof value.styles === 'object') {
|
||||||
@@ -272,7 +276,7 @@ function normalizeElement(value: any, depth: number, state: INormalizationState)
|
|||||||
const content: string = value.content === undefined || value.content === null
|
const content: string = value.content === undefined || value.content === null
|
||||||
? ''
|
? ''
|
||||||
: String(value.content).substring(0, 4000);
|
: String(value.content).substring(0, 4000);
|
||||||
const children: IBrandingElement[] = tag === 'img'
|
const children: IBrandingElement[] = tag === 'img' || tag === 'input'
|
||||||
? []
|
? []
|
||||||
: normalizeElements(value.children, depth + 1, state);
|
: normalizeElements(value.children, depth + 1, state);
|
||||||
if ((tag === 'a' || tag === 'button') && !content && children.length === 0 && !attributes['aria-label']) {
|
if ((tag === 'a' || tag === 'button') && !content && children.length === 0 && !attributes['aria-label']) {
|
||||||
@@ -293,6 +297,8 @@ function normalizeAttributes(tag: string, value: any, state: INormalizationState
|
|||||||
const result: { [key: string]: string } = {};
|
const result: { [key: string]: string } = {};
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
if (tag === 'button') { result.type = 'button'; }
|
if (tag === 'button') { result.type = 'button'; }
|
||||||
|
if (tag === 'form') { result.method = 'get'; }
|
||||||
|
if (tag === 'input') { result.type = 'search'; result.name = 'q'; }
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
for (const rawName in value) {
|
for (const rawName in value) {
|
||||||
@@ -303,9 +309,9 @@ function normalizeAttributes(tag: string, value: any, state: INormalizationState
|
|||||||
state.warnings.push('Attribute "' + name + '" was rejected.');
|
state.warnings.push('Attribute "' + name + '" was rejected.');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (name === 'href' || name === 'src') {
|
if (name === 'href' || name === 'src' || name === 'action') {
|
||||||
const safeUrl: string = sanitizeNavigationUrl(rawValue, state.siteCollectionUrl, name === 'href');
|
const safeUrl: string = sanitizeNavigationUrl(rawValue, state.siteCollectionUrl, name === 'href');
|
||||||
if (safeUrl) { result[name] = safeUrl; } else { state.warnings.push('URL attribute was rejected.'); }
|
if (safeUrl && safeUrl.indexOf('//') !== 0) { result[name] = safeUrl; } else { state.warnings.push('URL attribute was rejected.'); }
|
||||||
} else if (name === 'target') {
|
} else if (name === 'target') {
|
||||||
if (rawValue === '_blank' || rawValue === '_self') { result[name] = rawValue; }
|
if (rawValue === '_blank' || rawValue === '_self') { result[name] = rawValue; }
|
||||||
} else if (name === 'id' || name === 'class') {
|
} else if (name === 'id' || name === 'class') {
|
||||||
@@ -315,12 +321,24 @@ function normalizeAttributes(tag: string, value: any, state: INormalizationState
|
|||||||
} else if (name === 'aria-hidden') {
|
} else if (name === 'aria-hidden') {
|
||||||
if (rawValue === 'true' || rawValue === 'false') { result[name] = rawValue; }
|
if (rawValue === 'true' || rawValue === 'false') { result[name] = rawValue; }
|
||||||
} else if (name === 'type' && tag === 'button') {
|
} else if (name === 'type' && tag === 'button') {
|
||||||
result.type = 'button';
|
result.type = rawValue.toLowerCase() === 'submit' ? 'submit' : 'button';
|
||||||
|
} else if (name === 'type' && tag === 'input') {
|
||||||
|
result.type = 'search';
|
||||||
|
} else if (name === 'method' && tag === 'form') {
|
||||||
|
result.method = 'get';
|
||||||
|
} else if (name === 'name' && tag === 'input') {
|
||||||
|
if (/^[a-z0-9_-]{1,64}$/i.test(rawValue)) { result.name = rawValue; }
|
||||||
|
} else if (name === 'autocomplete' && tag === 'input') {
|
||||||
|
if (rawValue === 'on' || rawValue === 'off') { result.autocomplete = rawValue; }
|
||||||
|
} else if (name === 'maxlength' && tag === 'input') {
|
||||||
|
if (/^\d{1,4}$/.test(rawValue)) { result.maxlength = rawValue; }
|
||||||
} else {
|
} else {
|
||||||
result[name] = rawValue;
|
result[name] = rawValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tag === 'button') { result.type = 'button'; }
|
if (tag === 'button' && result.type !== 'submit') { result.type = 'button'; }
|
||||||
|
if (tag === 'form') { result.method = 'get'; }
|
||||||
|
if (tag === 'input') { result.type = 'search'; result.name = result.name || 'q'; }
|
||||||
if (tag === 'a' && result.target === '_blank') { result.rel = 'noopener noreferrer'; }
|
if (tag === 'a' && result.target === '_blank') { result.rel = 'noopener noreferrer'; }
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -330,6 +348,8 @@ function isAttributeAllowed(tag: string, name: string): boolean {
|
|||||||
if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; }
|
if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; }
|
||||||
if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; }
|
if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; }
|
||||||
if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; }
|
if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; }
|
||||||
|
if (tag === 'form') { return ['action', 'method'].indexOf(name) >= 0; }
|
||||||
|
if (tag === 'input') { return ['type', 'name', 'placeholder', 'autocomplete', 'maxlength'].indexOf(name) >= 0; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type BrandingElementType = 'div' | 'span' | 'p' | 'a' | 'button' | 'img' | 'h1' | 'h2' | 'h3' | 'strong' | 'em' | 'nav' | 'section';
|
export type BrandingElementType = 'div' | 'span' | 'p' | 'a' | 'button' | 'form' | 'input' | 'img' | 'h1' | 'h2' | 'h3' | 'strong' | 'em' | 'nav' | 'section';
|
||||||
|
|
||||||
export interface ICssFile {
|
export interface ICssFile {
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -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.5.', {
|
||||||
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: Node = 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;
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ assert(customBottom.config.placeholderbottom.elements.length === 1 &&
|
|||||||
var legacy = normalize({ elements: [{ type: 'span', content: 'Alt' }] }, siteUrl);
|
var legacy = normalize({ elements: [{ type: 'span', content: 'Alt' }] }, siteUrl);
|
||||||
assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.');
|
assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.');
|
||||||
|
|
||||||
['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section']
|
['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section']
|
||||||
.forEach(function (tag) {
|
.forEach(function (tag) {
|
||||||
var attributes = tag === 'img' ? { alt: '' } : undefined;
|
var attributes = tag === 'img' ? { alt: '' } : (tag === 'form' ? { action: '/search' } : undefined);
|
||||||
var content = tag === 'a' || tag === 'button' ? tag : undefined;
|
var content = tag === 'a' || tag === 'button' ? tag : undefined;
|
||||||
assert(!!element({ type: tag, content: content, attributes: attributes }), 'Erlaubter Tag fehlt: ' + tag);
|
assert(!!element({ type: tag, content: content, attributes: attributes }), 'Erlaubter Tag fehlt: ' + tag);
|
||||||
});
|
});
|
||||||
@@ -83,6 +83,24 @@ assert(!element({ type: 'img', attributes: { src: '/logo.png' } }), 'Bilder ohne
|
|||||||
assert(!!element({ type: 'img', attributes: { src: '/logo.png', alt: '' } }), 'Dekorative Bilder mit leerem alt sind erlaubt.');
|
assert(!!element({ type: 'img', attributes: { src: '/logo.png', alt: '' } }), 'Dekorative Bilder mit leerem alt sind erlaubt.');
|
||||||
assert(!element({ type: 'button' }), 'Leere Buttons muessen verworfen werden.');
|
assert(!element({ type: 'button' }), 'Leere Buttons muessen verworfen werden.');
|
||||||
|
|
||||||
|
var searchForm = element({
|
||||||
|
type: 'form',
|
||||||
|
attributes: { action: '~sitecollection/_layouts/15/search.aspx/siteall', method: 'post' },
|
||||||
|
children: [
|
||||||
|
{ type: 'input', attributes: { type: 'text', name: 'q', placeholder: 'Suchen', autocomplete: 'off' } },
|
||||||
|
{ type: 'button', attributes: { type: 'submit', 'aria-label': 'Suchen' } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
assert(searchForm.attributes.action === siteUrl + '/_layouts/15/search.aspx/siteall' && searchForm.attributes.method === 'get',
|
||||||
|
'Suchformulare muessen eine sichere GET-Action verwenden.');
|
||||||
|
assert(searchForm.children[0].attributes.type === 'search' && searchForm.children[0].attributes.name === 'q',
|
||||||
|
'Suchfelder muessen auf type=search begrenzt sein.');
|
||||||
|
assert(searchForm.children[1].attributes.type === 'submit', 'Submit-Buttons in Suchformularen muessen erhalten bleiben.');
|
||||||
|
assert(!element({ type: 'form', attributes: { action: 'javascript:alert(1)' } }),
|
||||||
|
'Formulare mit unsicherer Action muessen verworfen werden.');
|
||||||
|
assert(!element({ type: 'form', attributes: { action: '//evil.example.org/search' } }),
|
||||||
|
'Formulare mit protokollrelativer Action muessen verworfen werden.');
|
||||||
|
|
||||||
var css = normalize({
|
var css = normalize({
|
||||||
allowedCssHosts: ['cdn.example.org'],
|
allowedCssHosts: ['cdn.example.org'],
|
||||||
cssfiles: [
|
cssfiles: [
|
||||||
|
|||||||
@@ -21,25 +21,34 @@ var packageJson = json('package.json');
|
|||||||
var solution = json('config/package-solution.json').solution;
|
var solution = json('config/package-solution.json').solution;
|
||||||
var serveText = read('config/serve.json').toLowerCase();
|
var serveText = read('config/serve.json').toLowerCase();
|
||||||
var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts');
|
var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts');
|
||||||
|
var configSource = read('src/extensions/customBranding/BrandingConfig.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.5', 'package.json hat nicht Version 3.0.5.');
|
||||||
assert(solution.version === '3.0.3.0', 'Solution-Version ist inkonsistent.');
|
assert(solution.version === '3.0.5.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.');
|
||||||
assert(serveText.indexOf('onclick') < 0, 'serve.json enthaelt ein Event-Attribut.');
|
assert(serveText.indexOf('onclick') < 0, 'serve.json enthaelt ein Event-Attribut.');
|
||||||
assert(serveText.indexOf('placeholdertop') >= 0, 'serve.json verwendet nicht das echte Schema.');
|
assert(serveText.indexOf('placeholdertop') >= 0, 'serve.json verwendet nicht das echte Schema.');
|
||||||
assert(rendererSource.indexOf('innerHTML') < 0 && appSource.indexOf('innerHTML') < 0, 'Der moderne Renderer darf innerHTML nicht verwenden.');
|
assert(rendererSource.indexOf('innerHTML') < 0 && appSource.indexOf('innerHTML') < 0, 'Der moderne Renderer darf innerHTML nicht verwenden.');
|
||||||
|
assert(configSource.indexOf("'form', 'input'") >= 0 && configSource.indexOf("result.method = 'get'") >= 0,
|
||||||
|
'Sichere deklarative Suchformulare sind nicht freigeschaltet.');
|
||||||
assert(classicSource.indexOf('innerHTML') < 0, 'Die Classic-Runtime darf innerHTML nicht verwenden.');
|
assert(classicSource.indexOf('innerHTML') < 0, 'Die Classic-Runtime darf innerHTML nicht verwenden.');
|
||||||
assert(classicSource.indexOf('~sitecollection/SitePages/PortalSettings.aspx') >= 0,
|
assert(classicSource.indexOf('~sitecollection/SitePages/PortalSettings.aspx') >= 0,
|
||||||
'Der Standard-Footer fehlt in der Classic-Runtime.');
|
'Der Standard-Footer fehlt in der Classic-Runtime.');
|
||||||
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.');
|
||||||
|
|||||||
Reference in New Issue
Block a user