22 Commits
Author SHA1 Message Date
Torsten Brendgen a40a5ddc57 feat: Update version to 2.2.5 and enhance CustomHeader handling in MegaMenu 2026-08-23 21:59:29 +02:00
unknown 767d6bfd10 Remove generated SPFx build artifacts from repository 2026-08-19 12:13:00 -07:00
Torsten Brendgen 79b4a8a6c4 feat: Update version to 2.2.4 and enhance loading status handling in MegaMenu 2026-08-19 20:52:30 +02:00
Torsten Brendgen df3df26be9 feat: Update MegaMenu styles and grid layout for improved responsiveness 2026-08-19 20:43:24 +02:00
Torsten 6b4a1293f3 Merge pull request 'dev-3.0' (#2) from dev-3.0 into main
Reviewed-on: #2
2026-08-18 20:49:51 +00:00
Torsten Brendgen ea2e5a6088 feat: Update megaMenuZIndex variable for improved layering in MegaMenu 2026-08-18 22:13:02 +02:00
Torsten Brendgen f95accd1ac feat: Update version to 2.2.3 and enhance styling in MegaMenu 2026-08-18 20:20:54 +02:00
Torsten Brendgen e977fd7365 feat: Update version to 2.2.2, enhance global navigation exclusion logic, and improve hover behavior in MegaMenu 2026-08-18 20:20:20 +02:00
Torsten Brendgen f58c6f6135 feat: Update MegaMenu styles to use theme variables and enhance static asset validation 2026-07-21 22:03:29 +02:00
Torsten Brendgen 50b85468f1 feat: Enhance MegaMenu with localization, caching, and new properties
- Added localization support for loading states, empty navigation, and submenu actions in both German and English.
- Introduced new properties in IMenuItem for description, openInNewWindow, and external links.
- Updated SPTermStorePickerService to handle cache versioning and language-specific caching.
- Implemented a new MegaMenuCore service for handling term properties, cache management, and navigation URL resolution.
- Refactored TaxonomyNavigationService to build menu hierarchy and filter hidden terms.
- Added automated tests for MegaMenu core functionalities and static asset validation.
- Removed unused ItemDictionary service.
- Created a comprehensive ToDo document outlining the implementation status and future tasks.
2026-07-20 21:34:24 +02:00
Torsten Brendgen bc61166267 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.
2026-07-20 20:37:32 +02:00
Torsten 2d91c4ba97 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2026-07-19 21:57:44 +00:00
Torsten Brendgen e75b11117f Bump version to 2.0.1, update deployment strategy for central SPFx deployment, and enhance error handling in PowerShell script 2026-07-19 23:42:36 +02:00
Torsten Brendgen dd79e85725 Füge Überprüfung der SharePoint PowerShell-Komponenten hinzu und verbessere die Fehlerbehandlung in den Skripten 2026-07-19 23:33:47 +02:00
Torsten Brendgen e119573b5e Füge neues Skript für MegaMenu Site-Scope-Test hinzu, um die Konfiguration zu überprüfen und Site-Actions zu verwalten 2026-07-19 23:31:42 +02:00
Torsten Brendgen 85c56a1153 Bump version to 2.0.0, update component ID, and enhance central configuration for MegaMenu 2026-07-19 23:15:38 +02:00
Torsten Brendgen 07583b8122 Bump version to 1.1.4, remove unnecessary external dependency, and update term sorting logic 2026-07-19 22:54:29 +02:00
Torsten Brendgen 2d723da5a2 Bump version to 1.1.3, add detailed lifecycle logging in debug mode, and enhance error handling in services 2026-07-19 22:43:59 +02:00
Torsten Brendgen 5f1d26c7f6 Bump version to 1.1.2 and update TaxonomyNavigationService to hide tags not available for tagging
- Updated TaxonomyNavigationService to set hideTagsNotAvailableForTagging to true.
- Modified getAllTerms call to include the updated parameter for hiding unavailable tags.
- Removed external CSS loading functionality from MegaMenuApplicationCustomizer.
- Updated cache prefixes in SPTermStorePickerService for versioning.
- Bumped version in package.json and package-lock.json to 1.1.2.
- Updated deployment manifest files to reflect the new version.
2026-07-19 21:11:29 +02:00
Torsten Brendgen 0ab514ca21 Bump version to 1.1.1 and update related manifests
- Updated version in package.json and package-lock.json to 1.1.1
- Changed version in deployment manifest files to 1.1.1
- Added new elements.xml for MegaMenu Application Customizer
2026-07-19 01:30:18 +02:00
Torsten Brendgen 5cc88a83ba Aktualisiere .gitignore für verbesserte Ignorierung von Abhängigkeiten, Build-Ausgaben und Protokolldateien 2026-07-16 23:04:10 +02:00
Torsten Brendgen 97d2b6b5d2 Update MegaMenu Application Customizer: Version bump to 1.1.0, localization support added, and unnecessary files removed
- Deleted obsolete localization file for English (en-us).
- Updated version number from 1.0.4 to 1.1.0 in manifests.js and manifests.json.
- Added localization support for German (de-de) in a new localization file.
- Removed unnecessary dependencies from manifests.
- Added .gitignore to exclude build artifacts and temporary files.
2026-07-16 22:43:31 +02:00
118 changed files with 3328 additions and 10666 deletions
+47
View File
@@ -0,0 +1,47 @@
# Dependencies and local tools
node_modules/
.tools/
# SPFx build output
lib/
dist/
temp/
release/
solution/
sharepoint/solution/
*.sppkg
*.tgz
# Test output
coverage/
# Generated TypeScript files
*.resx.ts
*.scss.ts
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local environment files
.env
.env.*
!.env.example
# Visual Studio and IDE files
.vs/
.idea/
.ntvs_analysis.dat
*.suo
*.user
bin/
obj/
# Operating system files
.DS_Store
Thumbs.db
Desktop.ini
+270 -151
View File
@@ -1,199 +1,318 @@
# SharePoint MegaMenu Extension # SharePoint MegaMenu
**Version:** 1.0.2 Zentrale, barrierearme Portalnavigation aus SharePoint Managed Metadata für SharePoint Server Subscription Edition und SharePoint Server 2019.
**Framework:** SharePoint Framework (SPFx) 1.4.1
**Node.js:** v8.17.0
Eine moderne, barrierefreie Mega-Menü-Lösung für SharePoint 2019 oder SharePoint SE als ApplicationCustomizer Extension. | Eigenschaft | Wert |
|---|---|
| Version | 2.2.5 |
| 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` |
## 🌟 Features ## Funktionen
- **3-stufige Navigationshierarchie** basierend auf SharePoint Managed Metadata (Taxonomy) - zentrale Konfiguration pro Site Collection
- **Responsive Design** mit automatischer Anpassung an verschiedene Bildschirmgrößen - Termset-Auswahl bevorzugt per GUID, alternativ per Name
- **Barrierefreiheit** mit vollständiger Tastaturnavigation und Screenreader-Unterstützung - drei Navigationsebenen
- **Einstellungs-Panel** für Administratoren zur Konfiguration - zwei Darstellungsmodi: SharePoint-ähnliches Mega Menu und strukturelles Flyout
- **Externe CSS-Unterstützung** für individuelle Anpassungen - benutzerdefinierte Sortierung aus dem Term Store
- **Office UI Fabric Integration** für konsistente SharePoint-Optik - 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
## 📋 Voraussetzungen ## Architektur
- SharePoint 2019 oder SharePoint SE (Subscription Edition) ```text
- Node.js v8.17.0 (empfohlen) App Catalog
- SharePoint Framework Development Tools (SPFx 1.4.1) └── zentral bereitgestelltes MegaMenu-SPFx-Bundle
## 🚀 Installation & Entwicklung Site Collection
└── SPSite.UserCustomAction
├── ComponentId
└── ClientSideComponentProperties
├── termSetId / termSetName
├── cacheMinutes
├── menuMode
└── debug
### 1. Repository klonen und Abhängigkeiten installieren Moderne Seite Klassische Seite
└── Application Customizer └── ScriptLink + Classic-Runtime
```bash
git clone <repository-url>
cd MegaMenu
npm install
``` ```
### 2. Entwicklungsserver starten 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.
```bash 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.
gulp serve
## Konfiguration
```json
{
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
"termSetName": "Global Navigation",
"cacheMinutes": 15,
"cacheVersion": "1",
"menuMode": "megaMenu",
"debug": false
}
``` ```
### 3. Produktionspaket erstellen | 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 |
| `cacheVersion` | String | `1` | Frei wählbare Version zur sofortigen Invalidierung vorhandener Browser-Caches |
| `menuMode` | String | `megaMenu` | `megaMenu` oder `flyout` |
| `debug` | Boolean | `false` | ausführliche Browser-Konsolenausgaben |
```bash ### Mega Menu
gulp clean
gulp bundle --ship
gulp package-solution --ship
```
## 📁 Projektstruktur `"menuMode": "megaMenu"` zeigt die zweite Ebene als breites, spaltenbasiertes Panel. Die dritte Ebene wird als Linkliste unter der jeweiligen Kategorie dargestellt.
``` ### Flyout
src/
├── extensions/
│ └── megaMenu/
│ ├── MegaMenuApplicationCustomizer.ts # Haupteinstiegspunkt
│ ├── MegaMenuRenderer.ts # Menü-Rendering-Logik
│ ├── MegaMenuSettings.ts # Einstellungs-Panel
│ └── MegaMenu.css # Styling
├── services/
│ ├── TaxonomyNavigationService.ts # Taxonomy-Datenabfrage
│ ├── UserCustomActionService/ # UserCustomAction-Verwaltung
│ └── ...
└── deployment/
├── add-megamenu.ps1 # Installations-Script
└── remove-megamenu.ps1 # Deinstallations-Script
```
## ⚙️ Konfiguration `"menuMode": "flyout"` zeigt die zweite Ebene als kompaktes Dropdown. Die dritte Ebene öffnet sich seitlich als strukturelles Flyout.
### Einstellungs-Panel (Admin-Bereich) ## Lokale Term-Eigenschaften
Das MegaMenu verfügt über ein Einstellungs-Panel, das über das Zahnrad-Icon (⚙️) im Menü zugänglich ist: Die folgenden Eigenschaften werden im Term Store als lokale benutzerdefinierte Eigenschaften am jeweiligen Term gepflegt:
1. **Name des Navigations-Termsets**: Der Name des Managed Metadata Termsets, das als Datenquelle dient | Eigenschaft | Beispiel | Wirkung |
2. **Pfad zu zusätzlicher CSS-Datei**: Optional - URL zu einer benutzerdefinierten CSS-Datei |---|---|---|
| `MegaMenu.Hidden` | `true` | Term und darunterliegender Navigationszweig werden nicht angezeigt |
| `MegaMenu.OpenInNewWindow` | `true` | Ziel wird sicher in einem neuen Browserfenster geöffnet |
| `MegaMenu.Description` | `Anträge und Vorlagen` | optionale Beschreibung einer Kategorie im Mega-Menu-Modus |
### PowerShell-Deployment Zusätzlich werden die SharePoint-Navigationseigenschaften `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText` ausgewertet. Unsichere URL-Protokolle werden verworfen.
## Build
```powershell ```powershell
# Installation npm install
.\src\deployment\add-megamenu.ps1 npm run package
# Deinstallation
.\src\deployment\remove-megamenu.ps1
``` ```
## 🎨 Anpassungen `npm run package` erstellt automatisch beide Ausgaben:
### CSS-Customization - das SPFx-Paket für moderne Seiten
- das Classic-Paket unter `classic/dist/`
Das MegaMenu kann über externe CSS-Dateien angepasst werden. Wichtige CSS-Klassen: Vor dem Packaging werden automatisch die Kernlogik-, Classic-Syntax- und statischen Asset-Tests ausgeführt. Bei einem Fehler wird kein Paket erzeugt.
Nur die Classic-Dateien neu erzeugen:
```powershell
npm run build:classic
```
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
```
## Moderne Bereitstellung
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.
Beispiel für das Mega Menu:
```powershell
.\deployment\add-megamenu.ps1 `
-SiteUrl 'http://clshp001/sites/target' `
-TermSetId '7948e6f9-7af4-431e-b6fe-328122f2746c' `
-TermSetName 'Global Navigation' `
-CacheMinutes 15 `
-CacheVersion '1' `
-MenuMode megaMenu
```
Beispiel für das Flyout:
```powershell
.\deployment\add-megamenu.ps1 `
-SiteUrl 'http://clshp001/sites/target' `
-TermSetId '7948e6f9-7af4-431e-b6fe-328122f2746c' `
-MenuMode flyout `
-EnableDebug
```
Das Skript entfernt alte Web- und Site-Registrierungen, bereinigt die frühere Property-Bag-Konfiguration und erstellt genau eine neue Site-Collection-Action.
## Classic Pages
Die Classic-Variante bleibt erhalten, weil ein SPFx Application Customizer nur in der modernen Oberfläche ausgeführt wird.
Das Classic-Paket wird bei `npm run build` und `npm run package` automatisch erzeugt. Es kann bei Bedarf auch separat gebaut werden:
```powershell
npm run build:classic
```
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).
## Mobile Bedienung und Barrierefreiheit
- Unter 768 Pixeln wird die Navigation als Hamburger-Menü mit Accordion-Unterebenen dargestellt.
- Hover bleibt auf Desktop-Geräten als Komfortfunktion erhalten.
- Links und Schaltflächen zum Öffnen eines Untermenüs sind getrennt.
- Touch, Klick und Tastatur werden gleichwertig unterstützt.
- Escape schließt das aktive Menü; Links/Rechts wechselt zwischen Hauptpunkten.
- Fokusmarkierungen, Hochkontrast und reduzierte Bewegung werden unterstützt.
## Cache und Mehrsprachigkeit
Der Session-Cache ist nach Site Collection, Termset, SharePoint-Oberflächensprache, Cache-Schema und `cacheVersion` getrennt. Nach einem temporären Taxonomy-Fehler kann der letzte abgelaufene, aber strukturell gültige Eintrag als Fallback verwendet werden. Eine geänderte `cacheVersion` erzwingt beim nächsten Laden einen neuen Cache.
Term-Labels sowie sichtbare Status- und ARIA-Texte werden anhand der aktuellen SharePoint-Sprache auf Deutsch oder Englisch ausgegeben.
## Automatisierte Tests
```powershell
npm test
```
Die Tests prüfen Term-Eigenschaften, URL-Sicherheit, externe Links, sprachabhängige Labels, Cache-Schlüssel, Hierarchieaufbau und Sortierung.
## 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 ```css
/* Haupt-Container */ :root {
#Mega-Menu { } --megaMenuBarHeight: 44px;
--megaMenuContentWidth: 1200px;
/* Top-Level Menüpunkte */ --megaMenuFlyoutWidth: 288px;
#Mega-Menu > ul > li > a { } --megaMenuZIndex: 6000;
}
/* Mega-Menü Dropdown */
.mega-menu { }
/* Kategorien im Dropdown */
.mega-menu-category { }
/* Links in Kategorien */
.mega-menu-category ul li a { }
/* Einstellungs-Panel */
.mm-settings-panel { }
``` ```
### Accessibility Features Das MegaMenu lädt bewusst keine externe `cssUrl`-Property. Kundenspezifische Gestaltung gehört in die Solution Custom Branding.
- **Skip-Links** für Screenreader ## Diagnose
- **ARIA-Rollen** und Labels
- **Fokus-Management** mit sichtbaren Fokus-Indikatoren
- **Tastaturnavigation** mit Tab/Shift+Tab/Escape
- **High Contrast Modus** Unterstützung
## 🔧 Build-Befehle Mit `"debug": true` schreibt die moderne Extension Meldungen mit diesen Quellen:
| Befehl | Beschreibung | ```text
|--------|-------------| [MegaMenuApplicationCustomizer]
| `gulp serve` | Entwicklungsserver mit Live-Reload | [TaxonomyNavigationService]
| `gulp build` | Entwicklungs-Build (Debug-Modus) | [SPTermStorePickerService]
| `gulp bundle --ship` | Produktions-Build (Optimiert) | [MegaMenuRenderer]
| `gulp package-solution --ship` | SharePoint Package (.sppkg) erstellen |
| `gulp clean` | Build-Artifacts löschen |
| `gulp test` | Unit-Tests ausführen |
## 📦 Deployment
1. **Paket erstellen:**
```bash
gulp clean && gulp bundle --ship && gulp package-solution --ship
``` ```
2. **SharePoint App Catalog:** Geladenes Bundle prüfen:
- `.sppkg` Datei aus `sharepoint/solution/` hochladen
- App genehmigen und für alle Sites verfügbar machen
3. **Site-spezifische Aktivierung:** ```javascript
- PowerShell-Scripts verwenden performance.getEntriesByType('resource')
.map(function (entry) { return entry.name; })
## 🐛 Troubleshooting .filter(function (url) { return url.toLowerCase().indexOf('mega-menu') >= 0; });
### Häufige Probleme
**Problem:** Menü wird nicht angezeigt
**Lösung:** Termset-Name in den Einstellungen prüfen
**Problem:** CSS-Styling funktioniert nicht
**Lösung:** Externe CSS-URL und CORS-Einstellungen überprüfen
**Problem:** Build-Fehler
**Lösung:** Node.js Version prüfen (v8.17.0 empfohlen)
### Debug-Modus
```bash
# Detaillierte Build-Ausgabe
gulp bundle --verbose
# TypeScript-Fehler anzeigen
gulp build
``` ```
## 🔄 Versionshistorie Aktiven Modus prüfen:
### v1.0.2 (Aktuell) ```javascript
- Einstellungs-Panel implementiert document.getElementById('Mega-Menu').getAttribute('data-menu-mode');
- Fokus-Management verbessert ```
- CSS-Optimierungen für SharePoint-Konsistenz
- Barrierefreiheit-Verbesserungen
### v1.0.1 ## Projektstruktur
- Responsive Design hinzugefügt
- Performance-Optimierungen
### v1.0.0 ```text
- Initiale Release MegaMenu/
- 3-stufige Navigation ├── classic/ Classic-Runtime und CSS
- Grundlegende Accessibility ├── 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
```
## 👥 Mitwirkende ## Versionshistorie
- Entwicklung: Matthias Glubrecht ### 2.2.5
- Framework: SharePoint Framework (Microsoft)
- UI: Office UI Fabric (Microsoft)
## 📄 Lizenz - Das Mega Menu wartet bei vertauschter Ladefolge auf `#CustomHeader` und verschiebt seine bereits gerenderte
Navigation automatisch in den Custom-Branding-Header.
tbd ### 2.2.4
## 🆘 Support - Der Ladehinweis wird auch dann zuverlässig entfernt, wenn der Custom-Branding-Host während des asynchronen Ladens erzeugt wird.
Bei Fragen oder Problemen: ### 2.2.1
1. Issues im Repository erstellen
2. Dokumentation und Troubleshooting-Bereich prüfen - Terms mit `_Sys_Nav_ExcludedProviders = GlobalNavigationTaxonomyProvider` werden einschließlich ihres Unterbaums
aus der modernen und klassischen globalen Navigation ausgeblendet.
- Das Cache-Schema wurde erhöht, damit die neue Sichtbarkeitsregel sofort greift.
### 2.2.0
- mobiles Hamburger-/Accordion-Menü für Modern und Classic
- getrennte Link- und Untermenü-Schaltflächen für Touch und Tastatur
- sichere URL-Prüfung und Unterstützung externer Links
- lokale Term-Eigenschaften für Sichtbarkeit, neues Fenster und Beschreibung
- sprachabhängige Term-Labels, Status- und ARIA-Texte
- versionierter Cache pro Site Collection und Sprache mit Stale-Fallback
- automatisierte Kernlogiktests
### 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`.
- Keine App-Installation in einzelnen Inhaltswebsites erforderlich.
### 2.0.0
- Neue Application-Customizer-ID `c0abbffb-355d-4d4e-bd38-9e15fb811506`.
- Alte On-Premises-Manifest- und Loader-Zuordnungen umgangen.
+60
View File
@@ -0,0 +1,60 @@
# MegaMenu – Arbeitsplan Version 2.2.5
Status: Implementierung abgeschlossen; Ship-Build auf der vorgesehenen Node-8-Buildumgebung ausstehend
## Funktionsumfang
- [x] Mobile Navigation als Hamburger-Menü mit aufklappbaren Unterebenen umsetzen.
- [x] Hover als Komfortfunktion beibehalten; Bedienung per Klick, Touch und Tastatur gleichwertig unterstützen.
- [x] Für Menüpunkte mit Ziel und Unterpunkten eine getrennte Öffnen-Schaltfläche anbieten.
- [x] Navigations-URLs validieren und unsichere Protokolle blockieren.
- [x] Externe Links und `MegaMenu.OpenInNewWindow` sicher unterstützen.
- [x] Lade-, Leer- und Fehlerzustände verständlich und barrierearm darstellen.
- [x] Spät erzeugten Custom-Branding-Header beobachten und die Navigation ohne Reload dorthin verschieben.
## Lokale Term-Eigenschaften
- [x] `MegaMenu.Hidden` zum gezielten Ausblenden eines Terms unterstützen.
- [x] `MegaMenu.OpenInNewWindow` für ein neues Browserfenster unterstützen.
- [x] `MegaMenu.Description` als optionale Linkbeschreibung unterstützen.
- [x] Eigenschaften in Modern und Classic identisch auswerten.
Die Eigenschaften werden im Term Store als lokale benutzerdefinierte Eigenschaften am jeweiligen Term gepflegt. Styling bleibt Aufgabe der Solution Custom Branding.
## Cache
- [x] Cache-Schlüssel um Site Collection, Sprache und Schemaversion erweitern.
- [x] Konfigurierbare `cacheVersion` zur administrativen Invalidierung ergänzen.
- [x] Abgelaufene und ungültige Cache-Einträge sicher entfernen.
- [x] Bei einem temporären Taxonomy-Fehler auf den letzten vorhandenen Cache zurückfallen.
## Mehrsprachigkeit
- [x] Sämtliche sichtbaren Status- und ARIA-Texte lokalisieren.
- [x] Bevorzugtes Term-Label anhand der aktuellen SharePoint-Sprache verwenden.
- [x] Cache nach Sprache trennen.
- [x] Deutsche und englische Ressourcen pflegen.
## Barrierefreiheit
- [x] Semantische Schaltflächen für das Öffnen von Untermenüs verwenden.
- [x] Vollständige Tastaturbedienung inklusive Pfeiltasten und Escape sicherstellen.
- [x] Fokus beim Öffnen und Schließen nachvollziehbar führen.
- [x] Mobile Accordion-Zustände über `aria-expanded` kommunizieren.
- [x] Fokusdarstellung, Hochkontrast und reduzierte Bewegung berücksichtigen.
## Qualitätssicherung
- [x] Automatisierte Tests für URL-Validierung und externe Links ergänzen.
- [x] Automatisierte Tests für lokale Term-Eigenschaften ergänzen.
- [x] Automatisierte Tests für Cache-Schlüssel und Cache-Verhalten ergänzen.
- [x] Automatisierte Tests für Hierarchie, Sortierung und aktive URLs ergänzen.
- [x] TypeScript-, Classic-Syntax-, Classic-Build- und statische Asset-Prüfungen ausführen.
- [ ] `npm run package` mit Node.js 8.17.0 auf der vorgesehenen Buildumgebung ausführen.
Der lokale SPFx-Gulp-Build wurde mit Node.js 24 gestartet und erwartungsgemäß durch die alte Gulp-3-Abhängigkeit mit `primordials is not defined` beendet. Das ist kein TypeScript- oder Projektfehler; SPFx 1.4.1 benötigt für diesen Schritt Node.js 8.17.0.
## Nicht Bestandteil dieses Durchlaufs
- [ ] PortalSettings in Version 3 als SPFx-Webpart neu aufbauen.
- [ ] MegaMenu-Diagnose und Live-Vorschau in PortalSettings integrieren.
+20 -258
View File
@@ -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 "abc3361f-bb2d-491f-aba3-cd51c19a299b" `
-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( param(
[Parameter(Mandatory=`$true)] [string]$OutputPath = ''
[string]`$SiteUrl,
[string]`$LibraryName = "Site Assets",
[string]`$FolderName = "megamenu"
) )
Write-Host "Deploying MegaMenu Classic to `$SiteUrl" -ForegroundColor Green $sourcePath = Join-Path $PSScriptRoot 'classic'
$resolvedOutputPath = if ([string]::IsNullOrWhiteSpace($OutputPath)) {
# Connect to SharePoint Join-Path $sourcePath 'dist'
Connect-PnPOnline -Url `$SiteUrl -Interactive }
else {
# Create folder if it doesn't exist $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
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
} }
# Upload files if (-not (Test-Path -LiteralPath $resolvedOutputPath)) {
`$files = @( New-Item -ItemType Directory -Path $resolvedOutputPath -Force | Out-Null
@{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
}
} }
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 = @( $files = @(
@{Name="MegaMenu.css"; Desc="Shared stylesheet from SPFx"}, 'megamenu-classic.css',
@{Name="megamenu-services-standalone.js"; Desc="Extracted services (no SPFx deps)"}, 'megamenu-services-standalone.js',
@{Name="megamenu-classic.js"; Desc="Classic SharePoint wrapper"}, 'megamenu-classic.js',
@{Name="deployment-guide.md"; Desc="Complete deployment documentation"}, 'classic-deployment.md'
@{Name="deploy-to-sharepoint.ps1"; Desc="PowerShell deployment script"}
) )
foreach ($file in $files) { foreach ($file in $files) {
$exists = Test-Path ".\classic\$($file.Name)" $source = Join-Path $sourcePath $file
$status = if ($exists) { "✓" } else { "✗" } if (-not (Test-Path -LiteralPath $source)) {
$color = if ($exists) { "Green" } else { "Red" } throw ('Classic-Datei fehlt: ' + $source)
Write-Host " $status $($file.Name)" -ForegroundColor $color -NoNewline
Write-Host " - $($file.Desc)" -ForegroundColor Gray
} }
Write-Host "`nNext steps:" -ForegroundColor Yellow Copy-Item -LiteralPath $source -Destination (Join-Path $resolvedOutputPath $file) -Force
Write-Host "1. Review deployment-guide.md for instructions" -ForegroundColor White Write-Host ('Kopiert: ' + $file) -ForegroundColor Green
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 ('Classic-Paket erstellt: ' + $resolvedOutputPath) -ForegroundColor Green
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
+37 -188
View File
@@ -1,216 +1,65 @@
# MegaMenu für klassische SharePoint-Seiten # 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`
- `megamenu-classic.css` - Styling für klassische Seiten - `megamenu-services-standalone.js`
- `classic-deployment.md` - Diese Dokumentation - `megamenu-classic.js`
## 🚀 Installation Mit `build-classic.ps1` werden die Dateien nach `classic/dist` kopiert.
### Option 1: Master Page (empfohlen) ## Konfiguration
1. **Dateien hochladen:** Die Classic-Variante liest die `ClientSideComponentProperties` der zentralen MegaMenu-Site-Action:
```
/SiteAssets/megamenu/megamenu-classic.js ```json
/SiteAssets/megamenu/megamenu-classic.css {
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
"termSetName": "Global Navigation",
"cacheMinutes": 15,
"cacheVersion": "1",
"menuMode": "megaMenu",
"debug": false
}
``` ```
2. **Master Page bearbeiten:** `menuMode` erlaubt `megaMenu` oder `flyout`. `termSetId` hat Vorrang vor `termSetName`.
```html
<!-- In den <head> Bereich -->
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
<!-- Vor dem schließenden </body> Tag --> Die lokalen benutzerdefinierten Term-Eigenschaften `MegaMenu.Hidden`, `MegaMenu.OpenInNewWindow` und `MegaMenu.Description` werden wie in der modernen Variante ausgewertet. Der Cache ist nach Site Collection, Sprache und `cacheVersion` getrennt.
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
```
### Option 2: ScriptLink über PowerShell ## Einbindung
```powershell Die drei Dateien beispielsweise nach `/SiteAssets/megamenu/` hochladen und in dieser Reihenfolge laden:
# 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()
```
### Option 3: Content Editor Web Part
```html ```html
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" /> <link rel="stylesheet" href="/SiteAssets/megamenu/megamenu-classic.css" />
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script> <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 ```html
<script type="text/javascript"> <script>
// Konfiguration vor dem Laden des Scripts setzen
window.MegaMenuConfig = { window.MegaMenuConfig = {
termSetName: 'Navigation', // Name des Taxonomy Term Sets containerId: 's4-titlerow',
cssUrl: '', // Optional: Externe CSS-Datei debug: true
containerId: 's4-titlerow', // SharePoint Container ID
debug: true // Debug-Modus aktivieren
}; };
</script> </script>
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
``` ```
### Container IDs für verschiedene SharePoint-Versionen ## Diagnose
| 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
```javascript ```javascript
// Nach der Initialisierung eigene Logik hinzufügen typeof window.MegaMenuServices
document.addEventListener('DOMContentLoaded', function() { typeof window.MegaMenuClassic
// Warten bis MegaMenu geladen ist window.MegaMenuClassic.reload()
setTimeout(function() {
if (window.MegaMenuClassic) {
console.log('MegaMenu ist bereit!');
// Eigene Anpassungen hier...
}
}, 1000);
});
``` ```
## 🔧 Erweiterte Konfiguration Benutzer benötigen Leserechte auf den verwendeten Managed-Metadata-Termstore.
### 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
+387 -44
View File
@@ -1,71 +1,414 @@
/** /* SharePoint MegaMenu 2.2.3 - Classic pages */
* MegaMenu CSS for Classic SharePoint Pages :root {
* Version: 1.0.2 --megaMenuBarHeight: 44px;
* --megaMenuContentWidth: 1200px;
* This file imports the exact same CSS as the modern SPFx version --megaMenuFlyoutWidth: 288px;
* to ensure 100% visual consistency. --megaMenuZIndex: 6000;
* }
* 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.
*/
/*
* The main MegaMenu.css should be loaded first!
* This file only contains classic SharePoint specific overrides.
*/
/* Classic SharePoint specific container adjustments */
.megamenu-classic-container { .megamenu-classic-container {
/* Insert after s4-titlerow or other SharePoint containers */ position: relative;
box-sizing: border-box;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: visible;
border-top: 1px solid #edebe9;
border-bottom: 1px solid #edebe9;
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: 0;
border-bottom: 0;
background: #fff;
color: #323130;
}
.mega-menu-status {
box-sizing: border-box;
padding: 10px 24px;
border-bottom: 1px solid #edebe9;
background: #f3f9fd;
color: #323130;
font-size: 13px;
}
.mega-menu-mobile-toggle { display: none; }
.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-top-item:hover,
#Mega-Menu { .mega-menu-top-item:focus-within,
.mega-menu-top-item:active,
.mega-menu-top-item.is-open,
.mega-menu-top-item.is-active {
background: #f3f2f1;
color: #323130;
}
.mega-menu-mode-flyout .mega-menu-top-item {
position: relative; 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-toggle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 32px;
min-height: var(--megaMenuBarHeight);
margin: 0 8px 0 -10px;
padding: 0;
border: 0;
background: transparent;
color: #323130;
cursor: pointer;
}
.menu-item-toggle > span::after {
content: "";
display: block;
width: 6px;
height: 6px;
border-right: 1px solid currentColor;
border-bottom: 1px solid currentColor;
transform: rotate(45deg);
}
.menu-item-toggle[aria-expanded="true"] > span::after { transform: rotate(225deg); }
.menu-item-link:hover,
.menu-item-text:hover,
.menu-item-toggle:hover,
.menu-item-toggle[aria-expanded="true"] {
background: transparent;
color: inherit;
text-decoration: none;
}
.menu-item-link:focus,
.menu-item-text:focus,
.menu-item-toggle:focus,
.mega-menu-mobile-toggle:focus,
.mega-menu-category a:focus,
.flyout-link:focus {
outline: 2px solid #0078d4;
outline-offset: -2px;
} }
.mega-menu { .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 */ .mega-menu.js-open {
body.ms-backgroundImage #Mega-Menu { display: block;
/* Adjust if SharePoint has background images */ }
.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-description {
margin: -4px 0 12px;
color: #605e5c;
font-size: 12px;
line-height: 18px;
}
.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; position: relative;
margin: 0;
padding: 0;
} }
/* Ensure settings panel works in classic mode too */ .flyout-link {
.mm-settings-panel { position: relative;
z-index: 4001; /* Above everything else */ 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 */ .flyout-item:hover > .flyout-link,
.v4master #Mega-Menu { .flyout-item:focus-within > .flyout-link,
/* Any v4.master specific styles if needed */ .flyout-item:active > .flyout-link {
background: #f3f2f1;
color: #323130;
} }
/* Classic SharePoint seattle.master specific adjustments */ .flyout-toggle {
.seattle #Mega-Menu { position: absolute;
/* Any seattle.master specific styles if needed */ top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border: 0;
background: transparent;
color: #323130;
cursor: pointer;
} }
/* Responsive adjustments for classic SharePoint layouts */ .flyout-toggle > span::after {
@media (max-width: 1024px) { content: "";
.megamenu-classic-container { display: block;
/* Classic SharePoint is often used on older devices */ width: 6px;
overflow-x: auto; height: 6px;
} border-top: 1px solid currentColor;
border-right: 1px solid currentColor;
transform: rotate(45deg);
}
.flyout-toggle:focus { outline: 2px solid #0078d4; outline-offset: -2px; }
.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,
.flyout-item.is-open > .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-mobile-toggle {
display: flex;
align-items: center;
width: 100%;
min-height: var(--megaMenuBarHeight);
padding: 0 16px;
border: 0;
background: #fff;
color: #323130;
cursor: pointer;
font: inherit;
font-weight: 600;
}
.mega-menu-mobile-icon,
.mega-menu-mobile-icon::before,
.mega-menu-mobile-icon::after {
display: block;
width: 18px;
height: 2px;
background: currentColor;
}
.mega-menu-mobile-icon { position: relative; margin-right: 12px; }
.mega-menu-mobile-icon::before,
.mega-menu-mobile-icon::after { content: ""; position: absolute; left: 0; }
.mega-menu-mobile-icon::before { top: -6px; }
.mega-menu-mobile-icon::after { top: 6px; }
.mega-menu-top-level {
display: none;
flex-direction: column;
padding: 0;
border-top: 1px solid #edebe9;
}
.mega-menu-main.is-mobile-open .mega-menu-top-level { display: flex; }
.mega-menu-top-item,
.mega-menu-mode-flyout .mega-menu-top-item {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 44px;
border-bottom: 1px solid #edebe9;
}
.menu-item-toggle { width: 44px; margin: 0; border-left: 0; }
.mega-menu {
position: static;
grid-column: 1 / -1;
width: 100%;
border-right: 0;
border-left: 0;
box-shadow: none;
}
.mega-menu-grid {
grid-template-columns: 1fr;
padding: 16px;
}
.flyout-level-3 {
position: static;
display: none;
width: 100%;
border: 0;
box-shadow: none;
padding-left: 16px;
}
.flyout-item.is-open > .flyout-level-3 { display: block; }
} }
/* Print styles for classic SharePoint */
@media print { @media print {
#Mega-Menu, #Mega-Menu,
.megamenu-classic-container { .megamenu-classic-container {
+70 -50
View File
@@ -1,6 +1,6 @@
/** /**
* SharePoint MegaMenu for Classic Pages * SharePoint MegaMenu for Classic Pages
* Version: 1.0.2 * Version: 2.2.0
* *
* This version uses standalone services that replicate the SPFx logic * This version uses standalone services that replicate the SPFx logic
* without requiring the SharePoint Framework. * without requiring the SharePoint Framework.
@@ -19,12 +19,46 @@
(function() { (function() {
'use strict'; 'use strict';
var MEGAMENU_UCA_ID = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; // Same as in MegaMenuApplicationCustomizer.ts var MEGAMENU_UCA_ID = 'c0abbffb-355d-4d4e-bd38-9e15fb811506'; // Same as in MegaMenuApplicationCustomizer.ts
var config = window.MegaMenuConfig || { var config = window.MegaMenuConfig || {};
containerId: 's4-titlerow', config.containerId = config.containerId || 's4-titlerow';
debug: false config.debug = config.debug === true;
function getLanguageLcid() {
return Number(window._spPageContextInfo && _spPageContextInfo.currentLanguage) || 1033;
}
function getStrings() {
return getLanguageLcid() === 1031 ? {
navigationLabel: 'Hauptnavigation', openNavigation: 'Hauptnavigation öffnen', closeNavigation: 'Hauptnavigation schließen',
openSubmenu: 'Untermenü öffnen für', closeSubmenu: 'Untermenü schließen für', submenuLabel: 'Untermenü',
externalLink: 'externer Link', loading: 'Navigation wird geladen…', missing: 'Die Navigation ist nicht konfiguriert.',
empty: 'Die Navigation enthält keine sichtbaren Einträge.', error: 'Die Navigation konnte nicht geladen werden.'
} : {
navigationLabel: 'Main navigation', openNavigation: 'Open main navigation', closeNavigation: 'Close main navigation',
openSubmenu: 'Open submenu for', closeSubmenu: 'Close submenu for', submenuLabel: 'submenu',
externalLink: 'external link', loading: 'Loading navigation…', missing: 'Navigation is not configured.',
empty: 'The navigation does not contain any visible entries.', error: 'Navigation could not be loaded.'
}; };
}
function showStatus(message) {
var existing = document.querySelector('.megamenu-classic-container');
var container = existing || document.createElement('div');
container.className = 'megamenu-classic-container';
container.innerHTML = '';
var status = document.createElement('div');
status.className = 'mega-menu-status';
status.setAttribute('role', 'status');
status.textContent = message;
container.appendChild(status);
if (!existing) {
var anchor = document.getElementById(config.containerId) || document.body.firstChild;
anchor.parentNode.insertBefore(container, anchor.nextSibling);
}
return container;
}
function log(message, data) { function log(message, data) {
if (config.debug && console && console.log) { if (config.debug && console && console.log) {
@@ -84,44 +118,30 @@
return response.json(); return response.json();
}) })
.then(function(data) { .then(function(data) {
if (data && data.value && data.value.length > 0) { var actions = data && data.value
var uca = data.value[0]; ? data.value
: (data && data.d && data.d.results ? data.d.results : []);
if (actions.length > 0) {
var uca = actions[0];
try { try {
var props = JSON.parse(uca.ClientSideComponentProperties); var props = JSON.parse(uca.ClientSideComponentProperties);
log('Configuration found', props); log('Configuration found', props);
callback(props); callback(props);
} catch (e) { } catch (e) {
log('Error parsing UserCustomAction properties: ' + e.message); log('Error parsing UserCustomAction properties: ' + e.message);
callback({ termSetName: 'Navigation', cssUrl: '' }); callback({});
} }
} else { } else {
log('No UserCustomAction found - using defaults'); log('No UserCustomAction found - using defaults');
callback({ termSetName: 'Navigation', cssUrl: '' }); callback({});
} }
}) })
.catch(function(error) { .catch(function(error) {
log('Error reading configuration: ' + error.message); log('Error reading configuration: ' + error.message);
callback({ termSetName: 'Navigation', cssUrl: '' }); callback({});
}); });
} }
// 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 // Main initialization
function initMegaMenu() { function initMegaMenu() {
log('Initializing MegaMenu for classic SharePoint...'); log('Initializing MegaMenu for classic SharePoint...');
@@ -133,12 +153,15 @@
} }
readMegaMenuConfiguration(function(props) { readMegaMenuConfiguration(function(props) {
config.debug = props.debug === true || config.debug === true;
log('Using configuration:', props); log('Using configuration:', props);
var strings = getStrings();
// Load external CSS if specified var termSetIdentifier = props.termSetId || props.termSetName;
if (props.cssUrl) { if (!termSetIdentifier) {
loadExternalCSS(props.cssUrl); showStatus(strings.missing);
return;
} }
var loadingContainer = showStatus(strings.loading);
// Create context // Create context
var context = createClassicContext(); var context = createClassicContext();
@@ -146,13 +169,25 @@
// Create taxonomy service using standalone implementation // Create taxonomy service using standalone implementation
var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService( var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService(
context, context,
props.termSetName termSetIdentifier,
{
cacheMinutes: props.cacheMinutes,
cacheVersion: props.cacheVersion,
languageLcid: getLanguageLcid()
}
); );
// Load menu items // Load menu items
taxonomyService.getMenuItems() taxonomyService.getMenuItems()
.then(function(menuItems) { .then(function(menuItems) {
log('Menu items loaded:', menuItems); log('Menu items loaded:', menuItems);
if (!menuItems || menuItems.length === 0) {
showStatus(strings.empty);
return;
}
if (loadingContainer && loadingContainer.parentNode) {
loadingContainer.parentNode.removeChild(loadingContainer);
}
// Find target container // Find target container
var container = document.getElementById(config.containerId); var container = document.getElementById(config.containerId);
@@ -179,9 +214,8 @@
var renderer = new window.MegaMenuServices.MegaMenuRenderer( var renderer = new window.MegaMenuServices.MegaMenuRenderer(
context, context,
menuItems, menuItems,
function(updatedProps) { props.menuMode === 'flyout' ? 'flyout' : 'megaMenu',
log('Properties updated (not persisted in classic mode):', updatedProps); strings
}
); );
// Render the menu // Render the menu
@@ -191,21 +225,7 @@
}) })
.catch(function(error) { .catch(function(error) {
console.error('[MegaMenu] Error loading menu items:', error); console.error('[MegaMenu] Error loading menu items:', error);
showStatus(strings.error);
// Show error message to user
var container = document.getElementById(config.containerId);
if (container) {
var errorDiv = document.createElement('div');
errorDiv.style.cssText = 'background:#ffebee;color:#c62828;padding:10px;border:1px solid #ef5350;margin:5px 0;';
errorDiv.innerHTML = '<strong>MegaMenu Error:</strong> ' + error.message +
'<br><small>Check browser console for details.</small>';
if (container.nextSibling) {
container.parentNode.insertBefore(errorDiv, container.nextSibling);
} else {
container.parentNode.appendChild(errorDiv);
}
}
}); });
}); });
} }
+324 -169
View File
@@ -30,6 +30,10 @@
constructor(term, depth, siteCollectionUrl) { constructor(term, depth, siteCollectionUrl) {
this.title = term.Name; this.title = term.Name;
this.url = this._getNavigationUrl(term, siteCollectionUrl); this.url = this._getNavigationUrl(term, siteCollectionUrl);
const properties = term.LocalCustomProperties || {};
this.description = String(properties['MegaMenu.Description'] || '').trim();
this.openInNewWindow = this._asBoolean(properties['MegaMenu.OpenInNewWindow']);
this.external = this._isExternal(this.url, siteCollectionUrl);
this.items = []; this.items = [];
this.pathDepth = this._calculateDepth(term.PathOfTerm); this.pathDepth = this._calculateDepth(term.PathOfTerm);
this.parentId = this._getParentId(term.PathOfTerm); this.parentId = this._getParentId(term.PathOfTerm);
@@ -51,14 +55,32 @@
} }
_getNavigationUrl(term, siteCollectionUrl) { _getNavigationUrl(term, siteCollectionUrl) {
// Extract URL from term properties const properties = term.LocalCustomProperties || {};
if (term.LocalCustomProperties && term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl) { const rawUrl = properties._Sys_Nav_SimpleLinkUrl || properties._Sys_Nav_TargetUrl;
return term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl; if (!rawUrl) {
return undefined;
}
if (/[\u0000-\u001f\u007f]/.test(String(rawUrl))) {
return undefined;
}
const resolved = rawUrl.indexOf('~sitecollection') === 0
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
: rawUrl;
const protocol = String(resolved).match(/^([a-z][a-z0-9+.-]*):/i);
if (protocol && ['http', 'https', 'mailto'].indexOf(protocol[1].toLowerCase()) < 0) {
return undefined;
}
return resolved;
} }
// Fallback: generate URL based on term name _asBoolean(value) {
const termName = term.Name.toLowerCase().replace(/[^a-z0-9]/g, '-'); return value === true || ['true', '1', 'yes', 'ja'].indexOf(String(value || '').toLowerCase()) >= 0;
return `${siteCollectionUrl}/pages/${termName}.aspx`; }
_isExternal(url, siteCollectionUrl) {
const target = String(url || '').match(/^(?:(https?):)?\/\/([^/]+)/i);
const current = String(siteCollectionUrl || '').match(/^(https?):\/\/([^/]+)/i);
return !!target && (!current || (target[1] || current[1]).toLowerCase() !== current[1].toLowerCase() || target[2].toLowerCase() !== current[2].toLowerCase());
} }
} }
@@ -67,49 +89,90 @@
// =========================================== // ===========================================
class TaxonomyNavigationService { class TaxonomyNavigationService {
constructor(context, termSetName) { constructor(context, termSetIdentifier, options) {
this.context = context; this.context = context;
this.termSetName = termSetName; this.termSetIdentifier = termSetIdentifier;
this.options = options || {};
this._siteCollectionUrl = context.pageContext.site.absoluteUrl; this._siteCollectionUrl = context.pageContext.site.absoluteUrl;
this._languageLcid = Number(this.options.languageLcid) || 1033;
this._cacheMinutes = Math.min(Math.max(Number(this.options.cacheMinutes) || 15, 1), 1440);
this._cacheVersion = String(this.options.cacheVersion || '1');
} }
async getMenuItems() { async getMenuItems() {
console.log('[TaxonomyService] Loading terms for:', this.termSetName); console.log('[TaxonomyService] Loading terms for:', this.termSetIdentifier);
const cached = this._getCache(false);
if (cached) {
return this._processTerms(cached);
}
const stale = this._getCache(true);
try { try {
const termSet = await this._loadTermSet(this.termSetName); const termSet = await this._loadTermSet(this.termSetIdentifier);
this._setCache(termSet);
return this._processTerms(termSet); return this._processTerms(termSet);
} catch (error) { } catch (error) {
console.error('[TaxonomyService] Error loading terms:', error); console.error('[TaxonomyService] Error loading terms:', error);
return [this._createNoTermsItem()]; if (stale) {
console.warn('[TaxonomyService] Using stale cache after taxonomy error.');
return this._processTerms(stale);
}
throw error;
} }
} }
_loadTermSet(termSetName) { _cacheKey() {
const normalize = value => encodeURIComponent(String(value || '').trim().toLowerCase());
return 'MegaMenu:TermSet:v4:' + normalize(this._siteCollectionUrl) + ':' + normalize(this.termSetIdentifier) + ':' + this._languageLcid + ':' + normalize(this._cacheVersion);
}
_getCache(allowExpired) {
try {
const serialized = window.sessionStorage.getItem(this._cacheKey());
if (!serialized) return null;
const entry = JSON.parse(serialized);
if (!entry || !entry.value || !entry.expiresAt) {
window.sessionStorage.removeItem(this._cacheKey());
return null;
}
return entry.expiresAt > Date.now() || allowExpired ? entry.value : null;
} catch (error) {
return null;
}
}
_setCache(value) {
try {
window.sessionStorage.setItem(this._cacheKey(), JSON.stringify({
expiresAt: Date.now() + this._cacheMinutes * 60 * 1000,
value: value
}));
} catch (error) {
console.warn('[TaxonomyService] Cache could not be written.', error);
}
}
_loadTermSet(termSetIdentifier) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const context = SP.ClientContext.get_current(); const context = SP.ClientContext.get_current();
const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context); const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
const termStore = session.getDefaultSiteCollectionTermStore(); 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, this._languageLcid);
context.load(termSets); context.load(termSets);
context.executeQueryAsync( context.executeQueryAsync(
() => { () => {
if (termSets.get_count() > 0) { if (termSets.get_count() > 0) {
const termSet = termSets.get_item(0); const termSet = termSets.get_item(0);
const terms = termSet.get_terms(); this._loadAllTerms(context, termSet, resolve, reject);
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()))
);
} else { } 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())) (sender, args) => reject(new Error(args.get_message()))
@@ -117,32 +180,29 @@
}); });
} }
_loadAllTermsRecursively(context, terms, resolve, reject) { _loadAllTerms(context, termSet, resolve, reject) {
const allTerms = []; const terms = termSet.getAllTerms();
const termsEnum = terms.getEnumerator(); context.load(terms, 'Include(Id,Name,PathOfTerm,LocalCustomProperties,IsAvailableForTagging,IsDeprecated)');
// 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
context.executeQueryAsync( context.executeQueryAsync(
() => { () => {
const processedTerms = allTerms.map(term => ({ const processedTerms = [];
const enumerator = terms.getEnumerator();
while (enumerator.moveNext()) {
const term = enumerator.get_current();
const properties = this._getCustomProperties(term);
if (!term.get_isAvailableForTagging() || term.get_isDeprecated() ||
this._asBoolean(properties['MegaMenu.Hidden']) ||
this._isExcludedFromGlobalNavigation(properties)) {
continue;
}
processedTerms.push({
Id: term.get_id().toString(), Id: term.get_id().toString(),
Name: term.get_name(), Name: term.get_name(),
PathOfTerm: term.get_pathOfTerm(), PathOfTerm: term.get_pathOfTerm(),
LocalCustomProperties: this._getCustomProperties(term), LocalCustomProperties: properties,
IsRoot: term.get_pathOfTerm().split(';').length === 1 IsRoot: term.get_pathOfTerm().split(';').length === 1
})); });
}
resolve(processedTerms); resolve(processedTerms);
}, },
@@ -150,40 +210,48 @@
); );
} }
_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) { _getCustomProperties(term) {
try { try {
const props = term.get_localCustomProperties(); const props = term.get_localCustomProperties();
return { return {
_Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null, _Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null,
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null _Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null,
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null,
_Sys_Nav_ExcludedProviders: props._Sys_Nav_ExcludedProviders || null,
'MegaMenu.Hidden': props['MegaMenu.Hidden'] || null,
'MegaMenu.OpenInNewWindow': props['MegaMenu.OpenInNewWindow'] || null,
'MegaMenu.Description': props['MegaMenu.Description'] || null
}; };
} catch (e) { } catch (e) {
return {}; return {};
} }
} }
_asBoolean(value) {
return value === true || ['true', '1', 'yes', 'ja'].indexOf(String(value || '').toLowerCase()) >= 0;
}
_isExcludedFromGlobalNavigation(properties) {
const value = String((properties || {})._Sys_Nav_ExcludedProviders || '').replace(/["']/g, '');
return value.split(/[;,|]/).some(provider =>
provider.trim().toLowerCase() === 'globalnavigationtaxonomyprovider'
);
}
_processTerms(termsData) { _processTerms(termsData) {
const itemsDict = new ItemDictionary(); const itemsDict = new ItemDictionary();
const itemsByPath = {};
const menuItems = []; const menuItems = [];
const visibleTerms = termsData.filter(termData =>
!this._asBoolean((termData.LocalCustomProperties || {})['MegaMenu.Hidden']) &&
!this._isExcludedFromGlobalNavigation(termData.LocalCustomProperties)
);
// Create MenuItem objects // Create MenuItem objects
termsData.forEach(termData => { visibleTerms.forEach(termData => {
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl); const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
itemsDict.Add(termData.Id, menuItem); itemsDict.Add(termData.Id, menuItem);
itemsByPath[termData.PathOfTerm] = menuItem;
if (menuItem.pathDepth === 1) { if (menuItem.pathDepth === 1) {
menuItems.push(menuItem); menuItems.push(menuItem);
@@ -191,11 +259,12 @@
}); });
// Build hierarchy // Build hierarchy
termsData.forEach(termData => { visibleTerms.forEach(termData => {
if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) { if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) {
const menuItem = itemsDict.Get(termData.Id); const menuItem = itemsDict.Get(termData.Id);
const parentId = menuItem.parentId; const pathParts = termData.PathOfTerm.split(';');
const parentItem = itemsDict.Get(parentId); pathParts.pop();
const parentItem = itemsByPath[pathParts.join(';')];
if (parentItem) { if (parentItem) {
parentItem.items.push(menuItem); parentItem.items.push(menuItem);
@@ -221,10 +290,11 @@
// =========================================== // ===========================================
class MegaMenuRenderer { class MegaMenuRenderer {
constructor(context, menuItems, updateCallback) { constructor(context, menuItems, menuMode, strings) {
this.context = context; this.context = context;
this.menuItems = menuItems; this.menuItems = menuItems;
this.updateCallback = updateCallback; this.menuMode = menuMode === 'flyout' ? 'flyout' : 'megaMenu';
this.strings = strings || { navigationLabel: 'Main navigation', openNavigation: 'Open navigation', closeNavigation: 'Close navigation', openSubmenu: 'Open submenu for', closeSubmenu: 'Close submenu for', submenuLabel: 'submenu', externalLink: 'external link' };
} }
render(container) { render(container) {
@@ -232,23 +302,29 @@
const nav = document.createElement('nav'); const nav = document.createElement('nav');
nav.id = 'Mega-Menu'; nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main'; nav.className = 'mega-menu-main mega-menu-mode-' + this.menuMode;
nav.setAttribute('role', 'navigation'); nav.setAttribute('data-menu-mode', this.menuMode);
nav.setAttribute('aria-label', 'Hauptnavigation'); nav.setAttribute('aria-label', this.strings.navigationLabel);
const mobileToggle = document.createElement('button');
mobileToggle.type = 'button';
mobileToggle.className = 'mega-menu-mobile-toggle';
mobileToggle.setAttribute('aria-expanded', 'false');
mobileToggle.setAttribute('aria-controls', 'Mega-Menu-Items');
mobileToggle.setAttribute('aria-label', this.strings.openNavigation);
mobileToggle.innerHTML = '<span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label"></span>';
mobileToggle.querySelector('.mega-menu-mobile-label').textContent = this.strings.navigationLabel;
nav.appendChild(mobileToggle);
const topLevelUl = document.createElement('ul'); const topLevelUl = document.createElement('ul');
topLevelUl.setAttribute('role', 'menubar'); topLevelUl.className = 'mega-menu-top-level';
topLevelUl.id = 'Mega-Menu-Items';
this.menuItems.forEach(topLevelItem => { this.menuItems.forEach(topLevelItem => {
const topLevelLi = this.createTopLevelItem(topLevelItem); const topLevelLi = this.createTopLevelItem(topLevelItem);
topLevelUl.appendChild(topLevelLi); topLevelUl.appendChild(topLevelLi);
}); });
// Add settings if user has permissions (simplified check)
if (this._hasManagePermissions()) {
topLevelUl.appendChild(this.createSettingsItem());
}
nav.appendChild(topLevelUl); nav.appendChild(topLevelUl);
container.appendChild(nav); container.appendChild(nav);
@@ -258,32 +334,42 @@
createTopLevelItem(item) { createTopLevelItem(item) {
const li = document.createElement('li'); const li = document.createElement('li');
li.setAttribute('role', 'none'); const hasChildren = item.items && item.items.length > 0;
li.className = hasChildren ? 'mega-menu-top-item has-children' : 'mega-menu-top-item';
if (item.url && item.items.length === 0) { if (item.url) {
// Simple link
const a = document.createElement('a'); const a = document.createElement('a');
a.href = item.url; a.href = item.url;
a.textContent = item.title; a.textContent = item.title;
a.className = 'menu-item-link'; a.className = 'menu-item-link';
a.setAttribute('role', 'menuitem'); this.applyLinkBehavior(a, item);
a.setAttribute('tabindex', '0');
li.appendChild(a); li.appendChild(a);
} else { } else {
// Menu with submenu
const span = document.createElement('span'); const span = document.createElement('span');
span.textContent = item.title; span.textContent = item.title;
span.className = 'menu-item-text'; span.className = 'menu-item-text';
span.setAttribute('role', 'menuitem'); if (!hasChildren) span.setAttribute('tabindex', '0');
span.setAttribute('tabindex', '0');
span.setAttribute('aria-haspopup', 'true');
span.setAttribute('aria-expanded', 'false');
li.appendChild(span); li.appendChild(span);
if (item.items.length > 0) {
const megaMenu = this.createMegaMenu(item.items);
li.appendChild(megaMenu);
} }
if (hasChildren) {
const toggle = document.createElement('button');
const menuId = 'Mega-Menu-Panel-' + String(item.id).replace(/[^a-z0-9_-]/gi, '');
toggle.type = 'button';
toggle.className = 'menu-item-toggle';
toggle.setAttribute('aria-haspopup', 'true');
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-controls', menuId);
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.title);
toggle.innerHTML = '<span aria-hidden="true"></span>';
li.appendChild(toggle);
const megaMenu = this.menuMode === 'flyout'
? this.createFlyoutMenu(item.items)
: this.createMegaMenu(item.items);
megaMenu.id = menuId;
megaMenu.setAttribute('aria-expanded', 'false');
megaMenu.setAttribute('aria-label', item.title + ' ' + this.strings.submenuLabel);
li.appendChild(megaMenu);
} }
return li; return li;
@@ -292,7 +378,6 @@
createMegaMenu(categories) { createMegaMenu(categories) {
const megaMenuDiv = document.createElement('div'); const megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu'; megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('role', 'menu');
const gridDiv = document.createElement('div'); const gridDiv = document.createElement('div');
gridDiv.className = 'mega-menu-grid'; gridDiv.className = 'mega-menu-grid';
@@ -316,7 +401,7 @@
const a = document.createElement('a'); const a = document.createElement('a');
a.href = category.url; a.href = category.url;
a.textContent = category.title; a.textContent = category.title;
a.setAttribute('tabindex', '0'); this.applyLinkBehavior(a, category);
h3.appendChild(a); h3.appendChild(a);
} else { } else {
const span = document.createElement('span'); const span = document.createElement('span');
@@ -326,19 +411,23 @@
categoryDiv.appendChild(h3); categoryDiv.appendChild(h3);
if (category.description) {
const description = document.createElement('p');
description.className = 'mega-menu-description';
description.textContent = category.description;
categoryDiv.appendChild(description);
}
if (category.items.length > 0) { if (category.items.length > 0) {
const ul = document.createElement('ul'); const ul = document.createElement('ul');
ul.setAttribute('role', 'group');
category.items.forEach(link => { category.items.forEach(link => {
const li = document.createElement('li'); const li = document.createElement('li');
li.setAttribute('role', 'none');
const a = document.createElement('a'); const a = document.createElement('a');
a.href = link.url; a.href = link.url || '#';
a.textContent = link.title; a.textContent = link.title;
a.setAttribute('role', 'menuitem'); this.applyLinkBehavior(a, link);
a.setAttribute('tabindex', '0');
li.appendChild(a); li.appendChild(a);
ul.appendChild(li); ul.appendChild(li);
@@ -350,37 +439,129 @@
return categoryDiv; return categoryDiv;
} }
createSettingsItem() { createFlyoutMenu(items) {
const flyout = document.createElement('div');
flyout.className = 'mega-menu mega-menu-flyout';
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'); 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'); const element = document.createElement(item.url ? 'a' : 'span');
button.className = 'menu-item-settings'; if (item.url) {
button.setAttribute('type', 'button'); element.href = item.url;
button.setAttribute('role', 'menuitem'); this.applyLinkBehavior(element, item);
button.setAttribute('tabindex', '0'); } else {
button.setAttribute('aria-label', 'Einstellungen'); element.setAttribute('tabindex', '0');
button.onclick = () => this._openSettings(); }
element.textContent = item.title;
element.className = hasChildren ? 'flyout-link flyout-link-has-children' : 'flyout-link';
li.appendChild(element);
const icon = document.createElement('i'); if (hasChildren) {
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon'; const toggle = document.createElement('button');
icon.setAttribute('aria-hidden', 'true'); const childId = 'Mega-Menu-Flyout-' + String(item.id).replace(/[^a-z0-9_-]/gi, '');
button.appendChild(icon); toggle.type = 'button';
toggle.className = 'flyout-toggle';
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-controls', childId);
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.title);
toggle.innerHTML = '<span aria-hidden="true"></span>';
li.appendChild(toggle);
const childList = document.createElement('ul');
childList.className = 'flyout-list flyout-level-3';
childList.id = childId;
childList.setAttribute('aria-label', item.title + ' ' + this.strings.submenuLabel);
item.items.forEach(child => childList.appendChild(this.createFlyoutItem(child)));
li.appendChild(childList);
}
li.appendChild(button);
return li; return li;
} }
attachEventListeners() { attachEventListeners() {
// Keyboard and mouse event handling (simplified) const renderer = this;
const menuItems = document.querySelectorAll('#Mega-Menu [role="menuitem"]'); const toggles = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle');
menuItems.forEach(item => { Array.prototype.forEach.call(toggles, toggle => {
item.addEventListener('keydown', this.handleKeyDown.bind(this)); const parent = toggle.parentElement;
const panel = parent.querySelector('.mega-menu');
const setOpen = open => {
renderer.closeAll(panel);
toggle.setAttribute('aria-expanded', String(open));
toggle.setAttribute('aria-label', (open ? renderer.strings.closeSubmenu : renderer.strings.openSubmenu) + ' ' + renderer.getToggleLabel(toggle));
parent.classList.toggle('is-open', open);
panel.setAttribute('aria-expanded', String(open));
panel.classList.toggle('js-open', open);
};
toggle.addEventListener('click', event => { event.preventDefault(); setOpen(toggle.getAttribute('aria-expanded') !== 'true'); });
toggle.addEventListener('keydown', event => {
if (event.key === 'ArrowDown') {
event.preventDefault(); setOpen(true);
const firstLink = panel.querySelector('a');
if (firstLink) firstLink.focus();
} else if (event.key === 'Escape' || event.key === 'ArrowUp') {
event.preventDefault(); setOpen(false); toggle.focus();
}
});
let closeTimer;
parent.addEventListener('mouseenter', () => { window.clearTimeout(closeTimer); setOpen(true); });
parent.addEventListener('mouseleave', () => { closeTimer = window.setTimeout(() => setOpen(false), 0); });
});
const parent = item.parentElement; const flyoutToggles = document.querySelectorAll('#Mega-Menu .flyout-toggle');
if (parent && parent.querySelector('.mega-menu')) { Array.prototype.forEach.call(flyoutToggles, toggle => {
parent.addEventListener('mouseenter', this.showMegaMenu); const parent = toggle.parentElement;
parent.addEventListener('mouseleave', this.hideMegaMenu); const setOpen = open => {
toggle.setAttribute('aria-expanded', String(open));
const label = parent.querySelector('.flyout-link');
toggle.setAttribute('aria-label', (open ? renderer.strings.closeSubmenu : renderer.strings.openSubmenu) + ' ' + (label ? label.textContent : ''));
parent.classList.toggle('is-open', open);
};
toggle.addEventListener('click', event => { event.preventDefault(); setOpen(toggle.getAttribute('aria-expanded') !== 'true'); });
toggle.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); setOpen(false); toggle.focus(); } });
parent.addEventListener('mouseenter', () => setOpen(true));
parent.addEventListener('mouseleave', () => setOpen(false));
parent.addEventListener('focusin', () => setOpen(true));
parent.addEventListener('focusout', () => window.setTimeout(() => { if (!parent.contains(document.activeElement)) setOpen(false); }, 0));
});
const mobileToggle = document.querySelector('#Mega-Menu > .mega-menu-mobile-toggle');
if (mobileToggle) {
mobileToggle.addEventListener('click', () => {
const nav = mobileToggle.parentElement;
const open = mobileToggle.getAttribute('aria-expanded') !== 'true';
mobileToggle.setAttribute('aria-expanded', String(open));
mobileToggle.setAttribute('aria-label', open ? renderer.strings.closeNavigation : renderer.strings.openNavigation);
nav.classList.toggle('is-mobile-open', open);
if (!open) renderer.closeAll();
});
}
document.addEventListener('keydown', event => {
if (event.key === 'Escape') {
const openPanel = document.querySelector('#Mega-Menu .mega-menu.js-open');
if (openPanel) {
const toggle = openPanel.previousElementSibling;
renderer.closePanel(toggle, openPanel);
toggle.focus();
}
}
if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && document.activeElement.closest('.mega-menu-top-item')) {
const items = Array.prototype.slice.call(document.querySelectorAll('#Mega-Menu > ul > .mega-menu-top-item'));
const current = document.activeElement.closest('.mega-menu-top-item');
const index = items.indexOf(current);
const target = items[(index + (event.key === 'ArrowRight' ? 1 : -1) + items.length) % items.length];
const focusTarget = target.querySelector('a, .menu-item-toggle');
if (focusTarget) { event.preventDefault(); focusTarget.focus(); }
} }
}); });
} }
@@ -395,61 +576,35 @@
document.body.appendChild(announcer); document.body.appendChild(announcer);
} }
showMegaMenu() { closePanel(toggle, panel) {
const megaMenu = this.querySelector('.mega-menu'); toggle.setAttribute('aria-expanded', 'false');
if (megaMenu) { toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + this.getToggleLabel(toggle));
megaMenu.classList.add('js-open'); panel.setAttribute('aria-expanded', 'false');
const menuItem = this.querySelector('[role="menuitem"]'); panel.classList.remove('js-open');
if (menuItem) {
menuItem.setAttribute('aria-expanded', 'true');
} }
closeAll(exceptPanel) {
const panels = document.querySelectorAll('#Mega-Menu .mega-menu.js-open');
Array.prototype.forEach.call(panels, panel => {
if (panel !== exceptPanel) this.closePanel(panel.previousElementSibling, panel);
});
}
getToggleLabel(toggle) {
const label = toggle.parentElement.querySelector('.menu-item-link, .menu-item-text');
return label ? label.textContent : '';
}
applyLinkBehavior(link, item) {
if (item.openInNewWindow) {
link.target = '_blank';
link.rel = 'noopener noreferrer';
}
if (item.external) {
link.setAttribute('aria-label', item.title + ' (' + this.strings.externalLink + ')');
} }
} }
hideMegaMenu() {
const megaMenu = this.querySelector('.mega-menu');
if (megaMenu) {
megaMenu.classList.remove('js-open');
const menuItem = this.querySelector('[role="menuitem"]');
if (menuItem) {
menuItem.setAttribute('aria-expanded', 'false');
}
}
}
handleKeyDown(e) {
// Keyboard navigation logic (simplified)
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const parent = e.target.parentElement;
const megaMenu = parent.querySelector('.mega-menu');
if (megaMenu) {
megaMenu.classList.toggle('js-open');
e.target.setAttribute('aria-expanded',
megaMenu.classList.contains('js-open') ? 'true' : 'false');
}
} else if (e.key === 'Escape') {
const megaMenu = document.querySelector('.mega-menu.js-open');
if (megaMenu) {
megaMenu.classList.remove('js-open');
const menuItem = megaMenu.parentElement.querySelector('[role="menuitem"]');
if (menuItem) {
menuItem.setAttribute('aria-expanded', 'false');
menuItem.focus();
}
}
}
}
_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.');
}
} }
// =========================================== // ===========================================
-7
View File
@@ -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 -->"
}
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "mega-menu-client-side-solution", "name": "mega-menu-client-side-solution",
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133", "id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
"version": "1.0.0", "version": "2.2.5.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+11 -4
View File
@@ -6,11 +6,14 @@
"default": { "default": {
"pageUrl": "https://sharepoint.contoso.local/SitePages/Neues-Zuhause.aspx", "pageUrl": "https://sharepoint.contoso.local/SitePages/Neues-Zuhause.aspx",
"customActions": { "customActions": {
"abc3361f-bb2d-491f-aba3-cd51c19a299b": { "c0abbffb-355d-4d4e-bd38-9e15fb811506": {
"location": "ClientSideExtension.ApplicationCustomizer", "location": "ClientSideExtension.ApplicationCustomizer",
"properties": { "properties": {
"termSetName": "Horizontale Navigation", "termSetName": "Horizontale Navigation",
"additionalCssUrl": "/siteAssets/Navigation/Navigation.css" "cacheMinutes": 15,
"cacheVersion": "1",
"menuMode": "megaMenu",
"debug": true
} }
} }
} }
@@ -18,10 +21,14 @@
"megaMenu": { "megaMenu": {
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx", "pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
"customActions": { "customActions": {
"abc3361f-bb2d-491f-aba3-cd51c19a299b": { "c0abbffb-355d-4d4e-bd38-9e15fb811506": {
"location": "ClientSideExtension.ApplicationCustomizer", "location": "ClientSideExtension.ApplicationCustomizer",
"properties": { "properties": {
"testMessage": "Test message" "termSetName": "Global Navigation",
"cacheMinutes": 15,
"cacheVersion": "1",
"menuMode": "flyout",
"debug": true
} }
} }
} }
+81 -67
View File
@@ -1,104 +1,118 @@
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[Alias('WebUrl')]
[string]$SiteUrl, [string]$SiteUrl,
[string]$TermSetName = '', [string]$TermSetName = '',
[string]$CssUrl = '', [string]$TermSetId = '',
[ValidateRange(1, 1440)]
[int]$CacheMinutes = 15,
[string]$CacheVersion = '1',
[ValidateSet('megaMenu', 'flyout')]
[string]$MenuMode = 'megaMenu',
[switch]$EnableDebug, [switch]$EnableDebug,
[string]$Description = 'MSFT-Custom-Solution:MegaMenu' [string]$Description = 'MSFT-Custom-Solution:MegaMenu'
) )
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue | Out-Null if (-not (Get-Command -Name Get-SPSite -ErrorAction SilentlyContinue)) {
if (Get-Module -ListAvailable -Name Microsoft.SharePoint.PowerShell) {
$componentId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b' Import-Module Microsoft.SharePoint.PowerShell -DisableNameChecking
$location = 'ClientSideExtension.ApplicationCustomizer' }
$name = 'MegaMenu' elseif (Get-PSSnapin -Registered -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) {
$title = 'Mega Menu' Add-PSSnapin Microsoft.SharePoint.PowerShell
}
function Get-MegaMenuPropertiesJson { else {
param( throw 'Die SharePoint PowerShell-Komponenten wurden nicht gefunden. Bitte in der SharePoint Management Shell ausfuehren.'
[string]$CurrentTermSetName, }
[string]$CurrentCssUrl,
[bool]$CurrentDebug
)
return (@{
termSetName = $CurrentTermSetName
cssUrl = $CurrentCssUrl
debug = $CurrentDebug
} | ConvertTo-Json -Depth 10 -Compress)
} }
function Remove-WebScopedComponentActions { $componentId = [Guid]'c0abbffb-355d-4d4e-bd38-9e15fb811506'
param( $legacyComponentId = [Guid]'abc3361f-bb2d-491f-aba3-cd51c19a299b'
[Microsoft.SharePoint.SPSite]$CurrentSite, $legacyPropertyName = 'PortalSettings.MegaMenu.Configuration'
[string]$CurrentComponentId,
[string]$CurrentLocation
)
$removedCount = 0 if ([string]::IsNullOrWhiteSpace($TermSetId) -and [string]::IsNullOrWhiteSpace($TermSetName)) {
throw 'Bitte TermSetId oder TermSetName angeben.'
}
$properties = [string](@{
termSetName = $TermSetName.Trim()
termSetId = $TermSetId.Trim()
cacheMinutes = $CacheMinutes
cacheVersion = $CacheVersion
menuMode = $MenuMode
debug = [bool]$EnableDebug.IsPresent
} | ConvertTo-Json -Depth 10 -Compress)
$site = Get-SPSite -Identity $SiteUrl
foreach ($web in $CurrentSite.AllWebs) {
try { try {
$webActions = @($web.UserCustomActions | Where-Object { @($site.UserCustomActions | Where-Object {
$_.Location -eq $CurrentLocation -and $_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -and $_.ClientSideComponentId -eq $legacyComponentId
$_.ClientSideComponentId.ToString().ToLower() -eq $CurrentComponentId }) | ForEach-Object {
Write-Host ('Entferne alte Site-Action: ' + $_.Id)
$_.Delete()
}
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
$actions = @($web.UserCustomActions | Where-Object {
$_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -eq $legacyComponentId
}) })
foreach ($webAction in $webActions) { foreach ($action in $actions) {
$web.UserCustomActions.Delete($webAction.Id) Write-Host ('Entferne alte Web-Action: ' + $webUrl + ' / ' + $action.Id)
$removedCount++ $action.Delete()
}
if ($webActions.Count -gt 0) {
$web.Update()
} }
} }
finally { finally {
$web.Dispose() $web.Dispose()
} }
} }
}
return $removedCount finally {
$site.Dispose()
} }
$site = Get-SPSite -Identity $SiteUrl $site = Get-SPSite -Identity $SiteUrl
try { try {
$removedWebScopedActions = Remove-WebScopedComponentActions -CurrentSite $site -CurrentComponentId $componentId -CurrentLocation $location
$existingAction = $site.UserCustomActions | Where-Object {
$_.Location -eq $location -and $_.ClientSideComponentId -and $_.ClientSideComponentId.ToString().ToLower() -eq $componentId
} | Select-Object -First 1
if ($existingAction) {
$action = $existingAction
Write-Host 'Aktualisiere vorhandene site-scoped Mega Menu UserCustomAction...' -ForegroundColor Yellow
}
else {
$action = $site.UserCustomActions.Add() $action = $site.UserCustomActions.Add()
Write-Host 'Erstelle neue site-scoped Mega Menu UserCustomAction...' -ForegroundColor Yellow $action.Name = [Guid]::NewGuid().ToString('B')
} $action.Title = 'MegaMenu Application Customizer'
$action.Name = $name
$action.Title = $title
$action.Description = $Description $action.Description = $Description
$action.Location = $location $action.Location = 'ClientSideExtension.ApplicationCustomizer'
$action.ClientSideComponentId = [Guid]$componentId $action.ClientSideComponentId = $componentId
$action.ClientSideComponentProperties = Get-MegaMenuPropertiesJson -CurrentTermSetName $TermSetName -CurrentCssUrl $CssUrl -CurrentDebug ([bool]$EnableDebug.IsPresent) $action.ClientSideComponentProperties = $properties
$action.Sequence = 100
$action.Update() $action.Update()
Write-Host 'Mega Menu wurde site-scoped registriert.' -ForegroundColor Green Write-Host ''
Write-Host ('Entfernte web-scoped Eintraege: ' + $removedWebScopedActions) Write-Host 'MegaMenu wurde zentral fuer die Site Collection registriert.' -ForegroundColor Green
Write-Host ('Beschreibung: ' + $Description) Write-Host ('Site Collection: ' + $site.Url)
Write-Host ('Properties: ' + $action.ClientSideComponentProperties) 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 { finally {
if ($site) {
$site.Dispose() $site.Dispose()
} }
}
-56
View File
@@ -1,56 +0,0 @@
{
"id": "abc3361f-bb2d-491f-aba3-cd51c19a299b",
"alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension",
"extensionType": "ApplicationCustomizer",
"version": "1.0.4",
"manifestVersion": 2,
"requiresCustomScript": false,
"loaderConfig": {
"entryModuleId": "mega-menu-application-customizer",
"internalModuleBaseUrls": [
"https://localhost:4321/"
],
"scriptResources": {
"mega-menu-application-customizer": {
"type": "path",
"path": "dist/mega-menu-application-customizer.js"
},
"MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath",
"paths": {}
},
"@microsoft/decorators": {
"type": "component",
"version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3"
},
"@microsoft/sp-core-library": {
"type": "component",
"version": "1.4.1",
"id": "7263c7d0-1d6a-45ec-8d85-d4d1d234171b"
},
"@microsoft/sp-application-base": {
"type": "component",
"version": "1.4.1",
"id": "4df9bb86-ab0a-4aab-ab5f-48bf167048fb"
},
"@microsoft/sp-http": {
"type": "component",
"version": "1.4.1",
"id": "c07208f0-ea3b-4c1a-9965-ac1b825211a6"
},
"@microsoft/sp-lodash-subset": {
"type": "component",
"version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"
},
"@microsoft/sp-page-context": {
"type": "component",
"version": "1.4.1",
"id": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8"
}
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -3,5 +3,6 @@
const build = require('@microsoft/sp-build-web'); 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 - [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'));
File diff suppressed because one or more lines are too long
-5
View File
@@ -1,5 +0,0 @@
declare const styles: {
mmSlideIn: string;
mmFadeIn: string;
};
export default styles;
@@ -1,10 +0,0 @@
/* tslint:disable */
require('./MegaMenu.module.css');
var styles = {
mmSlideIn: 'mmSlideIn_09a8e1a7',
mmFadeIn: 'mmFadeIn_09a8e1a7',
};
export default styles;
/* tslint:enable */
//# sourceMappingURL=MegaMenu.module.scss.js.map
@@ -1 +0,0 @@
{"version":3,"sources":["extensions/megaMenu/MegaMenu.module.scss.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,CAAC,uBAAuB,CAAC,CAAC;AACjC,IAAM,MAAM,GAAG;IACb,SAAS,EAAE,oBAAoB;IAC/B,QAAQ,EAAE,mBAAmB;CAC9B,CAAC;AAEF,eAAe,MAAM,CAAC;AACtB,mBAAmB","file":"extensions/megaMenu/MegaMenu.module.scss.js","sourcesContent":["/* tslint:disable */\r\nrequire('./MegaMenu.module.css');\r\nconst styles = {\r\n mmSlideIn: 'mmSlideIn_09a8e1a7',\r\n mmFadeIn: 'mmFadeIn_09a8e1a7',\r\n};\r\n\r\nexport default styles;\r\n/* tslint:enable */"],"sourceRoot":"..\\..\\..\\src"}
@@ -1,27 +0,0 @@
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
import './MegaMenu.module.scss';
export declare const UserCustomActionMegaMenuId: string;
/**
* Properties for the MegaMenu Application Customizer
*/
export interface IMegaMenuApplicationCustomizerProperties {
/**
* The name of the termset to load menu items from
*/
termSetName: string;
/**
* Optional URL to an external CSS file
*/
cssUrl?: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
private _topPlaceholder;
onInit(): Promise<void>;
private _renderPlaceHolders();
private _updateCallback;
private _renderMegaMenu(termSetName);
private _getOrCreateContainer(id, placeholder);
private _loadExternalCss(cssUrl);
private _onDispose();
}
@@ -1,188 +0,0 @@
// tslint:disable:max-line-length
// tslint:disable:match-default-export-name
// tslint:disable:typedef
// tslint:disable:variable-name
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base';
import * as strings from 'MegaMenuApplicationCustomizerStrings';
import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService';
import { MegaMenuRenderer } from './MegaMenuRenderer';
// Import SCSS module
import './MegaMenu.module.scss';
var LOG_SOURCE = 'MegaMenuApplicationCustomizer';
export var UserCustomActionMegaMenuId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
/** A Custom Action which can be run during execution of a Client Side Application */
var MegaMenuApplicationCustomizer = (function (_super) {
__extends(MegaMenuApplicationCustomizer, _super);
function MegaMenuApplicationCustomizer() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this._updateCallback = function (data) {
_this._loadExternalCss(data.cssUrl);
_this._renderMegaMenu(data.termSetName);
};
return _this;
}
MegaMenuApplicationCustomizer.prototype.onInit = function () {
Log.info(LOG_SOURCE, "Initialized " + strings.Title);
// Load external CSS if provided
if (this.properties.cssUrl) {
this._loadExternalCss(this.properties.cssUrl);
}
// Wait for the page to be ready
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
// Initial render
this._renderPlaceHolders();
return Promise.resolve();
};
MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () {
console.log('Available placeholders: ', this.context.placeholderProvider.placeholderNames.map(function (name) { return PlaceholderName[name]; }).join(', '));
// Handling the top placeholder
if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose });
// The extension should not assume that the expected placeholder is available.
if (!this._topPlaceholder) {
console.error('The expected placeholder (Top) was not found.');
return;
}
if (!this.properties.termSetName) {
console.error('TermSetName property is required but not provided.');
1;
return;
}
this._renderMegaMenu(this.properties.termSetName);
}
};
MegaMenuApplicationCustomizer.prototype._renderMegaMenu = function (termSetName) {
return __awaiter(this, void 0, void 0, function () {
var taxonomyService, menuItems, renderer, container, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!this._topPlaceholder) {
return [2 /*return*/];
}
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
taxonomyService = new TaxonomyNavigationService(this.context, termSetName);
return [4 /*yield*/, taxonomyService.getMenuItems()];
case 2:
menuItems = _a.sent();
renderer = new MegaMenuRenderer(this.context, menuItems, this._updateCallback);
container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
if (container) {
renderer.render(container);
}
else {
renderer.render(this._topPlaceholder.domElement);
}
Log.info(LOG_SOURCE, "MegaMenu rendered successfully with " + menuItems.length + " top-level items");
return [3 /*break*/, 4];
case 3:
error_1 = _a.sent();
console.error('Error rendering MegaMenu:', error_1);
return [3 /*break*/, 4];
case 4: return [2 /*return*/];
}
});
});
};
MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) {
var container = document.getElementById(id);
if (container) {
var div = document.createElement('div');
container.appendChild(div);
return div;
}
else {
return placeholder.domElement;
}
};
MegaMenuApplicationCustomizer.prototype._loadExternalCss = function (cssUrl) {
var externalCssLinkId = 'mega-menu-additional-css-34FAB720';
var link = document.getElementById(externalCssLinkId);
if (cssUrl && cssUrl.trim() !== '') {
// update previous link, if present
if (!link) {
var head = document.getElementsByTagName('head')[0];
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.id = externalCssLinkId;
link.onload = function () {
Log.info(LOG_SOURCE, "External CSS loaded successfully from: " + cssUrl);
};
link.onerror = function () {
console.warn("Failed to load external CSS from: " + cssUrl);
};
head.appendChild(link);
}
link.href = cssUrl;
}
else if (link) {
link.remove();
}
};
MegaMenuApplicationCustomizer.prototype._onDispose = function () {
console.log('[MegaMenuApplicationCustomizer._onDispose] Disposed custom top placeholder.');
};
__decorate([
override
], MegaMenuApplicationCustomizer.prototype, "onInit", null);
return MegaMenuApplicationCustomizer;
}(BaseApplicationCustomizer));
export default MegaMenuApplicationCustomizer;
//# sourceMappingURL=MegaMenuApplicationCustomizer.js.map
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json",
"id": "abc3361f-bb2d-491f-aba3-cd51c19a299b",
"alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension",
"extensionType": "ApplicationCustomizer",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false
}
-28
View File
@@ -1,28 +0,0 @@
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { IMenuItem } from '../../services/IMenuItem';
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
export declare class MegaMenuRenderer {
private context;
private menuItems;
private updateCallback;
private _settingsPanel?;
constructor(context: ApplicationCustomizerContext, menuItems: IMenuItem[], updateCallback: (data: IMegaMenuApplicationCustomizerProperties) => void);
render(container: HTMLElement): void;
private createSettingsItem();
private openSettings();
private createTopLevelItem(item);
private createTopLevelElement(item);
private createMegaMenu(parentItem);
private createCategorySection(item);
private attachEventListeners();
private attachKeyboardNavigation(heading, megaMenu);
private attachMouseEvents(heading, megaMenu);
private attachFocusManagement(heading, megaMenu);
private attachGlobalKeyboardNavigation();
private openMegaMenu(trigger, menu);
private closeMegaMenu(trigger, menu);
private toggleMegaMenu(trigger, menu);
private closeAllMegaMenus();
private focusFirstLink(megaMenu);
private createScreenReaderAnnouncer();
}
-372
View File
@@ -1,372 +0,0 @@
// tslint:disable:max-line-length
// tslint:disable:match-default-export-name
// tslint:disable:typedef
// tslint:disable:variable-name
import { SPPermission } from '@microsoft/sp-page-context';
import { MegaMenuSettingsPanel } from './MegaMenuSettings';
var MegaMenuRenderer = (function () {
function MegaMenuRenderer(context, menuItems, updateCallback) {
this.context = context;
this.menuItems = menuItems;
this.updateCallback = updateCallback;
}
MegaMenuRenderer.prototype.render = function (container) {
var _this = this;
// Clear any existing content
container.innerHTML = '';
container.id = 'CustomNavigation';
// Create the main nav element
var nav = document.createElement('nav');
nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main';
nav.setAttribute('role', 'navigation');
nav.setAttribute('aria-label', 'Hauptnavigation');
// Create the top-level menubar
var topLevelUl = document.createElement('ul');
topLevelUl.setAttribute('role', 'menubar');
// Process each top-level menu item
this.menuItems.forEach(function (topLevelItem) {
var topLevelLi = _this.createTopLevelItem(topLevelItem);
topLevelUl.appendChild(topLevelLi);
});
// Only if current user has ManageWeb permissions in this website can they access the settings
if (this.context.pageContext.web.permissions.hasPermission(SPPermission.manageWeb)) {
topLevelUl.appendChild(this.createSettingsItem());
}
nav.appendChild(topLevelUl);
container.appendChild(nav);
// Attach accessibility event listeners after rendering
this.attachEventListeners();
this.createScreenReaderAnnouncer();
};
MegaMenuRenderer.prototype.createSettingsItem = function () {
var _this = this;
var li = document.createElement('li');
li.setAttribute('role', 'none');
// Verwenden eines Links-ähnlichen Buttons damit Styling identisch zu Top-Level Items ist
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'menu-item-link menu-item-settings'; // reuse same base styles
btn.setAttribute('role', 'menuitem');
btn.setAttribute('tabindex', '0');
btn.setAttribute('aria-haspopup', 'false');
btn.setAttribute('aria-label', 'Einstellungen');
btn.title = 'Einstellungen';
// Nur EIN Icon: Wenn Fabric Icons geladen sind, zeigt ms-Icon das Symbol. Fallback via aria-label für Screenreader
var icon = document.createElement('span');
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
icon.setAttribute('aria-hidden', 'true');
btn.appendChild(icon);
btn.addEventListener('click', function () { return _this.openSettings(); });
btn.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
_this.openSettings();
}
});
li.appendChild(btn);
return li;
};
MegaMenuRenderer.prototype.openSettings = function () {
if (!this._settingsPanel) {
this._settingsPanel = new MegaMenuSettingsPanel(this.context, this.updateCallback);
}
this._settingsPanel.open();
};
// Panel logic moved to MegaMenuSettingsPanel
MegaMenuRenderer.prototype.createTopLevelItem = function (item) {
var li = document.createElement('li');
li.setAttribute('role', 'none');
// Create the top-level element (link or span)
var topElement = this.createTopLevelElement(item);
li.appendChild(topElement);
// If the item has children, create the mega menu
if (item.hasChildren() && item.items && item.items.length > 0) {
var megaMenu = this.createMegaMenu(item);
li.appendChild(megaMenu);
}
return li;
};
MegaMenuRenderer.prototype.createTopLevelElement = function (item) {
var element;
if (item.url) {
// Create as link
element = document.createElement('a');
element.href = item.url;
element.className = 'menu-item-link';
}
else {
// Create as span (no link)
element = document.createElement('span');
element.className = 'menu-item-text';
element.setAttribute('tabindex', '0');
}
element.setAttribute('role', 'menuitem');
element.setAttribute('aria-haspopup', 'true');
element.setAttribute('aria-expanded', 'false');
element.textContent = item.label;
if (item.hoverText) {
element.title = item.hoverText;
}
return element;
};
MegaMenuRenderer.prototype.createMegaMenu = function (parentItem) {
var _this = this;
var megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('role', 'menu');
megaMenuDiv.setAttribute('aria-label', parentItem.label + " Unterkategorien");
var gridDiv = document.createElement('div');
gridDiv.className = 'mega-menu-grid';
// Process second-level items (categories)
if (parentItem.items) {
parentItem.items.forEach(function (secondLevelItem) {
var categoryDiv = _this.createCategorySection(secondLevelItem);
gridDiv.appendChild(categoryDiv);
});
}
megaMenuDiv.appendChild(gridDiv);
return megaMenuDiv;
};
MegaMenuRenderer.prototype.createCategorySection = function (item) {
var categoryDiv = document.createElement('div');
categoryDiv.className = 'mega-menu-category';
// Create the category header (h3)
var h3 = document.createElement('h3');
if (item.url) {
// Category header as link
var link = document.createElement('a');
link.href = item.url;
link.textContent = item.label;
if (item.hoverText) {
link.title = item.hoverText;
}
h3.appendChild(link);
}
else {
// Category header as span (no link)
var span = document.createElement('span');
span.textContent = item.label;
if (item.hoverText) {
span.title = item.hoverText;
}
h3.appendChild(span);
}
categoryDiv.appendChild(h3);
// Create the third-level items list if they exist
if (item.hasChildren() && item.items && item.items.length > 0) {
var ul_1 = document.createElement('ul');
item.items.forEach(function (thirdLevelItem) {
var li = document.createElement('li');
var link = document.createElement('a');
link.href = thirdLevelItem.url || '#';
link.textContent = thirdLevelItem.label;
if (thirdLevelItem.hoverText) {
link.title = thirdLevelItem.hoverText;
}
li.appendChild(link);
ul_1.appendChild(li);
});
categoryDiv.appendChild(ul_1);
}
return categoryDiv;
};
MegaMenuRenderer.prototype.attachEventListeners = function () {
var headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span[role="menuitem"]');
for (var i = 0; i < headings.length; i++) {
var heading = headings[i];
var megaMenu = heading.nextElementSibling;
if (megaMenu && megaMenu.classList.contains('mega-menu')) {
this.attachKeyboardNavigation(heading, megaMenu);
this.attachMouseEvents(heading, megaMenu);
this.attachFocusManagement(heading, megaMenu);
}
}
// Global keyboard navigation
this.attachGlobalKeyboardNavigation();
};
MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) {
var _this = this;
heading.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
// Enter: Navigation (nur bei Links ohne Mega-Menu-Override)
if (heading.tagName === 'A') {
// Lasse normale Link-Navigation zu (KEIN preventDefault!)
return;
}
else {
// Bei span: Toggle Menu
e.preventDefault();
_this.toggleMegaMenu(heading, megaMenu);
}
}
else if (e.key === ' ') {
// Space: Toggle Dropdown (immer)
e.preventDefault();
_this.toggleMegaMenu(heading, megaMenu);
}
else if (e.key === 'ArrowDown') {
// Pfeil runter: Menü öffnen + erster Link
e.preventDefault();
_this.openMegaMenu(heading, megaMenu);
_this.focusFirstLink(megaMenu);
}
else if (e.key === 'ArrowUp') {
// Pfeil hoch: Menü schließen
e.preventDefault();
_this.closeMegaMenu(heading, megaMenu);
}
else if (e.key === 'Escape') {
// Escape: Menü schließen
e.preventDefault();
_this.closeMegaMenu(heading, megaMenu);
heading.focus();
}
});
// Click Event für Top-Level Links - NORMALE Navigation erlauben
if (heading.tagName === 'A') {
heading.addEventListener('click', function (e) {
// Links sollen normal navigieren, nicht das Mega-Menu toglen
// Wenn der User das Menu öffnen will, soll er Space oder Pfeil↓ nutzen
console.log('Link geklickt:', heading.href);
// KEIN preventDefault() - normale Link-Navigation
});
}
// Focus Management - KEIN automatisches Öffnen oder Schließen
heading.addEventListener('focus', function () {
console.log('Focus auf:', heading.textContent);
// Einfach nur fokussiert - keine automatischen Aktionen!
});
};
MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) {
var _this = this;
var parentLi = heading.parentElement;
parentLi.addEventListener('mouseenter', function () {
_this.openMegaMenu(heading, megaMenu);
});
parentLi.addEventListener('mouseleave', function () {
_this.closeMegaMenu(heading, megaMenu);
});
};
MegaMenuRenderer.prototype.attachFocusManagement = function (heading, megaMenu) {
var _this = this;
// Focus-Verlust-Behandlung - vereinfacht
megaMenu.addEventListener('focusout', function (e) {
// Kurz warten, um zu prüfen ob Focus innerhalb des Mega-Menus bleibt
setTimeout(function () {
var focusedElement = document.activeElement;
var isInsideThisMenu = megaMenu.contains(focusedElement);
var isOnThisTrigger = focusedElement === heading;
var isInAnyMegaMenu = focusedElement.closest('.mega-menu');
var isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
// Nur schließen wenn Focus komplett außerhalb der Navigation ist
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
console.log('Schließe Menu wegen Focus-Verlust');
_this.closeMegaMenu(heading, megaMenu);
}
}, 150);
});
};
MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () {
var _this = this;
document.addEventListener('keydown', function (e) {
var activeElement = document.activeElement;
if (e.key === 'Escape') {
var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]');
if (openMenu) {
var triggerLink = openMenu.previousElementSibling;
_this.closeMegaMenu(triggerLink, openMenu);
triggerLink.focus();
}
}
// Tab-Navigation: Nur bei spezifischen Übergängen eingreifen
if (e.key === 'Tab') {
// Von Top-Level zum ersten Link im offenen Menu
if (!e.shiftKey) {
var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
if (currentTopLevel) {
var parentLi = currentTopLevel.closest('li');
var megaMenu = parentLi.querySelector('.mega-menu.js-open');
if (megaMenu) {
// Nur eingreifen wenn wir vom Trigger weg-tabben
e.preventDefault();
var firstLink = megaMenu.querySelector('a');
if (firstLink) {
firstLink.focus();
}
return;
}
}
}
// Shift+Tab: Vom ersten Link im Menu zurück zum Trigger
if (e.shiftKey) {
var megaMenu = activeElement.closest('.mega-menu');
if (megaMenu && megaMenu.classList.contains('js-open')) {
var allLinksInMenu = megaMenu.querySelectorAll('a');
var firstLinkInMenu = allLinksInMenu[0];
// Nur eingreifen wenn wir beim ersten Link sind
if (activeElement === firstLinkInMenu) {
e.preventDefault();
var triggerElement = megaMenu.previousElementSibling;
triggerElement.focus();
return;
}
}
}
// Ansonsten: Normale Tab-Navigation nicht unterbrechen!
}
});
};
MegaMenuRenderer.prototype.openMegaMenu = function (trigger, menu) {
trigger.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open');
console.log('Menu geöffnet:', trigger.textContent);
};
MegaMenuRenderer.prototype.closeMegaMenu = function (trigger, menu) {
trigger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open');
console.log('Menu geschlossen:', trigger.textContent);
};
MegaMenuRenderer.prototype.toggleMegaMenu = function (trigger, menu) {
var isOpen = trigger.getAttribute('aria-expanded') === 'true';
if (isOpen) {
this.closeMegaMenu(trigger, menu);
}
else {
this.closeAllMegaMenus();
this.openMegaMenu(trigger, menu);
}
};
MegaMenuRenderer.prototype.closeAllMegaMenus = function () {
var allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]');
for (var i = 0; i < allTriggers.length; i++) {
var trigger = allTriggers[i];
var menu = trigger.nextElementSibling;
if (menu) {
this.closeMegaMenu(trigger, menu);
}
}
};
// removed unused closeOtherMegaMenus (was previously declared but not used)
MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) {
var firstLink = megaMenu.querySelector('a');
if (firstLink) {
firstLink.focus();
}
};
MegaMenuRenderer.prototype.createScreenReaderAnnouncer = function () {
// Screenreader-Ankündigungen
var srAnnouncer = document.createElement('div');
srAnnouncer.setAttribute('aria-live', 'polite');
srAnnouncer.setAttribute('aria-atomic', 'true');
srAnnouncer.className = 'sr-only';
document.body.appendChild(srAnnouncer);
console.log('Screenreader-Ankündigungen sind jetzt bereit');
};
return MegaMenuRenderer;
}());
export { MegaMenuRenderer };
//# sourceMappingURL=MegaMenuRenderer.js.map
File diff suppressed because one or more lines are too long
-25
View File
@@ -1,25 +0,0 @@
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
export declare class MegaMenuSettingsPanel {
private context;
private dataUpdated;
private _service;
private _ucaId;
private _panelElement;
private _overlayElement;
constructor(context: ApplicationCustomizerContext, dataUpdated: (data: IMegaMenuApplicationCustomizerProperties) => void);
open(): Promise<void>;
close(): void;
private readApplicationCustomizerProps();
private saveApplicationCustomizerProps(componentProps);
private _createPanel(props);
private _getMarkup(termSet, cssUrl);
private _save();
/**
* Hält den Fokus innerhalb des Settings-Panels gefangen (Focus Trapping).
* Wichtig für Accessibility: Verhindert, dass Tab-Navigation aus dem modalen Dialog herausführt.
* Bei Tab am letzten Element → springt zum ersten, bei Shift+Tab am ersten → springt zum letzten.
*/
private _trapFocus(e);
private _escape(value);
}
-223
View File
@@ -1,223 +0,0 @@
// tslint:disable:max-line-length export-name
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { UserCustomActionMegaMenuId } from './MegaMenuApplicationCustomizer';
import { UserCustomActionService } from '../../services/UserCustomActionService/UserCustomActionService';
import { UserCustomActionScope } from '../../services/UserCustomActionService/UserCustomActionScope';
var MegaMenuSettingsPanel = (function () {
function MegaMenuSettingsPanel(context, dataUpdated) {
this.context = context;
this.dataUpdated = dataUpdated;
this._panelElement = undefined;
this._overlayElement = undefined;
this._service = new UserCustomActionService(this.context);
}
MegaMenuSettingsPanel.prototype.open = function () {
return __awaiter(this, void 0, void 0, function () {
var currentProps;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (this._panelElement) {
return [2 /*return*/];
}
return [4 /*yield*/, this.readApplicationCustomizerProps()];
case 1:
currentProps = _a.sent();
this._createPanel(currentProps);
return [2 /*return*/];
}
});
});
};
MegaMenuSettingsPanel.prototype.close = function () {
if (this._panelElement) {
this._panelElement.remove();
this._panelElement = undefined;
}
if (this._overlayElement) {
this._overlayElement.remove();
this._overlayElement = undefined;
}
// Fokus entfernen (kein sichtbarer Fokus-Rahmen nach Dialog-Schließung)
document.body.focus();
document.body.blur();
};
MegaMenuSettingsPanel.prototype.readApplicationCustomizerProps = function () {
return __awaiter(this, void 0, void 0, function () {
var ucas, candidates, uca;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this._service.getUserCustomActions(UserCustomActionScope.Site)];
case 1:
ucas = _a.sent();
candidates = ucas.filter(function (uca) { return uca.ClientSideComponentId === UserCustomActionMegaMenuId; });
if (candidates.length) {
uca = candidates[0];
this._ucaId = uca.Id;
return [2 /*return*/, JSON.parse(uca.ClientSideComponentProperties)];
}
else {
console.error('UserCustomAction für das Megamenü nicht gefunden. Alles ist sinnlos.');
return [2 /*return*/, {
termSetName: '',
cssUrl: ''
}];
}
return [2 /*return*/];
}
});
});
};
MegaMenuSettingsPanel.prototype.saveApplicationCustomizerProps = function (componentProps) {
return __awaiter(this, void 0, void 0, function () {
var newUserCustomActionsProperty, e_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
newUserCustomActionsProperty = {
ClientSideComponentProperties: JSON.stringify(componentProps)
};
return [4 /*yield*/, this._service.updateUserCustomAction(UserCustomActionScope.Site, this._ucaId, newUserCustomActionsProperty)];
case 1:
_a.sent();
this.dataUpdated(componentProps);
return [3 /*break*/, 3];
case 2:
e_1 = _a.sent();
console.error(e_1);
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
});
};
MegaMenuSettingsPanel.prototype._createPanel = function (props) {
var _this = this;
var overlay = document.createElement('div');
overlay.className = 'mm-settings-overlay';
overlay.tabIndex = -1;
overlay.onclick = function () { return _this.close(); };
this._overlayElement = overlay;
var panel = document.createElement('div');
panel.className = 'mm-settings-panel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-modal', 'true');
panel.setAttribute('aria-label', 'MegaMenu Einstellungen');
panel.innerHTML = this._getMarkup(props.termSetName, props.cssUrl);
this._panelElement = panel;
document.body.appendChild(overlay);
document.body.appendChild(panel);
var closeBtn = panel.querySelector('.mm-settings-close');
var cancelBtn = panel.querySelector('.mm-settings-cancel');
var saveBtn = panel.querySelector('.mm-settings-save');
var firstInput = panel.querySelector('#mm-setting-termset');
if (closeBtn) {
closeBtn.onclick = function () { return _this.close(); };
}
if (cancelBtn) {
cancelBtn.onclick = function () { return _this.close(); };
}
if (saveBtn) {
saveBtn.onclick = function () { return _this._save(); };
}
panel.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
e.preventDefault();
_this.close();
}
else if (e.key === 'Tab') {
_this._trapFocus(e);
}
});
setTimeout(function () { if (firstInput) {
firstInput.focus();
} }, 0);
};
MegaMenuSettingsPanel.prototype._getMarkup = function (termSet, cssUrl) {
return "<div class='mm-settings-header'>\n <h2 class='mm-settings-title'>Einstellungen</h2>\n <button type='button' class='mm-settings-close' aria-label='Schlie\u00DFen'>\u00D7</button>\n </div>\n <div class='mm-settings-body'>\n <div class='mm-settings-field'>\n <label for='mm-setting-termset'>Name des Navigations-Termsets</label>\n <input id='mm-setting-termset' type='text' value='" + this._escape(termSet) + "' />\n </div>\n <div class='mm-settings-field'>\n <label for='mm-setting-css'>Pfad zu zus\u00E4tzlicher CSS-Datei</label>\n <input id='mm-setting-css' type='text' value='" + this._escape(cssUrl) + "' />\n </div>\n </div>\n <div class='mm-settings-footer'>\n <button type='button' class='mm-settings-save ms-Button ms-Button--primary'><span>Speichern</span></button>\n <button type='button' class='mm-settings-cancel ms-Button'><span>Abbrechen</span></button>\n </div>";
};
MegaMenuSettingsPanel.prototype._save = function () {
if (!this._panelElement) {
return;
}
var termSetInput = this._panelElement.querySelector('#mm-setting-termset');
var cssInput = this._panelElement.querySelector('#mm-setting-css');
this.saveApplicationCustomizerProps({
termSetName: termSetInput && termSetInput.value ? termSetInput.value : '',
cssUrl: cssInput && cssInput.value ? cssInput.value : ''
});
this.close();
};
/**
* Hält den Fokus innerhalb des Settings-Panels gefangen (Focus Trapping).
* Wichtig für Accessibility: Verhindert, dass Tab-Navigation aus dem modalen Dialog herausführt.
* Bei Tab am letzten Element → springt zum ersten, bei Shift+Tab am ersten → springt zum letzten.
*/
MegaMenuSettingsPanel.prototype._trapFocus = function (e) {
if (!this._panelElement) {
return;
}
var focusable = this._panelElement.querySelectorAll('button, input');
if (!focusable || focusable.length === 0) {
return;
}
var first = focusable[0];
var last = focusable[focusable.length - 1];
var active = document.activeElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
}
else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
};
MegaMenuSettingsPanel.prototype._escape = function (value) {
if (value) {
return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
else {
return '';
}
};
return MegaMenuSettingsPanel;
}());
export { MegaMenuSettingsPanel };
//# sourceMappingURL=MegaMenuSettings.js.map
File diff suppressed because one or more lines are too long
-5
View File
@@ -1,5 +0,0 @@
define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer"
}
});
-4
View File
@@ -1,4 +0,0 @@
export { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
export { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';
export { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';
-17
View File
@@ -1,17 +0,0 @@
// 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';
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
};
}
//# sourceMappingURL=index.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,wFAAwF;AAExF,+CAA+C;AAC/C,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,4DAA4D,CAAC;AACrG,OAAO,EAAE,qBAAqB,EAAE,MAAM,0DAA0D,CAAC;AAKjG,EAAE,CAAC,CAAC,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,kBAAkB,GAAG;QACxB,yBAAyB,EAAE,OAAO,CAAC,sCAAsC,CAAC,CAAC,yBAAyB;QACpG,gBAAgB,EAAE,OAAO,CAAC,wCAAwC,CAAC,CAAC,gBAAgB;QACpF,uBAAuB,EAAE,OAAO,CAAC,4DAA4D,CAAC,CAAC,uBAAuB;QACtH,qBAAqB,EAAE,OAAO,CAAC,0DAA0D,CAAC,CAAC,qBAAqB;KACnH,CAAC;AACN,CAAC","file":"index.js","sourcesContent":["// tslint:disable:max-line-length no-any\r\n// A file is required to be in the root of the /src directory by the TypeScript compiler\r\n\r\n// Export services for classic SharePoint usage\r\nexport { TaxonomyNavigationService } from './services/TaxonomyNavigationService';\r\nexport { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';\r\nexport { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';\r\nexport { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';\r\n\r\n// Expose services globally for classic JavaScript consumption\r\ndeclare var window: any;\r\n\r\nif (typeof window !== 'undefined') {\r\n window.__megaMenuServices = {\r\n TaxonomyNavigationService: require('./services/TaxonomyNavigationService').TaxonomyNavigationService,\r\n MegaMenuRenderer: require('./extensions/megaMenu/MegaMenuRenderer').MegaMenuRenderer,\r\n UserCustomActionService: require('./services/UserCustomActionService/UserCustomActionService').UserCustomActionService,\r\n UserCustomActionScope: require('./services/UserCustomActionService/UserCustomActionScope').UserCustomActionScope\r\n };\r\n}\r\n"],"sourceRoot":"..\\src"}
-10
View File
@@ -1,10 +0,0 @@
export interface IMenuItem {
id: string;
label: string;
icon?: string;
hoverText: string;
url?: string;
pathDepth: number;
items?: IMenuItem[];
hasChildren: () => boolean;
}
-3
View File
@@ -1,3 +0,0 @@
//# sourceMappingURL=IMenuItem.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/IMenuItem.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
-9
View File
@@ -1,9 +0,0 @@
export interface IPickerTerm {
name: string;
key: string;
path: string;
termSet: string;
termSetName?: string;
}
export interface IPickerTerms extends Array<IPickerTerm> {
}
-3
View File
@@ -1,3 +0,0 @@
//# sourceMappingURL=IPickerTerm.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/IPickerTerm.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
-84
View File
@@ -1,84 +0,0 @@
/**
* Interfaces for Term store, groups and term sets
* This code is a copy from the library @pnp/sp-dev-fx-controls-react
*/
export interface ITermStore {
_ObjectType_: string;
_ObjectIdentity_: string;
Id: string;
Name: string;
Groups: IGroups;
}
export interface IGroups {
_ObjectType_: string;
_Child_Items_: IGroup[];
}
export interface IGroup {
_ObjectType_: string;
_ObjectIdentity_: string;
TermSets: ITermSets;
Id: string;
Name: string;
IsSystemGroup: boolean;
}
export interface ITermSets {
_ObjectType_: string;
_Child_Items_: ITermSet[];
}
export interface ITermSet {
_ObjectType_: string;
_ObjectIdentity_: string;
Id: string;
CustomSortOrder?: string;
Name: string;
Description: string;
Names: ITermSetNames;
Terms?: ITerm[];
}
export interface ITermSetMinimal {
_ObjectType_?: string;
_ObjectIdentity_?: string;
Id: string;
Name: string;
}
export interface ITermSetNames {
[locale: string]: string;
}
/**
* Interfaces for the terms
*/
export interface ITerms {
_ObjectType_: string;
_Child_Items_: ITerm[];
}
/**
* Term
*/
export interface ITerm {
_ObjectType_: string;
_ObjectIdentity_: string;
Id: string;
Name: string;
Description: string;
IsDeprecated: boolean;
IsAvailableForTagging: boolean;
IsRoot: boolean;
PathOfTerm: string;
TermSet: ITermSetMinimal;
CustomSortOrderIndex?: number;
PathDepth?: number;
ParentId?: string;
TermsCount?: number;
LocalCustomProperties?: {
[property: string]: any;
};
}
export interface ISuggestTerm {
Id: string;
DefaultLabel: string;
Description: string;
IsKeyword: boolean;
IsSynonym: boolean;
Paths: Array<string>;
Synonyms: string;
}
@@ -1,3 +0,0 @@
//# sourceMappingURL=ISPTermStorePickerService.js.map
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/ISPTermStorePickerService.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
-7
View File
@@ -1,7 +0,0 @@
export interface ISPTermStorePickerServiceProps {
termsetNameOrID: string;
useSessionStorage: boolean;
hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean;
anchorId: string;
}
@@ -1,3 +0,0 @@
//# sourceMappingURL=ISPTermStorePickerServiceProps.js.map
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/ISPTermStorePickerServiceProps.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
-4
View File
@@ -1,4 +0,0 @@
import { IMenuItem } from './IMenuItem';
export interface ITaxonomyNavigationService {
getMenuItems(): Promise<IMenuItem[]>;
}
@@ -1,3 +0,0 @@
//# sourceMappingURL=ITaxonomyNavigationService.js.map
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/ITaxonomyNavigationService.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
-4
View File
@@ -1,4 +0,0 @@
export declare class ItemDictionary<T> {
Get(key: string): T;
Add(key: string, value: T): void;
}
-14
View File
@@ -1,14 +0,0 @@
var ItemDictionary = (function () {
function ItemDictionary() {
}
ItemDictionary.prototype.Get = function (key) {
return this[key];
};
ItemDictionary.prototype.Add = function (key, value) {
this[key] = value;
};
return ItemDictionary;
}());
export { ItemDictionary };
//# sourceMappingURL=ItemDictionary.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["services/ItemDictionary.ts"],"names":[],"mappings":"AAAA;IAAA;IAQA,CAAC;IAPU,4BAAG,GAAV,UAAW,GAAW;QAClB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAEM,4BAAG,GAAV,UAAW,GAAW,EAAE,KAAQ;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IACL,qBAAC;AAAD,CARA,AAQC,IAAA","file":"services/ItemDictionary.js","sourcesContent":["export class ItemDictionary<T> {\r\n public Get(key: string): T {\r\n return this[key];\r\n }\r\n\r\n public Add(key: string, value: T): void {\r\n this[key] = value;\r\n }\r\n}\r\n"],"sourceRoot":"..\\..\\src"}
-14
View File
@@ -1,14 +0,0 @@
import { IMenuItem } from './IMenuItem';
import { ITerm } from './ISPTermStorePickerService';
export declare class MenuItem implements IMenuItem {
level: number;
id: string;
label: string;
hoverText: string;
pathDepth: number;
url?: string;
items?: IMenuItem[];
constructor(term: ITerm, level: number, siteCollectionUrl?: string);
hasChildren(): boolean;
command(): void;
}
-29
View File
@@ -1,29 +0,0 @@
// tslint:disable:no-any no-string-literal max-line-length
var MenuItem = (function () {
function MenuItem(term, level, siteCollectionUrl) {
this.level = level;
this.id = term.Id;
this.label = term.Name;
this.hoverText = term.LocalCustomProperties['_Sys_Nav_HoverText'];
this.pathDepth = term.PathDepth;
var rawUrl = term.LocalCustomProperties['_Sys_Nav_SimpleLinkUrl'] || term.LocalCustomProperties['_Sys_Nav_TargetUrl'];
if (rawUrl) {
this.url = siteCollectionUrl && rawUrl.indexOf('~sitecollection') === 0
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
: rawUrl;
}
this.items = [];
}
MenuItem.prototype.hasChildren = function () {
return this.items && this.items.length > 0;
};
MenuItem.prototype.command = function () {
if (this.url) {
window.location.href = this.url;
}
};
return MenuItem;
}());
export { MenuItem };
//# sourceMappingURL=MenuItem.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["services/MenuItem.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAK1D;IAQI,kBAAY,IAAW,EAAS,KAAa,EAAE,iBAA0B;QAAzC,UAAK,GAAL,KAAK,CAAQ;QACzC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;QAClE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAM,MAAM,GAAW,IAAI,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,IAAI,IAAI,CAAC,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;QAChI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACT,IAAI,CAAC,GAAG,GAAG,iBAAiB,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC;kBACjE,iBAAiB,GAAG,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,MAAM,CAAC;kBAC9D,MAAM,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IACpB,CAAC;IAEM,8BAAW,GAAlB;QACI,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/C,CAAC;IAEM,0BAAO,GAAd;QACI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACV,MAAc,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;QAC7C,CAAC;IACL,CAAC;IACL,eAAC;AAAD,CA/BA,AA+BC,IAAA","file":"services/MenuItem.js","sourcesContent":["// tslint:disable:no-any no-string-literal max-line-length\r\n\r\nimport { IMenuItem } from './IMenuItem';\r\nimport { ITerm } from './ISPTermStorePickerService';\r\n\r\nexport class MenuItem implements IMenuItem {\r\n public id: string;\r\n public label: string;\r\n public hoverText: string;\r\n public pathDepth: number;\r\n public url?: string;\r\n public items?: IMenuItem[];\r\n\r\n constructor(term: ITerm, public level: number, siteCollectionUrl?: string) {\r\n this.id = term.Id;\r\n this.label = term.Name;\r\n this.hoverText = term.LocalCustomProperties['_Sys_Nav_HoverText'];\r\n this.pathDepth = term.PathDepth;\r\n const rawUrl: string = term.LocalCustomProperties['_Sys_Nav_SimpleLinkUrl'] || term.LocalCustomProperties['_Sys_Nav_TargetUrl'];\r\n if (rawUrl) {\r\n this.url = siteCollectionUrl && rawUrl.indexOf('~sitecollection') === 0\r\n ? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)\r\n : rawUrl;\r\n }\r\n this.items = [];\r\n }\r\n\r\n public hasChildren(): boolean {\r\n return this.items && this.items.length > 0;\r\n }\r\n\r\n public command(): void {\r\n if (this.url) {\r\n (window as any).location.href = this.url;\r\n }\r\n }\r\n}\r\n"],"sourceRoot":"..\\..\\src"}
-5
View File
@@ -1,5 +0,0 @@
import { IMenuItem } from './IMenuItem';
import { ITaxonomyNavigationService } from './ITaxonomyNavigationService';
export default class MockTaxonomyNavigationService implements ITaxonomyNavigationService {
getMenuItems(): Promise<IMenuItem[]>;
}
@@ -1,109 +0,0 @@
import * as uuid from 'uuid';
var MockTaxonomyNavigationService = (function () {
function MockTaxonomyNavigationService() {
}
MockTaxonomyNavigationService.prototype.getMenuItems = function () {
return new Promise(function (resolve) {
resolve([
{
id: uuid.v4(),
label: 'Menu Item 1',
url: 'https://www.bing.com',
hoverText: 'Hover me!',
pathDepth: 1,
hasChildren: function () { return true; },
items: [
{
id: uuid.v4(),
label: 'Submenu Item 1',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 2,
hasChildren: function () { return false; }
},
{
id: uuid.v4(),
label: 'Submenu Item 2',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 2,
hasChildren: function () { return false; }
}
]
},
{
id: uuid.v4(),
label: 'Menu Item 2',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 1,
hasChildren: function () { return false; }
},
{
id: uuid.v4(),
label: 'Menu Item 3',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 1,
hasChildren: function () { return true; },
items: [
{
id: uuid.v4(),
label: 'Submenu Item 1',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 2,
hasChildren: function () { return false; }
},
{
id: uuid.v4(),
label: 'Submenu Item 2',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 2,
hasChildren: function () { return true; },
items: [
{
id: uuid.v4(),
label: 'Submenu Item 1',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 3,
hasChildren: function () { return false; }
},
{
id: uuid.v4(),
label: 'Submenu Item 2',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 3,
hasChildren: function () { return false; }
}
]
},
{
id: uuid.v4(),
label: 'Submenu Item 3',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 2,
hasChildren: function () { return false; }
},
{
id: uuid.v4(),
label: 'Submenu Item 4',
hoverText: 'Huch!',
url: 'https://www.bing.com',
pathDepth: 2,
hasChildren: function () { return false; }
}
]
}
]);
});
};
return MockTaxonomyNavigationService;
}());
export default MockTaxonomyNavigationService;
//# sourceMappingURL=MockTaxonomyNavigationService.js.map
File diff suppressed because one or more lines are too long
-76
View File
@@ -1,76 +0,0 @@
import { ITermStore, ITermSet } from './ISPTermStorePickerService';
import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps';
import { IPickerTerm } from './IPickerTerm';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
/**
* Service implementation to manage term stores in SharePoint
*/
export default class SPTermStorePickerService {
private props;
private context;
private clientServiceUrl;
private suggestionServiceUrl;
/**
* Service constructor
*/
constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext);
getTermLabels(termId: string): Promise<string[]>;
/**
* Gets the collection of term stores in the current SharePoint env
*/
getTermStores(): Promise<ITermStore[]>;
/**
* Gets the current term set
*/
getTermSet(): Promise<ITermSet>;
/**
* Retrieve all terms for the given term set
* @param termset
*/
getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean): Promise<ITermSet>;
/**
* Retrieve all terms that starts with the searchText
* @param searchText
*/
searchTermsByName(searchText: string): Promise<IPickerTerm[]>;
searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]>;
/**
* Retrieve all terms for the given term set and anchorId
*/
getAllTermsByAnchorId(termsetNameOrID: string, anchorId: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean): Promise<IPickerTerm[]>;
/**
* Clean the Guid from the Web Service response
* @param guid
*/
cleanGuid(guid: string): string;
/**
* Get the term set ID by its name
* @param termstore
* @param termset
*/
private getTermSetId(termstore, termsetName);
private getTermsById(termId, useSessionStorage?);
private searchTermsBySearchText(terms, searchText);
/**
* Searches terms for the given term set
* @param searchText
* @param termsetId
*/
private searchTermsByTermSet(searchText);
private isGuid(strGuid);
/**
* Sorting terms based on their path and depth
*
* @param terms
*/
private sortTerms(terms);
/**
* Sort the terms by their path
*
* @param a term 2
* @param b term 2
*/
private sortTermByPath(a, b);
private convertTermToPickerTerm(term);
private convertSuggestTermToPickerTerm(term);
}
-574
View File
@@ -1,574 +0,0 @@
/* tslint:disable:no-null-keyword max-line-length typedef no-any no-string-literal variable-name */
/**
* This code is a copy from the library @pnp/sp-dev-fx-controls-react
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { SPHttpClient } from '@microsoft/sp-http';
import { findIndex } from '@microsoft/sp-lodash-subset';
var EmptyGuid = '00000000-0000-0000-0000-000000000000';
/**
* Service implementation to manage term stores in SharePoint
*/
var SPTermStorePickerService = (function () {
/**
* Service constructor
*/
function SPTermStorePickerService(props, context) {
this.props = props;
this.context = context;
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
}
SPTermStorePickerService.prototype.getTermLabels = function (termId) {
return __awaiter(this, void 0, void 0, function () {
var result, data, reqHeaders, httpPostOptions, callResult, jsonResult, node, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
result = null;
_a.label = 1;
case 1:
_a.trys.push([1, 4, , 5]);
data = "<Request AddExpandoFieldTypeSuffix=\"true\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\".NET Library\" xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\"><Actions><ObjectPath Id=\"8\" ObjectPathId=\"7\" /><ObjectIdentityQuery Id=\"9\" ObjectPathId=\"7\" /><ObjectPath Id=\"11\" ObjectPathId=\"10\" /><ObjectIdentityQuery Id=\"12\" ObjectPathId=\"10\" /><ObjectPath Id=\"14\" ObjectPathId=\"13\" /><ObjectIdentityQuery Id=\"15\" ObjectPathId=\"13\" /><Query Id=\"16\" ObjectPathId=\"13\"><Query SelectAllProperties=\"false\"><Properties><Property Name=\"Labels\" SelectAll=\"true\"><Query SelectAllProperties=\"false\"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id=\"7\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"10\" ParentId=\"7\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"13\" ParentId=\"10\" Name=\"GetTerm\"><Parameters><Parameter Type=\"Guid\">" + termId + "</Parameter></Parameters></Method></ObjectPaths></Request>";
reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml');
httpPostOptions = {
headers: reqHeaders,
body: data
};
return [4 /*yield*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions)];
case 2:
callResult = _a.sent();
return [4 /*yield*/, callResult.json()];
case 3:
jsonResult = _a.sent();
node = jsonResult.find(function (x) { return x._ObjectType_ === 'SP.Taxonomy.Term'; });
if (node && node.Labels && node.Labels._Child_Items_) {
result = node.Labels._Child_Items_.map(function (termLabel) { return termLabel.Value; });
}
return [3 /*break*/, 5];
case 4:
error_1 = _a.sent();
result = null;
console.log(error_1.message);
return [3 /*break*/, 5];
case 5: return [2 /*return*/, result];
}
});
});
};
/**
* Gets the collection of term stores in the current SharePoint env
*/
SPTermStorePickerService.prototype.getTermStores = function () {
var _this = this;
// Retrieve the term store name, groups, and term sets
var data = '<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectIdentityQuery Id="3" ObjectPathId="1" /><ObjectPath Id="5" ObjectPathId="4" /><ObjectIdentityQuery Id="6" ObjectPathId="4" /><Query Id="7" ObjectPathId="4"><Query SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /><Property Name="Groups"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="IsSystemGroup" ScalarProperty="true" /><Property Name="TermSets"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="Description" ScalarProperty="true" /><Property Name="Names" ScalarProperty="true" /></Properties></ChildItemQuery></Property></Properties></ChildItemQuery></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="4" ParentId="1" Name="GetDefaultSiteCollectionTermStore" /></ObjectPaths></Request>';
var reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml');
var httpPostOptions = {
headers: reqHeaders,
body: data
};
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
return serviceResponse.json().then(function (serviceJSONResponse) {
// Construct results
var termStoreResult = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermStore'; });
// Check if term store was retrieved
if (termStoreResult.length > 0) {
// Check if the termstore needs to be filtered or limited
if (_this.props.termsetNameOrID) {
return termStoreResult.map(function (termstore) {
var termGroups = termstore.Groups._Child_Items_;
// Check if the groups have to be limited to a specific term set
if (_this.props.termsetNameOrID) {
var termsetNameOrId_1 = _this.props.termsetNameOrID;
termGroups = termGroups.map(function (group) {
group.TermSets._Child_Items_ = group.TermSets._Child_Items_.filter(function (termSet) { return termSet.Name === termsetNameOrId_1 || _this.cleanGuid(termSet.Id).toLowerCase() === _this.cleanGuid(termsetNameOrId_1).toLowerCase(); });
return group;
});
}
// Filter out all systen groups
termGroups = termGroups.filter(function (group) { return !group.IsSystemGroup; });
// Filter out empty groups
termGroups = termGroups.filter(function (group) { return group.TermSets._Child_Items_.length > 0; });
// Map the new groups
termstore.Groups._Child_Items_ = termGroups;
return termstore;
});
}
// Return the term store results
return termStoreResult;
}
return [];
});
});
};
/**
* Gets the current term set
*/
SPTermStorePickerService.prototype.getTermSet = function () {
return __awaiter(this, void 0, void 0, function () {
var termStore;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getTermStores()];
case 1:
termStore = _a.sent();
return [2 /*return*/, this.getTermSetId(termStore, this.props.termsetNameOrID)];
}
});
});
};
/**
* Retrieve all terms for the given term set
* @param termset
*/
SPTermStorePickerService.prototype.getAllTerms = function (termset, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; }
return __awaiter(this, void 0, void 0, function () {
var _this = this;
var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
termsetId = termset;
if (!!this.isGuid(termset)) return [3 /*break*/, 2];
return [4 /*yield*/, this.getTermStores()];
case 1:
termStore = _a.sent();
crntTermSet = this.getTermSetId(termStore, termset);
if (crntTermSet) {
termsetId = this.cleanGuid(crntTermSet.Id);
}
else {
return [2 /*return*/, null];
}
_a.label = 2;
case 2:
childTerms = this.getTermsById(termsetId, useSessionStorage);
if (childTerms) {
return [2 /*return*/, childTerms];
}
data = "<Request xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\"Javascript Library\"><Actions><ObjectPath Id=\"1\" ObjectPathId=\"0\" /><ObjectIdentityQuery Id=\"2\" ObjectPathId=\"0\" /><ObjectPath Id=\"4\" ObjectPathId=\"3\" /><ObjectIdentityQuery Id=\"5\" ObjectPathId=\"3\" /><ObjectPath Id=\"7\" ObjectPathId=\"6\" /><ObjectIdentityQuery Id=\"8\" ObjectPathId=\"6\" /><ObjectPath Id=\"10\" ObjectPathId=\"9\" /><Query Id=\"11\" ObjectPathId=\"6\"><Query SelectAllProperties=\"true\"><Properties /></Query></Query><Query Id=\"12\" ObjectPathId=\"9\"><Query SelectAllProperties=\"false\"><Properties /></Query><ChildItemQuery SelectAllProperties=\"false\"><Properties><Property Name=\"IsRoot\" SelectAll=\"true\" /><Property Name=\"Labels\" SelectAll=\"true\" /><Property Name=\"TermsCount\" SelectAll=\"true\" /><Property Name=\"CustomSortOrder\" SelectAll=\"true\" /><Property Name=\"Id\" SelectAll=\"true\" /><Property Name=\"Name\" SelectAll=\"true\" /><Property Name=\"PathOfTerm\" SelectAll=\"true\" /><Property Name=\"Parent\" SelectAll=\"true\" /><Property Name=\"LocalCustomProperties\" SelectAll=\"true\" /><Property Name=\"IsDeprecated\" ScalarProperty=\"true\" /><Property Name=\"IsAvailableForTagging\" ScalarProperty=\"true\" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id=\"0\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"3\" ParentId=\"0\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"6\" ParentId=\"3\" Name=\"GetTermSet\"><Parameters><Parameter Type=\"Guid\">" + termsetId + "</Parameter></Parameters></Method><Method Id=\"9\" ParentId=\"6\" Name=\"GetAllTerms\" /></ObjectPaths></Request>";
reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml');
httpPostOptions = {
headers: reqHeaders,
body: data
};
return [2 /*return*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
return serviceResponse.json().then(function (serviceJSONResponse) {
var termStoreResultTermSets = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermSet'; });
if (termStoreResultTermSets.length > 0) {
var termStoreResultTermSet_1 = termStoreResultTermSets[0];
termStoreResultTermSet_1.Terms = [];
// Retrieve the term collection results
var termStoreResultTerms = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermCollection'; });
if (termStoreResultTerms.length > 0) {
// Retrieve all terms
var terms = termStoreResultTerms[0]._Child_Items_;
if (hideDeprecatedTags === true) {
terms = terms.filter(function (d) { return d.IsDeprecated === false; });
}
if (hideTagsNotAvailableForTagging === true) {
terms = terms.filter(function (d) { return d.IsAvailableForTagging === true; });
}
// Clean the term ID and specify the path depth
terms = terms.map(function (term) {
if (term.IsRoot) {
term.CustomSortOrderIndex = (termStoreResultTermSet_1.CustomSortOrder) ? termStoreResultTermSet_1.CustomSortOrder.split(':').indexOf(_this.cleanGuid(term.Id)) : -1;
}
else {
term.CustomSortOrderIndex = (term['Parent'].CustomSortOrder) ? term['Parent'].CustomSortOrder.split(':').indexOf(_this.cleanGuid(term.Id)) : -1;
}
term.Id = _this.cleanGuid(term.Id);
term['PathDepth'] = term.PathOfTerm.split(';').length;
term.TermSet = { Id: _this.cleanGuid(termStoreResultTermSet_1.Id), Name: termStoreResultTermSet_1.Name };
if (term['Parent']) {
term.ParentId = _this.cleanGuid(term['Parent'].Id);
}
return term;
});
// Check if the term set was not empty
if (terms.length > 0) {
// Sort the terms by PathOfTerm and their depth
terms = _this.sortTerms(terms);
termStoreResultTermSet_1.Terms = terms;
}
}
try {
if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet_1));
}
}
catch (error) {
// Do nothing, sometimes "storage quota exceeded" error if too many items
}
return termStoreResultTermSet_1;
}
return null;
});
})];
}
});
});
};
/**
* Retrieve all terms that starts with the searchText
* @param searchText
*/
SPTermStorePickerService.prototype.searchTermsByName = function (searchText) {
return this.searchTermsByTermSet(searchText);
};
SPTermStorePickerService.prototype.searchTermsByTermId = function (searchText, termId) {
return __awaiter(this, void 0, void 0, function () {
var useSessionStorage, childTerms, _a, termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging, terms;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
useSessionStorage = this.props.useSessionStorage;
childTerms = this.getTermsById(termId, useSessionStorage);
if (!childTerms) return [3 /*break*/, 1];
return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)];
case 1:
_a = this.props, termsetNameOrID = _a.termsetNameOrID, hideDeprecatedTags = _a.hideDeprecatedTags, hideTagsNotAvailableForTagging = _a.hideTagsNotAvailableForTagging;
return [4 /*yield*/, this.getAllTermsByAnchorId(termsetNameOrID, termId, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage)];
case 2:
terms = _b.sent();
if (terms) {
return [2 /*return*/, this.searchTermsBySearchText(terms, searchText)];
}
_b.label = 3;
case 3: return [2 /*return*/, null];
}
});
});
};
/**
* Retrieve all terms for the given term set and anchorId
*/
SPTermStorePickerService.prototype.getAllTermsByAnchorId = function (termsetNameOrID, anchorId, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; }
return __awaiter(this, void 0, void 0, function () {
var _this = this;
var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
returnTerms = [];
childTerms = this.getTermsById(anchorId, useSessionStorage);
if (childTerms) {
return [2 /*return*/, childTerms];
}
return [4 /*yield*/, this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging)];
case 1:
termSet = _a.sent();
terms = termSet.Terms;
if (anchorId) {
anchorTerm_1 = terms.filter(function (t) { return t.Id.toLowerCase() === anchorId.toLowerCase(); }).shift();
if (anchorTerm_1) {
anchorTermPath_1 = anchorTerm_1.PathOfTerm + ";";
anchorTerms = terms.filter(function (t) { return t.PathOfTerm.substring(0, anchorTermPath_1.length) === anchorTermPath_1 && t.Id !== anchorTerm_1.Id; });
anchorTerms.forEach(function (term) {
returnTerms.push(_this.convertTermToPickerTerm(term));
});
try {
if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms));
}
}
catch (error) {
// Do nothing
}
}
}
else {
terms.forEach(function (term) {
returnTerms.push(_this.convertTermToPickerTerm(term));
});
}
return [2 /*return*/, returnTerms];
}
});
});
};
/**
* Clean the Guid from the Web Service response
* @param guid
*/
SPTermStorePickerService.prototype.cleanGuid = function (guid) {
if (guid !== undefined) {
return guid.replace('/Guid(', '').replace('/', '').replace(')', '');
}
else {
return '';
}
};
/**
* Get the term set ID by its name
* @param termstore
* @param termset
*/
SPTermStorePickerService.prototype.getTermSetId = function (termstore, termsetName) {
if (termstore && termstore.length > 0 && termsetName) {
// Get the first term store
var ts = termstore[0];
// Check if the term store contains groups
if (ts.Groups && ts.Groups._Child_Items_) {
for (var _i = 0, _a = ts.Groups._Child_Items_; _i < _a.length; _i++) {
var group = _a[_i];
// Check if the group contains term sets
if (group.TermSets && group.TermSets._Child_Items_) {
for (var _b = 0, _c = group.TermSets._Child_Items_; _b < _c.length; _b++) {
var termSet = _c[_b];
// Check if the term set is found
if (termSet.Name === termsetName) {
return termSet;
}
}
}
}
}
}
return null;
};
SPTermStorePickerService.prototype.getTermsById = function (termId, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; }
try {
if (useSessionStorage && window.sessionStorage) {
var terms = window.sessionStorage.getItem(termId);
if (terms) {
return JSON.parse(terms);
}
else {
return null;
}
}
else {
return null;
}
}
catch (error) {
return null;
}
};
SPTermStorePickerService.prototype.searchTermsBySearchText = function (terms, searchText) {
if (terms) {
return terms.filter(function (t) { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
}
else {
return [];
}
};
/**
* Searches terms for the given term set
* @param searchText
* @param termsetId
*/
SPTermStorePickerService.prototype.searchTermsByTermSet = function (searchText) {
var _this = this;
return new Promise(function (resolve) {
_this.getTermStores().then(function (termStore) {
var termSetId = _this.props.termsetNameOrID;
if (!_this.isGuid(termSetId)) {
// Get the ID of the provided term set name
var crntTermSet = _this.getTermSetId(termStore, termSetId);
if (crntTermSet) {
termSetId = _this.cleanGuid(crntTermSet.Id);
}
else {
resolve(null);
return;
}
}
if (termStore === undefined || termStore.length === 0) {
resolve(null);
return;
}
var loc = _this.context.pageContext.cultureInfo.currentUICultureName === 'de-de' ? 1031 : 1033;
var data = {
start: searchText,
lcid: loc !== 0 ? loc : _this.context.pageContext.web.language,
sspList: _this.cleanGuid(termStore[0].Id),
termSetList: termSetId,
anchorId: _this.props.anchorId ? _this.props.anchorId : EmptyGuid,
isSpanTermStores: false,
isSpanTermSets: false,
isIncludeUnavailable: _this.props.hideTagsNotAvailableForTagging === true,
isIncludeDeprecated: _this.props.hideDeprecatedTags === true,
isAddTerms: false,
isIncludePathData: false,
excludeKeyword: false,
excludedTermset: EmptyGuid
};
var reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/json');
var httpPostOptions = {
headers: reqHeaders,
body: JSON.stringify(data)
};
return _this.context.spHttpClient.post(_this.suggestionServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
return serviceResponse.json().then(function (serviceJSONResponse) {
var groups = serviceJSONResponse.d.Groups;
if (groups && groups.length > 0) {
// Retrieve the term collection results
var terms = groups[0].Suggestions;
if (terms.length > 0) {
// Retrieve all terms
var returnTerms = terms.map(function (term) { return _this.convertSuggestTermToPickerTerm(term); });
resolve(returnTerms);
return;
}
}
resolve([]);
});
});
});
});
};
SPTermStorePickerService.prototype.isGuid = function (strGuid) {
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(strGuid);
};
/**
* Sorting terms based on their path and depth
*
* @param terms
*/
SPTermStorePickerService.prototype.sortTerms = function (terms) {
// Start sorting by depth
var newTermsOrder = [];
var itemsToSort = true;
var pathLevel = 1;
while (itemsToSort) {
// Get terms for the current level
var crntTerms = terms.filter(function (term) { return term.PathDepth === pathLevel; });
if (crntTerms && crntTerms.length > 0) {
crntTerms = crntTerms.sort(this.sortTermByPath);
if (pathLevel !== 1) {
crntTerms = crntTerms.reverse();
var _loop_1 = function (crntTerm) {
var pathElms = crntTerm.PathOfTerm.split(';');
// Last item is not needed for parent path
pathElms.pop();
// Find the parent item and add the new item
var idx = findIndex(newTermsOrder, function (term) { return term.PathOfTerm === pathElms.join(';'); });
if (idx !== -1) {
newTermsOrder.splice(idx + 1, 0, crntTerm);
}
else {
// Push the item at the end if the parent couldn't be found
newTermsOrder.push(crntTerm);
}
};
for (var _i = 0, crntTerms_1 = crntTerms; _i < crntTerms_1.length; _i++) {
var crntTerm = crntTerms_1[_i];
_loop_1(crntTerm);
}
}
else {
newTermsOrder = crntTerms;
}
++pathLevel;
}
else {
itemsToSort = false;
}
}
return newTermsOrder;
};
/**
* Sort the terms by their path
*
* @param a term 2
* @param b term 2
*/
SPTermStorePickerService.prototype.sortTermByPath = function (a, b) {
if (a.CustomSortOrderIndex === -1) {
if (a.PathOfTerm.toLowerCase() < b.PathOfTerm.toLowerCase()) {
return -1;
}
if (a.PathOfTerm.toLowerCase() > b.PathOfTerm.toLowerCase()) {
return 1;
}
return 0;
}
else {
if (a.CustomSortOrderIndex < b.CustomSortOrderIndex) {
return -1;
}
if (a.CustomSortOrderIndex > b.CustomSortOrderIndex) {
return 1;
}
return 0;
}
};
SPTermStorePickerService.prototype.convertTermToPickerTerm = function (term) {
return {
key: this.cleanGuid(term.Id),
name: term.Name,
path: term.PathOfTerm,
termSet: this.cleanGuid(term.TermSet.Id),
termSetName: term.TermSet.Name
};
};
SPTermStorePickerService.prototype.convertSuggestTermToPickerTerm = function (term) {
var path = '';
var termSetName = '';
if (term.Paths && term.Paths.length > 0) {
var fullPath = term.Paths[0].replace(/^\[/, '').replace(/\]$/, '');
var fullPathParts = fullPath.split(':');
path = fullPathParts.join(';') + ';' + term.DefaultLabel;
termSetName = fullPathParts[0];
}
return {
key: this.cleanGuid(term.Id),
name: term.DefaultLabel,
path: path,
termSet: EmptyGuid,
termSetName: termSetName
};
};
return SPTermStorePickerService;
}());
export default SPTermStorePickerService;
//# sourceMappingURL=SPTermStorePickerService.js.map
File diff suppressed because one or more lines are too long
-11
View File
@@ -1,11 +0,0 @@
import { ITaxonomyNavigationService } from './ITaxonomyNavigationService';
import { IMenuItem } from './IMenuItem';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export declare class TaxonomyNavigationService implements ITaxonomyNavigationService {
private context;
private termSetName;
private _taxonomyPickerService;
private _noTerm;
constructor(context: ApplicationCustomizerContext, termSetName: string);
getMenuItems(): Promise<IMenuItem[]>;
}
-120
View File
@@ -1,120 +0,0 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { sp } from '@pnp/sp';
import { MenuItem } from './MenuItem';
import SPTermStorePickerService from './SPTermStorePickerService';
import { ItemDictionary } from './ItemDictionary';
var TaxonomyNavigationService = (function () {
function TaxonomyNavigationService(context /*, private lcid: number*/, termSetName) {
this.context = context; /*, private lcid: number*/
this.termSetName = termSetName;
this._noTerm = {
_ObjectType_: '',
_ObjectIdentity_: '',
CustomSortOrderIndex: 0,
Description: '',
Id: '',
IsAvailableForTagging: false,
IsDeprecated: false,
IsRoot: true,
LocalCustomProperties: {
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
_Sys_Nav_ExcludedProviders: undefined,
_Sys_Nav_SimpleLinkUrl: undefined
},
Name: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
PathOfTerm: '',
TermSet: undefined
};
sp.setup({
spfxContext: context
});
this._taxonomyPickerService = new SPTermStorePickerService({
anchorId: '',
termsetNameOrID: termSetName,
useSessionStorage: true,
hideDeprecatedTags: true,
hideTagsNotAvailableForTagging: false
}, this.context);
}
// Implement the methods from ITaxonomyNavigationService interface here
TaxonomyNavigationService.prototype.getMenuItems = function () {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
var siteCollectionUrl, termset, itemsDict, menuItems;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
siteCollectionUrl = this.context.pageContext.site.absoluteUrl;
return [4 /*yield*/, this._taxonomyPickerService.getAllTerms(this.termSetName)];
case 1:
termset = _a.sent();
itemsDict = new ItemDictionary();
menuItems = [];
if (!termset || !termset.Terms) {
console.warn('No terms found in the term set');
return [2 /*return*/, [new MenuItem(this._noTerm, 0, siteCollectionUrl)]];
}
termset.Terms.forEach(function (term) { return __awaiter(_this, void 0, void 0, function () {
var menuItem, parentItem;
return __generator(this, function (_a) {
menuItem = new MenuItem(term, 0, siteCollectionUrl);
itemsDict.Add(term.Id, menuItem);
if (menuItem.pathDepth === 1) {
menuItems.push(menuItem);
}
else {
parentItem = itemsDict.Get(term.ParentId);
if (parentItem) {
parentItem.items.push(menuItem);
}
else {
console.warn("Item without parent: " + term.PathOfTerm);
}
}
return [2 /*return*/];
});
}); });
return [2 /*return*/, menuItems];
}
});
});
};
return TaxonomyNavigationService;
}());
export { TaxonomyNavigationService };
//# sourceMappingURL=TaxonomyNavigationService.js.map
File diff suppressed because one or more lines are too long
@@ -1,20 +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,3 +0,0 @@
//# sourceMappingURL=IUserCustomActionProps.js.map
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/UserCustomActionService/IUserCustomActionProps.js","sourcesContent":[],"sourceRoot":"..\\..\\..\\src"}
@@ -1,10 +0,0 @@
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,3 +0,0 @@
//# sourceMappingURL=IUserCustomActionService.js.map
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/UserCustomActionService/IUserCustomActionService.js","sourcesContent":[],"sourceRoot":"..\\..\\..\\src"}
@@ -1,5 +0,0 @@
export declare enum UserCustomActionScope {
Web = "web",
Site = "site",
List = "list",
}
@@ -1,8 +0,0 @@
export var UserCustomActionScope;
(function (UserCustomActionScope) {
UserCustomActionScope["Web"] = "web";
UserCustomActionScope["Site"] = "site";
UserCustomActionScope["List"] = "list";
})(UserCustomActionScope = UserCustomActionScope || (UserCustomActionScope = {}));
//# sourceMappingURL=UserCustomActionScope.js.map
@@ -1 +0,0 @@
{"version":3,"sources":["services/UserCustomActionService/UserCustomActionScope.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,qBAIX;AAJD,WAAY,qBAAqB;IAC7B,oCAAW,CAAA;IACX,sCAAa,CAAA;IACb,sCAAa,CAAA;AACjB,CAAC,EAJW,qBAAqB,GAArB,qBAAqB,KAArB,qBAAqB,QAIhC","file":"services/UserCustomActionService/UserCustomActionScope.js","sourcesContent":["export enum UserCustomActionScope {\r\n Web = 'web',\r\n Site = 'site',\r\n List = 'list'\r\n}\r\n"],"sourceRoot":"..\\..\\..\\src"}
@@ -1,13 +0,0 @@
import { UserCustomActionAddResult, UserCustomActionUpdateResult } from '@pnp/sp/src/usercustomactions';
import { IUserCustomActionService } from './IUserCustomActionService';
import { UserCustomActionScope } from './UserCustomActionScope';
import { IUserCustomActionProps } from './IUserCustomActionProps';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export declare class UserCustomActionService implements IUserCustomActionService {
constructor(context: ApplicationCustomizerContext);
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,210 +0,0 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { sp } from '@pnp/sp';
import { UserCustomActionScope } from './UserCustomActionScope';
var UserCustomActionService = (function () {
function UserCustomActionService(context) {
sp.setup({
spfxContext: context
});
}
UserCustomActionService.prototype.getUserCustomActions = function (scope, listId) {
return __awaiter(this, void 0, void 0, function () {
var actions, _a, error_1;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 9, , 10]);
actions = void 0;
_a = scope;
switch (_a) {
case UserCustomActionScope.Web: return [3 /*break*/, 1];
case UserCustomActionScope.Site: return [3 /*break*/, 3];
case UserCustomActionScope.List: return [3 /*break*/, 5];
}
return [3 /*break*/, 7];
case 1: return [4 /*yield*/, sp.web.userCustomActions.get()];
case 2:
actions = _b.sent();
return [3 /*break*/, 8];
case 3: return [4 /*yield*/, sp.site.userCustomActions.get()];
case 4:
actions = _b.sent();
return [3 /*break*/, 8];
case 5:
if (!listId) {
throw new Error('List ID is required for List scope');
}
return [4 /*yield*/, sp.web.lists.getById(listId).userCustomActions.get()];
case 6:
actions = _b.sent();
return [3 /*break*/, 8];
case 7: throw new Error('Invalid scope');
case 8: return [2 /*return*/, actions];
case 9:
error_1 = _b.sent();
console.error('Error getting user custom actions: ', error_1);
throw error_1;
case 10: return [2 /*return*/];
}
});
});
};
UserCustomActionService.prototype.getUserCustomActionById = function (scope, id, listId) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
try {
switch (scope) {
case UserCustomActionScope.Web:
return [2 /*return*/, sp.web.userCustomActions.getById(id)];
case UserCustomActionScope.Site:
return [2 /*return*/, sp.site.userCustomActions.getById(id)];
case UserCustomActionScope.List:
if (!listId) {
throw new Error('List ID is required for List scope');
}
return [2 /*return*/, sp.web.lists.getById(listId).userCustomActions.getById(id)];
default:
throw new Error('Invalid scope');
}
}
catch (error) {
console.error('Error getting user custom action by ID: ', error);
throw error;
}
return [2 /*return*/];
});
});
};
UserCustomActionService.prototype.addUserCustomAction = function (scope, customAction, listId) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
try {
switch (scope) {
case UserCustomActionScope.Web:
return [2 /*return*/, sp.web.userCustomActions.add(customAction)];
case UserCustomActionScope.Site:
return [2 /*return*/, sp.site.userCustomActions.add(customAction)];
case UserCustomActionScope.List:
if (!listId) {
throw new Error('List ID is required for List scope');
}
return [2 /*return*/, sp.web.lists.getById(listId).userCustomActions.add(customAction)];
default:
throw new Error('Invalid scope');
}
}
catch (error) {
console.error('Error adding user custom action: ', error);
throw error;
}
return [2 /*return*/];
});
});
};
UserCustomActionService.prototype.updateUserCustomAction = function (scope, id, props, listId) {
return __awaiter(this, void 0, void 0, function () {
var result, _a, error_2;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 9, , 10]);
result = void 0;
_a = scope;
switch (_a) {
case UserCustomActionScope.Web: return [3 /*break*/, 1];
case UserCustomActionScope.Site: return [3 /*break*/, 3];
case UserCustomActionScope.List: return [3 /*break*/, 5];
}
return [3 /*break*/, 7];
case 1: return [4 /*yield*/, sp.web.userCustomActions.getById(id).update(props)];
case 2:
result = _b.sent();
return [3 /*break*/, 8];
case 3: return [4 /*yield*/, sp.site.userCustomActions.getById(id).update(props)];
case 4:
result = _b.sent();
return [3 /*break*/, 8];
case 5:
if (!listId) {
throw new Error('List ID is required for List scope');
}
return [4 /*yield*/, sp.web.lists.getById(listId).userCustomActions.getById(id).update(props)];
case 6:
result = _b.sent();
return [3 /*break*/, 8];
case 7: throw new Error('Invalid scope');
case 8: return [2 /*return*/, result];
case 9:
error_2 = _b.sent();
console.error('Error updating user custom action: ', error_2);
throw error_2;
case 10: return [2 /*return*/];
}
});
});
};
UserCustomActionService.prototype.deleteUserCustomAction = function (scope, customAction, listId) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
try {
switch (scope) {
case UserCustomActionScope.Web:
return [2 /*return*/, sp.web.userCustomActions.getById(customAction.Id).delete()];
case UserCustomActionScope.Site:
return [2 /*return*/, sp.site.userCustomActions.getById(customAction.Id).delete()];
case UserCustomActionScope.List:
if (!listId) {
throw new Error('List ID is required for List scope');
}
return [2 /*return*/, sp.web.lists.getById(listId).userCustomActions.getById(customAction.Id).delete()];
default:
throw new Error('Invalid scope');
}
}
catch (error) {
console.error('Error deleting user custom action: ', error);
throw error;
}
return [2 /*return*/];
});
});
};
return UserCustomActionService;
}());
export { UserCustomActionService };
//# sourceMappingURL=UserCustomActionService.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "1.0.4", "version": "2.2.5",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+6 -4
View File
@@ -1,15 +1,17 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "1.0.4", "version": "2.2.5",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=6.9.0 <9.0.0"
}, },
"scripts": { "scripts": {
"build": "gulp bundle", "build:classic": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\build-classic.ps1",
"build": "npm run build:classic && gulp bundle",
"clean": "gulp clean", "clean": "gulp clean",
"test": "gulp test" "test": "tsc -p tests/tsconfig.json && node temp/tests/tests/MegaMenuCore.test.js && node --check classic/megamenu-services-standalone.js && node --check classic/megamenu-classic.js && node tests/validate-static-assets.js",
"package": "npm test && npm run build:classic && gulp clean && gulp bundle --ship && gulp package-solution --ship"
}, },
"dependencies": { "dependencies": {
"@microsoft/decorators": "~1.4.1", "@microsoft/decorators": "~1.4.1",
+97
View File
@@ -0,0 +1,97 @@
<!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">
<button type="button" class="mega-menu-mobile-toggle" aria-expanded="false" aria-controls="Mega-Menu-Items" aria-label="Hauptnavigation öffnen"><span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label">Hauptnavigation</span></button>
<ul id="Mega-Menu-Items" 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">Unternehmen</a>
<button type="button" class="menu-item-toggle" aria-haspopup="true" aria-expanded="true" aria-controls="Mega-Menu-Panel-Unternehmen" aria-label="Untermenü schließen für Unternehmen"><span aria-hidden="true"></span></button>
<div id="Mega-Menu-Panel-Unternehmen" 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">Über uns</a>
<button type="button" class="flyout-toggle" aria-expanded="false" aria-controls="Flyout-UeberUns" aria-label="Untermenü öffnen für Über uns"><span aria-hidden="true"></span></button>
<ul id="Flyout-UeberUns" 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">Organisation</a>
<button type="button" class="flyout-toggle" aria-expanded="false" aria-controls="Flyout-Organisation" aria-label="Untermenü öffnen für Organisation"><span aria-hidden="true"></span></button>
<ul id="Flyout-Organisation" 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 > .menu-item-toggle');
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));
});
});
var mobile = document.querySelector('.mega-menu-mobile-toggle');
mobile.addEventListener('click', function () { var open = mobile.getAttribute('aria-expanded') !== 'true'; mobile.setAttribute('aria-expanded', String(open)); document.getElementById('Mega-Menu').classList.toggle('is-mobile-open', open); });
Array.prototype.forEach.call(document.querySelectorAll('.flyout-toggle'), function (toggle) { toggle.addEventListener('click', function () { var open = toggle.getAttribute('aria-expanded') !== 'true'; toggle.setAttribute('aria-expanded', String(open)); toggle.parentElement.classList.toggle('is-open', open); }); });
}());
</script>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!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">
<button type="button" class="mega-menu-mobile-toggle" aria-expanded="false" aria-controls="Mega-Menu-Items" aria-label="Hauptnavigation öffnen"><span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label">Hauptnavigation</span></button>
<ul id="Mega-Menu-Items" 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">Unternehmen</a>
<button type="button" class="menu-item-toggle" aria-haspopup="true" aria-expanded="true" aria-controls="Mega-Menu-Panel-Unternehmen" aria-label="Untermenü schließen für Unternehmen"><span aria-hidden="true"></span></button>
<div id="Mega-Menu-Panel-Unternehmen" 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 > .menu-item-toggle');
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));
});
});
var mobile = document.querySelector('.mega-menu-mobile-toggle');
mobile.addEventListener('click', function () { var open = mobile.getAttribute('aria-expanded') !== 'true'; mobile.setAttribute('aria-expanded', String(open)); document.getElementById('Mega-Menu').classList.toggle('is-mobile-open', open); });
}());
</script>
</body>
</html>
+388 -330
View File
@@ -1,383 +1,386 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; @import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
/* $white: "[theme: white, default: #ffffff]";
Global override example: $neutralLighter: "[theme: neutralLighter, default: #f4f4f4]";
:root { $neutralLight: "[theme: neutralLight, default: #eaeaea]";
--themePrimary: #00407f; $neutralTertiary: "[theme: neutralTertiary, default: #a6a6a6]";
--themeLighterAlt: #f0f5fa; $neutralSecondary: "[theme: neutralSecondary, default: #666666]";
--themeLighter: #c5d8eb; $neutralPrimary: "[theme: neutralPrimary, default: #333333]";
--themeLight: #98b8d9; $themePrimary: "[theme: themePrimary, default: #0078d7]";
--themeTertiary: #477db3; $themeDark: "[theme: themeDark, default: #005a9e]";
--themeSecondary: #11508f; $themeLighterAlt: "[theme: themeLighterAlt, default: #eff6fc]";
--themeDarkAlt: #003973; $menuShadow: 0 8px 24px rgba(0, 0, 0, 0.16);
--themeDark: #003061; $menuLayer: var(--megaMenuZIndex, 6000);
--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);
:global { :global {
:root { :root {
--megaMenuNavBackground: #5f6f74; --megaMenuBarHeight: 44px;
--megaMenuNavHoverBackground: #6f8085; --megaMenuContentWidth: 1200px;
--megaMenuNavOpenBackground: #728388; --megaMenuFlyoutWidth: 288px;
--megaMenuNavTextColor: #ffffff; --megaMenuZIndex: 9.5;
--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 { #CustomNavigation {
position: relative; position: relative;
box-sizing: border-box;
width: 100%;
overflow: visible; overflow: visible;
z-index: $mm-layer-index; border-top: 1px solid #edebe9;
border-bottom: 1px solid #edebe9;
z-index: $menuLayer;
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 $neutralLight;
background: $themeLighterAlt;
color: $neutralPrimary;
font-size: 13px;
} }
.mega-menu-main { .mega-menu-main {
position: relative; position: relative;
box-sizing: border-box;
width: 100%; width: 100%;
background-color: #5f6f74; min-height: var(--megaMenuBarHeight, 44px);
background-color: $mm-nav-background;
border-bottom: 1px solid rgba(0, 0, 0, 0.18);
box-shadow: none;
overflow: visible; overflow: visible;
z-index: $mm-layer-index; border-top: 0;
border-bottom: 0;
background: $white;
color: $neutralPrimary;
z-index: $menuLayer;
} }
.mega-menu-main > ul,
.mega-menu-top-level { .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; display: flex;
align-items: stretch; align-items: stretch;
justify-content: flex-start; box-sizing: border-box;
gap: 0; 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 { .mega-menu-top-item {
position: static; position: static;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex: 0 0 auto; margin: 0;
border-right: 1px solid $mm-nav-divider; padding: 0;
} }
.mega-menu-main > ul > li:last-child, .mega-menu-top-item:hover,
.mega-menu-top-item:last-child { .mega-menu-top-item:focus-within,
border-right: 0; .mega-menu-top-item:active,
.mega-menu-top-item.is-open,
.mega-menu-top-item.is-active {
background: $neutralLighter;
color: $neutralPrimary;
}
.mega-menu-mobile-toggle {
display: none;
}
.mega-menu-mode-flyout .mega-menu-top-item {
position: relative;
} }
.mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"],
.menu-item-link, .menu-item-link,
.menu-item-text { .menu-item-text {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
min-height: $mm-nav-height; box-sizing: border-box;
padding: 0 $mm-item-inline-padding; min-height: var(--megaMenuBarHeight, 44px);
color: $mm-nav-text; padding: 0 16px;
font-size: 12px;
font-weight: 400;
line-height: 1;
text-decoration: none;
background: transparent;
border: 0; border: 0;
transition: background-color 0.14s ease, color 0.14s ease; background: transparent;
outline: none; color: $neutralPrimary;
cursor: pointer;
font-size: 14px;
font-weight: 400;
line-height: 20px;
text-decoration: none;
white-space: nowrap; white-space: nowrap;
outline: none;
} }
.menu-item-text { .menu-item-toggle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 32px;
min-height: var(--megaMenuBarHeight, 44px);
margin: 0 8px 0 -10px;
padding: 0;
border: 0;
background: transparent;
color: $neutralPrimary;
cursor: pointer; cursor: pointer;
} }
.menu-item-has-children::after { .menu-item-toggle > span::after {
content: ''; content: '';
width: 5px; display: block;
height: 5px; width: 6px;
margin-left: 8px; height: 6px;
border-right: 1px solid currentColor; border-right: 1px solid currentColor;
border-bottom: 1px solid currentColor; border-bottom: 1px solid currentColor;
transform: translateY(-1px) rotate(45deg); transform: rotate(45deg);
transition: transform 0.14s ease;
} }
.menu-item-has-children[aria-expanded="true"]::after { .menu-item-toggle[aria-expanded='true'] > span::after {
transform: translateY(1px) rotate(-135deg); transform: rotate(225deg);
} }
.mega-menu-top-item.is-active > a, .menu-item-link:hover,
.mega-menu-top-item.is-active > span, .menu-item-text:hover,
.menu-item-link.is-active, .menu-item-toggle:hover,
.menu-item-text.is-active { .menu-item-toggle[aria-expanded='true'] {
background: $mm-nav-open-background; background: transparent;
color: inherit;
text-decoration: none;
} }
.mega-menu-top-item.is-current > a, .mega-menu-top-item.is-active > .menu-item-link::before,
.mega-menu-top-item.is-current > span, .mega-menu-top-item.is-active > .menu-item-text::before,
.menu-item-link.is-current, .mega-menu-top-item.is-current > .menu-item-link::before,
.menu-item-text.is-current { .mega-menu-top-item.is-current > .menu-item-text::before {
content: '';
position: absolute;
right: 12px;
bottom: 0;
left: 12px;
height: 2px;
background: $themePrimary;
}
.mega-menu-top-item.is-current > .menu-item-link,
.mega-menu-top-item.is-current > .menu-item-text {
color: $themeDark;
font-weight: 600; font-weight: 600;
} }
.mega-menu-main > ul > li:hover > a,
.mega-menu-main > ul > li:hover > span[role="menuitem"],
.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 > span[role="menuitem"]:focus,
.menu-item-link:focus, .menu-item-link:focus,
.menu-item-text:focus { .menu-item-text:focus,
color: $mm-nav-text; .menu-item-toggle:focus,
background: $mm-nav-open-background; .mega-menu-mobile-toggle:focus,
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.65); .mega-menu-category a:focus,
.flyout-link:focus {
outline: 2px solid $themePrimary;
outline-offset: -2px;
} }
.mega-menu { .mega-menu {
position: absolute; position: absolute;
top: 100%;
left: 0; left: 0;
right: 0; right: 0;
top: 100%;
display: none; display: none;
box-sizing: border-box;
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
background-color: #ffffff; border: 1px solid #edebe9;
background-color: $mm-panel-surface;
background-image: none;
mix-blend-mode: normal;
border: 1px solid $mm-panel-border;
border-top: 0; border-top: 0;
box-shadow: $mm-panel-shadow; background: $white;
max-height: calc(100vh - #{$mm-nav-height}); box-shadow: $menuShadow;
isolation: isolate; color: $neutralPrimary;
overflow-y: auto; z-index: calc(#{$menuLayer} + 1);
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 { .mega-menu.js-open {
display: block;
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
display: block;
background-color: #ffffff;
background-color: $mm-panel-surface;
background-image: none;
} }
.mega-menu-grid { .mega-menu-grid {
max-width: $mm-panel-width;
background-color: inherit;
margin: 0 auto;
display: grid; display: grid;
grid-template-columns: repeat(var(--megaMenuColumnCount, 4), minmax(0, 1fr)); grid-template-columns: repeat(var(--megaMenuColumnCount, 6), minmax(0, 1fr));
gap: 0; box-sizing: border-box;
align-items: start; width: 100%;
max-width: var(--megaMenuContentWidth, 1200px);
margin: 0;
padding: 24px;
gap: 24px 32px;
background: $white;
} }
.mega-menu-category { .mega-menu-category {
min-width: 0; min-width: 0;
padding: $mm-column-padding-y $mm-column-padding-x 14px; padding: 0 0 8px;
border-right: 1px solid $mm-panel-divider;
} }
.mega-menu-grid > .mega-menu-category:last-child { .mega-menu-description {
border-right: 0; margin: -4px 0 12px;
color: $neutralSecondary;
font-size: 12px;
line-height: 18px;
} }
.mega-menu-category > h3,
.mega-menu-category-title { .mega-menu-category-title {
margin: 0 0 10px; margin: 0 0 10px;
padding: 0 0 8px;
border-bottom: 1px solid $neutralLight;
}
.mega-menu-category-title > a,
.mega-menu-category-title > span {
display: block;
color: $neutralPrimary;
font-size: 14px;
font-weight: 600;
line-height: 20px;
text-decoration: none;
}
.mega-menu-category-title > a:hover {
color: $themePrimary;
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: $neutralSecondary;
font-size: 14px;
line-height: 20px;
text-decoration: none;
}
.mega-menu-links a:hover {
background: $neutralLighter;
color: $neutralPrimary;
text-decoration: none;
}
.mega-menu-links a.is-current,
.mega-menu-links li.is-current > a {
color: $themeDark;
font-weight: 600;
}
.mega-menu-flyout {
right: auto;
width: var(--megaMenuFlyoutWidth, 288px);
min-width: 240px;
border-top: 1px solid $neutralLight;
}
.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: $neutralPrimary;
cursor: pointer;
font-size: 14px;
line-height: 20px;
text-decoration: none;
outline: none;
}
.flyout-item:hover > .flyout-link,
.flyout-item:focus-within > .flyout-link,
.flyout-item:active > .flyout-link,
.flyout-item.is-active > .flyout-link {
background: $neutralLighter;
color: $neutralPrimary;
text-decoration: none;
}
.flyout-item.is-current > .flyout-link {
color: $themeDark;
font-weight: 600;
}
.flyout-toggle {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0; padding: 0;
border: 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; background: transparent;
color: $mm-text; color: $neutralPrimary;
text-decoration: underline; cursor: pointer;
} }
.mega-menu-category ul li a:focus, .flyout-toggle > span::after {
.mega-menu-links li a:focus { content: '';
background: $mm-focus-soft; display: block;
color: $mm-text; width: 6px;
box-shadow: 0 0 0 2px $mm-focus-accent; height: 6px;
outline: none; border-top: 1px solid currentColor;
border-right: 1px solid currentColor;
transform: rotate(45deg);
}
.flyout-toggle:focus {
outline: 2px solid $themePrimary;
outline-offset: -2px;
}
.flyout-level-3 {
position: absolute;
top: -9px;
left: 100%;
display: none;
width: var(--megaMenuFlyoutWidth, 288px);
border: 1px solid $neutralLight;
box-shadow: $menuShadow;
z-index: calc(#{$menuLayer} + 2);
}
.flyout-item:hover > .flyout-level-3,
.flyout-item:focus-within > .flyout-level-3,
.flyout-item.is-open > .flyout-level-3 {
display: block;
} }
.sr-only { .sr-only {
@@ -393,82 +396,137 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.mega-menu-grid { .mega-menu-top-level {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); padding: 0 12px;
} }
.mega-menu-category { .mega-menu-grid {
padding: 14px 16px 12px; grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 20px;
} }
} }
@media (max-width: 767px) { @media (max-width: 767px) {
.mega-menu-main > ul, .mega-menu-mobile-toggle {
display: flex;
align-items: center;
width: 100%;
min-height: var(--megaMenuBarHeight, 44px);
padding: 0 16px;
border: 0;
background: $white;
color: $neutralPrimary;
cursor: pointer;
font: inherit;
font-weight: 600;
}
.mega-menu-mobile-icon,
.mega-menu-mobile-icon::before,
.mega-menu-mobile-icon::after {
display: block;
width: 18px;
height: 2px;
background: currentColor;
}
.mega-menu-mobile-icon {
position: relative;
margin-right: 12px;
}
.mega-menu-mobile-icon::before,
.mega-menu-mobile-icon::after {
content: '';
position: absolute;
left: 0;
}
.mega-menu-mobile-icon::before { top: -6px; }
.mega-menu-mobile-icon::after { top: 6px; }
.mega-menu-top-level { .mega-menu-top-level {
padding: 0 6px; display: none;
padding-inline-start: 6px; flex-direction: column;
flex-wrap: wrap; padding: 0;
border-top: 1px solid $neutralLight;
} }
.mega-menu-main > ul > li, .mega-menu-main.is-mobile-open .mega-menu-top-level {
.mega-menu-top-item { display: flex;
border-right: 0; }
.mega-menu-top-item,
.mega-menu-mode-flyout .mega-menu-top-item {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 44px;
border-bottom: 1px solid $neutralLight;
} }
.mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"],
.menu-item-link, .menu-item-link,
.menu-item-text { .menu-item-text {
min-height: 36px; min-height: 40px;
padding: 0 10px; padding: 0 12px;
font-size: 12px; font-size: 13px;
}
.menu-item-toggle {
width: 44px;
margin: 0;
border-left: 0;
} }
.mega-menu { .mega-menu {
border-left: 0; position: static;
grid-column: 1 / -1;
width: 100%;
border-right: 0; border-right: 0;
border-left: 0;
box-shadow: none;
} }
.mega-menu-grid { .mega-menu-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: 16px;
} }
.mega-menu-category { .mega-menu-mode-flyout .mega-menu-flyout {
padding: 14px 12px; position: fixed;
border-right: 0; right: 8px;
border-bottom: 1px solid $mm-panel-divider; left: 8px;
width: auto;
} }
.mega-menu-grid > .mega-menu-category:last-child { .flyout-level-3 {
border-bottom: 0; position: static;
display: none;
width: 100%;
border: 0;
box-shadow: none;
padding: 0 0 4px 16px;
}
.flyout-item.is-open > .flyout-level-3 {
display: block;
} }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.skip-link, .mega-menu,
.menu-item-has-children::after,
.mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"],
.menu-item-link, .menu-item-link,
.menu-item-text, .menu-item-text,
.mega-menu, .flyout-link {
.mega-menu-category ul li a,
.mega-menu-links li a {
transition: none; transition: none;
} }
} }
@media (prefers-contrast: high) { @media (forced-colors: active) {
.mega-menu-main > ul > li > a:focus,
.mega-menu-main > ul > li > span[role="menuitem"]:focus,
.menu-item-link:focus, .menu-item-link:focus,
.menu-item-text:focus, .menu-item-text:focus,
.mega-menu-category > h3 > a:focus, .mega-menu-category a:focus,
.mega-menu-category ul li a:focus, .flyout-link:focus {
.mega-menu-links li a:focus { outline: 2px solid CanvasText;
outline: 2px solid; }
outline-offset: 2px;
box-shadow: none;
} }
} }
} /* End :global */
@@ -1,8 +1,6 @@
/* tslint:disable */ /* tslint:disable */
require('./MegaMenu.module.css'); require('./MegaMenu.module.css');
const styles = { const styles = {
mmSlideIn: 'mmSlideIn_09a8e1a7',
mmFadeIn: 'mmFadeIn_09a8e1a7',
}; };
export default styles; export default styles;
@@ -1,7 +1,7 @@
{ {
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json",
"id": "abc3361f-bb2d-491f-aba3-cd51c19a299b", "id": "c0abbffb-355d-4d4e-bd38-9e15fb811506",
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
@@ -15,139 +15,311 @@ import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationServ
import { MegaMenuRenderer } from './MegaMenuRenderer'; import { MegaMenuRenderer } from './MegaMenuRenderer';
import { IMenuItem } from '../../services/IMenuItem'; import { IMenuItem } from '../../services/IMenuItem';
import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug'; import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
import { MegaMenuMode, normalizeMegaMenuMode } from './MegaMenuMode';
import './MegaMenu.module.scss'; import './MegaMenu.module.scss';
const LOG_SOURCE: string = 'MegaMenuApplicationCustomizer'; const LOG_SOURCE: string = 'MegaMenuApplicationCustomizer';
export const UserCustomActionMegaMenuId: string = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; export const UserCustomActionMegaMenuId: string = 'c0abbffb-355d-4d4e-bd38-9e15fb811506';
export interface IMegaMenuApplicationCustomizerProperties { export interface IMegaMenuApplicationCustomizerProperties {
termSetName: string; termSetName?: string;
cssUrl?: string; termSetId?: string;
cacheMinutes?: number;
debug?: boolean; debug?: boolean;
menuMode?: MegaMenuMode;
cacheVersion?: string;
} }
export default class MegaMenuApplicationCustomizer export default class MegaMenuApplicationCustomizer
extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> { extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined; private _topPlaceholder: PlaceholderContent | undefined;
private _menuContainer: HTMLElement | undefined;
private _renderer: MegaMenuRenderer | undefined;
private _statusElement: HTMLElement | undefined;
private _customHeaderObserver: MutationObserver | undefined;
@override @override
public onInit(): Promise<void> { public onInit(): Promise<void> {
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Initialized ' + strings.Title); const debug: boolean = this._isDebugEnabled();
debugLog(debug, LOG_SOURCE, 'onInit started.', {
if (this.properties.cssUrl) { componentId: UserCustomActionMegaMenuId,
this._loadExternalCss(this.properties.cssUrl); version: '2.2.5',
} properties: this.properties || {},
pageUrl: window.location.href,
webUrl: this.context.pageContext.web.absoluteUrl,
siteUrl: this.context.pageContext.site.absoluteUrl
});
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders); this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
debugLog(debug, LOG_SOURCE, 'Placeholder changedEvent handler registered.');
this._renderPlaceHolders(); this._renderPlaceHolders();
debugLog(debug, LOG_SOURCE, 'onInit completed. Asynchronous menu loading may still be running.');
return Promise.resolve(); return Promise.resolve();
} }
private _renderPlaceHolders(): void { private _renderPlaceHolders(): void {
const debug: boolean = this._isDebugEnabled();
const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames
.map(name => PlaceholderName[name]) .map(name => PlaceholderName[name])
.join(', '); .join(', ');
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders); debugLog(debug, LOG_SOURCE, 'Rendering placeholders.', {
availablePlaceholders: availablePlaceholders,
topPlaceholderAlreadyCreated: !!this._topPlaceholder
});
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
debugLog(debug, LOG_SOURCE, 'Requesting the Top placeholder.');
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent( this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top, PlaceholderName.Top,
{ onDispose: this._onDispose } { onDispose: this._onDispose }
); );
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
debugError(this._isDebugEnabled(), LOG_SOURCE, 'The expected placeholder (Top) was not found.'); debugError(debug, LOG_SOURCE, 'The expected placeholder (Top) was not found.');
return; return;
} }
if (!this.properties.termSetName) { debugLog(debug, LOG_SOURCE, 'Top placeholder acquired.', {
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetName configured. Mega Menu rendering is skipped.'); domElementId: this._topPlaceholder.domElement.id || '',
domElementClassName: this._topPlaceholder.domElement.className || ''
});
const termSetIdentifier: string = this._getTermSetIdentifier();
if (!termSetIdentifier) {
debugWarn(debug, LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.', this.properties || {});
this._renderStatus(strings.ConfigurationMissing);
return; return;
} }
this._renderMegaMenu(this.properties.termSetName); debugLog(debug, LOG_SOURCE, 'Starting MegaMenu rendering.', {
termSetIdentifier: termSetIdentifier,
cacheMinutes: this._getCacheMinutes(),
menuMode: this._getMenuMode()
});
this._renderMegaMenu(termSetIdentifier, debug);
} else {
debugLog(debug, LOG_SOURCE, 'Top placeholder is already initialized; no duplicate render is started.');
} }
} }
private async _renderMegaMenu(termSetName: string, debug: boolean = this._isDebugEnabled()): Promise<void> { private async _renderMegaMenu(termSetIdentifier: string, debug: boolean = this._isDebugEnabled()): Promise<void> {
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
return; return;
} }
this._renderStatus(strings.Loading);
try { try {
debugLog(debug, LOG_SOURCE, 'Creating TaxonomyNavigationService.');
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService( const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
this.context, this.context,
termSetName, termSetIdentifier,
debug debug,
this._getCacheMinutes(),
this._getCacheVersion(),
this._getLanguageLcid()
); );
debugLog(debug, LOG_SOURCE, 'Requesting navigation items from taxonomy.');
const menuItems: IMenuItem[] = await taxonomyService.getMenuItems(); const menuItems: IMenuItem[] = await taxonomyService.getMenuItems();
debugLog(debug, LOG_SOURCE, 'Navigation items received.', {
topLevelItemCount: menuItems.length
});
const renderer: MegaMenuRenderer = new MegaMenuRenderer( if (!menuItems || menuItems.length === 0) {
this._renderStatus(strings.EmptyNavigation);
return;
}
this._clearStatus();
if (this._renderer) {
debugLog(debug, LOG_SOURCE, 'Disposing the previous renderer instance.');
this._renderer.dispose();
}
this._renderer = new MegaMenuRenderer(
menuItems, menuItems,
debug debug,
this._getMenuMode(),
{
navigationLabel: strings.NavigationLabel,
openNavigation: strings.OpenNavigation,
closeNavigation: strings.CloseNavigation,
openSubmenu: strings.OpenSubmenu,
closeSubmenu: strings.CloseSubmenu,
submenuLabel: strings.SubmenuLabel,
externalLink: strings.ExternalLink,
opened: strings.Opened,
closed: strings.Closed
}
); );
const container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder); this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
if (container) { if (this._menuContainer) {
renderer.render(container); debugLog(debug, LOG_SOURCE, 'Rendering into the selected container.', {
id: this._menuContainer.id || '',
className: this._menuContainer.className || ''
});
this._renderer.render(this._menuContainer);
} else { } else {
renderer.render(this._topPlaceholder.domElement); debugWarn(debug, LOG_SOURCE, 'No dedicated container was returned; rendering into the Top placeholder.');
this._renderer.render(this._topPlaceholder.domElement);
} }
debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.'); debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
} catch (error) { } catch (error) {
debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error); debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', this._describeError(error), error);
this._renderStatus(strings.LoadError);
} }
} }
private _getOrCreateContainer(id: string, placeholder: PlaceholderContent): HTMLElement { private _getOrCreateContainer(id: string, placeholder: PlaceholderContent): HTMLElement {
const container = document.getElementById(id); const container: HTMLElement = document.getElementById(id);
let host: HTMLElement = this._menuContainer || document.getElementById('MegaMenuHost') ||
document.getElementById('CustomNavigation');
if (container) { if (container) {
const div = document.createElement('div'); debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing host container found.', {
container.appendChild(div); requestedId: id,
return div; containerId: container.id,
containerClassName: container.className || ''
});
if (!host) {
host = document.createElement('div');
host.id = 'MegaMenuHost';
}
if (host.parentElement !== container) { container.appendChild(host); }
this._stopCustomHeaderObserver();
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'MegaMenu host is located inside the existing CustomHeader.');
return host;
} }
return placeholder.domElement; if (!host) {
host = document.createElement('div');
host.id = 'MegaMenuHost';
}
if (host.parentElement !== placeholder.domElement) { placeholder.domElement.appendChild(host); }
this._startCustomHeaderObserver(id);
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'No external host container found; using a temporary host in the Top placeholder.', {
requestedId: id
});
return host;
} }
private _loadExternalCss(cssUrl?: string, debug: boolean = this._isDebugEnabled()): void { private _startCustomHeaderObserver(id: string): void {
const externalCssLinkId: string = 'mega-menu-additional-css-34FAB720'; if (this._customHeaderObserver || typeof MutationObserver === 'undefined') { return; }
let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement; this._customHeaderObserver = new MutationObserver((): void => {
const customHeader: HTMLElement = document.getElementById(id);
if (cssUrl && cssUrl.trim() !== '') { if (!customHeader || !this._menuContainer || this._menuContainer === customHeader) { return; }
if (!link) { if (this._menuContainer.parentElement !== customHeader) {
const head: HTMLHeadElement = document.getElementsByTagName('head')[0]; customHeader.appendChild(this._menuContainer);
link = document.createElement('link'); debugLog(this._isDebugEnabled(), LOG_SOURCE, 'MegaMenu host moved into the late CustomHeader.', {
link.rel = 'stylesheet'; containerId: this._menuContainer.id || ''
link.type = 'text/css'; });
link.id = externalCssLinkId; }
link.onload = () => { this._stopCustomHeaderObserver();
debugLog(debug, LOG_SOURCE, 'External CSS loaded successfully from:', cssUrl); });
}; this._customHeaderObserver.observe(document.documentElement, { childList: true, subtree: true });
link.onerror = () => { debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Waiting for CustomHeader DOM registration.');
debugWarn(debug, LOG_SOURCE, 'Failed to load external CSS from:', cssUrl);
};
head.appendChild(link);
} }
link.href = cssUrl; private _stopCustomHeaderObserver(): void {
} else if (link) { if (this._customHeaderObserver) {
link.remove(); this._customHeaderObserver.disconnect();
this._customHeaderObserver = undefined;
} }
} }
private _onDispose = (): void => { private _onDispose = (): void => {
this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders);
this._stopCustomHeaderObserver();
this._clearStatus();
if (this._renderer) {
this._renderer.dispose();
this._renderer = undefined;
}
if (this._menuContainer && this._menuContainer.parentElement) {
this._menuContainer.parentElement.removeChild(this._menuContainer);
}
this._menuContainer = undefined;
this._topPlaceholder = undefined;
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.'); debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
} }
private _getTermSetIdentifier(): string {
const termSetId: string = this.properties && this.properties.termSetId
? this.properties.termSetId.trim()
: '';
if (termSetId && this._isGuid(termSetId)) {
return termSetId;
}
return this.properties && this.properties.termSetName
? this.properties.termSetName.trim()
: '';
}
private _getCacheMinutes(): number {
const value: number = this.properties ? Number(this.properties.cacheMinutes) : 15;
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
}
private _getMenuMode(): MegaMenuMode {
return normalizeMegaMenuMode(this.properties && this.properties.menuMode);
}
private _getCacheVersion(): string {
const value: string = this.properties && this.properties.cacheVersion
? String(this.properties.cacheVersion).trim()
: '1';
return value || '1';
}
private _getLanguageLcid(): number {
const legacyContext: any = (this.context.pageContext as any).legacyPageContext; // tslint:disable-line:no-any
const value: number = legacyContext ? Number(legacyContext.currentLanguage) : 0;
return isFinite(value) && value > 0 ? value : 1033;
}
private _renderStatus(message: string): void {
if (!this._topPlaceholder) {
return;
}
this._clearStatus();
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
const container: HTMLElement = this._menuContainer || this._topPlaceholder.domElement;
container.innerHTML = '';
const status: HTMLElement = document.createElement('div');
status.className = 'mega-menu-status';
status.setAttribute('role', 'status');
status.textContent = message;
container.appendChild(status);
this._statusElement = status;
}
private _clearStatus(): void {
if (this._statusElement && this._statusElement.parentNode) {
this._statusElement.parentNode.removeChild(this._statusElement);
}
this._statusElement = undefined;
}
private _isGuid(value: string): boolean {
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
}
private _describeError(error: any): { name: string; message: string; stack: string } { // tslint:disable-line:no-any
return {
name: error && error.name ? String(error.name) : '',
message: error && error.message ? String(error.message) : String(error || 'Unknown error'),
stack: error && error.stack ? String(error.stack) : ''
};
}
private _isDebugEnabled(debugOverride?: boolean): boolean { private _isDebugEnabled(debugOverride?: boolean): boolean {
if (typeof debugOverride === 'boolean') { if (typeof debugOverride === 'boolean') {
return debugOverride; return debugOverride;
+5
View File
@@ -0,0 +1,5 @@
export type MegaMenuMode = 'megaMenu' | 'flyout';
export function normalizeMegaMenuMode(value?: string): MegaMenuMode {
return value && value.toLowerCase() === 'flyout' ? 'flyout' : 'megaMenu';
}
+347 -79
View File
@@ -5,33 +5,70 @@
import { IMenuItem } from '../../services/IMenuItem'; import { IMenuItem } from '../../services/IMenuItem';
import { debugLog } from '../../services/MegaMenuDebug'; import { debugLog } from '../../services/MegaMenuDebug';
import { MegaMenuMode } from './MegaMenuMode';
import styles from './MegaMenu.module.scss'; // tslint:disable-line:no-unused-variable import { isNavigationUrlActive } from '../../services/MegaMenuCore';
const LOG_SOURCE: string = 'MegaMenuRenderer'; const LOG_SOURCE: string = 'MegaMenuRenderer';
export interface IMegaMenuRendererStrings {
navigationLabel: string;
openNavigation: string;
closeNavigation: string;
openSubmenu: string;
closeSubmenu: string;
submenuLabel: string;
externalLink: string;
opened: string;
closed: string;
}
export class MegaMenuRenderer { export class MegaMenuRenderer {
private static readonly hoverOpenDelayMs: number = 140; private static readonly hoverOpenDelayMs: number = 0;
private static readonly hoverCloseDelayMs: number = 180; private static readonly hoverCloseDelayMs: number = 0;
private _container: HTMLElement | undefined;
private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined;
private _documentClickHandler: ((event: MouseEvent) => void) | undefined;
private _announcer: HTMLElement | undefined;
private _mobileToggle: HTMLButtonElement | undefined;
constructor( constructor(
private menuItems: IMenuItem[], private menuItems: IMenuItem[],
private debug: boolean = false private debug: boolean = false,
private menuMode: MegaMenuMode = 'megaMenu',
private strings: IMegaMenuRendererStrings
) { } ) { }
public render(container: HTMLElement) { public render(container: HTMLElement): void {
debugLog(this.debug, LOG_SOURCE, 'Render started.', {
menuItemCount: this.menuItems.length,
containerId: container.id || '',
containerClassName: container.className || ''
});
this.dispose();
this._container = container;
container.innerHTML = ''; container.innerHTML = '';
container.id = 'CustomNavigation'; container.id = 'CustomNavigation';
const nav = document.createElement('nav'); const nav = document.createElement('nav');
nav.id = 'Mega-Menu'; nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main'; nav.className = this.joinClasses('mega-menu-main', 'mega-menu-mode-' + this.menuMode);
nav.setAttribute('role', 'navigation'); nav.setAttribute('data-menu-mode', this.menuMode);
nav.setAttribute('aria-label', 'Hauptnavigation'); nav.setAttribute('aria-label', this.strings.navigationLabel);
this._mobileToggle = document.createElement('button');
this._mobileToggle.type = 'button';
this._mobileToggle.className = 'mega-menu-mobile-toggle';
this._mobileToggle.setAttribute('aria-expanded', 'false');
this._mobileToggle.setAttribute('aria-controls', 'Mega-Menu-Items');
this._mobileToggle.setAttribute('aria-label', this.strings.openNavigation);
this._mobileToggle.innerHTML = '<span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label"></span>';
const mobileLabel = this._mobileToggle.querySelector('.mega-menu-mobile-label') as HTMLElement;
mobileLabel.textContent = this.strings.navigationLabel;
nav.appendChild(this._mobileToggle);
const topLevelUl = document.createElement('ul'); const topLevelUl = document.createElement('ul');
topLevelUl.className = 'mega-menu-top-level'; topLevelUl.className = 'mega-menu-top-level';
topLevelUl.setAttribute('role', 'menubar'); topLevelUl.id = 'Mega-Menu-Items';
this.menuItems.forEach(topLevelItem => { this.menuItems.forEach(topLevelItem => {
const topLevelLi = this.createTopLevelItem(topLevelItem); const topLevelLi = this.createTopLevelItem(topLevelItem);
@@ -43,6 +80,35 @@ export class MegaMenuRenderer {
this.attachEventListeners(); this.attachEventListeners();
this.createScreenReaderAnnouncer(); this.createScreenReaderAnnouncer();
debugLog(this.debug, LOG_SOURCE, 'Render completed.', {
navigationId: nav.id,
topLevelElements: topLevelUl.children.length
});
}
public dispose(): void {
debugLog(this.debug, LOG_SOURCE, 'Dispose started.', {
hasContainer: !!this._container,
hasAnnouncer: !!this._announcer
});
if (this._documentKeydownHandler) {
document.removeEventListener('keydown', this._documentKeydownHandler);
this._documentKeydownHandler = undefined;
}
if (this._documentClickHandler) {
document.removeEventListener('click', this._documentClickHandler);
this._documentClickHandler = undefined;
}
if (this._announcer && this._announcer.parentElement) {
this._announcer.parentElement.removeChild(this._announcer);
}
this._announcer = undefined;
this._mobileToggle = undefined;
if (this._container) {
this._container.innerHTML = '';
}
this._container = undefined;
debugLog(this.debug, LOG_SOURCE, 'Dispose completed.');
} }
private createTopLevelItem(item: IMenuItem): HTMLLIElement { private createTopLevelItem(item: IMenuItem): HTMLLIElement {
@@ -57,14 +123,26 @@ export class MegaMenuRenderer {
isActive ? 'is-active' : undefined, isActive ? 'is-active' : undefined,
isCurrent ? 'is-current' : undefined isCurrent ? 'is-current' : undefined
); );
li.setAttribute('role', 'none');
const topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent); const topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent);
li.appendChild(topElement); li.appendChild(topElement);
if (hasChildren) { if (hasChildren) {
const megaMenu = this.createMegaMenu(item); const toggle: HTMLButtonElement = document.createElement('button');
li.appendChild(megaMenu); const menuId: string = 'Mega-Menu-Panel-' + item.id.replace(/[^a-z0-9_-]/gi, '');
toggle.type = 'button';
toggle.className = 'menu-item-toggle';
toggle.setAttribute('aria-haspopup', 'true');
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-controls', menuId);
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.label);
toggle.innerHTML = '<span aria-hidden="true"></span>';
li.appendChild(toggle);
const childMenu = this.menuMode === 'flyout'
? this.createFlyoutMenu(item)
: this.createMegaMenu(item);
childMenu.id = menuId;
li.appendChild(childMenu);
} }
return li; return li;
@@ -80,10 +158,11 @@ export class MegaMenuRenderer {
} else { } else {
element = document.createElement('span'); element = document.createElement('span');
element.className = 'menu-item-text'; element.className = 'menu-item-text';
if (!hasChildren) {
element.setAttribute('tabindex', '0'); element.setAttribute('tabindex', '0');
} }
}
element.setAttribute('role', 'menuitem');
element.textContent = item.label; element.textContent = item.label;
element.className = this.joinClasses( element.className = this.joinClasses(
element.className, element.className,
@@ -92,11 +171,6 @@ export class MegaMenuRenderer {
isCurrent ? 'is-current' : undefined isCurrent ? 'is-current' : undefined
); );
if (hasChildren) {
element.setAttribute('aria-haspopup', 'true');
element.setAttribute('aria-expanded', 'false');
}
if (isCurrent && item.url) { if (isCurrent && item.url) {
element.setAttribute('aria-current', 'page'); element.setAttribute('aria-current', 'page');
} }
@@ -105,15 +179,18 @@ export class MegaMenuRenderer {
element.title = item.hoverText; element.title = item.hoverText;
} }
if (item.url) {
this.applyLinkBehavior(element as HTMLAnchorElement, item);
}
return element; return element;
} }
private createMegaMenu(parentItem: IMenuItem): HTMLDivElement { private createMegaMenu(parentItem: IMenuItem): HTMLDivElement {
const megaMenuDiv = document.createElement('div'); const megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu'; megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('role', 'menu');
megaMenuDiv.setAttribute('aria-expanded', 'false'); megaMenuDiv.setAttribute('aria-expanded', 'false');
megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien'); megaMenuDiv.setAttribute('aria-label', parentItem.label + ' ' + this.strings.submenuLabel);
const gridDiv = document.createElement('div'); const gridDiv = document.createElement('div');
gridDiv.className = 'mega-menu-grid'; gridDiv.className = 'mega-menu-grid';
@@ -147,6 +224,7 @@ export class MegaMenuRenderer {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = item.url; link.href = item.url;
link.textContent = item.label; link.textContent = item.label;
this.applyLinkBehavior(link, item);
link.className = this.joinClasses( link.className = this.joinClasses(
isActive ? 'is-active' : undefined, isActive ? 'is-active' : undefined,
isCurrent ? 'is-current' : undefined isCurrent ? 'is-current' : undefined
@@ -170,6 +248,13 @@ export class MegaMenuRenderer {
h3.appendChild(span); h3.appendChild(span);
} }
if (item.description) {
const description: HTMLParagraphElement = document.createElement('p');
description.className = 'mega-menu-description';
description.textContent = item.description;
categoryDiv.appendChild(description);
}
categoryDiv.appendChild(h3); categoryDiv.appendChild(h3);
if (item.hasChildren() && item.items && item.items.length > 0) { if (item.hasChildren() && item.items && item.items.length > 0) {
@@ -189,6 +274,7 @@ export class MegaMenuRenderer {
link.href = thirdLevelItem.url || '#'; link.href = thirdLevelItem.url || '#';
link.textContent = thirdLevelItem.label; link.textContent = thirdLevelItem.label;
this.applyLinkBehavior(link, thirdLevelItem);
link.className = this.joinClasses( link.className = this.joinClasses(
isLinkActive ? 'is-active' : undefined, isLinkActive ? 'is-active' : undefined,
isLinkCurrent ? 'is-current' : undefined isLinkCurrent ? 'is-current' : undefined
@@ -212,8 +298,98 @@ export class MegaMenuRenderer {
return categoryDiv; 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 + ' ' + this.strings.submenuLabel);
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 toggle: HTMLButtonElement = document.createElement('button');
const childId: string = 'Mega-Menu-Flyout-' + item.id.replace(/[^a-z0-9_-]/gi, '');
toggle.type = 'button';
toggle.className = 'flyout-toggle';
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-controls', childId);
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.label);
toggle.innerHTML = '<span aria-hidden="true"></span>';
li.appendChild(toggle);
const childList = document.createElement('ul');
childList.className = 'flyout-list flyout-level-3';
childList.id = childId;
childList.setAttribute('aria-label', item.label + ' ' + this.strings.submenuLabel);
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;
this.applyLinkBehavior(element as HTMLAnchorElement, item);
} 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 (isCurrent && item.url) {
element.setAttribute('aria-current', 'page');
}
if (item.hoverText) {
element.title = item.hoverText;
}
return element;
}
private attachEventListeners(): void { private attachEventListeners(): void {
const headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span[role="menuitem"]'); const headings = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle');
for (let i = 0; i < headings.length; i++) { for (let i = 0; i < headings.length; i++) {
const heading = headings[i] as HTMLElement; const heading = headings[i] as HTMLElement;
@@ -228,21 +404,17 @@ export class MegaMenuRenderer {
this.attachGlobalKeyboardNavigation(); this.attachGlobalKeyboardNavigation();
this.attachGlobalClickOutsideHandler(); this.attachGlobalClickOutsideHandler();
this.attachMobileToggle();
this.attachFlyoutToggles();
} }
private attachKeyboardNavigation(heading: HTMLElement, megaMenu: HTMLElement): void { private attachKeyboardNavigation(heading: HTMLElement, megaMenu: HTMLElement): void {
heading.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.toggleMegaMenu(heading, megaMenu);
});
heading.addEventListener('keydown', (e: KeyboardEvent) => { heading.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'ArrowDown') {
if (heading.tagName === 'A') {
return;
}
e.preventDefault();
this.toggleMegaMenu(heading, megaMenu);
} else if (e.key === ' ') {
e.preventDefault();
this.toggleMegaMenu(heading, megaMenu);
} else if (e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
this.openMegaMenu(heading, megaMenu); this.openMegaMenu(heading, megaMenu);
this.focusFirstLink(megaMenu); this.focusFirstLink(megaMenu);
@@ -256,12 +428,6 @@ export class MegaMenuRenderer {
} }
}); });
if (heading.tagName === 'A') {
heading.addEventListener('click', () => {
debugLog(this.debug, LOG_SOURCE, 'Link clicked:', (heading as HTMLAnchorElement).href);
});
}
heading.addEventListener('focus', () => { heading.addEventListener('focus', () => {
debugLog(this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent); debugLog(this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
}); });
@@ -325,7 +491,7 @@ export class MegaMenuRenderer {
const isInsideThisMenu = megaMenu.contains(focusedElement); const isInsideThisMenu = megaMenu.contains(focusedElement);
const isOnThisTrigger = focusedElement === heading; const isOnThisTrigger = focusedElement === heading;
const isInAnyMegaMenu = focusedElement.closest('.mega-menu'); const isInAnyMegaMenu = focusedElement.closest('.mega-menu');
const isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span'); const isOnAnyTopLevel = focusedElement.closest('.mega-menu-top-item');
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) { if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
debugLog(this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.'); debugLog(this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.');
@@ -336,7 +502,7 @@ export class MegaMenuRenderer {
} }
private attachGlobalKeyboardNavigation(): void { private attachGlobalKeyboardNavigation(): void {
document.addEventListener('keydown', (e: KeyboardEvent) => { this._documentKeydownHandler = (e: KeyboardEvent): void => {
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;
if (e.key === 'Escape') { if (e.key === 'Escape') {
@@ -348,9 +514,30 @@ export class MegaMenuRenderer {
} }
} }
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && activeElement.closest('.mega-menu-top-item')) {
const currentItem = activeElement.closest('.mega-menu-top-item') as HTMLElement;
const items = document.querySelectorAll('#Mega-Menu > ul > .mega-menu-top-item');
let currentIndex: number = -1;
for (let index: number = 0; index < items.length; index++) {
if (items[index] === currentItem) {
currentIndex = index;
break;
}
}
if (currentIndex >= 0) {
e.preventDefault();
const offset: number = e.key === 'ArrowRight' ? 1 : -1;
const targetIndex: number = (currentIndex + offset + items.length) % items.length;
const target = (items[targetIndex] as HTMLElement).querySelector('a, .menu-item-toggle') as HTMLElement;
if (target) {
target.focus();
}
}
}
if (e.key === 'Tab') { if (e.key === 'Tab') {
if (!e.shiftKey) { if (!e.shiftKey) {
const currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span') as HTMLElement; const currentTopLevel = activeElement.closest('.menu-item-toggle') as HTMLElement;
if (currentTopLevel) { if (currentTopLevel) {
const parentLi = currentTopLevel.closest('li') as HTMLElement; const parentLi = currentTopLevel.closest('li') as HTMLElement;
const megaMenu = parentLi.querySelector('.mega-menu.js-open') as HTMLElement; const megaMenu = parentLi.querySelector('.mega-menu.js-open') as HTMLElement;
@@ -380,11 +567,12 @@ export class MegaMenuRenderer {
} }
} }
} }
}); };
document.addEventListener('keydown', this._documentKeydownHandler);
} }
private attachGlobalClickOutsideHandler(): void { private attachGlobalClickOutsideHandler(): void {
document.addEventListener('click', (e: MouseEvent) => { this._documentClickHandler = (e: MouseEvent): void => {
const eventTarget = e.target as Element; const eventTarget = e.target as Element;
const isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' && const isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' &&
eventTarget.closest('#CustomNavigation')); eventTarget.closest('#CustomNavigation'));
@@ -399,22 +587,92 @@ export class MegaMenuRenderer {
debugLog(this.debug, LOG_SOURCE, 'Closing menu because of outside click.'); debugLog(this.debug, LOG_SOURCE, 'Closing menu because of outside click.');
this.closeAllMegaMenus(); this.closeAllMegaMenus();
} }
};
document.addEventListener('click', this._documentClickHandler);
}
private attachMobileToggle(): void {
if (!this._mobileToggle) {
return;
}
this._mobileToggle.addEventListener('click', (): void => {
const nav: HTMLElement = this._mobileToggle.closest('#Mega-Menu') as HTMLElement;
const isOpen: boolean = this._mobileToggle.getAttribute('aria-expanded') === 'true';
this._mobileToggle.setAttribute('aria-expanded', String(!isOpen));
this._mobileToggle.setAttribute('aria-label', isOpen ? this.strings.openNavigation : this.strings.closeNavigation);
nav.classList.toggle('is-mobile-open', !isOpen);
if (isOpen) {
this.closeAllMegaMenus();
}
}); });
const nav: HTMLElement = this._mobileToggle.closest('#Mega-Menu') as HTMLElement;
nav.addEventListener('click', (event: MouseEvent): void => {
const target: Element = event.target as Element;
if (target && target.closest('a') && window.innerWidth <= 767) {
this._mobileToggle.setAttribute('aria-expanded', 'false');
this._mobileToggle.setAttribute('aria-label', this.strings.openNavigation);
nav.classList.remove('is-mobile-open');
}
});
}
private attachFlyoutToggles(): void {
const toggles = document.querySelectorAll('#Mega-Menu .flyout-toggle');
for (let i: number = 0; i < toggles.length; i++) {
const toggle: HTMLButtonElement = toggles[i] as HTMLButtonElement;
const parent: HTMLElement = toggle.parentElement as HTMLElement;
const setOpen = (open: boolean): void => {
toggle.setAttribute('aria-expanded', String(open));
toggle.setAttribute('aria-label', (open ? this.strings.closeSubmenu : this.strings.openSubmenu)
+ ' ' + this.getFlyoutToggleLabel(toggle));
parent.classList.toggle('is-open', open);
};
toggle.addEventListener('click', (event: MouseEvent): void => {
event.preventDefault();
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
});
toggle.addEventListener('keydown', (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
event.preventDefault();
setOpen(false);
toggle.focus();
}
});
parent.addEventListener('mouseenter', (): void => setOpen(true));
parent.addEventListener('mouseleave', (): void => setOpen(false));
parent.addEventListener('focusin', (): void => setOpen(true));
parent.addEventListener('focusout', (): void => {
window.setTimeout((): void => {
if (!parent.contains(document.activeElement)) {
setOpen(false);
}
}, 0);
});
}
} }
private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
this.closeAllMegaMenus(menu); this.closeAllMegaMenus(menu);
const label: string = this.getTriggerLabel(trigger);
trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-expanded', 'true');
trigger.setAttribute('aria-label', this.strings.closeSubmenu + ' ' + label);
(trigger.parentElement as HTMLElement).classList.add('is-open');
menu.setAttribute('aria-expanded', 'true'); menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open'); menu.classList.add('js-open');
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent); this.announce(label + ' ' + this.strings.opened);
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', label);
} }
private closeMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { private closeMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
const label: string = this.getTriggerLabel(trigger);
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-expanded', 'false');
trigger.setAttribute('aria-label', this.strings.openSubmenu + ' ' + label);
(trigger.parentElement as HTMLElement).classList.remove('is-open');
menu.setAttribute('aria-expanded', 'false'); menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open'); menu.classList.remove('js-open');
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent); this.announce(label + ' ' + this.strings.closed);
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', label);
} }
private toggleMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { private toggleMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
@@ -427,7 +685,7 @@ export class MegaMenuRenderer {
} }
private closeAllMegaMenus(exceptMenu?: HTMLElement): void { private closeAllMegaMenus(exceptMenu?: HTMLElement): void {
const allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]'); const allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle[aria-expanded="true"]');
for (let i = 0; i < allTriggers.length; i++) { for (let i = 0; i < allTriggers.length; i++) {
const trigger = allTriggers[i] as HTMLElement; const trigger = allTriggers[i] as HTMLElement;
const menu = trigger.nextElementSibling as HTMLElement; const menu = trigger.nextElementSibling as HTMLElement;
@@ -463,44 +721,40 @@ export class MegaMenuRenderer {
} }
private isCurrentUrl(url?: string): boolean { private isCurrentUrl(url?: string): boolean {
const currentPath = this.normalizePath(window.location.href); return isNavigationUrlActive(window.location.href, url || '', window.location.origin);
const itemPath = this.normalizePath(url);
if (!currentPath || !itemPath) {
return false;
}
if (itemPath === '/') {
return currentPath === '/';
}
if (currentPath === itemPath) {
return true;
}
return currentPath.indexOf(itemPath + '/') === 0;
}
private normalizePath(url?: string): string {
if (!url) {
return '';
}
try {
const parsedUrl = new URL(url, window.location.origin);
const pathname = (parsedUrl.pathname || '/').replace(/\/+$/, '');
return (pathname || '/').toLowerCase();
} catch (error) {
return '';
}
} }
private getColumnCount(items?: IMenuItem[]): string { private getColumnCount(items?: IMenuItem[]): string {
const itemCount = items ? items.length : 0; const itemCount = items ? items.length : 0;
const columnCount = Math.min(Math.max(itemCount, 1), 4); const columnCount = Math.min(Math.max(itemCount, 1), 6);
return columnCount.toString(); return columnCount.toString();
} }
private applyLinkBehavior(link: HTMLAnchorElement, item: IMenuItem): void {
if (item.openInNewWindow) {
link.target = '_blank';
link.rel = 'noopener noreferrer';
}
if (item.external) {
const currentLabel: string = link.getAttribute('aria-label') || item.label;
link.setAttribute('aria-label', currentLabel + ' (' + this.strings.externalLink + ')');
}
}
private getTriggerLabel(trigger: HTMLElement): string {
const labelElement: HTMLElement = trigger.parentElement
? trigger.parentElement.querySelector('.menu-item-link, .menu-item-text') as HTMLElement
: undefined;
return labelElement ? labelElement.textContent || '' : '';
}
private getFlyoutToggleLabel(toggle: HTMLElement): string {
const labelElement: HTMLElement = toggle.parentElement
? toggle.parentElement.querySelector('.flyout-link') as HTMLElement
: undefined;
return labelElement ? labelElement.textContent || '' : '';
}
private joinClasses(...classNames: Array<string | undefined>): string { private joinClasses(...classNames: Array<string | undefined>): string {
const filteredClassNames: string[] = []; const filteredClassNames: string[] = [];
@@ -514,7 +768,9 @@ export class MegaMenuRenderer {
} }
private createScreenReaderAnnouncer(): void { private createScreenReaderAnnouncer(): void {
if (document.getElementById('mega-menu-sr-announcer')) { const existingAnnouncer: HTMLElement = document.getElementById('mega-menu-sr-announcer');
if (existingAnnouncer) {
this._announcer = existingAnnouncer;
return; return;
} }
@@ -524,7 +780,19 @@ export class MegaMenuRenderer {
srAnnouncer.setAttribute('aria-atomic', 'true'); srAnnouncer.setAttribute('aria-atomic', 'true');
srAnnouncer.className = 'sr-only'; srAnnouncer.className = 'sr-only';
document.body.appendChild(srAnnouncer); document.body.appendChild(srAnnouncer);
this._announcer = srAnnouncer;
debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.'); debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.');
} }
private announce(message: string): void {
if (this._announcer) {
this._announcer.textContent = '';
window.setTimeout((): void => {
if (this._announcer) {
this._announcer.textContent = message;
}
}, 20);
}
}
} }
-210
View File
@@ -1,210 +0,0 @@
// tslint:disable:max-line-length export-name
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { IMegaMenuApplicationCustomizerProperties, UserCustomActionMegaMenuId } from './MegaMenuApplicationCustomizer';
import { UserCustomActionService } from '../../services/UserCustomActionService/UserCustomActionService';
import { UserCustomActionScope } from '../../services/UserCustomActionService/UserCustomActionScope';
import { IUserCustomActionProps } from '../../services/UserCustomActionService/IUserCustomActionProps';
import { debugError } from '../../services/MegaMenuDebug';
const LOG_SOURCE: string = 'MegaMenuSettingsPanel';
export class MegaMenuSettingsPanel {
private _service: UserCustomActionService;
private _ucaId: string;
private _panelElement: HTMLElement | undefined = undefined;
private _overlayElement: HTMLElement | undefined = undefined;
constructor(
private context: ApplicationCustomizerContext,
private dataUpdated: (data: IMegaMenuApplicationCustomizerProperties) => void,
private debug: boolean = false
) {
this._service = new UserCustomActionService(this.context, this.debug);
}
public async open(): Promise<void> {
if (this._panelElement) {
return;
}
const currentProps: IMegaMenuApplicationCustomizerProperties = await this.readApplicationCustomizerProps();
this._createPanel(currentProps);
}
public close(): void {
if (this._panelElement) {
this._panelElement.remove();
this._panelElement = undefined;
}
if (this._overlayElement) {
this._overlayElement.remove();
this._overlayElement = undefined;
}
document.body.focus();
document.body.blur();
}
private async readApplicationCustomizerProps(): Promise<IMegaMenuApplicationCustomizerProperties> {
const ucas: IUserCustomActionProps[] = await this._service.getUserCustomActions(UserCustomActionScope.Site);
const candidates: IUserCustomActionProps[] = ucas.filter(uca => uca.ClientSideComponentId === UserCustomActionMegaMenuId);
if (candidates.length) {
const uca: IUserCustomActionProps = candidates[0];
this._ucaId = uca.Id;
if (uca.ClientSideComponentProperties) {
return JSON.parse(uca.ClientSideComponentProperties) as IMegaMenuApplicationCustomizerProperties;
}
return {
termSetName: '',
cssUrl: '',
debug: false
};
}
debugError(this.debug, LOG_SOURCE, 'UserCustomAction for the MegaMenu was not found.');
return {
termSetName: '',
cssUrl: '',
debug: false
};
}
private async saveApplicationCustomizerProps(componentProps: IMegaMenuApplicationCustomizerProperties): Promise<void> {
try {
const newUserCustomActionsProperty: {} = {
ClientSideComponentProperties: JSON.stringify(componentProps)
};
await this._service.updateUserCustomAction(UserCustomActionScope.Site, this._ucaId, newUserCustomActionsProperty);
this.debug = componentProps.debug === true;
this._service = new UserCustomActionService(this.context, this.debug);
this.dataUpdated(componentProps);
} catch (e) {
debugError(this.debug, LOG_SOURCE, 'Error saving MegaMenu settings.', e);
}
}
private _createPanel(props: IMegaMenuApplicationCustomizerProperties): void {
const overlay: HTMLElement = document.createElement('div');
overlay.className = 'mm-settings-overlay';
overlay.tabIndex = -1;
overlay.onclick = () => this.close();
this._overlayElement = overlay;
const panel: HTMLElement = document.createElement('div');
panel.className = 'mm-settings-panel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-modal', 'true');
panel.setAttribute('aria-label', 'MegaMenu Einstellungen');
panel.innerHTML = this._getMarkup(props.termSetName, props.cssUrl || '', props.debug === true);
this._panelElement = panel;
document.body.appendChild(overlay);
document.body.appendChild(panel);
const closeBtn: HTMLButtonElement = panel.querySelector('.mm-settings-close') as HTMLButtonElement;
const cancelBtn: HTMLButtonElement = panel.querySelector('.mm-settings-cancel') as HTMLButtonElement;
const saveBtn: HTMLButtonElement = panel.querySelector('.mm-settings-save') as HTMLButtonElement;
const firstInput: HTMLInputElement = panel.querySelector('#mm-setting-termset') as HTMLInputElement;
if (closeBtn) { closeBtn.onclick = () => this.close(); }
if (cancelBtn) { cancelBtn.onclick = () => this.close(); }
if (saveBtn) { saveBtn.onclick = () => this._save(); }
panel.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
this.close();
} else if (e.key === 'Tab') {
this._trapFocus(e);
}
});
setTimeout(() => {
if (firstInput) {
firstInput.focus();
}
}, 0);
}
private _getMarkup(termSet: string, cssUrl: string, debug: boolean): string {
return `<div class='mm-settings-header'>
<h2 class='mm-settings-title'>Einstellungen</h2>
<button type='button' class='mm-settings-close' aria-label='Schliessen'>&times;</button>
</div>
<div class='mm-settings-body'>
<div class='mm-settings-field'>
<label for='mm-setting-termset'>Name des Navigations-Termsets</label>
<input id='mm-setting-termset' type='text' value='${this._escape(termSet)}' />
</div>
<div class='mm-settings-field'>
<label for='mm-setting-css'>Pfad zu zusaetzlicher CSS-Datei</label>
<input id='mm-setting-css' type='text' value='${this._escape(cssUrl)}' />
</div>
<div class='mm-settings-field'>
<label for='mm-setting-debug'>
<input id='mm-setting-debug' type='checkbox' ${debug ? 'checked' : ''} />
Debug-Ausgaben in der Browser-Konsole aktivieren
</label>
</div>
</div>
<div class='mm-settings-footer'>
<button type='button' class='mm-settings-save ms-Button ms-Button--primary'><span>Speichern</span></button>
<button type='button' class='mm-settings-cancel ms-Button'><span>Abbrechen</span></button>
</div>`;
}
private _save(): void {
if (!this._panelElement) {
return;
}
const termSetInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-termset') as HTMLInputElement;
const cssInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-css') as HTMLInputElement;
const debugInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-debug') as HTMLInputElement;
this.saveApplicationCustomizerProps({
termSetName: termSetInput && termSetInput.value ? termSetInput.value : '',
cssUrl: cssInput && cssInput.value ? cssInput.value : '',
debug: !!(debugInput && debugInput.checked)
});
this.close();
}
private _trapFocus(e: KeyboardEvent): void {
if (!this._panelElement) {
return;
}
const focusable: NodeListOf<Element> = this._panelElement.querySelectorAll('button, input');
if (!focusable || focusable.length === 0) {
return;
}
const first: HTMLElement = focusable[0] as HTMLElement;
const last: HTMLElement = focusable[focusable.length - 1] as HTMLElement;
const active: HTMLElement = document.activeElement as HTMLElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
}
private _escape(value: string): string {
if (value) {
return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
return '';
}
}
+18
View File
@@ -0,0 +1,18 @@
define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Die Navigation ist nicht konfiguriert.",
"LoadError": "Die Navigation konnte nicht geladen werden.",
"Loading": "Navigation wird geladen…",
"EmptyNavigation": "Die Navigation enthält keine sichtbaren Einträge.",
"NavigationLabel": "Hauptnavigation",
"OpenNavigation": "Hauptnavigation öffnen",
"CloseNavigation": "Hauptnavigation schließen",
"OpenSubmenu": "Untermenü öffnen für",
"CloseSubmenu": "Untermenü schließen für",
"SubmenuLabel": "Untermenü",
"ExternalLink": "externer Link",
"Opened": "geöffnet",
"Closed": "geschlossen"
};
});
+14 -1
View File
@@ -1,5 +1,18 @@
define([], function() { define([], function() {
return { return {
"Title": "MegaMenuApplicationCustomizer" "Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Navigation is not configured.",
"LoadError": "Navigation could not be loaded.",
"Loading": "Loading navigation…",
"EmptyNavigation": "The navigation does not contain any visible entries.",
"NavigationLabel": "Main navigation",
"OpenNavigation": "Open main navigation",
"CloseNavigation": "Close main navigation",
"OpenSubmenu": "Open submenu for",
"CloseSubmenu": "Close submenu for",
"SubmenuLabel": "submenu",
"ExternalLink": "external link",
"Opened": "opened",
"Closed": "closed"
} }
}); });
+13
View File
@@ -1,5 +1,18 @@
declare interface IMegaMenuApplicationCustomizerStrings { declare interface IMegaMenuApplicationCustomizerStrings {
Title: string; Title: string;
ConfigurationMissing: string;
LoadError: string;
Loading: string;
EmptyNavigation: string;
NavigationLabel: string;
OpenNavigation: string;
CloseNavigation: string;
OpenSubmenu: string;
CloseSubmenu: string;
SubmenuLabel: string;
ExternalLink: string;
Opened: string;
Closed: string;
} }
declare module 'MegaMenuApplicationCustomizerStrings' { declare module 'MegaMenuApplicationCustomizerStrings' {
+1 -15
View File
@@ -1,20 +1,6 @@
// tslint:disable:max-line-length no-any // tslint:disable:max-line-length no-any
// A file is required to be in the root of the /src directory by the TypeScript compiler // 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 { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer'; export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
export { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService'; export { MegaMenuMode, normalizeMegaMenuMode } from './extensions/megaMenu/MegaMenuMode';
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
};
}
+3
View File
@@ -3,6 +3,9 @@ export interface IMenuItem {
label: string; label: string;
icon?: string; icon?: string;
hoverText: string; hoverText: string;
description?: string;
openInNewWindow?: boolean;
external?: boolean;
url?: string; url?: string;
pathDepth: number; pathDepth: number;
items?: IMenuItem[]; items?: IMenuItem[];
+11
View File
@@ -78,11 +78,22 @@ export interface ITerm {
PathDepth?: number; PathDepth?: number;
ParentId?: string; ParentId?: string;
TermsCount?: number; TermsCount?: number;
Labels?: ITermLabelCollection;
LocalCustomProperties?: { LocalCustomProperties?: {
[property: string]: any [property: string]: any
}; };
} }
export interface ITermLabelCollection {
_Child_Items_: ITermLabel[];
}
export interface ITermLabel {
Language: number;
Value: string;
IsDefaultForLanguage?: boolean;
}
export interface ISuggestTerm { export interface ISuggestTerm {
Id: string; Id: string;
DefaultLabel: string; DefaultLabel: string;
@@ -4,4 +4,8 @@ export interface ISPTermStorePickerServiceProps {
hideDeprecatedTags: boolean; hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean; hideTagsNotAvailableForTagging: boolean;
anchorId: string; anchorId: string;
cacheMinutes: number;
siteCollectionUrl: string;
languageLcid: number;
cacheVersion: string;
} }
-9
View File
@@ -1,9 +0,0 @@
export class ItemDictionary<T> {
public Get(key: string): T {
return this[key];
}
public Add(key: string, value: T): void {
this[key] = value;
}
}
+194
View File
@@ -0,0 +1,194 @@
import { ITerm, ITermLabelCollection } from './ISPTermStorePickerService';
export const MegaMenuCacheSchemaVersion: string = '4';
export const MegaMenuTermProperties = {
hidden: 'MegaMenu.Hidden',
openInNewWindow: 'MegaMenu.OpenInNewWindow',
description: 'MegaMenu.Description',
excludedProviders: '_Sys_Nav_ExcludedProviders'
};
export const GlobalNavigationTaxonomyProvider: string = 'GlobalNavigationTaxonomyProvider';
export interface IMegaMenuCacheEnvelope<T> {
expiresAt: number;
value: T;
}
export function readCachedValue<T>(serialized: string, now: number, allowExpired: boolean): T | undefined {
if (!serialized) {
return undefined;
}
try {
const entry: IMegaMenuCacheEnvelope<T> = JSON.parse(serialized) as IMegaMenuCacheEnvelope<T>;
if (!entry || !entry.value || !entry.expiresAt || (!allowExpired && entry.expiresAt <= now)) {
return undefined;
}
return entry.value;
} catch (error) {
return undefined;
}
}
export function parseBooleanProperty(value: any): boolean { // tslint:disable-line:no-any
if (value === true || value === 1) {
return true;
}
const normalized: string = String(value || '').trim().toLowerCase();
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'ja';
}
export function getTermProperty(term: ITerm, propertyName: string): string {
const properties: { [property: string]: any } = term && term.LocalCustomProperties // tslint:disable-line:no-any
? term.LocalCustomProperties
: {};
const value: any = properties[propertyName]; // tslint:disable-line:no-any
return value === undefined || value === null ? '' : String(value).trim();
}
export function isTermHidden(term: ITerm): boolean {
return parseBooleanProperty(getTermProperty(term, MegaMenuTermProperties.hidden));
}
export function isTermExcludedFromGlobalNavigation(term: ITerm): boolean {
const value: string = getTermProperty(term, MegaMenuTermProperties.excludedProviders);
if (!value) {
return false;
}
const providers: string[] = value.replace(/["']/g, '').split(/[;,|]/);
const expected: string = GlobalNavigationTaxonomyProvider.toLowerCase();
return providers.some((provider: string): boolean => provider.trim().toLowerCase() === expected);
}
export function getLocalizedTermLabel(term: ITerm, languageLcid: number): string {
const labels: ITermLabelCollection | undefined = term && term.Labels;
const items = labels && labels._Child_Items_ ? labels._Child_Items_ : [];
let fallback: string = term && term.Name ? term.Name : '';
for (let i: number = 0; i < items.length; i++) {
const labelLanguage: number = Number(items[i].Language);
if (labelLanguage === languageLcid && items[i].Value) {
return items[i].Value;
}
if (items[i].IsDefaultForLanguage && items[i].Value) {
fallback = items[i].Value;
}
}
return fallback;
}
export function resolveNavigationUrl(rawUrl: string, siteCollectionUrl: string): string | undefined {
const value: string = String(rawUrl || '').trim();
if (!value) {
return undefined;
}
if (/[\u0000-\u001f\u007f]/.test(value)) {
return undefined;
}
const resolved: string = value.toLowerCase().indexOf('~sitecollection') === 0
? String(siteCollectionUrl || '').replace(/\/+$/, '') + value.substring('~sitecollection'.length)
: value;
const protocolMatch: RegExpMatchArray | null = resolved.match(/^([a-z][a-z0-9+.-]*):/i);
if (protocolMatch) {
const protocol: string = protocolMatch[1].toLowerCase();
if (protocol !== 'http' && protocol !== 'https' && protocol !== 'mailto') {
return undefined;
}
}
return resolved;
}
export function isExternalNavigationUrl(url: string, currentOrigin: string): boolean {
const absoluteMatch: RegExpMatchArray | null = String(url || '').match(/^(?:(https?):)?\/\/([^/]+)/i);
if (!absoluteMatch) {
return false;
}
const originMatch: RegExpMatchArray | null = String(currentOrigin || '').match(/^(https?):\/\/([^/]+)/i);
if (!originMatch) {
return true;
}
const targetProtocol: string = absoluteMatch[1] || originMatch[1];
return targetProtocol.toLowerCase() !== originMatch[1].toLowerCase()
|| absoluteMatch[2].toLowerCase() !== originMatch[2].toLowerCase();
}
export function normalizeNavigationPath(url: string, currentOrigin: string): string {
let value: string = String(url || '').trim();
if (!value) {
return '';
}
const origin: string = String(currentOrigin || '').replace(/\/+$/, '');
if (origin && value.toLowerCase().indexOf(origin.toLowerCase()) === 0) {
value = value.substring(origin.length) || '/';
} else {
value = value.replace(/^https?:\/\/[^/]+/i, '');
}
value = value.split('#')[0].split('?')[0].replace(/\/+$/, '');
return (value || '/').toLowerCase();
}
export function isNavigationUrlActive(currentUrl: string, itemUrl: string, currentOrigin: string): boolean {
const currentPath: string = normalizeNavigationPath(currentUrl, currentOrigin);
const itemPath: string = normalizeNavigationPath(itemUrl, currentOrigin);
if (!currentPath || !itemPath) {
return false;
}
return itemPath === '/' ? currentPath === '/' : currentPath === itemPath || currentPath.indexOf(itemPath + '/') === 0;
}
export function buildTermCacheKey(
siteCollectionUrl: string,
termSetId: string,
languageLcid: number,
cacheVersion?: string
): string {
const normalize = (value: string): string => encodeURIComponent(String(value || '').trim().toLowerCase());
return 'MegaMenu:TermSet:v' + MegaMenuCacheSchemaVersion + ':'
+ normalize(siteCollectionUrl) + ':'
+ normalize(termSetId) + ':'
+ String(languageLcid || 0) + ':'
+ normalize(cacheVersion || '1');
}
export interface IMegaMenuHierarchyEntry<T> {
id: string;
parentId?: string;
depth: number;
item: T;
}
export function buildMenuHierarchy<T extends { items?: T[] }>(entries: Array<IMegaMenuHierarchyEntry<T>>): T[] {
const byId: { [id: string]: T } = {};
const roots: T[] = [];
for (let i: number = 0; i < entries.length; i++) {
byId[String(entries[i].id || '').toLowerCase()] = entries[i].item;
entries[i].item.items = [];
}
for (let i: number = 0; i < entries.length; i++) {
const entry: IMegaMenuHierarchyEntry<T> = entries[i];
const parent: T = entry.parentId ? byId[String(entry.parentId).toLowerCase()] : undefined;
if (entry.depth <= 1) {
roots.push(entry.item);
} else if (parent) {
parent.items.push(entry.item);
}
}
return roots;
}
export function compareTermsByConfiguredOrder(a: ITerm, b: ITerm): number {
const aIndex: number = typeof a.CustomSortOrderIndex === 'number' ? a.CustomSortOrderIndex : -1;
const bIndex: number = typeof b.CustomSortOrderIndex === 'number' ? b.CustomSortOrderIndex : -1;
if (aIndex >= 0 || bIndex >= 0) {
if (aIndex < 0) { return 1; }
if (bIndex < 0) { return -1; }
return aIndex - bIndex;
}
const aPath: string = String(a.PathOfTerm || '').toLowerCase();
const bPath: string = String(b.PathOfTerm || '').toLowerCase();
return aPath < bPath ? -1 : (aPath > bPath ? 1 : 0);
}
+2 -1
View File
@@ -1,4 +1,5 @@
export function debugLog(enabled: boolean, source: string, message: string, ...args: any[]): void { // tslint:disable:no-any
export function debugLog(enabled: boolean, source: string, message: string, ...args: any[]): void {
if (!enabled) { if (!enabled) {
return; return;
} }
+25 -9
View File
@@ -2,26 +2,37 @@
import { IMenuItem } from './IMenuItem'; import { IMenuItem } from './IMenuItem';
import { ITerm } from './ISPTermStorePickerService'; import { ITerm } from './ISPTermStorePickerService';
import {
getLocalizedTermLabel,
getTermProperty,
isExternalNavigationUrl,
MegaMenuTermProperties,
parseBooleanProperty,
resolveNavigationUrl
} from './MegaMenuCore';
export class MenuItem implements IMenuItem { export class MenuItem implements IMenuItem {
public id: string; public id: string;
public label: string; public label: string;
public hoverText: string; public hoverText: string;
public description: string;
public openInNewWindow: boolean;
public external: boolean;
public pathDepth: number; public pathDepth: number;
public url?: string; public url?: string;
public items?: IMenuItem[]; public items?: IMenuItem[];
constructor(term: ITerm, public level: number, siteCollectionUrl?: string) { constructor(term: ITerm, public level: number, siteCollectionUrl?: string, languageLcid: number = 1033) {
this.id = term.Id; this.id = term.Id;
this.label = term.Name; this.label = getLocalizedTermLabel(term, languageLcid);
this.hoverText = term.LocalCustomProperties['_Sys_Nav_HoverText']; const properties = term.LocalCustomProperties || {};
this.hoverText = properties['_Sys_Nav_HoverText'];
this.description = getTermProperty(term, MegaMenuTermProperties.description);
this.openInNewWindow = parseBooleanProperty(getTermProperty(term, MegaMenuTermProperties.openInNewWindow));
this.pathDepth = term.PathDepth; this.pathDepth = term.PathDepth;
const rawUrl: string = term.LocalCustomProperties['_Sys_Nav_SimpleLinkUrl'] || term.LocalCustomProperties['_Sys_Nav_TargetUrl']; const rawUrl: string = properties['_Sys_Nav_SimpleLinkUrl'] || properties['_Sys_Nav_TargetUrl'];
if (rawUrl) { this.url = resolveNavigationUrl(rawUrl, siteCollectionUrl || '');
this.url = siteCollectionUrl && rawUrl.indexOf('~sitecollection') === 0 this.external = !!this.url && isExternalNavigationUrl(this.url, this.getOrigin(siteCollectionUrl || ''));
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
: rawUrl;
}
this.items = []; this.items = [];
} }
@@ -34,4 +45,9 @@ export class MenuItem implements IMenuItem {
(window as any).location.href = this.url; (window as any).location.href = this.url;
} }
} }
private getOrigin(url: string): string {
const match: RegExpMatchArray | null = String(url || '').match(/^(https?:\/\/[^/]+)/i);
return match ? match[1] : '';
}
} }
@@ -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
}
]
}
]);
});
}
}
+177 -35
View File
@@ -5,13 +5,24 @@
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http'; import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
import { ITermStore, ITerms, ITerm, IGroup, ITermSet, ISuggestTerm } from './ISPTermStorePickerService'; import { ITermStore, ITerms, ITerm, IGroup, ITermSet, ISuggestTerm } from './ISPTermStorePickerService';
import { findIndex } from '@microsoft/sp-lodash-subset';
import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps'; import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps';
import { IPickerTerm } from './IPickerTerm'; import { IPickerTerm } from './IPickerTerm';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { debugError } from './MegaMenuDebug'; import { debugError, debugLog, debugWarn } from './MegaMenuDebug';
import { buildTermCacheKey, compareTermsByConfiguredOrder, readCachedValue } from './MegaMenuCore';
const EmptyGuid: string = '00000000-0000-0000-0000-000000000000'; const EmptyGuid: string = '00000000-0000-0000-0000-000000000000';
const PickerCachePrefix: string = 'MegaMenu:PickerTerms:v2:';
interface ITermCacheEntry {
expiresAt: number;
value: ITermSet;
}
interface IPickerTermCacheEntry {
expiresAt: number;
value: IPickerTerm[];
}
/** /**
* Service implementation to manage term stores in SharePoint * Service implementation to manage term stores in SharePoint
@@ -26,12 +37,20 @@ export default class SPTermStorePickerService {
constructor(private props: ISPTermStorePickerServiceProps, private context: ApplicationCustomizerContext, private debug: boolean = false) { constructor(private props: ISPTermStorePickerServiceProps, private context: ApplicationCustomizerContext, private debug: boolean = false) {
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery'; this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions'; this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
debugLog(this.debug, 'SPTermStorePickerService', 'Service initialized.', {
clientServiceUrl: this.clientServiceUrl,
termSetNameOrId: this.props.termsetNameOrID,
useSessionStorage: this.props.useSessionStorage,
cacheMinutes: this.props.cacheMinutes,
cacheVersion: this.props.cacheVersion,
languageLcid: this.props.languageLcid
});
} }
public async getTermLabels(termId: string): Promise<string[]> { public async getTermLabels(termId: string): Promise<string[]> {
let result: string[] = null; let result: string[] = null;
try { try {
const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultKeywordsTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${termId}</Parameter></Parameters></Method></ObjectPaths></Request>`; const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultSiteCollectionTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${termId}</Parameter></Parameters></Method></ObjectPaths></Request>`;
const reqHeaders = new Headers(); const reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
@@ -43,6 +62,7 @@ export default class SPTermStorePickerService {
}; };
const callResult = await this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions); const callResult = await this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions);
this.ensureSuccessfulResponse(callResult, 'Term labels');
const jsonResult = await callResult.json(); const jsonResult = await callResult.json();
const node = jsonResult.find(x => x._ObjectType_ === 'SP.Taxonomy.Term'); const node = jsonResult.find(x => x._ObjectType_ === 'SP.Taxonomy.Term');
@@ -73,6 +93,7 @@ export default class SPTermStorePickerService {
}; };
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
return serviceResponse.json().then((serviceJSONResponse: any) => { return serviceResponse.json().then((serviceJSONResponse: any) => {
// Construct results // Construct results
const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore'); const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore');
@@ -124,8 +145,21 @@ export default class SPTermStorePickerService {
* Retrieve all terms for the given term set * Retrieve all terms for the given term set
* @param termset * @param termset
*/ */
public async getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage: boolean = true): Promise<ITermSet> { public async getAllTerms(
termset: string,
hideDeprecatedTags?: boolean,
hideTagsNotAvailableForTagging?: boolean,
useSessionStorage: boolean = true,
cacheMinutes: number = 15
): Promise<ITermSet> {
let termsetId: string = termset; let termsetId: string = termset;
debugLog(this.debug, 'SPTermStorePickerService', 'getAllTerms started.', {
configuredTermSet: termset,
hideDeprecatedTags: hideDeprecatedTags === true,
hideTagsNotAvailableForTagging: hideTagsNotAvailableForTagging === true,
useSessionStorage: useSessionStorage,
cacheMinutes: cacheMinutes
});
// Check if the provided term set property is a GUID or string // Check if the provided term set property is a GUID or string
if (!this.isGuid(termset)) { if (!this.isGuid(termset)) {
// Fetch the term store information // Fetch the term store information
@@ -134,19 +168,33 @@ export default class SPTermStorePickerService {
const crntTermSet = this.getTermSetId(termStore, termset); const crntTermSet = this.getTermSetId(termStore, termset);
if (crntTermSet) { if (crntTermSet) {
termsetId = this.cleanGuid(crntTermSet.Id); termsetId = this.cleanGuid(crntTermSet.Id);
debugLog(this.debug, 'SPTermStorePickerService', 'Term set name resolved to an ID.', {
termsetId: termsetId
});
} else { } else {
debugWarn(this.debug, 'SPTermStorePickerService', 'Configured term set could not be resolved.', termset);
return null; return null;
} }
} }
const childTerms = this.getTermsById(termsetId, useSessionStorage); const childTerms = this.getCachedTermSet(termsetId, useSessionStorage, false);
const staleTerms = this.getCachedTermSet(termsetId, useSessionStorage, true);
if (childTerms) { if (childTerms) {
debugLog(this.debug, 'SPTermStorePickerService', 'Returning terms from session cache.', {
termsetId: termsetId,
termCount: childTerms.Terms ? childTerms.Terms.length : 0
});
return childTerms; return childTerms;
} }
debugLog(this.debug, 'SPTermStorePickerService', 'No valid cache entry found; sending ProcessQuery request.', {
termsetId: termsetId,
url: this.clientServiceUrl
});
// Request body to retrieve all terms for the given term set // Request body to retrieve all terms for the given term set
const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultKeywordsTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`; const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultSiteCollectionTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`;
const reqHeaders = new Headers(); const reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
@@ -158,7 +206,16 @@ export default class SPTermStorePickerService {
}; };
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
debugLog(this.debug, 'SPTermStorePickerService', 'ProcessQuery response received.', {
status: serviceResponse.status,
statusText: serviceResponse.statusText,
ok: serviceResponse.ok
});
this.ensureSuccessfulResponse(serviceResponse, 'Terms');
return serviceResponse.json().then((serviceJSONResponse: any) => { return serviceResponse.json().then((serviceJSONResponse: any) => {
if (!Array.isArray(serviceJSONResponse)) {
throw new Error('The taxonomy service returned an unexpected response.');
}
const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet'); const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet');
if (termStoreResultTermSets.length > 0) { if (termStoreResultTermSets.length > 0) {
@@ -171,11 +228,21 @@ export default class SPTermStorePickerService {
let terms = termStoreResultTerms[0]._Child_Items_; let terms = termStoreResultTerms[0]._Child_Items_;
if (hideDeprecatedTags === true) { if (hideDeprecatedTags === true) {
const countBeforeDeprecatedFilter: number = terms.length;
terms = terms.filter(d => d.IsDeprecated === false); terms = terms.filter(d => d.IsDeprecated === false);
debugLog(this.debug, 'SPTermStorePickerService', 'Deprecated terms filtered.', {
before: countBeforeDeprecatedFilter,
after: terms.length
});
} }
if (hideTagsNotAvailableForTagging === true) { if (hideTagsNotAvailableForTagging === true) {
const countBeforeAvailableFilter: number = terms.length;
terms = terms.filter(d => d.IsAvailableForTagging === true); terms = terms.filter(d => d.IsAvailableForTagging === true);
debugLog(this.debug, 'SPTermStorePickerService', 'Terms unavailable for tagging filtered.', {
before: countBeforeAvailableFilter,
after: terms.length
});
} }
// Clean the term ID and specify the path depth // Clean the term ID and specify the path depth
@@ -203,15 +270,36 @@ export default class SPTermStorePickerService {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet)); const ttlMinutes: number = this.normalizeCacheMinutes(cacheMinutes);
const cacheEntry: ITermCacheEntry = {
expiresAt: Date.now() + ttlMinutes * 60 * 1000,
value: termStoreResultTermSet
};
const cacheKey: string = this.getTermCacheKey(termsetId);
window.sessionStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
debugLog(this.debug, 'SPTermStorePickerService', 'Term set stored in session cache.', {
key: cacheKey,
expiresAt: cacheEntry.expiresAt
});
} }
} catch (error) { } catch (error) {
// Do nothing, sometimes "storage quota exceeded" error if too many items debugWarn(this.debug, 'SPTermStorePickerService', 'Term cache could not be written.', error);
} }
debugLog(this.debug, 'SPTermStorePickerService', 'getAllTerms completed.', {
termsetId: termsetId,
termCount: termStoreResultTermSet.Terms ? termStoreResultTermSet.Terms.length : 0
});
return termStoreResultTermSet; return termStoreResultTermSet;
} }
debugWarn(this.debug, 'SPTermStorePickerService', 'ProcessQuery response contained no term set.');
return null; return null;
}); });
}).catch((error: any) => {
if (staleTerms) {
debugWarn(this.debug, 'SPTermStorePickerService', 'Taxonomy request failed; returning stale cache.', error);
return staleTerms;
}
throw error;
}); });
} }
@@ -225,7 +313,7 @@ export default class SPTermStorePickerService {
public async searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]> { public async searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]> {
const { useSessionStorage } = this.props; const { useSessionStorage } = this.props;
const childTerms = this.getTermsById(termId, useSessionStorage); const childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
if (childTerms) { if (childTerms) {
return this.searchTermsBySearchText(childTerms, searchText); return this.searchTermsBySearchText(childTerms, searchText);
} else { } else {
@@ -257,12 +345,18 @@ export default class SPTermStorePickerService {
const returnTerms: IPickerTerm[] = []; const returnTerms: IPickerTerm[] = [];
const childTerms = this.getTermsById(anchorId, useSessionStorage); const childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
if (childTerms) { if (childTerms) {
return childTerms; return childTerms;
} }
const termSet = await this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging); const termSet = await this.getAllTerms(
termsetNameOrID,
hideDeprecatedTags,
hideTagsNotAvailableForTagging,
useSessionStorage,
this.props.cacheMinutes
);
const terms = termSet.Terms; const terms = termSet.Terms;
if (anchorId) { if (anchorId) {
const anchorTerm = terms.filter(t => t.Id.toLowerCase() === anchorId.toLowerCase()).shift(); const anchorTerm = terms.filter(t => t.Id.toLowerCase() === anchorId.toLowerCase()).shift();
@@ -277,7 +371,11 @@ export default class SPTermStorePickerService {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms)); const cacheEntry: IPickerTermCacheEntry = {
expiresAt: Date.now() + this.normalizeCacheMinutes(this.props.cacheMinutes) * 60 * 1000,
value: returnTerms
};
window.sessionStorage.setItem(PickerCachePrefix + anchorId, JSON.stringify(cacheEntry));
} }
} catch (error) { } catch (error) {
// Do nothing // Do nothing
@@ -332,15 +430,20 @@ export default class SPTermStorePickerService {
return null; return null;
} }
private getTermsById(termId, useSessionStorage: boolean = true) { private getCachedTermSet(termId: string, useSessionStorage: boolean = true, allowExpired: boolean = false): ITermSet {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
const terms = window.sessionStorage.getItem(termId); const key: string = this.getTermCacheKey(termId);
if (terms) { const terms: string = window.sessionStorage.getItem(key);
return JSON.parse(terms); if (!terms) {
} else {
return null; return null;
} }
const cachedValue: ITermSet = readCachedValue<ITermSet>(terms, Date.now(), allowExpired);
if (!cachedValue && allowExpired) {
window.sessionStorage.removeItem(key);
}
return cachedValue || null;
} else { } else {
return null; return null;
} }
@@ -349,6 +452,51 @@ export default class SPTermStorePickerService {
} }
} }
private getCachedPickerTerms(termId: string, useSessionStorage: boolean = true): IPickerTerm[] {
try {
if (!useSessionStorage || !window.sessionStorage) {
return null;
}
const key: string = PickerCachePrefix + termId;
const serialized: string = window.sessionStorage.getItem(key);
if (!serialized) {
return null;
}
const cacheEntry: IPickerTermCacheEntry = JSON.parse(serialized) as IPickerTermCacheEntry;
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} catch (error) {
return null;
}
}
private normalizeCacheMinutes(value: number): number {
if (typeof value !== 'number' || !isFinite(value)) {
return 15;
}
return Math.min(Math.max(Math.floor(value), 1), 1440);
}
private getTermCacheKey(termId: string): string {
return buildTermCacheKey(
this.props.siteCollectionUrl,
termId,
this.props.languageLcid,
this.props.cacheVersion
);
}
private ensureSuccessfulResponse(response: SPHttpClientResponse, operation: string): void {
if (!response.ok) {
throw new Error(operation + ' request failed (' + response.status + ' ' + response.statusText + ').');
}
}
private searchTermsBySearchText(terms, searchText) { private searchTermsBySearchText(terms, searchText) {
if (terms) { if (terms) {
return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; }); return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
@@ -456,7 +604,7 @@ export default class SPTermStorePickerService {
// Last item is not needed for parent path // Last item is not needed for parent path
pathElms.pop(); pathElms.pop();
// Find the parent item and add the new item // Find the parent item and add the new item
const idx: number = findIndex(newTermsOrder, term => term.PathOfTerm === pathElms.join(';')); const idx: number = this.findTermIndexByPath(newTermsOrder, pathElms.join(';'));
if (idx !== -1) { if (idx !== -1) {
newTermsOrder.splice(idx + 1, 0, crntTerm); newTermsOrder.splice(idx + 1, 0, crntTerm);
} else { } else {
@@ -476,6 +624,16 @@ export default class SPTermStorePickerService {
return newTermsOrder; return newTermsOrder;
} }
private findTermIndexByPath(terms: ITerm[], path: string): number {
for (let index: number = 0; index < terms.length; index++) {
if (terms[index].PathOfTerm === path) {
return index;
}
}
return -1;
}
/** /**
* Sort the terms by their path * Sort the terms by their path
* *
@@ -483,23 +641,7 @@ export default class SPTermStorePickerService {
* @param b term 2 * @param b term 2
*/ */
private sortTermByPath(a: ITerm, b: ITerm) { private sortTermByPath(a: ITerm, b: ITerm) {
if (a.CustomSortOrderIndex === -1) { return compareTermsByConfiguredOrder(a, b);
if (a.PathOfTerm.toLowerCase() < b.PathOfTerm.toLowerCase()) {
return -1;
}
if (a.PathOfTerm.toLowerCase() > b.PathOfTerm.toLowerCase()) {
return 1;
}
return 0;
} else {
if (a.CustomSortOrderIndex < b.CustomSortOrderIndex) {
return -1;
}
if (a.CustomSortOrderIndex > b.CustomSortOrderIndex) {
return 1;
}
return 0;
}
} }
private convertTermToPickerTerm(term: ITerm): IPickerTerm { private convertTermToPickerTerm(term: ITerm): IPickerTerm {

Some files were not shown because too many files have changed in this diff Show More