feat: Add support for flyout menu mode in MegaMenu
- Introduced `MegaMenuMode` type and `normalizeMegaMenuMode` function to handle menu modes. - Updated `MegaMenuApplicationCustomizer` to accept `menuMode` property. - Enhanced `MegaMenuRenderer` to render flyout menus based on the selected mode. - Created new HTML previews for both flyout and mega menu modes. - Removed unused mock services and user custom action service interfaces.
This commit is contained in:
@@ -1,312 +1,253 @@
|
||||
# SharePoint MegaMenu
|
||||
|
||||
Barrierearme, dreistufige Portalnavigation auf Basis von SharePoint Managed Metadata für SharePoint Server Subscription Edition und SharePoint Server 2019.
|
||||
Zentrale, barrierearme Portalnavigation aus SharePoint Managed Metadata für SharePoint Server Subscription Edition und SharePoint Server 2019.
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
|---|---|
|
||||
| Version | 2.0.1 |
|
||||
| SharePoint Framework | SPFx 1.4.1 |
|
||||
| Node.js | 8.17.0 |
|
||||
| Zieloberfläche | Moderne SharePoint-Seiten |
|
||||
| Version | 2.1.0 |
|
||||
| SharePoint Framework | 1.4.1 |
|
||||
| Node.js für den Build | 8.17.0 |
|
||||
| Moderne Seiten | SPFx Application Customizer |
|
||||
| Klassische Seiten | separate ScriptLink-Variante |
|
||||
| Paket | `sharepoint/solution/mega-menu.sppkg` |
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Dreistufige Navigation aus einem Managed-Metadata-Termset
|
||||
- Auswahl des Termsets per GUID oder, abwärtskompatibel, per Name
|
||||
- Benutzerdefinierte Sortierung aus dem Term Store
|
||||
- Unterstützung der Navigations-Eigenschaften `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText`
|
||||
- Auflösung von `~sitecollection` in Navigations-URLs
|
||||
- Markierung des aktuellen Menüpunktes und seiner übergeordneten Einträge
|
||||
- Bedienung mit Maus, Tastatur und Touch
|
||||
- Responsive Darstellung
|
||||
- Optionales zusätzliches Stylesheet
|
||||
- Taxonomy-Cache mit konfigurierbarer Ablaufzeit
|
||||
- Bereinigter SPFx-Lifecycle ohne verbleibende globale Event Listener
|
||||
- Deutsche und englische Statusmeldungen
|
||||
- zentrale Konfiguration pro Site Collection
|
||||
- Termset-Auswahl bevorzugt per GUID, alternativ per Name
|
||||
- drei Navigationsebenen
|
||||
- zwei Darstellungsmodi: SharePoint-ähnliches Mega Menu und strukturelles Flyout
|
||||
- benutzerdefinierte Sortierung aus dem Term Store
|
||||
- Unterstützung für `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText`
|
||||
- Auflösung von `~sitecollection`
|
||||
- Markierung des aktuellen Navigationspfads
|
||||
- Tastatur-, Fokus-, Maus- und Touch-Bedienung
|
||||
- responsive Darstellung und Unterstützung reduzierter Bewegung
|
||||
- Session-Cache mit konfigurierbarer Laufzeit
|
||||
- Debug-Protokollierung ohne Ausgabe im Normalbetrieb
|
||||
- Unterstützung moderner und klassischer SharePoint-Seiten
|
||||
|
||||
## Architektur
|
||||
|
||||
```text
|
||||
UserCustomAction.ClientSideComponentProperties
|
||||
│
|
||||
▼
|
||||
MegaMenuApplicationCustomizer
|
||||
│
|
||||
├── TaxonomyNavigationService
|
||||
│ └── SPTermStorePickerService
|
||||
│ └── Default Site Collection Term Store
|
||||
│
|
||||
└── MegaMenuRenderer
|
||||
├── semantische Navigation
|
||||
├── Fokus- und Tastatursteuerung
|
||||
└── responsive Darstellung
|
||||
App Catalog
|
||||
└── zentral bereitgestelltes MegaMenu-SPFx-Bundle
|
||||
|
||||
Site Collection
|
||||
└── SPSite.UserCustomAction
|
||||
├── ComponentId
|
||||
└── ClientSideComponentProperties
|
||||
├── termSetId / termSetName
|
||||
├── cacheMinutes
|
||||
├── menuMode
|
||||
└── debug
|
||||
|
||||
Moderne Seite Klassische Seite
|
||||
└── Application Customizer └── ScriptLink + Classic-Runtime
|
||||
```
|
||||
|
||||
Das MegaMenu wird einmal pro Site Collection konfiguriert. Das Paket wird mit `skipFeatureDeployment: true` zentral über den App Catalog bereitgestellt, damit das SPFx-Bundle ohne eine App-Installation in jedem einzelnen Web verfügbar ist. PortalSettings speichert die zentrale Konfiguration im Property Bag des Root Webs. Eine Site-Collection-Action aktiviert das MegaMenu auf dem Root Web sowie auf vorhandenen und zukünftigen Unterwebs. Das MegaMenu selbst enthält keine eigene Administrationsoberfläche.
|
||||
Die Site-Collection-Action ist die einzige Konfigurationsquelle. Es gibt keine versteckte Konfigurationsliste, keine MegaMenu-Konfiguration im Property Bag und keine Web-scoped Kopien. PortalSettings kann diese Action zentral lesen und aktualisieren.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- SharePoint Server Subscription Edition oder SharePoint Server 2019
|
||||
- konfigurierter App Catalog
|
||||
- konfigurierter App Management Service
|
||||
- ein Termset im Standard-Term-Store der Site Collection
|
||||
- Leseberechtigung der Benutzer auf den Managed-Metadata-Service
|
||||
- für Entwicklung und Build: Node.js 8.17.0 und eine zu SPFx 1.4.1 passende npm-Version
|
||||
Das SPFx-Paket verwendet `skipFeatureDeployment: true`. Es wird im App Catalog zentral bereitgestellt und nicht in Root Web oder Unterwebs über „App hinzufügen“ installiert.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Die UserCustomAction unterstützt folgende `ClientSideComponentProperties`:
|
||||
|
||||
```json
|
||||
{
|
||||
"termSetId": "6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc",
|
||||
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
|
||||
"termSetName": "Global Navigation",
|
||||
"cacheMinutes": 15,
|
||||
"menuMode": "megaMenu",
|
||||
"debug": false
|
||||
}
|
||||
```
|
||||
|
||||
| Eigenschaft | Typ | Standard | Beschreibung |
|
||||
|---|---:|---:|---|
|
||||
| `termSetId` | GUID | leer | Bevorzugte, eindeutige Identifikation des Termsets |
|
||||
| `termSetName` | String | leer | Rückwärtskompatibler Fallback, wenn keine gültige `termSetId` vorhanden ist |
|
||||
| `cacheMinutes` | Zahl | `15` | Gültigkeit des Session-Caches, zulässig sind 1 bis 1440 Minuten |
|
||||
| `debug` | Boolean | `false` | Aktiviert zusätzliche Konsolenausgaben |
|
||||
| Property | Typ | Standard | Beschreibung |
|
||||
|---|---|---|---|
|
||||
| `termSetId` | String | leer | GUID des Termsets; hat Vorrang vor dem Namen |
|
||||
| `termSetName` | String | leer | Fallback für bestehende Konfigurationen |
|
||||
| `cacheMinutes` | Number | `15` | Cachezeit von 1 bis 1440 Minuten |
|
||||
| `menuMode` | String | `megaMenu` | `megaMenu` oder `flyout` |
|
||||
| `debug` | Boolean | `false` | ausführliche Browser-Konsolenausgaben |
|
||||
|
||||
`termSetId` hat Vorrang vor `termSetName`. Für neue Installationen wird die GUID empfohlen, weil Termset-Namen nicht zwingend eindeutig sind.
|
||||
### Mega Menu
|
||||
|
||||
### Kompatibilität mit PortalSettings
|
||||
`"menuMode": "megaMenu"` zeigt die zweite Ebene als breites, spaltenbasiertes Panel. Die dritte Ebene wird als Linkliste unter der jeweiligen Kategorie dargestellt.
|
||||
|
||||
PortalSettings verwaltet `termSetId`, `termSetName`, `cacheMinutes` und `debug`. Individuelle Gestaltung wird nicht vom MegaMenu selbst geladen, sondern zentral über die Solution Custom Branding bereitgestellt.
|
||||
### Flyout
|
||||
|
||||
## Termset-Aufbau
|
||||
|
||||
Die Darstellung unterstützt drei Ebenen:
|
||||
|
||||
```text
|
||||
Ebene 1: Hauptnavigation
|
||||
├── Ebene 2: Kategorie
|
||||
│ ├── Ebene 3: Link
|
||||
│ └── Ebene 3: Link
|
||||
└── Ebene 2: Kategorie
|
||||
```
|
||||
|
||||
Für Zieladressen werden die lokalen benutzerdefinierten Eigenschaften des Terms ausgewertet:
|
||||
|
||||
1. `_Sys_Nav_SimpleLinkUrl`
|
||||
2. `_Sys_Nav_TargetUrl`
|
||||
|
||||
Optional kann `_Sys_Nav_HoverText` als ergänzender Hilfetext verwendet werden. Veraltete Terms werden nicht angezeigt. Terms, die nicht für Tagging verfügbar sind, bleiben für Navigationsszenarien sichtbar.
|
||||
`"menuMode": "flyout"` zeigt die zweite Ebene als kompaktes Dropdown. Die dritte Ebene öffnet sich seitlich als strukturelles Flyout.
|
||||
|
||||
## Build
|
||||
|
||||
SPFx 1.4.1 verwendet die Legacy-Gulp-Toolchain. Neuere Node-Versionen sind nicht kompatibel.
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm run package
|
||||
```
|
||||
|
||||
Alternativ einzeln:
|
||||
`npm run package` erstellt automatisch beide Ausgaben:
|
||||
|
||||
- das SPFx-Paket für moderne Seiten
|
||||
- das Classic-Paket unter `classic/dist/`
|
||||
|
||||
Nur die Classic-Dateien neu erzeugen:
|
||||
|
||||
```powershell
|
||||
gulp clean
|
||||
gulp bundle --ship
|
||||
gulp package-solution --ship
|
||||
npm run build:classic
|
||||
```
|
||||
|
||||
Das fertige Paket befindet sich anschließend unter:
|
||||
Erwartete Umgebung:
|
||||
|
||||
```text
|
||||
Node.js 8.17.0
|
||||
npm 6.x
|
||||
lokales Gulp 3.9.1
|
||||
```
|
||||
|
||||
Das Ergebnis liegt unter:
|
||||
|
||||
```text
|
||||
sharepoint/solution/mega-menu.sppkg
|
||||
```
|
||||
|
||||
## Deployment
|
||||
## Moderne Bereitstellung
|
||||
|
||||
1. `mega-menu.sppkg` in den App Catalog laden.
|
||||
2. Die Solution freigeben.
|
||||
3. Die App im Root Web der gewünschten Site Collection installieren. Der MegaMenu Application Customizer wird dort zunächst web-scoped registriert und von PortalSettings erkannt.
|
||||
4. Das Termset in PortalSettings konfigurieren.
|
||||
1. `mega-menu.sppkg` in den Farm App Catalog hochladen beziehungsweise ersetzen.
|
||||
2. Die Lösung zentral für alle Sites verfügbar machen.
|
||||
3. MegaMenu nicht über „App hinzufügen“ in Inhaltswebsites installieren.
|
||||
4. Pro gewünschter Site Collection eine Site-Collection-Action anlegen.
|
||||
|
||||
Das folgende Skript ist nur erforderlich, wenn PortalSettings nicht verwendet wird oder eine vollständig skriptbasierte Vorkonfiguration gewünscht ist.
|
||||
|
||||
Empfohlen mit Termset-GUID:
|
||||
Beispiel für das Mega Menu:
|
||||
|
||||
```powershell
|
||||
.\deployment\add-megamenu.ps1 `
|
||||
-SiteUrl 'https://sharepoint/sites/portal' `
|
||||
-TermSetId '6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc' `
|
||||
-SiteUrl 'http://clshp001/sites/target' `
|
||||
-TermSetId '7948e6f9-7af4-431e-b6fe-328122f2746c' `
|
||||
-TermSetName 'Global Navigation' `
|
||||
-CacheMinutes 15
|
||||
-CacheMinutes 15 `
|
||||
-MenuMode megaMenu
|
||||
```
|
||||
|
||||
Abwärtskompatibel nur mit Namen:
|
||||
Beispiel für das Flyout:
|
||||
|
||||
```powershell
|
||||
.\deployment\add-megamenu.ps1 `
|
||||
-SiteUrl 'https://sharepoint/sites/portal' `
|
||||
-TermSetName 'Global Navigation'
|
||||
-SiteUrl 'http://clshp001/sites/target' `
|
||||
-TermSetId '7948e6f9-7af4-431e-b6fe-328122f2746c' `
|
||||
-MenuMode flyout `
|
||||
-EnableDebug
|
||||
```
|
||||
|
||||
Das Skript speichert die Konfiguration zentral unter `PortalSettings.MegaMenu.Configuration`, entfernt alte beziehungsweise doppelte Component IDs und synchronisiert die neue web-scoped Runtime-Bindung idempotent auf alle vorhandenen Webs. Fuer spaeter angelegte Unterwebs kann es ohne Nebenwirkungen erneut ausgefuehrt werden.
|
||||
Das Skript entfernt alte Web- und Site-Registrierungen, bereinigt die frühere Property-Bag-Konfiguration und erstellt genau eine neue Site-Collection-Action.
|
||||
|
||||
## Individuelles Branding
|
||||
## Classic Pages
|
||||
|
||||
Zusätzliche Stylesheets werden zentral über die Solution Custom Branding eingebunden. Das MegaMenu besitzt bewusst keine eigene `cssUrl`-Property.
|
||||
Die Classic-Variante bleibt erhalten, weil ein SPFx Application Customizer nur in der modernen Oberfläche ausgeführt wird.
|
||||
|
||||
Die wichtigsten CSS-Klassen sind:
|
||||
Das Classic-Paket wird bei `npm run build` und `npm run package` automatisch erzeugt. Es kann bei Bedarf auch separat gebaut werden:
|
||||
|
||||
```css
|
||||
#CustomNavigation { }
|
||||
#Mega-Menu { }
|
||||
.mega-menu-top-level { }
|
||||
.mega-menu-top-item { }
|
||||
.mega-menu { }
|
||||
.mega-menu-grid { }
|
||||
.mega-menu-category { }
|
||||
.mega-menu-links { }
|
||||
```powershell
|
||||
npm run build:classic
|
||||
```
|
||||
|
||||
Viele Farben und Abstände können über CSS Custom Properties überschrieben werden, beispielsweise:
|
||||
Ausgabe:
|
||||
|
||||
```text
|
||||
classic/dist/
|
||||
├── megamenu-classic.css
|
||||
├── megamenu-services-standalone.js
|
||||
├── megamenu-classic.js
|
||||
└── classic-deployment.md
|
||||
```
|
||||
|
||||
Die Classic-Runtime liest dieselben `ClientSideComponentProperties` der zentralen Site-Collection-Action und unterstützt ebenfalls beide Menümodi. Die Installationsdetails stehen in [classic/classic-deployment.md](classic/classic-deployment.md).
|
||||
|
||||
## Styling
|
||||
|
||||
Das moderne Styling liegt in:
|
||||
|
||||
```text
|
||||
src/extensions/megaMenu/MegaMenu.module.scss
|
||||
```
|
||||
|
||||
SPFx kompiliert diese Datei in das Bundle-CSS. Die eigenständige Classic-CSS liegt unter:
|
||||
|
||||
```text
|
||||
classic/megamenu-classic.css
|
||||
```
|
||||
|
||||
Beide Varianten orientieren sich an SharePoint mit Segoe UI, neutralen Flächen, Theme-Farbe für den aktiven Pfad, kompakten Abständen und Fabric-kompatiblen Fokusmarkierungen.
|
||||
|
||||
Die wichtigsten CSS-Variablen können durch Custom Branding überschrieben werden:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--megaMenuNavBackground: #5f6f74;
|
||||
--megaMenuNavTextColor: #ffffff;
|
||||
--megaMenuPanelWidth: 1280px;
|
||||
--megaMenuBarHeight: 44px;
|
||||
--megaMenuContentWidth: 1200px;
|
||||
--megaMenuFlyoutWidth: 288px;
|
||||
--megaMenuZIndex: 6000;
|
||||
}
|
||||
```
|
||||
|
||||
## Cache
|
||||
Das MegaMenu lädt bewusst keine externe `cssUrl`-Property. Kundenspezifische Gestaltung gehört in die Solution Custom Branding.
|
||||
|
||||
Das geladene Termset wird im `sessionStorage` des Browsers gespeichert. Der Schlüssel ist mit `MegaMenu:TermSet:` namensräumlich getrennt. Nach Ablauf von `cacheMinutes` wird das Termset automatisch neu geladen.
|
||||
## Diagnose
|
||||
|
||||
Für einen sofortigen administrativen Test kann der Session-Cache in den Browser-Entwicklertools gelöscht oder ein neues privates Browserfenster verwendet werden.
|
||||
Mit `"debug": true` schreibt die moderne Extension Meldungen mit diesen Quellen:
|
||||
|
||||
## Barrierefreiheit
|
||||
```text
|
||||
[MegaMenuApplicationCustomizer]
|
||||
[TaxonomyNavigationService]
|
||||
[SPTermStorePickerService]
|
||||
[MegaMenuRenderer]
|
||||
```
|
||||
|
||||
- semantisches `<nav aria-label="Hauptnavigation">`
|
||||
- native Listen- und Linksemantik
|
||||
- `aria-expanded` und `aria-haspopup` für aufklappbare Einträge
|
||||
- `aria-current="page"` für den aktuellen Navigationspfad
|
||||
- Öffnen über Leertaste oder Pfeil nach unten
|
||||
- Schließen über Escape
|
||||
- sichtbare Fokusindikatoren
|
||||
- Unterstützung von `prefers-reduced-motion` und kontrastreichen Darstellungen
|
||||
Geladenes Bundle prüfen:
|
||||
|
||||
Bewusst wird kein `role="menubar"` verwendet: Eine Portalnavigation ist eine normale Navigation und kein Desktop-Menü-Widget. Dadurch bleiben native Browser- und Screenreader-Erwartungen erhalten.
|
||||
```javascript
|
||||
performance.getEntriesByType('resource')
|
||||
.map(function (entry) { return entry.name; })
|
||||
.filter(function (url) { return url.toLowerCase().indexOf('mega-menu') >= 0; });
|
||||
```
|
||||
|
||||
## Fehlerdiagnose
|
||||
Aktiven Modus prüfen:
|
||||
|
||||
### „Die Navigation ist nicht konfiguriert“
|
||||
|
||||
Weder eine gültige `termSetId` noch ein `termSetName` ist gesetzt. UserCustomAction beziehungsweise PortalSettings-Konfiguration prüfen.
|
||||
|
||||
### „Die Navigation konnte nicht geladen werden“
|
||||
|
||||
- Existiert das Termset im Standard-Term-Store der Site Collection?
|
||||
- Ist die GUID korrekt?
|
||||
- Haben Benutzer Zugriff auf den Managed-Metadata-Service?
|
||||
- Enthält das Termset mindestens einen sichtbaren Term?
|
||||
- Ist der App-Catalog-Eintrag aktuell?
|
||||
|
||||
Für weitere technische Details kann vorübergehend `debug: true` gesetzt werden.
|
||||
|
||||
Bei aktiviertem Debug-Modus protokolliert MegaMenu den kompletten Startpfad mit den Quellen
|
||||
`MegaMenuApplicationCustomizer`, `TaxonomyNavigationService`, `SPTermStorePickerService` und
|
||||
`MegaMenuRenderer`. Dazu gehören die übergebenen Properties, Placeholder-Erkennung,
|
||||
Taxonomy-Anfrage, Cache-Nutzung, Term-Anzahl, Containerwahl und vollständige Fehlerdetails.
|
||||
Wenn keine dieser Meldungen erscheint, wurde das MegaMenu-Bundle nicht durch den SPFx-Loader
|
||||
ausgeführt.
|
||||
|
||||
### Änderungen am Termset erscheinen verzögert
|
||||
|
||||
Die Navigation wird bis zu `cacheMinutes` im Session-Cache gehalten. Cache leeren oder Ablaufzeit abwarten.
|
||||
|
||||
### Zusätzliches CSS wird nicht geladen
|
||||
|
||||
- URL und Dateiberechtigungen prüfen.
|
||||
- Für externe Quellen HTTPS verwenden.
|
||||
- Browser-Konsole bei aktiviertem Debug-Modus prüfen.
|
||||
|
||||
## Upgrade von 1.0.x
|
||||
|
||||
- Bestehende `termSetName`-Konfigurationen funktionieren unverändert.
|
||||
- Der alte unversionierte Session-Cache wird nicht weiterverwendet.
|
||||
- `cacheMinutes` verwendet ohne Konfiguration den Wert 15.
|
||||
- Die eigene, nicht verwendete MegaMenu-Einstellungsseite wurde entfernt; PortalSettings ist der zentrale Konfigurationsweg.
|
||||
- Globale Event Listener werden beim Dispose der Extension entfernt.
|
||||
- Paket-, npm- und Dokumentationsversion wurden auf 1.1.0 vereinheitlicht.
|
||||
|
||||
## Classic Pages
|
||||
|
||||
Unter `classic/` befindet sich eine separate Kompatibilitätsimplementierung für klassische SharePoint-Seiten. Sie wird nicht vom SPFx-Bundle ausgeführt und besitzt einen eigenen Deploymentweg. Neue Funktionen sollten zuerst in der modernen SPFx-Variante umgesetzt und anschließend bewusst in die Classic-Variante übernommen werden.
|
||||
```javascript
|
||||
document.getElementById('Mega-Menu').getAttribute('data-menu-mode');
|
||||
```
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```text
|
||||
MegaMenu/
|
||||
├── config/ SPFx-Build- und Paketkonfiguration
|
||||
├── deployment/ Farm-/Site-Registrierung
|
||||
├── src/
|
||||
│ ├── extensions/megaMenu/ Application Customizer und Renderer
|
||||
│ └── services/ Taxonomy- und Datenmodelle
|
||||
├── classic/ optionale Classic-Kompatibilität
|
||||
├── build-classic.ps1 Classic-Build
|
||||
├── classic/ Classic-Runtime und CSS
|
||||
├── config/ SPFx-Buildkonfiguration
|
||||
├── deployment/add-megamenu.ps1 zentrale Site-Registrierung
|
||||
├── src/extensions/megaMenu/ Application Customizer, Renderer und SCSS
|
||||
├── src/services/ Taxonomy- und Navigationsdienste
|
||||
├── build-classic.ps1
|
||||
├── gulpfile.js
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Versionshistorie
|
||||
|
||||
### 2.1.0
|
||||
|
||||
- SharePoint-ähnliches, helles Navigationsdesign.
|
||||
- Neue Property `menuMode` mit `megaMenu` und `flyout`.
|
||||
- Eigenständiges Flyout-Rendering für zweite und dritte Ebene.
|
||||
- Classic-Runtime und Classic-CSS auf beide Modi aktualisiert.
|
||||
- Konfiguration auf eine Site-Collection-Action konsolidiert.
|
||||
- Veraltete `elements.xml`, Azure-CDN-Konfiguration, Mock- und UserCustomAction-Service-Dateien entfernt.
|
||||
- Bereitstellungs- und Classic-Buildskripte vereinfacht.
|
||||
|
||||
### 2.0.1
|
||||
|
||||
- Zentrale SPFx-Bereitstellung mit `skipFeatureDeployment: true`.
|
||||
- Das MegaMenu-Paket muss nicht mehr als App in jedem Unterweb installiert werden.
|
||||
- Feature-Framework-Registrierung aus dem Paket entfernt; die Aktivierung erfolgt zentral über PortalSettings oder das Deploymentskript.
|
||||
- Keine App-Installation in einzelnen Inhaltswebsites erforderlich.
|
||||
|
||||
### 2.0.0
|
||||
|
||||
- Neue Application-Customizer-ID `c0abbffb-355d-4d4e-bd38-9e15fb811506`, um alte On-Premises-Manifest- und Loader-Zuordnungen sicher zu umgehen.
|
||||
- Eine zentrale MegaMenu-Konfiguration pro Site Collection im Property Bag des Root Webs.
|
||||
- Web-scoped Runtime-Bindungen werden auf Root Web und alle vorhandenen Unterwebs synchronisiert.
|
||||
- Aktivierungsskript entfernt alte Component IDs und kann fuer neue Unterwebs idempotent erneut ausgefuehrt werden.
|
||||
|
||||
### 1.1.4
|
||||
|
||||
- Unnötige externe Abhängigkeit zu `@microsoft/sp-lodash-subset` entfernt.
|
||||
- Terms werden beim Sortieren über eine lokale, SPFx-1.4.1-kompatible Pfadsuche zugeordnet.
|
||||
- Verhindert, dass der Application Customizer bereits bei der Systemkomponenten-Auflösung übersprungen wird.
|
||||
|
||||
### 1.1.3
|
||||
|
||||
- Ausführliches, ausschließlich über `debug: true` aktiviertes Lifecycle-Logging ergänzt.
|
||||
- Taxonomy-Anfragen, Session-Cache, Term-Filterung und Renderer werden nachvollziehbar protokolliert.
|
||||
- Fehlerausgaben enthalten Name, Meldung und Stacktrace.
|
||||
|
||||
### 1.1.2
|
||||
|
||||
- `cssUrl` zugunsten der zentralen Custom-Branding-Solution entfernt
|
||||
- deaktivierte, veraltete oder nicht zum Tagging verfügbare Terms werden ausgeblendet
|
||||
- Cache-Namespace aktualisiert, damit alte deaktivierte Einträge nicht weiter angezeigt werden
|
||||
|
||||
### 1.1.1
|
||||
|
||||
- automatische Registrierung des MegaMenu Application Customizers bei der App-Installation
|
||||
- direkte Erkennung durch PortalSettings ohne vorherigen Skriptaufruf
|
||||
|
||||
### 1.1.0
|
||||
|
||||
- konsistente Verwendung des Default Site Collection Term Store
|
||||
- optionale Konfiguration per Termset-GUID
|
||||
- Cache-TTL und namensräumlich getrennter Session-Cache
|
||||
- korrekte Anwendung der Deprecated-/Availability-Filter
|
||||
- Prüfung der Taxonomy-HTTP-Antworten
|
||||
- Lifecycle-Cleanup für globale Event Listener
|
||||
- korrigierte Navigationssemantik und aktive Screenreader-Statusmeldungen
|
||||
- Validierung externer CSS-URLs
|
||||
- sichtbare Konfigurations- und Ladefehler
|
||||
- Entfernung des ungenutzten lokalen Settings-Panels
|
||||
- vereinheitlichte Projektversion und neue Build-/Deployment-Dokumentation
|
||||
- Neue Application-Customizer-ID `c0abbffb-355d-4d4e-bd38-9e15fb811506`.
|
||||
- Alte On-Premises-Manifest- und Loader-Zuordnungen umgangen.
|
||||
|
||||
+21
-259
@@ -1,272 +1,34 @@
|
||||
# Build script for classic SharePoint version
|
||||
# Creates a standalone distribution with proper service extraction
|
||||
|
||||
Write-Host "Building MegaMenu Classic Distribution..." -ForegroundColor Green
|
||||
|
||||
# Ensure output directory exists
|
||||
$outputPath = ".\classic"
|
||||
if (-not (Test-Path $outputPath)) {
|
||||
New-Item -ItemType Directory -Path $outputPath -Force
|
||||
}
|
||||
|
||||
# Step 1: Build the modern SPFx version to ensure CSS is up to date
|
||||
Write-Host "Building SPFx version for CSS..." -ForegroundColor Yellow
|
||||
try {
|
||||
gulp bundle --ship
|
||||
Write-Host "✓ SPFx build completed" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠ SPFx build failed, using existing CSS" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Step 2: Copy CSS from the built version (same CSS for both classic and modern)
|
||||
Write-Host "Copying CSS files..." -ForegroundColor Yellow
|
||||
$cssSource = ".\lib\extensions\megaMenu\MegaMenu.css"
|
||||
$cssTarget = ".\classic\MegaMenu.css"
|
||||
|
||||
if (Test-Path $cssSource) {
|
||||
Copy-Item $cssSource $cssTarget -Force
|
||||
Write-Host "✓ CSS copied from SPFx build" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠ Built CSS not found, keeping existing classic CSS" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Step 3: Verify standalone services exist
|
||||
$servicesFile = ".\classic\megamenu-services-standalone.js"
|
||||
if (Test-Path $servicesFile) {
|
||||
Write-Host "✓ Standalone services found" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "✗ Missing megamenu-services-standalone.js" -ForegroundColor Red
|
||||
Write-Host " Please ensure this file exists in the classic folder" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Step 4: Verify classic wrapper exists
|
||||
$wrapperFile = ".\classic\megamenu-classic.js"
|
||||
if (Test-Path $wrapperFile) {
|
||||
Write-Host "✓ Classic wrapper found" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "✗ Missing megamenu-classic.js" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# Step 5: Create complete deployment package info
|
||||
Write-Host "Creating deployment documentation..." -ForegroundColor Yellow
|
||||
|
||||
$deploymentContent = @"
|
||||
# SharePoint MegaMenu Classic Deployment Guide
|
||||
Version 1.0.2 - Classic SharePoint Support
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This classic version reuses the exact same service logic as the modern SPFx version:
|
||||
- **Same CSS** (MegaMenu.css from SPFx build)
|
||||
- **Same Services** (TaxonomyNavigationService, MegaMenuRenderer extracted to standalone)
|
||||
- **Same Configuration** (Reads UserCustomAction properties like SPFx version)
|
||||
- **Same Menu Structure** (Identical 3-level navigation rendering)
|
||||
|
||||
## Required Files
|
||||
|
||||
### Core Files (Required)
|
||||
1. **MegaMenu.css** - Main stylesheet (shared with SPFx version)
|
||||
2. **megamenu-services-standalone.js** - Extracted services without SPFx dependencies
|
||||
3. **megamenu-classic.js** - Classic SharePoint integration wrapper
|
||||
|
||||
### Dependencies (SharePoint Built-in)
|
||||
- SP.js (SharePoint JSOM)
|
||||
- SP.Taxonomy.js (Taxonomy/Metadata services)
|
||||
- jQuery (usually available in SharePoint)
|
||||
|
||||
## Deployment Methods
|
||||
|
||||
### Method 1: Site Assets (Recommended)
|
||||
````html
|
||||
<!-- Add to master page before closing </head> tag -->
|
||||
<link rel="stylesheet" href="/SiteAssets/megamenu/MegaMenu.css" />
|
||||
<script src="/SiteAssets/megamenu/megamenu-services-standalone.js"></script>
|
||||
<script src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||
````
|
||||
|
||||
### Method 2: Style Library
|
||||
````html
|
||||
<link rel="stylesheet" href="/Style Library/megamenu/MegaMenu.css" />
|
||||
<script src="/Style Library/megamenu/megamenu-services-standalone.js"></script>
|
||||
<script src="/Style Library/megamenu/megamenu-classic.js"></script>
|
||||
````
|
||||
|
||||
### Method 3: CDN or Document Library
|
||||
Upload to any accessible location and reference accordingly.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Automatic Configuration Reading
|
||||
The classic version automatically reads the same UserCustomAction configuration as the SPFx version:
|
||||
- Term Set Name (default: "Navigation")
|
||||
- External CSS URL (optional)
|
||||
|
||||
### Setting Configuration
|
||||
Use one of these methods:
|
||||
|
||||
**Option A: Via Modern SPFx Version**
|
||||
1. Install modern SPFx version on any modern SharePoint site
|
||||
2. Use the settings panel to configure termset and CSS URL
|
||||
3. Configuration is stored site-wide and will be read by classic version
|
||||
|
||||
**Option B: Via PowerShell (Advanced)**
|
||||
````powershell
|
||||
# Connect to SharePoint site
|
||||
Add-PnPCustomAction -Name "MegaMenuConfig" -Location "ClientSideExtension.ApplicationCustomizer" `
|
||||
-ClientSideComponentId "c0abbffb-355d-4d4e-bd38-9e15fb811506" `
|
||||
-ClientSideComponentProperties '{"termSetName":"Your Navigation Termset","cssUrl":"/SiteAssets/custom-menu.css"}'
|
||||
````
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
### 1. Verify Dependencies
|
||||
Open browser console and check:
|
||||
````javascript
|
||||
// These should all return objects/functions:
|
||||
typeof SP
|
||||
typeof SP.Taxonomy
|
||||
typeof window.MegaMenuServices
|
||||
typeof window.MegaMenuClassic
|
||||
````
|
||||
|
||||
### 2. Enable Debug Mode
|
||||
````javascript
|
||||
window.MegaMenuConfig = { debug: true };
|
||||
````
|
||||
Then reload page to see console logging.
|
||||
|
||||
### 3. Manual Initialization
|
||||
If auto-loading fails:
|
||||
````javascript
|
||||
window.MegaMenuClassic.reload();
|
||||
````
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **"SP is undefined"** - SP.js not loaded yet, classic wrapper will retry
|
||||
2. **"MegaMenuServices not found"** - Include megamenu-services-standalone.js first
|
||||
3. **"No menu items found"** - Check term set name and user permissions
|
||||
4. **Menu renders but no styling** - Verify CSS path and inclusion
|
||||
|
||||
### Debug Steps
|
||||
1. Check browser network tab for 404s on CSS/JS files
|
||||
2. Verify SharePoint libraries load: SP.js, SP.Taxonomy.js
|
||||
3. Check term store permissions and term set existence
|
||||
4. Enable debug mode for detailed console logging
|
||||
|
||||
### Permissions Required
|
||||
- Read access to term store
|
||||
- Read access to site UserCustomActions (for configuration)
|
||||
- Basic site user permissions
|
||||
|
||||
## Differences from SPFx Version
|
||||
|
||||
| Feature | SPFx Version | Classic Version |
|
||||
|---------|-------------|----------------|
|
||||
| Deployment | App Catalog | Master Page/Web Part |
|
||||
| Configuration | Settings Panel | PowerShell or via SPFx |
|
||||
| Auto-loading | Yes | Manual inclusion |
|
||||
| Modern Sites | Yes | No |
|
||||
| Classic Sites | No | Yes |
|
||||
| SharePoint Online | Yes | Yes |
|
||||
| SharePoint On-Premises | 2019+ | 2013+ |
|
||||
|
||||
## File Structure After Deployment
|
||||
````
|
||||
/SiteAssets/megamenu/
|
||||
├── MegaMenu.css (from SPFx build)
|
||||
├── megamenu-services-standalone.js (extracted services)
|
||||
└── megamenu-classic.js (classic wrapper)
|
||||
````
|
||||
|
||||
"@
|
||||
|
||||
Set-Content -Path ".\classic\deployment-guide.md" -Value $deploymentContent -Encoding UTF8
|
||||
|
||||
# Step 6: Create a simple deployment script template
|
||||
$deployScript = @"
|
||||
# PowerShell script to deploy MegaMenu Classic to SharePoint
|
||||
# Requires PnP PowerShell module: Install-Module PnP.PowerShell
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=`$true)]
|
||||
[string]`$SiteUrl,
|
||||
|
||||
[string]`$LibraryName = "Site Assets",
|
||||
[string]`$FolderName = "megamenu"
|
||||
[string]$OutputPath = ''
|
||||
)
|
||||
|
||||
Write-Host "Deploying MegaMenu Classic to `$SiteUrl" -ForegroundColor Green
|
||||
|
||||
# Connect to SharePoint
|
||||
Connect-PnPOnline -Url `$SiteUrl -Interactive
|
||||
|
||||
# Create folder if it doesn't exist
|
||||
try {
|
||||
Get-PnPFolder -Url "`$LibraryName/`$FolderName" -ErrorAction Stop
|
||||
Write-Host "✓ Folder exists: `$LibraryName/`$FolderName" -ForegroundColor Green
|
||||
} catch {
|
||||
Add-PnPFolder -Name `$FolderName -Folder `$LibraryName
|
||||
Write-Host "✓ Created folder: `$LibraryName/`$FolderName" -ForegroundColor Green
|
||||
$sourcePath = Join-Path $PSScriptRoot 'classic'
|
||||
$resolvedOutputPath = if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
Join-Path $sourcePath 'dist'
|
||||
}
|
||||
else {
|
||||
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
|
||||
}
|
||||
|
||||
# Upload files
|
||||
`$files = @(
|
||||
@{Source=".\MegaMenu.css"; Target="`$LibraryName/`$FolderName/MegaMenu.css"},
|
||||
@{Source=".\megamenu-services-standalone.js"; Target="`$LibraryName/`$FolderName/megamenu-services-standalone.js"},
|
||||
@{Source=".\megamenu-classic.js"; Target="`$LibraryName/`$FolderName/megamenu-classic.js"}
|
||||
)
|
||||
|
||||
foreach (`$file in `$files) {
|
||||
if (Test-Path `$file.Source) {
|
||||
Add-PnPFile -Path `$file.Source -Folder "`$LibraryName/`$FolderName"
|
||||
Write-Host "✓ Uploaded: `$(`$file.Source)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "✗ File not found: `$(`$file.Source)" -ForegroundColor Red
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $resolvedOutputPath)) {
|
||||
New-Item -ItemType Directory -Path $resolvedOutputPath -Force | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "`nDeployment completed!" -ForegroundColor Green
|
||||
Write-Host "Add these lines to your master page:" -ForegroundColor Yellow
|
||||
Write-Host "<link rel=`"stylesheet`" href=`"/`$LibraryName/`$FolderName/MegaMenu.css`" />" -ForegroundColor Gray
|
||||
Write-Host "<script src=`"/`$LibraryName/`$FolderName/megamenu-services-standalone.js`"></script>" -ForegroundColor Gray
|
||||
Write-Host "<script src=`"/`$LibraryName/`$FolderName/megamenu-classic.js`"></script>" -ForegroundColor Gray
|
||||
"@
|
||||
|
||||
Set-Content -Path ".\classic\deploy-to-sharepoint.ps1" -Value $deployScript -Encoding UTF8
|
||||
|
||||
# Step 7: Summary
|
||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
||||
Write-Host "MegaMenu Classic Build Completed!" -ForegroundColor Green
|
||||
Write-Host "="*60 -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDistribution files created:" -ForegroundColor Yellow
|
||||
$files = @(
|
||||
@{Name="MegaMenu.css"; Desc="Shared stylesheet from SPFx"},
|
||||
@{Name="megamenu-services-standalone.js"; Desc="Extracted services (no SPFx deps)"},
|
||||
@{Name="megamenu-classic.js"; Desc="Classic SharePoint wrapper"},
|
||||
@{Name="deployment-guide.md"; Desc="Complete deployment documentation"},
|
||||
@{Name="deploy-to-sharepoint.ps1"; Desc="PowerShell deployment script"}
|
||||
'megamenu-classic.css',
|
||||
'megamenu-services-standalone.js',
|
||||
'megamenu-classic.js',
|
||||
'classic-deployment.md'
|
||||
)
|
||||
|
||||
foreach ($file in $files) {
|
||||
$exists = Test-Path ".\classic\$($file.Name)"
|
||||
$status = if ($exists) { "✓" } else { "✗" }
|
||||
$color = if ($exists) { "Green" } else { "Red" }
|
||||
Write-Host " $status $($file.Name)" -ForegroundColor $color -NoNewline
|
||||
Write-Host " - $($file.Desc)" -ForegroundColor Gray
|
||||
$source = Join-Path $sourcePath $file
|
||||
if (-not (Test-Path -LiteralPath $source)) {
|
||||
throw ('Classic-Datei fehlt: ' + $source)
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $source -Destination (Join-Path $resolvedOutputPath $file) -Force
|
||||
Write-Host ('Kopiert: ' + $file) -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "`nNext steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Review deployment-guide.md for instructions" -ForegroundColor White
|
||||
Write-Host "2. Use deploy-to-sharepoint.ps1 for automated deployment" -ForegroundColor White
|
||||
Write-Host "3. Configure termset via modern SPFx version or PowerShell" -ForegroundColor White
|
||||
Write-Host "4. Test on classic SharePoint pages" -ForegroundColor White
|
||||
|
||||
Write-Host "`nArchitectural Achievement:" -ForegroundColor Cyan
|
||||
Write-Host "✓ Same CSS as SPFx version" -ForegroundColor Green
|
||||
Write-Host "✓ Same service logic (extracted to standalone)" -ForegroundColor Green
|
||||
Write-Host "✓ Same configuration system" -ForegroundColor Green
|
||||
Write-Host "✓ Same menu rendering output" -ForegroundColor Green
|
||||
Write-Host "✓ No SPFx dependencies in classic version" -ForegroundColor Green
|
||||
Write-Host ('Classic-Paket erstellt: ' + $resolvedOutputPath) -ForegroundColor Green
|
||||
|
||||
+38
-192
@@ -1,216 +1,62 @@
|
||||
# MegaMenu für klassische SharePoint-Seiten
|
||||
|
||||
Diese Dateien ermöglichen die Verwendung des MegaMenus auf klassischen SharePoint-Seiten (2019/SE) ohne SPFx Framework.
|
||||
Die Classic-Variante verwendet dieselbe Site-Collection-Konfiguration wie der moderne Application Customizer. Sie unterstützt `megaMenu` und `flyout`, wird aber unabhängig von SPFx über ScriptLink geladen.
|
||||
|
||||
## 📁 Dateien
|
||||
## Dateien
|
||||
|
||||
- `megamenu-classic.js` - Hauptlogik für klassische Seiten
|
||||
- `megamenu-classic.css` - Styling für klassische Seiten
|
||||
- `classic-deployment.md` - Diese Dokumentation
|
||||
- `megamenu-classic.css`
|
||||
- `megamenu-services-standalone.js`
|
||||
- `megamenu-classic.js`
|
||||
|
||||
## 🚀 Installation
|
||||
Mit `build-classic.ps1` werden die Dateien nach `classic/dist` kopiert.
|
||||
|
||||
### Option 1: Master Page (empfohlen)
|
||||
## Konfiguration
|
||||
|
||||
1. **Dateien hochladen:**
|
||||
```
|
||||
/SiteAssets/megamenu/megamenu-classic.js
|
||||
/SiteAssets/megamenu/megamenu-classic.css
|
||||
```
|
||||
Die Classic-Variante liest die `ClientSideComponentProperties` der zentralen MegaMenu-Site-Action:
|
||||
|
||||
2. **Master Page bearbeiten:**
|
||||
```html
|
||||
<!-- In den <head> Bereich -->
|
||||
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
||||
|
||||
<!-- Vor dem schließenden </body> Tag -->
|
||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||
```
|
||||
|
||||
### Option 2: ScriptLink über PowerShell
|
||||
|
||||
```powershell
|
||||
# CSS hinzufügen
|
||||
$web = Get-SPWeb "http://your-site-url"
|
||||
$web.AlternateCSSUrl = "/SiteAssets/megamenu/megamenu-classic.css"
|
||||
$web.Update()
|
||||
|
||||
# JavaScript als Custom Action hinzufügen
|
||||
$customAction = $web.UserCustomActions.Add()
|
||||
$customAction.Location = "ScriptLink"
|
||||
$customAction.ScriptSrc = "/SiteAssets/megamenu/megamenu-classic.js"
|
||||
$customAction.Sequence = 1000
|
||||
$customAction.Update()
|
||||
$web.Update()
|
||||
```json
|
||||
{
|
||||
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
|
||||
"termSetName": "Global Navigation",
|
||||
"cacheMinutes": 15,
|
||||
"menuMode": "megaMenu",
|
||||
"debug": false
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: Content Editor Web Part
|
||||
`menuMode` erlaubt `megaMenu` oder `flyout`. `termSetId` hat Vorrang vor `termSetName`.
|
||||
|
||||
## Einbindung
|
||||
|
||||
Die drei Dateien beispielsweise nach `/SiteAssets/megamenu/` hochladen und in dieser Reihenfolge laden:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||
<link rel="stylesheet" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
||||
<script src="/SiteAssets/megamenu/megamenu-services-standalone.js"></script>
|
||||
<script src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||
```
|
||||
|
||||
## ⚙️ Konfiguration
|
||||
Die Einbindung kann über die Masterpage oder eine Site-scoped ScriptLink-CustomAction erfolgen. Der moderne SPFx Application Customizer wird auf klassischen Seiten nicht ausgeführt.
|
||||
|
||||
### Basis-Konfiguration
|
||||
## Optionaler Container
|
||||
|
||||
Vor dem Laden kann ein anderer Zielcontainer gesetzt werden:
|
||||
|
||||
```html
|
||||
<script type="text/javascript">
|
||||
// Konfiguration vor dem Laden des Scripts setzen
|
||||
window.MegaMenuConfig = {
|
||||
termSetName: 'Navigation', // Name des Taxonomy Term Sets
|
||||
cssUrl: '', // Optional: Externe CSS-Datei
|
||||
containerId: 's4-titlerow', // SharePoint Container ID
|
||||
debug: true // Debug-Modus aktivieren
|
||||
};
|
||||
<script>
|
||||
window.MegaMenuConfig = {
|
||||
containerId: 's4-titlerow',
|
||||
debug: true
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||
```
|
||||
|
||||
### Container IDs für verschiedene SharePoint-Versionen
|
||||
|
||||
| SharePoint Version | Container ID | Beschreibung |
|
||||
|-------------------|--------------|-------------|
|
||||
| 2019 | `s4-titlerow` | Standard Titel-Bereich |
|
||||
| SE | `suiteBarTop` | Suite Bar Bereich |
|
||||
| Custom | `custom-menu-container` | Eigener Container |
|
||||
|
||||
### Term Set Konfiguration
|
||||
|
||||
Das MegaMenu liest die Navigation aus einem Managed Metadata Term Set:
|
||||
|
||||
```
|
||||
Navigation (Term Set)
|
||||
├── Produkte (Level 1)
|
||||
│ ├── Software (Level 2)
|
||||
│ │ ├── Office 365 (Level 3) → URL
|
||||
│ │ └── SharePoint (Level 3) → URL
|
||||
│ └── Hardware (Level 2)
|
||||
│ ├── Laptops (Level 3) → URL
|
||||
│ └── Server (Level 3) → URL
|
||||
└── Services (Level 1)
|
||||
└── Consulting (Level 2)
|
||||
└── SharePoint Beratung (Level 3) → URL
|
||||
```
|
||||
|
||||
## 🎨 Anpassungen
|
||||
|
||||
### CSS Customization
|
||||
|
||||
```css
|
||||
/* Eigene Farben */
|
||||
#Mega-Menu-Classic {
|
||||
background: #your-color;
|
||||
}
|
||||
|
||||
#Mega-Menu > ul > li > span[role="menuitem"] {
|
||||
color: #your-text-color;
|
||||
}
|
||||
|
||||
/* Eigene Schriftarten */
|
||||
#Mega-Menu {
|
||||
font-family: 'Your Font', Arial, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript Events
|
||||
## Diagnose
|
||||
|
||||
```javascript
|
||||
// Nach der Initialisierung eigene Logik hinzufügen
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Warten bis MegaMenu geladen ist
|
||||
setTimeout(function() {
|
||||
if (window.MegaMenuClassic) {
|
||||
console.log('MegaMenu ist bereit!');
|
||||
// Eigene Anpassungen hier...
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
typeof window.MegaMenuServices
|
||||
typeof window.MegaMenuClassic
|
||||
window.MegaMenuClassic.reload()
|
||||
```
|
||||
|
||||
## 🔧 Erweiterte Konfiguration
|
||||
|
||||
### Custom URL Mapping
|
||||
|
||||
Die URLs für die Navigation-Links können angepasst werden:
|
||||
|
||||
```javascript
|
||||
// Überschreibe die getTermUrl Funktion
|
||||
window.MegaMenuGetTermUrl = function(term) {
|
||||
var termName = term.get_name();
|
||||
// Eigene URL-Logik
|
||||
return '/custom-pages/' + termName.toLowerCase() + '.aspx';
|
||||
};
|
||||
```
|
||||
|
||||
### Mehrsprachigkeit
|
||||
|
||||
```javascript
|
||||
window.MegaMenuConfig = {
|
||||
termSetName: _spPageContextInfo.currentUICultureName === 'de-DE' ? 'Navigation_DE' : 'Navigation_EN',
|
||||
// ... andere Konfigurationen
|
||||
};
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
**Problem:** Menü wird nicht angezeigt
|
||||
**Lösung:**
|
||||
- Browser-Konsole auf Fehler prüfen
|
||||
- Taxonomy Term Set Name überprüfen
|
||||
- Berechtigungen für Term Store prüfen
|
||||
|
||||
**Problem:** JavaScript-Fehler "SP is not defined"
|
||||
**Lösung:**
|
||||
- Script erst nach SharePoint-Bibliotheken laden
|
||||
- `SP.SOD.executeFunc` verwenden
|
||||
|
||||
**Problem:** Styling funktioniert nicht
|
||||
**Lösung:**
|
||||
- CSS-Pfad überprüfen
|
||||
- Cache leeren
|
||||
- CSS-Spezifität erhöhen
|
||||
|
||||
### Debug-Modus
|
||||
|
||||
```javascript
|
||||
window.MegaMenuConfig = {
|
||||
debug: true, // Aktiviert Console-Logging
|
||||
// ... andere Optionen
|
||||
};
|
||||
```
|
||||
|
||||
## 📝 Browser-Unterstützung
|
||||
|
||||
- Internet Explorer 11+
|
||||
- Microsoft Edge (alle Versionen)
|
||||
- Chrome 60+
|
||||
- Firefox 55+
|
||||
- Safari 12+
|
||||
|
||||
## ⚠️ Wichtige Hinweise
|
||||
|
||||
1. **Berechtigungen:** Benutzer benötigen Leserechte auf den Term Store
|
||||
2. **Performance:** Bei großen Term Sets kann das Laden länger dauern
|
||||
3. **Caching:** SharePoint cached Taxonomy-Daten - Änderungen können verzögert sichtbar werden
|
||||
4. **Responsive:** Das Menü ist für mobile Geräte optimiert
|
||||
|
||||
## 🔄 Migration von SPFx Version
|
||||
|
||||
Falls Sie von der SPFx-Version migrieren:
|
||||
|
||||
1. SPFx ApplicationCustomizer deaktivieren
|
||||
2. Klassische Dateien hochladen und einbinden
|
||||
3. Konfiguration anpassen (gleiche Term Sets verwendbar)
|
||||
4. Testen und CSS bei Bedarf anpassen
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Bei Problemen:
|
||||
1. Debug-Modus aktivieren
|
||||
2. Browser-Konsole prüfen
|
||||
3. Term Set Struktur validieren
|
||||
4. Dateipfade und Berechtigungen überprüfen
|
||||
Benutzer benötigen Leserechte auf den verwendeten Managed-Metadata-Termstore.
|
||||
|
||||
+265
-44
@@ -1,74 +1,295 @@
|
||||
/**
|
||||
* MegaMenu CSS for Classic SharePoint Pages
|
||||
* Version: 1.0.2
|
||||
*
|
||||
* This file imports the exact same CSS as the modern SPFx version
|
||||
* to ensure 100% visual consistency.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Copy the compiled MegaMenu.css from src/extensions/megaMenu/ to your SharePoint assets
|
||||
* 2. Reference it in your master page or via alternate CSS URL:
|
||||
* <link rel="stylesheet" href="/SiteAssets/MegaMenu.css" />
|
||||
*
|
||||
* This file provides additional classic-specific adjustments if needed.
|
||||
*/
|
||||
/* SharePoint MegaMenu 2.1.0 - Classic pages */
|
||||
:root {
|
||||
--megaMenuBarHeight: 44px;
|
||||
--megaMenuContentWidth: 1200px;
|
||||
--megaMenuFlyoutWidth: 288px;
|
||||
--megaMenuZIndex: 6000;
|
||||
}
|
||||
|
||||
/*
|
||||
* The main MegaMenu.css should be loaded first!
|
||||
* This file only contains classic SharePoint specific overrides.
|
||||
*/
|
||||
|
||||
/* Classic SharePoint specific container adjustments */
|
||||
.megamenu-classic-container {
|
||||
/* Insert after s4-titlerow or other SharePoint containers */
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
z-index: var(--megaMenuZIndex);
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.mega-menu-main {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: var(--megaMenuBarHeight);
|
||||
overflow: visible;
|
||||
border-top: 1px solid #edebe9;
|
||||
border-bottom: 1px solid #edebe9;
|
||||
background: #fff;
|
||||
color: #323130;
|
||||
}
|
||||
|
||||
.mega-menu-top-level {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: var(--megaMenuContentWidth);
|
||||
min-height: var(--megaMenuBarHeight);
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mega-menu-top-item {
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure proper z-index in classic SharePoint context */
|
||||
#Mega-Menu {
|
||||
.mega-menu-mode-flyout .mega-menu-top-item {
|
||||
position: relative;
|
||||
z-index: 999; /* Below SharePoint dialogs but above content */
|
||||
}
|
||||
|
||||
.menu-item-link,
|
||||
.menu-item-text {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-height: var(--megaMenuBarHeight);
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #323130;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu-item-text[aria-haspopup="true"]::after {
|
||||
content: "";
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: -3px 0 0 10px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.menu-item-link:hover,
|
||||
.menu-item-text:hover,
|
||||
.menu-item-text[aria-expanded="true"] {
|
||||
background: #f3f2f1;
|
||||
color: #323130;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.menu-item-link:focus,
|
||||
.menu-item-text:focus,
|
||||
.mega-menu-category a:focus,
|
||||
.flyout-link:focus {
|
||||
outline: 2px solid #0078d4;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.mega-menu {
|
||||
z-index: 1000; /* Above the main menu */
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #edebe9;
|
||||
border-top: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .16);
|
||||
color: #323130;
|
||||
z-index: calc(var(--megaMenuZIndex) + 1);
|
||||
}
|
||||
|
||||
/* Classic SharePoint ribbon compatibility */
|
||||
body.ms-backgroundImage #Mega-Menu {
|
||||
/* Adjust if SharePoint has background images */
|
||||
.mega-menu.js-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mega-menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: var(--megaMenuContentWidth);
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
gap: 24px 32px;
|
||||
}
|
||||
|
||||
.mega-menu-category {
|
||||
min-width: 0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.mega-menu-category h3 {
|
||||
margin: 0 0 10px;
|
||||
padding: 0 0 8px;
|
||||
border-bottom: 1px solid #edebe9;
|
||||
}
|
||||
|
||||
.mega-menu-category h3 a,
|
||||
.mega-menu-category h3 span {
|
||||
display: block;
|
||||
color: #323130;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mega-menu-category h3 a:hover {
|
||||
color: #0078d4;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mega-menu-category ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mega-menu-category li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mega-menu-category li a {
|
||||
display: block;
|
||||
margin: 0 -8px;
|
||||
padding: 6px 8px;
|
||||
color: #605e5c;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mega-menu-category li a:hover {
|
||||
background: #f3f2f1;
|
||||
color: #323130;
|
||||
}
|
||||
|
||||
.mega-menu-flyout {
|
||||
right: auto;
|
||||
width: var(--megaMenuFlyoutWidth);
|
||||
min-width: 240px;
|
||||
border-top: 1px solid #edebe9;
|
||||
}
|
||||
|
||||
.flyout-list {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
background: #fff;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.flyout-item {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure settings panel works in classic mode too */
|
||||
.mm-settings-panel {
|
||||
z-index: 4001; /* Above everything else */
|
||||
.flyout-link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 8px 36px 8px 16px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
color: #323130;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Classic SharePoint v4.master specific adjustments */
|
||||
.v4master #Mega-Menu {
|
||||
/* Any v4.master specific styles if needed */
|
||||
.flyout-link:hover,
|
||||
.flyout-item:focus-within > .flyout-link {
|
||||
background: #f3f2f1;
|
||||
color: #323130;
|
||||
}
|
||||
|
||||
/* Classic SharePoint seattle.master specific adjustments */
|
||||
.seattle #Mega-Menu {
|
||||
/* Any seattle.master specific styles if needed */
|
||||
.flyout-link-has-children::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-top: -4px;
|
||||
border-top: 1px solid currentColor;
|
||||
border-right: 1px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for classic SharePoint layouts */
|
||||
@media (max-width: 1024px) {
|
||||
.megamenu-classic-container {
|
||||
/* Classic SharePoint is often used on older devices */
|
||||
overflow-x: auto;
|
||||
.flyout-level-3 {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 100%;
|
||||
display: none;
|
||||
width: var(--megaMenuFlyoutWidth);
|
||||
border: 1px solid #edebe9;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .16);
|
||||
z-index: calc(var(--megaMenuZIndex) + 2);
|
||||
}
|
||||
|
||||
.flyout-item:hover > .flyout-level-3,
|
||||
.flyout-item:focus-within > .flyout-level-3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.mega-menu-top-level {
|
||||
flex-wrap: wrap;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.mega-menu-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.flyout-level-3 {
|
||||
position: static;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles for classic SharePoint */
|
||||
@media print {
|
||||
#Mega-Menu,
|
||||
.megamenu-classic-container {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-36
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* SharePoint MegaMenu for Classic Pages
|
||||
* Version: 1.0.2
|
||||
* Version: 2.1.0
|
||||
*
|
||||
* This version uses standalone services that replicate the SPFx logic
|
||||
* without requiring the SharePoint Framework.
|
||||
@@ -21,10 +21,9 @@
|
||||
|
||||
var MEGAMENU_UCA_ID = 'c0abbffb-355d-4d4e-bd38-9e15fb811506'; // Same as in MegaMenuApplicationCustomizer.ts
|
||||
|
||||
var config = window.MegaMenuConfig || {
|
||||
containerId: 's4-titlerow',
|
||||
debug: false
|
||||
};
|
||||
var config = window.MegaMenuConfig || {};
|
||||
config.containerId = config.containerId || 's4-titlerow';
|
||||
config.debug = config.debug === true;
|
||||
|
||||
function log(message, data) {
|
||||
if (config.debug && console && console.log) {
|
||||
@@ -84,44 +83,30 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && data.value && data.value.length > 0) {
|
||||
var uca = data.value[0];
|
||||
var actions = data && data.value
|
||||
? data.value
|
||||
: (data && data.d && data.d.results ? data.d.results : []);
|
||||
if (actions.length > 0) {
|
||||
var uca = actions[0];
|
||||
try {
|
||||
var props = JSON.parse(uca.ClientSideComponentProperties);
|
||||
log('Configuration found', props);
|
||||
callback(props);
|
||||
} catch (e) {
|
||||
log('Error parsing UserCustomAction properties: ' + e.message);
|
||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
||||
callback({ termSetName: 'Navigation', menuMode: 'megaMenu', debug: false });
|
||||
}
|
||||
} else {
|
||||
log('No UserCustomAction found - using defaults');
|
||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
||||
callback({ termSetName: 'Navigation', menuMode: 'megaMenu', debug: false });
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
log('Error reading configuration: ' + error.message);
|
||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
||||
callback({ termSetName: 'Navigation', menuMode: 'megaMenu', debug: false });
|
||||
});
|
||||
}
|
||||
|
||||
// Load external CSS (same as SPFx version)
|
||||
function loadExternalCSS(cssUrl) {
|
||||
if (!cssUrl) return;
|
||||
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = cssUrl;
|
||||
link.onerror = function() {
|
||||
log('Failed to load external CSS: ' + cssUrl);
|
||||
};
|
||||
link.onload = function() {
|
||||
log('External CSS loaded successfully: ' + cssUrl);
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// Main initialization
|
||||
function initMegaMenu() {
|
||||
log('Initializing MegaMenu for classic SharePoint...');
|
||||
@@ -133,20 +118,16 @@
|
||||
}
|
||||
|
||||
readMegaMenuConfiguration(function(props) {
|
||||
config.debug = props.debug === true || config.debug === true;
|
||||
log('Using configuration:', props);
|
||||
|
||||
// Load external CSS if specified
|
||||
if (props.cssUrl) {
|
||||
loadExternalCSS(props.cssUrl);
|
||||
}
|
||||
|
||||
// Create context
|
||||
var context = createClassicContext();
|
||||
|
||||
// Create taxonomy service using standalone implementation
|
||||
var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService(
|
||||
context,
|
||||
props.termSetName
|
||||
props.termSetId || props.termSetName
|
||||
);
|
||||
|
||||
// Load menu items
|
||||
@@ -179,9 +160,7 @@
|
||||
var renderer = new window.MegaMenuServices.MegaMenuRenderer(
|
||||
context,
|
||||
menuItems,
|
||||
function(updatedProps) {
|
||||
log('Properties updated (not persisted in classic mode):', updatedProps);
|
||||
}
|
||||
props.menuMode === 'flyout' ? 'flyout' : 'megaMenu'
|
||||
);
|
||||
|
||||
// Render the menu
|
||||
|
||||
@@ -51,14 +51,14 @@
|
||||
}
|
||||
|
||||
_getNavigationUrl(term, siteCollectionUrl) {
|
||||
// Extract URL from term properties
|
||||
if (term.LocalCustomProperties && term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl) {
|
||||
return term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl;
|
||||
const properties = term.LocalCustomProperties || {};
|
||||
const rawUrl = properties._Sys_Nav_SimpleLinkUrl || properties._Sys_Nav_TargetUrl;
|
||||
if (!rawUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Fallback: generate URL based on term name
|
||||
const termName = term.Name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
return `${siteCollectionUrl}/pages/${termName}.aspx`;
|
||||
return rawUrl.indexOf('~sitecollection') === 0
|
||||
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
|
||||
: rawUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,17 +67,17 @@
|
||||
// ===========================================
|
||||
|
||||
class TaxonomyNavigationService {
|
||||
constructor(context, termSetName) {
|
||||
constructor(context, termSetIdentifier) {
|
||||
this.context = context;
|
||||
this.termSetName = termSetName;
|
||||
this.termSetIdentifier = termSetIdentifier;
|
||||
this._siteCollectionUrl = context.pageContext.site.absoluteUrl;
|
||||
}
|
||||
|
||||
async getMenuItems() {
|
||||
console.log('[TaxonomyService] Loading terms for:', this.termSetName);
|
||||
console.log('[TaxonomyService] Loading terms for:', this.termSetIdentifier);
|
||||
|
||||
try {
|
||||
const termSet = await this._loadTermSet(this.termSetName);
|
||||
const termSet = await this._loadTermSet(this.termSetIdentifier);
|
||||
return this._processTerms(termSet);
|
||||
} catch (error) {
|
||||
console.error('[TaxonomyService] Error loading terms:', error);
|
||||
@@ -85,31 +85,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
_loadTermSet(termSetName) {
|
||||
_loadTermSet(termSetIdentifier) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const context = SP.ClientContext.get_current();
|
||||
const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
|
||||
const termStore = session.getDefaultSiteCollectionTermStore();
|
||||
const termSets = termStore.getTermSetsByName(termSetName, 1033);
|
||||
|
||||
|
||||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(termSetIdentifier)) {
|
||||
const termSet = termStore.getTermSet(new SP.Guid(termSetIdentifier));
|
||||
this._loadAllTerms(context, termSet, resolve, reject);
|
||||
return;
|
||||
}
|
||||
|
||||
const termSets = termStore.getTermSetsByName(termSetIdentifier, 1033);
|
||||
context.load(termSets);
|
||||
|
||||
context.executeQueryAsync(
|
||||
() => {
|
||||
if (termSets.get_count() > 0) {
|
||||
const termSet = termSets.get_item(0);
|
||||
const terms = termSet.get_terms();
|
||||
context.load(terms);
|
||||
|
||||
context.executeQueryAsync(
|
||||
() => {
|
||||
// Load all terms with their properties and children
|
||||
this._loadAllTermsRecursively(context, terms, resolve, reject);
|
||||
},
|
||||
(sender, args) => reject(new Error(args.get_message()))
|
||||
);
|
||||
this._loadAllTerms(context, termSet, resolve, reject);
|
||||
} else {
|
||||
reject(new Error(`Term set '${termSetName}' not found`));
|
||||
reject(new Error(`Term set '${termSetIdentifier}' not found`));
|
||||
}
|
||||
},
|
||||
(sender, args) => reject(new Error(args.get_message()))
|
||||
@@ -117,32 +114,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
_loadAllTermsRecursively(context, terms, resolve, reject) {
|
||||
const allTerms = [];
|
||||
const termsEnum = terms.getEnumerator();
|
||||
|
||||
// First pass: collect all terms
|
||||
while (termsEnum.moveNext()) {
|
||||
const term = termsEnum.get_current();
|
||||
context.load(term, 'Id', 'Name', 'PathOfTerm', 'LocalCustomProperties');
|
||||
allTerms.push(term);
|
||||
|
||||
// Load child terms
|
||||
const childTerms = term.get_terms();
|
||||
context.load(childTerms);
|
||||
this._loadChildTermsRecursively(context, childTerms, allTerms);
|
||||
}
|
||||
|
||||
// Execute query to load all data
|
||||
_loadAllTerms(context, termSet, resolve, reject) {
|
||||
const terms = termSet.getAllTerms();
|
||||
context.load(terms, 'Include(Id,Name,PathOfTerm,LocalCustomProperties,IsAvailableForTagging,IsDeprecated)');
|
||||
context.executeQueryAsync(
|
||||
() => {
|
||||
const processedTerms = allTerms.map(term => ({
|
||||
Id: term.get_id().toString(),
|
||||
Name: term.get_name(),
|
||||
PathOfTerm: term.get_pathOfTerm(),
|
||||
LocalCustomProperties: this._getCustomProperties(term),
|
||||
IsRoot: term.get_pathOfTerm().split(';').length === 1
|
||||
}));
|
||||
const processedTerms = [];
|
||||
const enumerator = terms.getEnumerator();
|
||||
while (enumerator.moveNext()) {
|
||||
const term = enumerator.get_current();
|
||||
if (!term.get_isAvailableForTagging() || term.get_isDeprecated()) {
|
||||
continue;
|
||||
}
|
||||
processedTerms.push({
|
||||
Id: term.get_id().toString(),
|
||||
Name: term.get_name(),
|
||||
PathOfTerm: term.get_pathOfTerm(),
|
||||
LocalCustomProperties: this._getCustomProperties(term),
|
||||
IsRoot: term.get_pathOfTerm().split(';').length === 1
|
||||
});
|
||||
}
|
||||
|
||||
resolve(processedTerms);
|
||||
},
|
||||
@@ -150,25 +141,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
_loadChildTermsRecursively(context, childTerms, allTerms) {
|
||||
const childEnum = childTerms.getEnumerator();
|
||||
while (childEnum.moveNext()) {
|
||||
const childTerm = childEnum.get_current();
|
||||
context.load(childTerm, 'Id', 'Name', 'PathOfTerm', 'LocalCustomProperties');
|
||||
allTerms.push(childTerm);
|
||||
|
||||
// Recursively load grandchildren
|
||||
const grandChildTerms = childTerm.get_terms();
|
||||
context.load(grandChildTerms);
|
||||
this._loadChildTermsRecursively(context, grandChildTerms, allTerms);
|
||||
}
|
||||
}
|
||||
|
||||
_getCustomProperties(term) {
|
||||
try {
|
||||
const props = term.get_localCustomProperties();
|
||||
return {
|
||||
_Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null,
|
||||
_Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null,
|
||||
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -178,12 +156,14 @@
|
||||
|
||||
_processTerms(termsData) {
|
||||
const itemsDict = new ItemDictionary();
|
||||
const itemsByPath = {};
|
||||
const menuItems = [];
|
||||
|
||||
// Create MenuItem objects
|
||||
termsData.forEach(termData => {
|
||||
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
|
||||
itemsDict.Add(termData.Id, menuItem);
|
||||
itemsByPath[termData.PathOfTerm] = menuItem;
|
||||
|
||||
if (menuItem.pathDepth === 1) {
|
||||
menuItems.push(menuItem);
|
||||
@@ -194,8 +174,9 @@
|
||||
termsData.forEach(termData => {
|
||||
if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) {
|
||||
const menuItem = itemsDict.Get(termData.Id);
|
||||
const parentId = menuItem.parentId;
|
||||
const parentItem = itemsDict.Get(parentId);
|
||||
const pathParts = termData.PathOfTerm.split(';');
|
||||
pathParts.pop();
|
||||
const parentItem = itemsByPath[pathParts.join(';')];
|
||||
|
||||
if (parentItem) {
|
||||
parentItem.items.push(menuItem);
|
||||
@@ -221,10 +202,10 @@
|
||||
// ===========================================
|
||||
|
||||
class MegaMenuRenderer {
|
||||
constructor(context, menuItems, updateCallback) {
|
||||
constructor(context, menuItems, menuMode) {
|
||||
this.context = context;
|
||||
this.menuItems = menuItems;
|
||||
this.updateCallback = updateCallback;
|
||||
this.menuMode = menuMode === 'flyout' ? 'flyout' : 'megaMenu';
|
||||
}
|
||||
|
||||
render(container) {
|
||||
@@ -232,11 +213,13 @@
|
||||
|
||||
const nav = document.createElement('nav');
|
||||
nav.id = 'Mega-Menu';
|
||||
nav.className = 'mega-menu-main';
|
||||
nav.className = 'mega-menu-main mega-menu-mode-' + this.menuMode;
|
||||
nav.setAttribute('data-menu-mode', this.menuMode);
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
||||
|
||||
const topLevelUl = document.createElement('ul');
|
||||
topLevelUl.className = 'mega-menu-top-level';
|
||||
topLevelUl.setAttribute('role', 'menubar');
|
||||
|
||||
this.menuItems.forEach(topLevelItem => {
|
||||
@@ -244,11 +227,6 @@
|
||||
topLevelUl.appendChild(topLevelLi);
|
||||
});
|
||||
|
||||
// Add settings if user has permissions (simplified check)
|
||||
if (this._hasManagePermissions()) {
|
||||
topLevelUl.appendChild(this.createSettingsItem());
|
||||
}
|
||||
|
||||
nav.appendChild(topLevelUl);
|
||||
container.appendChild(nav);
|
||||
|
||||
@@ -258,6 +236,7 @@
|
||||
|
||||
createTopLevelItem(item) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'mega-menu-top-item';
|
||||
li.setAttribute('role', 'none');
|
||||
|
||||
if (item.url && item.items.length === 0) {
|
||||
@@ -281,7 +260,9 @@
|
||||
li.appendChild(span);
|
||||
|
||||
if (item.items.length > 0) {
|
||||
const megaMenu = this.createMegaMenu(item.items);
|
||||
const megaMenu = this.menuMode === 'flyout'
|
||||
? this.createFlyoutMenu(item.items)
|
||||
: this.createMegaMenu(item.items);
|
||||
li.appendChild(megaMenu);
|
||||
}
|
||||
}
|
||||
@@ -350,24 +331,46 @@
|
||||
return categoryDiv;
|
||||
}
|
||||
|
||||
createSettingsItem() {
|
||||
createFlyoutMenu(items) {
|
||||
const flyout = document.createElement('div');
|
||||
flyout.className = 'mega-menu mega-menu-flyout';
|
||||
flyout.setAttribute('role', 'menu');
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'flyout-list flyout-level-2';
|
||||
items.forEach(item => list.appendChild(this.createFlyoutItem(item)));
|
||||
|
||||
flyout.appendChild(list);
|
||||
return flyout;
|
||||
}
|
||||
|
||||
createFlyoutItem(item) {
|
||||
const li = document.createElement('li');
|
||||
li.setAttribute('role', 'none');
|
||||
const hasChildren = item.items && item.items.length > 0;
|
||||
li.className = hasChildren ? 'flyout-item has-children' : 'flyout-item';
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'menu-item-settings';
|
||||
button.setAttribute('type', 'button');
|
||||
button.setAttribute('role', 'menuitem');
|
||||
button.setAttribute('tabindex', '0');
|
||||
button.setAttribute('aria-label', 'Einstellungen');
|
||||
button.onclick = () => this._openSettings();
|
||||
const element = document.createElement(item.url ? 'a' : 'span');
|
||||
if (item.url) {
|
||||
element.href = item.url;
|
||||
} else {
|
||||
element.setAttribute('tabindex', '0');
|
||||
}
|
||||
element.textContent = item.title;
|
||||
element.className = hasChildren ? 'flyout-link flyout-link-has-children' : 'flyout-link';
|
||||
element.setAttribute('role', 'menuitem');
|
||||
if (hasChildren) {
|
||||
element.setAttribute('aria-haspopup', 'true');
|
||||
}
|
||||
li.appendChild(element);
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
button.appendChild(icon);
|
||||
if (hasChildren) {
|
||||
const childList = document.createElement('ul');
|
||||
childList.className = 'flyout-list flyout-level-3';
|
||||
childList.setAttribute('role', 'menu');
|
||||
item.items.forEach(child => childList.appendChild(this.createFlyoutItem(child)));
|
||||
li.appendChild(childList);
|
||||
}
|
||||
|
||||
li.appendChild(button);
|
||||
return li;
|
||||
}
|
||||
|
||||
@@ -441,15 +444,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
_hasManagePermissions() {
|
||||
// Simplified permission check for classic SharePoint
|
||||
return _spPageContextInfo.isSiteAdmin || false;
|
||||
}
|
||||
|
||||
_openSettings() {
|
||||
console.log('[MegaMenu] Settings not implemented in classic mode');
|
||||
alert('Einstellungen sind nur in der modernen SPFx-Version verfügbar.');
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
@@ -465,4 +459,4 @@
|
||||
|
||||
console.log('[MegaMenu] Standalone services loaded successfully');
|
||||
|
||||
})(window);
|
||||
})(window);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "mega-menu",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"solution": {
|
||||
"name": "mega-menu-client-side-solution",
|
||||
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
|
||||
"version": "2.0.1.0",
|
||||
"version": "2.1.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true
|
||||
},
|
||||
|
||||
+7
-2
@@ -10,7 +10,9 @@
|
||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||
"properties": {
|
||||
"termSetName": "Horizontale Navigation",
|
||||
"additionalCssUrl": "/siteAssets/Navigation/Navigation.css"
|
||||
"cacheMinutes": 15,
|
||||
"menuMode": "megaMenu",
|
||||
"debug": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +23,10 @@
|
||||
"c0abbffb-355d-4d4e-bd38-9e15fb811506": {
|
||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||
"properties": {
|
||||
"testMessage": "Test message"
|
||||
"termSetName": "Global Navigation",
|
||||
"cacheMinutes": 15,
|
||||
"menuMode": "flyout",
|
||||
"debug": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-80
@@ -1,6 +1,5 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Alias('WebUrl')]
|
||||
[string]$SiteUrl,
|
||||
|
||||
[string]$TermSetName = '',
|
||||
@@ -10,6 +9,9 @@ param(
|
||||
[ValidateRange(1, 1440)]
|
||||
[int]$CacheMinutes = 15,
|
||||
|
||||
[ValidateSet('megaMenu', 'flyout')]
|
||||
[string]$MenuMode = 'megaMenu',
|
||||
|
||||
[switch]$EnableDebug,
|
||||
|
||||
[string]$Description = 'MSFT-Custom-Solution:MegaMenu'
|
||||
@@ -29,107 +31,85 @@ if (-not (Get-Command -Name Get-SPSite -ErrorAction SilentlyContinue)) {
|
||||
|
||||
$componentId = [Guid]'c0abbffb-355d-4d4e-bd38-9e15fb811506'
|
||||
$legacyComponentId = [Guid]'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
||||
$location = 'ClientSideExtension.ApplicationCustomizer'
|
||||
$configurationProperty = 'PortalSettings.MegaMenu.Configuration'
|
||||
$legacyPropertyName = 'PortalSettings.MegaMenu.Configuration'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($TermSetId) -and [string]::IsNullOrWhiteSpace($TermSetName)) {
|
||||
throw 'Bitte TermSetId oder TermSetName angeben.'
|
||||
}
|
||||
|
||||
$properties = [string](@{
|
||||
termSetName = $TermSetName
|
||||
termSetId = $TermSetId
|
||||
termSetName = $TermSetName.Trim()
|
||||
termSetId = $TermSetId.Trim()
|
||||
cacheMinutes = $CacheMinutes
|
||||
menuMode = $MenuMode
|
||||
debug = [bool]$EnableDebug.IsPresent
|
||||
} | ConvertTo-Json -Depth 10 -Compress)
|
||||
|
||||
$site = Get-SPSite -Identity $SiteUrl
|
||||
|
||||
try {
|
||||
$rootWeb = $site.RootWeb
|
||||
$rootWeb.AllProperties[$configurationProperty] = [string]$properties
|
||||
$rootWeb.Update()
|
||||
|
||||
$siteActions = @(
|
||||
$site.UserCustomActions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}
|
||||
)
|
||||
|
||||
foreach ($siteAction in $siteActions) {
|
||||
$siteAction.Delete()
|
||||
@($site.UserCustomActions | Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}) | ForEach-Object {
|
||||
Write-Host ('Entferne alte Site-Action: ' + $_.Id)
|
||||
$_.Delete()
|
||||
}
|
||||
|
||||
$created = 0
|
||||
$updated = 0
|
||||
$removedLegacy = 0
|
||||
try {
|
||||
if ($site.RootWeb.AllProperties.ContainsKey($legacyPropertyName)) {
|
||||
$site.RootWeb.AllProperties.Remove($legacyPropertyName)
|
||||
$site.RootWeb.Update()
|
||||
Write-Host ('Alte Property-Bag-Konfiguration entfernt: ' + $legacyPropertyName)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning ('Die alte Property-Bag-Konfiguration konnte nicht entfernt werden: ' + $_.Exception.Message)
|
||||
}
|
||||
|
||||
foreach ($web in $site.AllWebs) {
|
||||
try {
|
||||
$webUrl = $web.Url
|
||||
$userCustomActions = $web.UserCustomActions
|
||||
$actions = @($web.UserCustomActions | Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
})
|
||||
|
||||
if ($null -eq $userCustomActions) {
|
||||
throw ('UserCustomActions konnten fuer das Web nicht geladen werden: ' + $webUrl)
|
||||
foreach ($action in $actions) {
|
||||
Write-Host ('Entferne alte Web-Action: ' + $webUrl + ' / ' + $action.Id)
|
||||
$action.Delete()
|
||||
}
|
||||
|
||||
$actions = @(
|
||||
$userCustomActions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}
|
||||
)
|
||||
|
||||
$currentAction = $actions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId
|
||||
} |
|
||||
Select-Object -First 1
|
||||
|
||||
foreach ($existingAction in $actions) {
|
||||
if ($currentAction -and $existingAction.Id -eq $currentAction.Id) {
|
||||
continue
|
||||
}
|
||||
|
||||
$existingAction.Delete()
|
||||
$removedLegacy++
|
||||
}
|
||||
|
||||
if (-not $currentAction) {
|
||||
$currentAction = $userCustomActions.Add()
|
||||
$currentAction.Name = [Guid]::NewGuid().ToString('B')
|
||||
$created++
|
||||
}
|
||||
else {
|
||||
$updated++
|
||||
}
|
||||
|
||||
$currentAction.Title = 'MegaMenu Application Customizer'
|
||||
$currentAction.Description = $Description
|
||||
$currentAction.Location = $location
|
||||
$currentAction.ClientSideComponentId = $componentId
|
||||
$currentAction.ClientSideComponentProperties = $properties
|
||||
$currentAction.Sequence = 100
|
||||
$currentAction.Update()
|
||||
$web.Update()
|
||||
|
||||
Write-Host ('MegaMenu synchronisiert: ' + $webUrl) -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
$web.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'MegaMenu wurde fuer die gesamte Site Collection konfiguriert.' -ForegroundColor Green
|
||||
Write-Host ('Zentrale Konfiguration: ' + $configurationProperty)
|
||||
Write-Host ('Neu registrierte Webs: ' + $created)
|
||||
Write-Host ('Aktualisierte Webs: ' + $updated)
|
||||
Write-Host ('Entfernte alte oder doppelte Registrierungen: ' + $removedLegacy)
|
||||
Write-Host ('Properties: ' + $properties)
|
||||
Write-Host 'Neue Unterwebs koennen durch erneutes Ausfuehren idempotent synchronisiert werden.' -ForegroundColor Yellow
|
||||
}
|
||||
finally {
|
||||
if ($site) {
|
||||
$site.Dispose()
|
||||
}
|
||||
$site.Dispose()
|
||||
}
|
||||
|
||||
$site = Get-SPSite -Identity $SiteUrl
|
||||
|
||||
try {
|
||||
$action = $site.UserCustomActions.Add()
|
||||
$action.Name = [Guid]::NewGuid().ToString('B')
|
||||
$action.Title = 'MegaMenu Application Customizer'
|
||||
$action.Description = $Description
|
||||
$action.Location = 'ClientSideExtension.ApplicationCustomizer'
|
||||
$action.ClientSideComponentId = $componentId
|
||||
$action.ClientSideComponentProperties = $properties
|
||||
$action.Sequence = 100
|
||||
$action.Update()
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'MegaMenu wurde zentral fuer die Site Collection registriert.' -ForegroundColor Green
|
||||
Write-Host ('Site Collection: ' + $site.Url)
|
||||
Write-Host ('Action-ID: ' + $action.Id)
|
||||
Write-Host ('Menu mode: ' + $MenuMode)
|
||||
Write-Host ('Properties: ' + $properties)
|
||||
Write-Host 'Das MegaMenu-SPFx-Paket muss im App Catalog zentral bereitgestellt sein.' -ForegroundColor Yellow
|
||||
}
|
||||
finally {
|
||||
$site.Dispose()
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SiteUrl,
|
||||
|
||||
[string]$Description = 'MSFT-Custom-Solution:MegaMenu'
|
||||
)
|
||||
|
||||
if (-not (Get-Command -Name Get-SPSite -ErrorAction SilentlyContinue)) {
|
||||
if (Get-Module -ListAvailable -Name Microsoft.SharePoint.PowerShell) {
|
||||
Import-Module Microsoft.SharePoint.PowerShell -DisableNameChecking
|
||||
}
|
||||
elseif (Get-PSSnapin -Registered -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) {
|
||||
Add-PSSnapin Microsoft.SharePoint.PowerShell
|
||||
}
|
||||
else {
|
||||
throw 'Die SharePoint PowerShell-Komponenten wurden nicht gefunden. Bitte in der SharePoint Management Shell ausfuehren.'
|
||||
}
|
||||
}
|
||||
|
||||
$componentId = [Guid]'c0abbffb-355d-4d4e-bd38-9e15fb811506'
|
||||
$legacyComponentId = [Guid]'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
||||
$location = 'ClientSideExtension.ApplicationCustomizer'
|
||||
$configurationProperty = 'PortalSettings.MegaMenu.Configuration'
|
||||
|
||||
$site = Get-SPSite -Identity $SiteUrl
|
||||
|
||||
try {
|
||||
$rootWeb = $site.RootWeb
|
||||
$properties = [string]$rootWeb.AllProperties[$configurationProperty]
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($properties)) {
|
||||
$existingAction = $null
|
||||
|
||||
foreach ($web in $site.AllWebs) {
|
||||
try {
|
||||
$existingAction = @(
|
||||
$web.UserCustomActions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}
|
||||
) | Select-Object -First 1
|
||||
|
||||
if ($existingAction) {
|
||||
$properties = [string]$existingAction.ClientSideComponentProperties
|
||||
break
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$web.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($properties)) {
|
||||
throw ('Keine MegaMenu-Konfiguration im Property Bag oder in einer bestehenden ' +
|
||||
'Web-Registrierung gefunden. Bitte zuerst add-megamenu.ps1 ausfuehren.')
|
||||
}
|
||||
|
||||
try {
|
||||
$null = $properties | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
throw ('Die gefundene MegaMenu-Konfiguration ist kein gueltiges JSON: ' + $_.Exception.Message)
|
||||
}
|
||||
|
||||
$removedSiteActions = 0
|
||||
@(
|
||||
$site.UserCustomActions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}
|
||||
) | ForEach-Object {
|
||||
$_.Delete()
|
||||
$removedSiteActions++
|
||||
}
|
||||
|
||||
$siteAction = $site.UserCustomActions.Add()
|
||||
$siteAction.Name = [Guid]::NewGuid().ToString('B')
|
||||
$siteAction.Title = 'MegaMenu Application Customizer'
|
||||
$siteAction.Description = $Description
|
||||
$siteAction.Location = $location
|
||||
$siteAction.ClientSideComponentId = $componentId
|
||||
$siteAction.ClientSideComponentProperties = $properties
|
||||
$siteAction.Sequence = 100
|
||||
$siteAction.Update()
|
||||
|
||||
$removedWebActions = 0
|
||||
$testedWebUrls = @()
|
||||
|
||||
foreach ($web in $site.AllWebs) {
|
||||
try {
|
||||
$testedWebUrls += $web.Url
|
||||
|
||||
@(
|
||||
$web.UserCustomActions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}
|
||||
) | ForEach-Object {
|
||||
$_.Delete()
|
||||
$removedWebActions++
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$web.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$verification = @(
|
||||
$site.UserCustomActions |
|
||||
Where-Object { $_.ClientSideComponentId -eq $componentId }
|
||||
)
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '=== MegaMenu Site-Scope-Test vorbereitet ===' -ForegroundColor Green
|
||||
Write-Host ('Site-Action-ID: ' + $siteAction.Id)
|
||||
Write-Host ('Component-ID: ' + $componentId)
|
||||
Write-Host ('Entfernte vorherige Site-Actions: ' + $removedSiteActions)
|
||||
Write-Host ('Entfernte Web-Actions: ' + $removedWebActions)
|
||||
Write-Host ('Verifizierte Site-Actions: ' + $verification.Count)
|
||||
Write-Host ('Properties: ' + $properties)
|
||||
Write-Host ''
|
||||
Write-Host 'Bitte jeweils mit Strg+F5 testen:' -ForegroundColor Cyan
|
||||
$testedWebUrls | ForEach-Object { Write-Host (' ' + $_) }
|
||||
Write-Host ''
|
||||
Write-Host 'Rueckkehr zum bisherigen Web-Scope:' -ForegroundColor Yellow
|
||||
Write-Host ('.\deployment\add-megamenu.ps1 -SiteUrl ''' + $site.Url + '''')
|
||||
}
|
||||
finally {
|
||||
if ($site) {
|
||||
$site.Dispose()
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -3,5 +3,6 @@
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
build.addSuppression(`Warning - MegaMenuApplicationCustomizer: Admins can make this solution available to all sites in the organization, but extensions won’t automatically appear. SharePoint Framework extensions must be specifically associated to sites, lists, and fields programmatically to be visible to site users.`);
|
||||
|
||||
build.initialize(require('gulp'));
|
||||
build.initialize(require('gulp'));
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mega-menu",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+4
-3
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "mega-menu",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=6.9.0 <9.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"build:classic": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\build-classic.ps1",
|
||||
"build": "npm run build:classic && gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test",
|
||||
"package": "gulp clean && gulp bundle --ship && gulp package-solution --ship"
|
||||
"package": "npm run build:classic && gulp clean && gulp bundle --ship && gulp package-solution --ship"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/decorators": "~1.4.1",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>MegaMenu – Flyout-Vorschau</title>
|
||||
<link rel="stylesheet" href="../classic/megamenu-classic.css" />
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #faf9f8; color: #323130; font-family: "Segoe UI", Arial, sans-serif; }
|
||||
.suite-bar { height: 48px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: #0078d4; color: #fff; }
|
||||
.suite-brand { display: flex; align-items: center; gap: 14px; font-size: 16px; font-weight: 600; }
|
||||
.waffle { display: grid; grid-template-columns: repeat(3, 4px); gap: 3px; }
|
||||
.waffle span { width: 4px; height: 4px; background: #fff; }
|
||||
.suite-user { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
.avatar { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: #fff; color: #0078d4; font-weight: 700; }
|
||||
.site-header { display: flex; align-items: center; justify-content: space-between; min-height: 72px; padding: 0 32px; background: #fff; }
|
||||
.site-title { font-size: 24px; font-weight: 300; }
|
||||
.site-search { width: 280px; padding: 9px 12px; border: 1px solid #8a8886; background: #fff; }
|
||||
.preview-label { padding: 7px 24px; background: #deecf9; color: #605e5c; font-size: 12px; text-align: center; }
|
||||
.page { max-width: 1200px; margin: 28px auto; padding: 0 24px; }
|
||||
.page h1 { margin: 0 0 8px; font-size: 32px; font-weight: 300; }
|
||||
.page p { color: #605e5c; }
|
||||
.news { max-width: 760px; margin-top: 28px; border-top: 1px solid #edebe9; }
|
||||
.news article { padding: 18px 0; border-bottom: 1px solid #edebe9; }
|
||||
.news time { color: #605e5c; font-size: 12px; }
|
||||
.news h2 { margin: 5px 0; font-size: 18px; font-weight: 600; }
|
||||
@media (max-width: 760px) { .site-search { display: none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="suite-bar">
|
||||
<div class="suite-brand"><span class="waffle" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>SharePoint</div>
|
||||
<div class="suite-user"><span>Max Mustermann</span><span class="avatar">MM</span></div>
|
||||
</header>
|
||||
<div class="site-header"><div class="site-title">Intranet Portal</div><input class="site-search" type="search" placeholder="Auf dieser Website suchen" /></div>
|
||||
|
||||
<div class="megamenu-classic-container">
|
||||
<nav id="Mega-Menu" class="mega-menu-main mega-menu-mode-flyout" data-menu-mode="flyout" aria-label="Hauptnavigation">
|
||||
<ul class="mega-menu-top-level">
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link" href="#">Startseite</a></li>
|
||||
<li class="mega-menu-top-item is-active">
|
||||
<a class="menu-item-link menu-item-has-children" href="#unternehmen" aria-haspopup="true" aria-expanded="true">Unternehmen</a>
|
||||
<div class="mega-menu mega-menu-flyout js-open" aria-expanded="true">
|
||||
<ul class="flyout-list flyout-level-2">
|
||||
<li class="flyout-item has-children">
|
||||
<a class="flyout-link flyout-link-has-children" href="#ueber-uns" aria-haspopup="true">Über uns</a>
|
||||
<ul class="flyout-list flyout-level-3"><li class="flyout-item"><a class="flyout-link" href="#profil">Unternehmensprofil</a></li><li class="flyout-item"><a class="flyout-link" href="#leitung">Geschäftsleitung</a></li><li class="flyout-item"><a class="flyout-link" href="#standorte">Standorte</a></li></ul>
|
||||
</li>
|
||||
<li class="flyout-item has-children">
|
||||
<a class="flyout-link flyout-link-has-children" href="#organisation" aria-haspopup="true">Organisation</a>
|
||||
<ul class="flyout-list flyout-level-3"><li class="flyout-item"><a class="flyout-link" href="#bereiche">Fachbereiche</a></li><li class="flyout-item"><a class="flyout-link" href="#organigramm">Organigramm</a></li><li class="flyout-item"><a class="flyout-link" href="#ansprechpartner">Ansprechpartner</a></li></ul>
|
||||
</li>
|
||||
<li class="flyout-item is-current"><a class="flyout-link" href="#news">Aktuelle Meldungen</a></li>
|
||||
<li class="flyout-item"><a class="flyout-link" href="#termine">Veranstaltungen</a></li>
|
||||
<li class="flyout-item"><a class="flyout-link" href="#telefonbuch">Telefonbuch</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#personal" aria-haspopup="true" aria-expanded="false">Personal</a></li>
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#services" aria-haspopup="true" aria-expanded="false">Services</a></li>
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link" href="#projekte">Projekte</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="preview-label">Vorschau: menuMode = "flyout" · Für Ebene 3 mit der Maus über „Über uns“ oder „Organisation“ fahren</div>
|
||||
|
||||
<main class="page">
|
||||
<h1>Aktuelle Meldungen</h1>
|
||||
<p>Die Flyout-Navigation bleibt kompakt und folgt der hierarchischen Struktur des Termsets.</p>
|
||||
<section class="news"><article><time>20. Juli 2026</time><h2>Neue Services im Intranet verfügbar</h2><p>Alle wichtigen Anwendungen sind jetzt zentral über das Serviceportal erreichbar.</p></article><article><time>18. Juli 2026</time><h2>Informationen aus der Geschäftsleitung</h2><p>Die aktuelle Mitarbeitendeninformation steht zum Abruf bereit.</p></article></section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var triggers = document.querySelectorAll('.mega-menu-top-level > .mega-menu-top-item > [aria-haspopup="true"]');
|
||||
Array.prototype.forEach.call(triggers, function (trigger) {
|
||||
trigger.addEventListener('click', function (event) {
|
||||
var panel = trigger.nextElementSibling;
|
||||
if (!panel || !panel.classList.contains('mega-menu')) { return; }
|
||||
event.preventDefault();
|
||||
var open = panel.classList.contains('js-open');
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.mega-menu.js-open'), function (item) { item.classList.remove('js-open'); if (item.previousElementSibling) { item.previousElementSibling.setAttribute('aria-expanded', 'false'); } });
|
||||
panel.classList.toggle('js-open', !open); trigger.setAttribute('aria-expanded', String(!open));
|
||||
});
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>MegaMenu – Mega-Menu-Vorschau</title>
|
||||
<link rel="stylesheet" href="../classic/megamenu-classic.css" />
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #faf9f8; color: #323130; font-family: "Segoe UI", Arial, sans-serif; }
|
||||
.suite-bar { height: 48px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: #0078d4; color: #fff; }
|
||||
.suite-brand { display: flex; align-items: center; gap: 14px; font-size: 16px; font-weight: 600; }
|
||||
.waffle { display: grid; grid-template-columns: repeat(3, 4px); gap: 3px; }
|
||||
.waffle span { width: 4px; height: 4px; background: #fff; }
|
||||
.suite-user { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
.avatar { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: #fff; color: #0078d4; font-weight: 700; }
|
||||
.site-header { display: flex; align-items: center; justify-content: space-between; min-height: 72px; padding: 0 32px; background: #fff; }
|
||||
.site-title { font-size: 24px; font-weight: 300; }
|
||||
.site-search { width: 280px; padding: 9px 12px; border: 1px solid #8a8886; background: #fff; }
|
||||
.preview-label { padding: 7px 24px; background: #fff4ce; color: #605e5c; font-size: 12px; text-align: center; }
|
||||
.page { max-width: 1200px; margin: 28px auto; padding: 0 24px; }
|
||||
.page h1 { margin: 0 0 8px; font-size: 32px; font-weight: 300; }
|
||||
.page p { color: #605e5c; }
|
||||
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 28px; }
|
||||
.card { min-height: 150px; padding: 20px; border: 1px solid #edebe9; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.06); }
|
||||
.card strong { color: #0078d4; }
|
||||
@media (max-width: 760px) { .site-search { display: none; } .cards { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="suite-bar">
|
||||
<div class="suite-brand"><span class="waffle" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>SharePoint</div>
|
||||
<div class="suite-user"><span>Max Mustermann</span><span class="avatar">MM</span></div>
|
||||
</header>
|
||||
<div class="site-header"><div class="site-title">Intranet Portal</div><input class="site-search" type="search" placeholder="Auf dieser Website suchen" /></div>
|
||||
|
||||
<div class="megamenu-classic-container">
|
||||
<nav id="Mega-Menu" class="mega-menu-main mega-menu-mode-megaMenu" data-menu-mode="megaMenu" aria-label="Hauptnavigation">
|
||||
<ul class="mega-menu-top-level">
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link" href="#">Startseite</a></li>
|
||||
<li class="mega-menu-top-item is-active">
|
||||
<a class="menu-item-link menu-item-has-children" href="#unternehmen" aria-haspopup="true" aria-expanded="true">Unternehmen</a>
|
||||
<div class="mega-menu js-open" aria-expanded="true">
|
||||
<div class="mega-menu-grid" style="--megaMenuColumnCount:4">
|
||||
<section class="mega-menu-category">
|
||||
<h3 class="mega-menu-category-title"><a href="#ueber-uns">Über uns</a></h3>
|
||||
<ul class="mega-menu-links"><li><a href="#profil">Unternehmensprofil</a></li><li><a href="#leitung">Geschäftsleitung</a></li><li><a href="#standorte">Standorte</a></li></ul>
|
||||
</section>
|
||||
<section class="mega-menu-category">
|
||||
<h3 class="mega-menu-category-title"><a href="#organisation">Organisation</a></h3>
|
||||
<ul class="mega-menu-links"><li><a href="#bereiche">Fachbereiche</a></li><li><a href="#organigramm">Organigramm</a></li><li><a href="#ansprechpartner">Ansprechpartner</a></li></ul>
|
||||
</section>
|
||||
<section class="mega-menu-category">
|
||||
<h3 class="mega-menu-category-title"><a href="#kommunikation">Kommunikation</a></h3>
|
||||
<ul class="mega-menu-links"><li class="is-current"><a class="is-current" href="#news">Aktuelle Meldungen</a></li><li><a href="#termine">Veranstaltungen</a></li><li><a href="#medien">Mediencenter</a></li></ul>
|
||||
</section>
|
||||
<section class="mega-menu-category">
|
||||
<h3 class="mega-menu-category-title"><span>Direktzugriff</span></h3>
|
||||
<ul class="mega-menu-links"><li><a href="#telefonbuch">Telefonbuch</a></li><li><a href="#formulare">Formulare</a></li><li><a href="#vorlagen">Vorlagen</a></li></ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#personal" aria-haspopup="true" aria-expanded="false">Personal</a></li>
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#services" aria-haspopup="true" aria-expanded="false">Services</a></li>
|
||||
<li class="mega-menu-top-item"><a class="menu-item-link" href="#projekte">Projekte</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="preview-label">Vorschau: menuMode = "megaMenu" · „Unternehmen“ ist zur Ansicht geöffnet</div>
|
||||
|
||||
<main class="page">
|
||||
<h1>Willkommen im Intranet</h1>
|
||||
<p>Diese statische Vorschau verwendet dieselben CSS-Klassen wie die MegaMenu-Solution.</p>
|
||||
<div class="cards"><div class="card"><strong>Aktuelle Meldungen</strong><p>Neuigkeiten und wichtige Informationen aus dem Unternehmen.</p></div><div class="card"><strong>Schnellzugriff</strong><p>Formulare, Vorlagen und häufig verwendete Anwendungen.</p></div><div class="card"><strong>Meine Aufgaben</strong><p>Persönliche Aufgaben und zuletzt verwendete Dokumente.</p></div></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var triggers = document.querySelectorAll('.mega-menu-top-level > .mega-menu-top-item > [aria-haspopup="true"]');
|
||||
function closeAll(except) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.mega-menu.js-open'), function (panel) {
|
||||
if (panel !== except) { panel.classList.remove('js-open'); panel.setAttribute('aria-expanded', 'false'); if (panel.previousElementSibling) { panel.previousElementSibling.setAttribute('aria-expanded', 'false'); } }
|
||||
});
|
||||
}
|
||||
Array.prototype.forEach.call(triggers, function (trigger) {
|
||||
trigger.addEventListener('click', function (event) {
|
||||
var panel = trigger.nextElementSibling;
|
||||
if (!panel || !panel.classList.contains('mega-menu')) { return; }
|
||||
event.preventDefault();
|
||||
var open = panel.classList.contains('js-open'); closeAll(panel);
|
||||
panel.classList.toggle('js-open', !open); panel.setAttribute('aria-expanded', String(!open)); trigger.setAttribute('aria-expanded', String(!open));
|
||||
});
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
|
||||
<CustomAction
|
||||
Title="MegaMenu Application Customizer"
|
||||
Location="ClientSideExtension.ApplicationCustomizer"
|
||||
ClientSideComponentId="c0abbffb-355d-4d4e-bd38-9e15fb811506"
|
||||
ClientSideComponentProperties="{}" />
|
||||
</Elements>
|
||||
@@ -1,482 +1,396 @@
|
||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
/*
|
||||
Global override example:
|
||||
:root {
|
||||
--themePrimary: #00407f;
|
||||
--themeLighterAlt: #f0f5fa;
|
||||
--themeLighter: #c5d8eb;
|
||||
--themeLight: #98b8d9;
|
||||
--themeTertiary: #477db3;
|
||||
--themeSecondary: #11508f;
|
||||
--themeDarkAlt: #003973;
|
||||
--themeDark: #003061;
|
||||
--themeDarker: #002447;
|
||||
--neutralLighterAlt: #faf9f8;
|
||||
--neutralLighter: #f3f2f1;
|
||||
--neutralLight: #edebe9;
|
||||
--neutralQuaternaryAlt: #e1dfdd;
|
||||
--neutralQuaternary: #d0d0d0;
|
||||
--neutralTertiaryAlt: #c8c6c4;
|
||||
--neutralTertiary: #a19f9d;
|
||||
--neutralSecondary: #605e5c;
|
||||
--neutralSecondaryAlt: #8a8886;
|
||||
--neutralPrimaryAlt: #3b3a39;
|
||||
--neutralPrimary: #323130;
|
||||
--neutralDark: #201f1e;
|
||||
--black: #000000;
|
||||
--white: #ffffff;
|
||||
|
||||
--megaMenuNavBackground: #5f6f74;
|
||||
--megaMenuNavHoverBackground: #6f8085;
|
||||
--megaMenuNavOpenBackground: #728388;
|
||||
--megaMenuNavTextColor: #ffffff;
|
||||
--megaMenuNavDividerColor: rgba(255, 255, 255, 0.22);
|
||||
--megaMenuPanelWidth: 1280px;
|
||||
--megaMenuZIndex: 6000;
|
||||
}
|
||||
*/
|
||||
|
||||
$mm-white: var(--white, #{$ms-color-white});
|
||||
$mm-panel-surface: var(--white, #{$ms-color-white});
|
||||
$mm-panel-border: var(--neutralLight, #{$ms-color-neutralLight});
|
||||
$mm-panel-divider: var(--neutralQuaternaryAlt, #{$ms-color-neutralQuaternaryAlt});
|
||||
$mm-text: var(--neutralPrimary, #{$ms-color-neutralPrimary});
|
||||
$mm-text-muted: var(--neutralSecondary, #{$ms-color-neutralSecondary});
|
||||
$mm-nav-background: var(--megaMenuNavBackground, #5f6f74);
|
||||
$mm-nav-hover-background: var(--megaMenuNavHoverBackground, #6f8085);
|
||||
$mm-nav-open-background: var(--megaMenuNavOpenBackground, #728388);
|
||||
$mm-nav-divider: var(--megaMenuNavDividerColor, rgba(255, 255, 255, 0.22));
|
||||
$mm-nav-text: var(--megaMenuNavTextColor, #ffffff);
|
||||
$mm-focus-accent: var(--themePrimary, #{$ms-color-themePrimary});
|
||||
$mm-focus-soft: var(--themeLighterAlt, #{$ms-color-themeLighterAlt});
|
||||
$mm-panel-shadow: var(--megaMenuPanelShadow, 0 14px 28px rgba(0, 0, 0, 0.18));
|
||||
$mm-panel-width: var(--megaMenuPanelWidth, 1280px);
|
||||
$mm-nav-height: var(--megaMenuTopNavHeight, 34px);
|
||||
$mm-nav-inline-padding: var(--megaMenuNavInlinePadding, 8px);
|
||||
$mm-item-inline-padding: var(--megaMenuItemInlinePadding, 12px);
|
||||
$mm-column-padding-x: var(--megaMenuColumnPaddingX, 18px);
|
||||
$mm-column-padding-y: var(--megaMenuColumnPaddingY, 16px);
|
||||
$mm-link-radius: var(--megaMenuLinkRadius, 2px);
|
||||
$mm-layer-index: var(--megaMenuZIndex, 6000);
|
||||
$white: var(--white, #{$ms-color-white});
|
||||
$neutral-lighter: var(--neutralLighter, #{$ms-color-neutralLighter});
|
||||
$neutral-light: var(--neutralLight, #{$ms-color-neutralLight});
|
||||
$neutral-tertiary: var(--neutralTertiary, #{$ms-color-neutralTertiary});
|
||||
$neutral-secondary: var(--neutralSecondary, #{$ms-color-neutralSecondary});
|
||||
$neutral-primary: var(--neutralPrimary, #{$ms-color-neutralPrimary});
|
||||
$theme-primary: var(--themePrimary, #{$ms-color-themePrimary});
|
||||
$theme-dark: var(--themeDark, #{$ms-color-themeDark});
|
||||
$theme-lighter-alt: var(--themeLighterAlt, #{$ms-color-themeLighterAlt});
|
||||
$menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||
$menu-layer: var(--megaMenuZIndex, 6000);
|
||||
|
||||
:global {
|
||||
:root {
|
||||
--megaMenuNavBackground: #5f6f74;
|
||||
--megaMenuNavHoverBackground: #6f8085;
|
||||
--megaMenuNavOpenBackground: #728388;
|
||||
--megaMenuNavTextColor: #ffffff;
|
||||
--megaMenuNavDividerColor: rgba(255, 255, 255, 0.22);
|
||||
--megaMenuTopNavHeight: 34px;
|
||||
--megaMenuPanelWidth: 1280px;
|
||||
--megaMenuPanelShadow: 0 14px 28px rgba(0, 0, 0, 0.18);
|
||||
--megaMenuZIndex: 6000;
|
||||
--megaMenuNavInlinePadding: 8px;
|
||||
--megaMenuItemInlinePadding: 12px;
|
||||
--megaMenuColumnPaddingX: 18px;
|
||||
--megaMenuColumnPaddingY: 16px;
|
||||
--megaMenuLinkRadius: 2px;
|
||||
--megaMenuColumnCount: 4;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 6px;
|
||||
background: $mm-focus-accent;
|
||||
color: $mm-white;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
z-index: 10000;
|
||||
transition: top 0.2s ease;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
#CustomNavigation {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
z-index: $mm-layer-index;
|
||||
}
|
||||
|
||||
.mega-menu-status {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid $mm-panel-border;
|
||||
background: $mm-focus-soft;
|
||||
color: $mm-text;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mega-menu-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #5f6f74;
|
||||
background-color: $mm-nav-background;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.18);
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
z-index: $mm-layer-index;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul,
|
||||
.mega-menu-top-level {
|
||||
margin: 0 auto;
|
||||
max-width: $mm-panel-width;
|
||||
background-color: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
padding: 0 $mm-nav-inline-padding;
|
||||
padding-inline-start: $mm-nav-inline-padding;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li,
|
||||
.mega-menu-top-item {
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex: 0 0 auto;
|
||||
border-right: 1px solid $mm-nav-divider;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li:last-child,
|
||||
.mega-menu-top-item:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li > a,
|
||||
.mega-menu-main > ul > li > .menu-item-text,
|
||||
.menu-item-link,
|
||||
.menu-item-text {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: $mm-nav-height;
|
||||
padding: 0 $mm-item-inline-padding;
|
||||
color: $mm-nav-text;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
transition: background-color 0.14s ease, color 0.14s ease;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-item-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-item-has-children::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-left: 8px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
transform: translateY(-1px) rotate(45deg);
|
||||
transition: transform 0.14s ease;
|
||||
}
|
||||
|
||||
.menu-item-has-children[aria-expanded="true"]::after {
|
||||
transform: translateY(1px) rotate(-135deg);
|
||||
}
|
||||
|
||||
.mega-menu-top-item.is-active > a,
|
||||
.mega-menu-top-item.is-active > span,
|
||||
.menu-item-link.is-active,
|
||||
.menu-item-text.is-active {
|
||||
background: $mm-nav-open-background;
|
||||
}
|
||||
|
||||
.mega-menu-top-item.is-current > a,
|
||||
.mega-menu-top-item.is-current > span,
|
||||
.menu-item-link.is-current,
|
||||
.menu-item-text.is-current {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li:hover > a,
|
||||
.mega-menu-main > ul > li:hover > .menu-item-text,
|
||||
.mega-menu-main > ul > li > a[aria-expanded="true"],
|
||||
.mega-menu-main > ul > li > span[aria-expanded="true"],
|
||||
.menu-item-link:hover,
|
||||
.menu-item-text:hover {
|
||||
color: $mm-nav-text;
|
||||
background: $mm-nav-hover-background;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li > a[aria-expanded="true"],
|
||||
.mega-menu-main > ul > li > span[aria-expanded="true"] {
|
||||
background: $mm-nav-open-background;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li > a:focus,
|
||||
.mega-menu-main > ul > li > .menu-item-text:focus,
|
||||
.menu-item-link:focus,
|
||||
.menu-item-text:focus {
|
||||
color: $mm-nav-text;
|
||||
background: $mm-nav-open-background;
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.mega-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
background-color: #ffffff;
|
||||
background-color: $mm-panel-surface;
|
||||
background-image: none;
|
||||
mix-blend-mode: normal;
|
||||
border: 1px solid $mm-panel-border;
|
||||
border-top: 0;
|
||||
box-shadow: $mm-panel-shadow;
|
||||
max-height: calc(100vh - #{$mm-nav-height});
|
||||
isolation: isolate;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: calc(#{$mm-layer-index} + 1);
|
||||
transition: opacity 0.16s ease, visibility 0.16s ease;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li:hover .mega-menu,
|
||||
.mega-menu.js-open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
background-color: #ffffff;
|
||||
background-color: $mm-panel-surface;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.mega-menu-grid {
|
||||
max-width: $mm-panel-width;
|
||||
background-color: inherit;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--megaMenuColumnCount, 4), minmax(0, 1fr));
|
||||
gap: 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.mega-menu-category {
|
||||
min-width: 0;
|
||||
padding: $mm-column-padding-y $mm-column-padding-x 14px;
|
||||
border-right: 1px solid $mm-panel-divider;
|
||||
}
|
||||
|
||||
.mega-menu-grid > .mega-menu-category:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.mega-menu-category > h3,
|
||||
.mega-menu-category-title {
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.mega-menu-category > h3 > span,
|
||||
.mega-menu-category > h3 > a {
|
||||
display: block;
|
||||
color: $mm-text;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mega-menu-category.is-active > h3 > span,
|
||||
.mega-menu-category.is-active > h3 > a,
|
||||
.mega-menu-category > h3 > span.is-active,
|
||||
.mega-menu-category > h3 > a.is-active {
|
||||
color: $mm-text;
|
||||
}
|
||||
|
||||
.mega-menu-category.is-current > h3 > a,
|
||||
.mega-menu-category > h3 > a.is-current {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mega-menu-category > h3 > a:hover {
|
||||
color: $mm-text;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mega-menu-category > h3 > a:focus {
|
||||
background: $mm-focus-soft;
|
||||
color: $mm-text;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 2px $mm-focus-accent;
|
||||
padding: 1px 4px;
|
||||
margin: -1px -4px;
|
||||
}
|
||||
|
||||
.mega-menu-category ul,
|
||||
.mega-menu-links {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li,
|
||||
.mega-menu-links li {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li:last-child,
|
||||
.mega-menu-links li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li a,
|
||||
.mega-menu-links li a {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 1px 2px;
|
||||
border-radius: $mm-link-radius;
|
||||
color: $mm-text-muted;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li.is-active > a,
|
||||
.mega-menu-links li.is-active > a,
|
||||
.mega-menu-category ul li a.is-active,
|
||||
.mega-menu-links li a.is-active {
|
||||
color: $mm-text;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li.is-current > a,
|
||||
.mega-menu-links li.is-current > a,
|
||||
.mega-menu-category ul li a.is-current,
|
||||
.mega-menu-links li a.is-current {
|
||||
color: $mm-text;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li a:hover,
|
||||
.mega-menu-links li a:hover {
|
||||
background: transparent;
|
||||
color: $mm-text;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mega-menu-category ul li a:focus,
|
||||
.mega-menu-links li a:focus {
|
||||
background: $mm-focus-soft;
|
||||
color: $mm-text;
|
||||
box-shadow: 0 0 0 2px $mm-focus-accent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.mega-menu-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
:root {
|
||||
--megaMenuBarHeight: 44px;
|
||||
--megaMenuContentWidth: 1200px;
|
||||
--megaMenuFlyoutWidth: 288px;
|
||||
--megaMenuZIndex: 6000;
|
||||
}
|
||||
|
||||
.mega-menu-category {
|
||||
padding: 14px 16px 12px;
|
||||
#CustomNavigation {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
z-index: $menu-layer;
|
||||
font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.mega-menu-status {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 24px;
|
||||
border-bottom: 1px solid $neutral-light;
|
||||
background: $theme-lighter-alt;
|
||||
color: $neutral-primary;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mega-menu-main {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: var(--megaMenuBarHeight, 44px);
|
||||
overflow: visible;
|
||||
border-top: 1px solid $neutral-light;
|
||||
border-bottom: 1px solid $neutral-light;
|
||||
background: $white;
|
||||
color: $neutral-primary;
|
||||
z-index: $menu-layer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.mega-menu-main > ul,
|
||||
.mega-menu-top-level {
|
||||
padding: 0 6px;
|
||||
padding-inline-start: 6px;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: var(--megaMenuContentWidth, 1200px);
|
||||
min-height: var(--megaMenuBarHeight, 44px);
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li,
|
||||
.mega-menu-top-item {
|
||||
border-right: 0;
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mega-menu-mode-flyout .mega-menu-top-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mega-menu-main > ul > li > a,
|
||||
.mega-menu-main > ul > li > .menu-item-text,
|
||||
.menu-item-link,
|
||||
.menu-item-text {
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-height: var(--megaMenuBarHeight, 44px);
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $neutral-primary;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu-item-has-children::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: -3px 0 0 10px;
|
||||
border-right: 1px solid currentColor;
|
||||
border-bottom: 1px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.menu-item-link:hover,
|
||||
.menu-item-text:hover,
|
||||
.menu-item-link[aria-expanded='true'],
|
||||
.menu-item-text[aria-expanded='true'] {
|
||||
background: $neutral-lighter;
|
||||
color: $neutral-primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mega-menu-top-item.is-active > .menu-item-link::before,
|
||||
.mega-menu-top-item.is-active > .menu-item-text::before,
|
||||
.mega-menu-top-item.is-current > .menu-item-link::before,
|
||||
.mega-menu-top-item.is-current > .menu-item-text::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
left: 12px;
|
||||
height: 2px;
|
||||
background: $theme-primary;
|
||||
}
|
||||
|
||||
.mega-menu-top-item.is-current > .menu-item-link,
|
||||
.mega-menu-top-item.is-current > .menu-item-text {
|
||||
color: $theme-dark;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-item-link:focus,
|
||||
.menu-item-text:focus,
|
||||
.mega-menu-category a:focus,
|
||||
.flyout-link:focus {
|
||||
outline: 2px solid $theme-primary;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.mega-menu {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
border: 1px solid $neutral-light;
|
||||
border-top: 0;
|
||||
background: $white;
|
||||
box-shadow: $menu-shadow;
|
||||
color: $neutral-primary;
|
||||
z-index: calc(#{$menu-layer} + 1);
|
||||
}
|
||||
|
||||
.mega-menu.js-open {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mega-menu-grid {
|
||||
grid-template-columns: 1fr;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--megaMenuColumnCount, 4), minmax(0, 1fr));
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: var(--megaMenuContentWidth, 1200px);
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
gap: 24px 32px;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.mega-menu-category {
|
||||
padding: 14px 12px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid $mm-panel-divider;
|
||||
min-width: 0;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.mega-menu-grid > .mega-menu-category:last-child {
|
||||
border-bottom: 0;
|
||||
.mega-menu-category-title {
|
||||
margin: 0 0 10px;
|
||||
padding: 0 0 8px;
|
||||
border-bottom: 1px solid $neutral-light;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skip-link,
|
||||
.menu-item-has-children::after,
|
||||
.mega-menu-main > ul > li > a,
|
||||
.mega-menu-main > ul > li > .menu-item-text,
|
||||
.menu-item-link,
|
||||
.menu-item-text,
|
||||
.mega-menu,
|
||||
.mega-menu-category ul li a,
|
||||
.mega-menu-links li a {
|
||||
transition: none;
|
||||
.mega-menu-category-title > a,
|
||||
.mega-menu-category-title > span {
|
||||
display: block;
|
||||
color: $neutral-primary;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.mega-menu-main > ul > li > a:focus,
|
||||
.mega-menu-main > ul > li > .menu-item-text:focus,
|
||||
.menu-item-link:focus,
|
||||
.menu-item-text:focus,
|
||||
.mega-menu-category > h3 > a:focus,
|
||||
.mega-menu-category ul li a:focus,
|
||||
.mega-menu-links li a:focus {
|
||||
outline: 2px solid;
|
||||
outline-offset: 2px;
|
||||
box-shadow: none;
|
||||
.mega-menu-category-title > a:hover {
|
||||
color: $theme-primary;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mega-menu-links {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mega-menu-links li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mega-menu-links a {
|
||||
display: block;
|
||||
margin: 0 -8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 2px;
|
||||
color: $neutral-secondary;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mega-menu-links a:hover {
|
||||
background: $neutral-lighter;
|
||||
color: $neutral-primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mega-menu-links a.is-current,
|
||||
.mega-menu-links li.is-current > a {
|
||||
color: $theme-dark;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mega-menu-flyout {
|
||||
right: auto;
|
||||
width: var(--megaMenuFlyoutWidth, 288px);
|
||||
min-width: 240px;
|
||||
border-top: 1px solid $neutral-light;
|
||||
}
|
||||
|
||||
.flyout-list {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
background: $white;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.flyout-item {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.flyout-link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 8px 36px 8px 16px;
|
||||
border: 0;
|
||||
background: $white;
|
||||
color: $neutral-primary;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.flyout-link:hover,
|
||||
.flyout-item:focus-within > .flyout-link,
|
||||
.flyout-item.is-active > .flyout-link {
|
||||
background: $neutral-lighter;
|
||||
color: $neutral-primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.flyout-item.is-current > .flyout-link {
|
||||
color: $theme-dark;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.flyout-link-has-children::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-top: -4px;
|
||||
border-top: 1px solid currentColor;
|
||||
border-right: 1px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.flyout-level-3 {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 100%;
|
||||
display: none;
|
||||
width: var(--megaMenuFlyoutWidth, 288px);
|
||||
border: 1px solid $neutral-light;
|
||||
box-shadow: $menu-shadow;
|
||||
z-index: calc(#{$menu-layer} + 2);
|
||||
}
|
||||
|
||||
.flyout-item:hover > .flyout-level-3,
|
||||
.flyout-item:focus-within > .flyout-level-3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.mega-menu-top-level {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.mega-menu-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.mega-menu-top-level {
|
||||
flex-wrap: wrap;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.menu-item-link,
|
||||
.menu-item-text {
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mega-menu-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mega-menu-mode-flyout .mega-menu-flyout {
|
||||
position: fixed;
|
||||
right: 8px;
|
||||
left: 8px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.flyout-level-3 {
|
||||
position: static;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0 0 4px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mega-menu,
|
||||
.menu-item-link,
|
||||
.menu-item-text,
|
||||
.flyout-link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.menu-item-link:focus,
|
||||
.menu-item-text:focus,
|
||||
.mega-menu-category a:focus,
|
||||
.flyout-link:focus {
|
||||
outline: 2px solid CanvasText;
|
||||
}
|
||||
}
|
||||
}
|
||||
} /* End :global */
|
||||
|
||||
@@ -15,6 +15,7 @@ import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationServ
|
||||
import { MegaMenuRenderer } from './MegaMenuRenderer';
|
||||
import { IMenuItem } from '../../services/IMenuItem';
|
||||
import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
|
||||
import { MegaMenuMode, normalizeMegaMenuMode } from './MegaMenuMode';
|
||||
|
||||
import './MegaMenu.module.scss';
|
||||
|
||||
@@ -26,6 +27,7 @@ export interface IMegaMenuApplicationCustomizerProperties {
|
||||
termSetId?: string;
|
||||
cacheMinutes?: number;
|
||||
debug?: boolean;
|
||||
menuMode?: MegaMenuMode;
|
||||
}
|
||||
|
||||
export default class MegaMenuApplicationCustomizer
|
||||
@@ -40,7 +42,7 @@ export default class MegaMenuApplicationCustomizer
|
||||
const debug: boolean = this._isDebugEnabled();
|
||||
debugLog(debug, LOG_SOURCE, 'onInit started.', {
|
||||
componentId: UserCustomActionMegaMenuId,
|
||||
version: '2.0.1',
|
||||
version: '2.1.0',
|
||||
properties: this.properties || {},
|
||||
pageUrl: window.location.href,
|
||||
webUrl: this.context.pageContext.web.absoluteUrl,
|
||||
@@ -92,7 +94,8 @@ export default class MegaMenuApplicationCustomizer
|
||||
|
||||
debugLog(debug, LOG_SOURCE, 'Starting MegaMenu rendering.', {
|
||||
termSetIdentifier: termSetIdentifier,
|
||||
cacheMinutes: this._getCacheMinutes()
|
||||
cacheMinutes: this._getCacheMinutes(),
|
||||
menuMode: this._getMenuMode()
|
||||
});
|
||||
this._renderMegaMenu(termSetIdentifier, debug);
|
||||
} else {
|
||||
@@ -127,7 +130,8 @@ export default class MegaMenuApplicationCustomizer
|
||||
|
||||
this._renderer = new MegaMenuRenderer(
|
||||
menuItems,
|
||||
debug
|
||||
debug,
|
||||
this._getMenuMode()
|
||||
);
|
||||
|
||||
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
||||
@@ -205,6 +209,10 @@ export default class MegaMenuApplicationCustomizer
|
||||
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
|
||||
}
|
||||
|
||||
private _getMenuMode(): MegaMenuMode {
|
||||
return normalizeMegaMenuMode(this.properties && this.properties.menuMode);
|
||||
}
|
||||
|
||||
private _renderStatus(message: string): void {
|
||||
if (!this._topPlaceholder) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type MegaMenuMode = 'megaMenu' | 'flyout';
|
||||
|
||||
export function normalizeMegaMenuMode(value?: string): MegaMenuMode {
|
||||
return value && value.toLowerCase() === 'flyout' ? 'flyout' : 'megaMenu';
|
||||
}
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
import { IMenuItem } from '../../services/IMenuItem';
|
||||
import { debugLog } from '../../services/MegaMenuDebug';
|
||||
|
||||
import styles from './MegaMenu.module.scss'; // tslint:disable-line:no-unused-variable
|
||||
import { MegaMenuMode } from './MegaMenuMode';
|
||||
|
||||
const LOG_SOURCE: string = 'MegaMenuRenderer';
|
||||
|
||||
@@ -20,7 +19,8 @@ export class MegaMenuRenderer {
|
||||
|
||||
constructor(
|
||||
private menuItems: IMenuItem[],
|
||||
private debug: boolean = false
|
||||
private debug: boolean = false,
|
||||
private menuMode: MegaMenuMode = 'megaMenu'
|
||||
) { }
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
@@ -36,7 +36,8 @@ export class MegaMenuRenderer {
|
||||
|
||||
const nav = document.createElement('nav');
|
||||
nav.id = 'Mega-Menu';
|
||||
nav.className = 'mega-menu-main';
|
||||
nav.className = this.joinClasses('mega-menu-main', 'mega-menu-mode-' + this.menuMode);
|
||||
nav.setAttribute('data-menu-mode', this.menuMode);
|
||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
||||
|
||||
const topLevelUl = document.createElement('ul');
|
||||
@@ -99,8 +100,10 @@ export class MegaMenuRenderer {
|
||||
li.appendChild(topElement);
|
||||
|
||||
if (hasChildren) {
|
||||
const megaMenu = this.createMegaMenu(item);
|
||||
li.appendChild(megaMenu);
|
||||
const childMenu = this.menuMode === 'flyout'
|
||||
? this.createFlyoutMenu(item)
|
||||
: this.createMegaMenu(item);
|
||||
li.appendChild(childMenu);
|
||||
}
|
||||
|
||||
return li;
|
||||
@@ -246,6 +249,88 @@ export class MegaMenuRenderer {
|
||||
return categoryDiv;
|
||||
}
|
||||
|
||||
private createFlyoutMenu(parentItem: IMenuItem): HTMLDivElement {
|
||||
const flyout = document.createElement('div');
|
||||
flyout.className = 'mega-menu mega-menu-flyout';
|
||||
flyout.setAttribute('aria-expanded', 'false');
|
||||
flyout.setAttribute('aria-label', parentItem.label + ' Untermenü');
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'flyout-list flyout-level-2';
|
||||
|
||||
if (parentItem.items) {
|
||||
parentItem.items.forEach(secondLevelItem => {
|
||||
list.appendChild(this.createFlyoutItem(secondLevelItem));
|
||||
});
|
||||
}
|
||||
|
||||
flyout.appendChild(list);
|
||||
return flyout;
|
||||
}
|
||||
|
||||
private createFlyoutItem(item: IMenuItem): HTMLLIElement {
|
||||
const li = document.createElement('li');
|
||||
const hasChildren = item.hasChildren() && item.items && item.items.length > 0;
|
||||
const isActive = this.isItemActive(item);
|
||||
const isCurrent = this.isCurrentUrl(item.url);
|
||||
|
||||
li.className = this.joinClasses(
|
||||
'flyout-item',
|
||||
hasChildren ? 'has-children' : undefined,
|
||||
isActive ? 'is-active' : undefined,
|
||||
isCurrent ? 'is-current' : undefined
|
||||
);
|
||||
|
||||
const element = this.createFlyoutElement(item, hasChildren, isActive, isCurrent);
|
||||
li.appendChild(element);
|
||||
|
||||
if (hasChildren && item.items) {
|
||||
const childList = document.createElement('ul');
|
||||
childList.className = 'flyout-list flyout-level-3';
|
||||
childList.setAttribute('aria-label', item.label + ' Untermenü');
|
||||
|
||||
item.items.forEach(thirdLevelItem => {
|
||||
childList.appendChild(this.createFlyoutItem(thirdLevelItem));
|
||||
});
|
||||
|
||||
li.appendChild(childList);
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
private createFlyoutElement(item: IMenuItem, hasChildren: boolean, isActive: boolean, isCurrent: boolean): HTMLElement {
|
||||
const element: HTMLElement = item.url
|
||||
? document.createElement('a')
|
||||
: document.createElement('span');
|
||||
|
||||
if (item.url) {
|
||||
(element as HTMLAnchorElement).href = item.url;
|
||||
} else {
|
||||
element.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
element.textContent = item.label;
|
||||
element.className = this.joinClasses(
|
||||
'flyout-link',
|
||||
hasChildren ? 'flyout-link-has-children' : undefined,
|
||||
isActive ? 'is-active' : undefined,
|
||||
isCurrent ? 'is-current' : undefined
|
||||
);
|
||||
|
||||
if (hasChildren) {
|
||||
element.setAttribute('aria-haspopup', 'true');
|
||||
}
|
||||
if (isCurrent && item.url) {
|
||||
element.setAttribute('aria-current', 'page');
|
||||
}
|
||||
if (item.hoverText) {
|
||||
element.title = item.hoverText;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
private attachEventListeners(): void {
|
||||
const headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > .menu-item-text');
|
||||
|
||||
|
||||
+1
-15
@@ -1,20 +1,6 @@
|
||||
// tslint:disable:max-line-length no-any
|
||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
||||
|
||||
// Export services for classic SharePoint usage
|
||||
export { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
|
||||
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
|
||||
export { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';
|
||||
export { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';
|
||||
|
||||
// Expose services globally for classic JavaScript consumption
|
||||
declare var window: any;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__megaMenuServices = {
|
||||
TaxonomyNavigationService: require('./services/TaxonomyNavigationService').TaxonomyNavigationService,
|
||||
MegaMenuRenderer: require('./extensions/megaMenu/MegaMenuRenderer').MegaMenuRenderer,
|
||||
UserCustomActionService: require('./services/UserCustomActionService/UserCustomActionService').UserCustomActionService,
|
||||
UserCustomActionScope: require('./services/UserCustomActionService/UserCustomActionScope').UserCustomActionScope
|
||||
};
|
||||
}
|
||||
export { MegaMenuMode, normalizeMegaMenuMode } from './extensions/megaMenu/MegaMenuMode';
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { IMenuItem } from './IMenuItem';
|
||||
import { ITaxonomyNavigationService } from './ITaxonomyNavigationService';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export default class MockTaxonomyNavigationService implements ITaxonomyNavigationService {
|
||||
public getMenuItems(): Promise<IMenuItem[]> {
|
||||
return new Promise<IMenuItem[]>((resolve) => {
|
||||
resolve([
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Menu Item 1',
|
||||
url: 'https://www.bing.com',
|
||||
hoverText: 'Hover me!',
|
||||
pathDepth: 1,
|
||||
hasChildren: () => true,
|
||||
items: [
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 1',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 2,
|
||||
hasChildren: () => false
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 2',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 2,
|
||||
hasChildren: () => false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Menu Item 2',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 1,
|
||||
hasChildren: () => false
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Menu Item 3',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 1,
|
||||
hasChildren: () => true,
|
||||
items: [
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 1',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 2,
|
||||
hasChildren: () => false
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 2',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 2,
|
||||
hasChildren: () => true,
|
||||
items: [
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 1',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 3,
|
||||
hasChildren: () => false
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 2',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 3,
|
||||
hasChildren: () => false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 3',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 2,
|
||||
hasChildren: () => false
|
||||
},
|
||||
{
|
||||
id: uuid.v4(),
|
||||
label: 'Submenu Item 4',
|
||||
hoverText: 'Huch!',
|
||||
url: 'https://www.bing.com',
|
||||
pathDepth: 2,
|
||||
hasChildren: () => false
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
export interface IUserCustomActionProps {
|
||||
Id?: string;
|
||||
Title: string;
|
||||
Name?: string;
|
||||
Description?: string;
|
||||
Location: string;
|
||||
ScriptSrc?: string;
|
||||
ScriptBlock?: string;
|
||||
Url?: string;
|
||||
Sequence?: number;
|
||||
Group?: string;
|
||||
ImageUrl?: string;
|
||||
CommandUIExtension?: string;
|
||||
RegistrationType?: number;
|
||||
RegistrationId?: string;
|
||||
Rights?: {};
|
||||
Scope?: number;
|
||||
ClientSideComponentId?: string;
|
||||
ClientSideComponentProperties?: string;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/* tslint:disable:max-line-length */
|
||||
import { UserCustomActionScope } from './UserCustomActionScope';
|
||||
import { IUserCustomActionProps } from './IUserCustomActionProps';
|
||||
import { UserCustomActionAddResult, UserCustomActionUpdateResult } from '@pnp/sp';
|
||||
|
||||
export interface IUserCustomActionService {
|
||||
getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>;
|
||||
getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>;
|
||||
addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>;
|
||||
updateUserCustomAction(scope: UserCustomActionScope, id: string, props: {}, listId?: string): Promise<UserCustomActionUpdateResult>;
|
||||
deleteUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<void>;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum UserCustomActionScope {
|
||||
Web = 'web',
|
||||
Site = 'site',
|
||||
List = 'list'
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// tslint:disable:max-line-length
|
||||
// tslint:disable:export-name
|
||||
import { UserCustomActionAddResult, UserCustomActions, UserCustomActionUpdateResult } from '@pnp/sp/src/usercustomactions';
|
||||
import { IUserCustomActionService } from './IUserCustomActionService';
|
||||
import { sp } from '@pnp/sp';
|
||||
import { UserCustomActionScope } from './UserCustomActionScope';
|
||||
import { IUserCustomActionProps } from './IUserCustomActionProps';
|
||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||
import { debugError } from '../MegaMenuDebug';
|
||||
|
||||
const LOG_SOURCE: string = 'UserCustomActionService';
|
||||
|
||||
export class UserCustomActionService implements IUserCustomActionService {
|
||||
constructor(context: ApplicationCustomizerContext, private debug: boolean = false) {
|
||||
sp.setup({
|
||||
spfxContext: context
|
||||
});
|
||||
}
|
||||
|
||||
public async getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]> {
|
||||
try {
|
||||
let actions: UserCustomActions | IUserCustomActionProps[];
|
||||
switch (scope) {
|
||||
case UserCustomActionScope.Web:
|
||||
actions = await sp.web.userCustomActions.get();
|
||||
break;
|
||||
case UserCustomActionScope.Site:
|
||||
actions = await sp.site.userCustomActions.get();
|
||||
break;
|
||||
case UserCustomActionScope.List:
|
||||
if (!listId) {
|
||||
throw new Error('List ID is required for List scope');
|
||||
}
|
||||
actions = await sp.web.lists.getById(listId).userCustomActions.get();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid scope');
|
||||
}
|
||||
return actions as IUserCustomActionProps[];
|
||||
} catch (error) {
|
||||
debugError(this.debug, LOG_SOURCE, 'Error getting user custom actions.', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps> {
|
||||
try {
|
||||
switch (scope) {
|
||||
case UserCustomActionScope.Web:
|
||||
return sp.web.userCustomActions.getById(id) as {} as IUserCustomActionProps;
|
||||
case UserCustomActionScope.Site:
|
||||
return sp.site.userCustomActions.getById(id) as {} as IUserCustomActionProps;
|
||||
case UserCustomActionScope.List:
|
||||
if (!listId) {
|
||||
throw new Error('List ID is required for List scope');
|
||||
}
|
||||
return sp.web.lists.getById(listId).userCustomActions.getById(id) as {} as IUserCustomActionProps;
|
||||
default:
|
||||
throw new Error('Invalid scope');
|
||||
}
|
||||
} catch (error) {
|
||||
debugError(this.debug, LOG_SOURCE, 'Error getting user custom action by ID.', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult> {
|
||||
try {
|
||||
switch (scope) {
|
||||
case UserCustomActionScope.Web:
|
||||
return sp.web.userCustomActions.add(customAction);
|
||||
case UserCustomActionScope.Site:
|
||||
return sp.site.userCustomActions.add(customAction);
|
||||
case UserCustomActionScope.List:
|
||||
if (!listId) {
|
||||
throw new Error('List ID is required for List scope');
|
||||
}
|
||||
return sp.web.lists.getById(listId).userCustomActions.add(customAction);
|
||||
default:
|
||||
throw new Error('Invalid scope');
|
||||
}
|
||||
} catch (error) {
|
||||
debugError(this.debug, LOG_SOURCE, 'Error adding user custom action.', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateUserCustomAction(scope: UserCustomActionScope, id: string, props: {}, listId?: string): Promise<UserCustomActionUpdateResult> {
|
||||
try {
|
||||
let result: UserCustomActionUpdateResult;
|
||||
switch (scope) {
|
||||
case UserCustomActionScope.Web:
|
||||
result = await sp.web.userCustomActions.getById(id).update(props);
|
||||
break;
|
||||
case UserCustomActionScope.Site:
|
||||
result = await sp.site.userCustomActions.getById(id).update(props);
|
||||
break;
|
||||
case UserCustomActionScope.List:
|
||||
if (!listId) {
|
||||
throw new Error('List ID is required for List scope');
|
||||
}
|
||||
result = await sp.web.lists.getById(listId).userCustomActions.getById(id).update(props);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid scope');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
debugError(this.debug, LOG_SOURCE, 'Error updating user custom action.', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<void> {
|
||||
try {
|
||||
switch (scope) {
|
||||
case UserCustomActionScope.Web:
|
||||
return sp.web.userCustomActions.getById(customAction.Id).delete();
|
||||
case UserCustomActionScope.Site:
|
||||
return sp.site.userCustomActions.getById(customAction.Id).delete();
|
||||
case UserCustomActionScope.List:
|
||||
if (!listId) {
|
||||
throw new Error('List ID is required for List scope');
|
||||
}
|
||||
return sp.web.lists.getById(listId).userCustomActions.getById(customAction.Id).delete();
|
||||
default:
|
||||
throw new Error('Invalid scope');
|
||||
}
|
||||
} catch (error) {
|
||||
debugError(this.debug, LOG_SOURCE, 'Error deleting user custom action.', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user