11 Commits
Author SHA1 Message Date
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
59 changed files with 1475 additions and 1200 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
+263 -150
View File
@@ -1,199 +1,312 @@
# SharePoint MegaMenu Extension # SharePoint MegaMenu
**Version:** 1.0.2 Barrierearme, dreistufige Portalnavigation auf Basis von 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.0.1 |
| SharePoint Framework | SPFx 1.4.1 |
| Node.js | 8.17.0 |
| Zieloberfläche | Moderne SharePoint-Seiten |
| Paket | `sharepoint/solution/mega-menu.sppkg` |
## 🌟 Features ## Funktionen
- **3-stufige Navigationshierarchie** basierend auf SharePoint Managed Metadata (Taxonomy) - Dreistufige Navigation aus einem Managed-Metadata-Termset
- **Responsive Design** mit automatischer Anpassung an verschiedene Bildschirmgrößen - Auswahl des Termsets per GUID oder, abwärtskompatibel, per Name
- **Barrierefreiheit** mit vollständiger Tastaturnavigation und Screenreader-Unterstützung - Benutzerdefinierte Sortierung aus dem Term Store
- **Einstellungs-Panel** für Administratoren zur Konfiguration - Unterstützung der Navigations-Eigenschaften `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText`
- **Externe CSS-Unterstützung** für individuelle Anpassungen - Auflösung von `~sitecollection` in Navigations-URLs
- **Office UI Fabric Integration** für konsistente SharePoint-Optik - Markierung des aktuellen Menüpunktes und seiner übergeordneten Einträge
- Bedienung mit Maus, Tastatur und Touch
- Responsive Darstellung
- Optionales zusätzliches Stylesheet
- Taxonomy-Cache mit konfigurierbarer Ablaufzeit
- Bereinigter SPFx-Lifecycle ohne verbleibende globale Event Listener
- Deutsche und englische Statusmeldungen
## 📋 Voraussetzungen ## Architektur
- SharePoint 2019 oder SharePoint SE (Subscription Edition) ```text
- Node.js v8.17.0 (empfohlen) UserCustomAction.ClientSideComponentProperties
- SharePoint Framework Development Tools (SPFx 1.4.1) │
▼
MegaMenuApplicationCustomizer
│
├── TaxonomyNavigationService
│ └── SPTermStorePickerService
│ └── Default Site Collection Term Store
│
└── MegaMenuRenderer
├── semantische Navigation
├── Fokus- und Tastatursteuerung
└── responsive Darstellung
```
## 🚀 Installation & Entwicklung Das MegaMenu wird einmal pro Site Collection konfiguriert. Das Paket wird mit `skipFeatureDeployment: true` zentral über den App Catalog bereitgestellt, damit das SPFx-Bundle ohne eine App-Installation in jedem einzelnen Web verfügbar ist. PortalSettings speichert die zentrale Konfiguration im Property Bag des Root Webs. Eine Site-Collection-Action aktiviert das MegaMenu auf dem Root Web sowie auf vorhandenen und zukünftigen Unterwebs. Das MegaMenu selbst enthält keine eigene Administrationsoberfläche.
### 1. Repository klonen und Abhängigkeiten installieren ## Voraussetzungen
```bash - SharePoint Server Subscription Edition oder SharePoint Server 2019
git clone <repository-url> - konfigurierter App Catalog
cd MegaMenu - konfigurierter App Management Service
- ein Termset im Standard-Term-Store der Site Collection
- Leseberechtigung der Benutzer auf den Managed-Metadata-Service
- für Entwicklung und Build: Node.js 8.17.0 und eine zu SPFx 1.4.1 passende npm-Version
## Konfiguration
Die UserCustomAction unterstützt folgende `ClientSideComponentProperties`:
```json
{
"termSetId": "6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc",
"termSetName": "Global Navigation",
"cacheMinutes": 15,
"debug": false
}
```
| Eigenschaft | Typ | Standard | Beschreibung |
|---|---:|---:|---|
| `termSetId` | GUID | leer | Bevorzugte, eindeutige Identifikation des Termsets |
| `termSetName` | String | leer | Rückwärtskompatibler Fallback, wenn keine gültige `termSetId` vorhanden ist |
| `cacheMinutes` | Zahl | `15` | Gültigkeit des Session-Caches, zulässig sind 1 bis 1440 Minuten |
| `debug` | Boolean | `false` | Aktiviert zusätzliche Konsolenausgaben |
`termSetId` hat Vorrang vor `termSetName`. Für neue Installationen wird die GUID empfohlen, weil Termset-Namen nicht zwingend eindeutig sind.
### Kompatibilität mit PortalSettings
PortalSettings verwaltet `termSetId`, `termSetName`, `cacheMinutes` und `debug`. Individuelle Gestaltung wird nicht vom MegaMenu selbst geladen, sondern zentral über die Solution Custom Branding bereitgestellt.
## Termset-Aufbau
Die Darstellung unterstützt drei Ebenen:
```text
Ebene 1: Hauptnavigation
├── Ebene 2: Kategorie
│ ├── Ebene 3: Link
│ └── Ebene 3: Link
└── Ebene 2: Kategorie
```
Für Zieladressen werden die lokalen benutzerdefinierten Eigenschaften des Terms ausgewertet:
1. `_Sys_Nav_SimpleLinkUrl`
2. `_Sys_Nav_TargetUrl`
Optional kann `_Sys_Nav_HoverText` als ergänzender Hilfetext verwendet werden. Veraltete Terms werden nicht angezeigt. Terms, die nicht für Tagging verfügbar sind, bleiben für Navigationsszenarien sichtbar.
## Build
SPFx 1.4.1 verwendet die Legacy-Gulp-Toolchain. Neuere Node-Versionen sind nicht kompatibel.
```powershell
npm install npm install
npm run package
``` ```
### 2. Entwicklungsserver starten Alternativ einzeln:
```bash ```powershell
gulp serve
```
### 3. Produktionspaket erstellen
```bash
gulp clean gulp clean
gulp bundle --ship gulp bundle --ship
gulp package-solution --ship gulp package-solution --ship
``` ```
## 📁 Projektstruktur Das fertige Paket befindet sich anschließend unter:
``` ```text
src/ sharepoint/solution/mega-menu.sppkg
├── 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 ## Deployment
### Einstellungs-Panel (Admin-Bereich) 1. `mega-menu.sppkg` in den App Catalog laden.
2. Die Solution freigeben.
3. Die App im Root Web der gewünschten Site Collection installieren. Der MegaMenu Application Customizer wird dort zunächst web-scoped registriert und von PortalSettings erkannt.
4. Das Termset in PortalSettings konfigurieren.
Das MegaMenu verfügt über ein Einstellungs-Panel, das über das Zahnrad-Icon (⚙️) im Menü zugänglich ist: Das folgende Skript ist nur erforderlich, wenn PortalSettings nicht verwendet wird oder eine vollständig skriptbasierte Vorkonfiguration gewünscht ist.
1. **Name des Navigations-Termsets**: Der Name des Managed Metadata Termsets, das als Datenquelle dient Empfohlen mit Termset-GUID:
2. **Pfad zu zusätzlicher CSS-Datei**: Optional - URL zu einer benutzerdefinierten CSS-Datei
### PowerShell-Deployment
```powershell ```powershell
# Installation .\deployment\add-megamenu.ps1 `
.\src\deployment\add-megamenu.ps1 -SiteUrl 'https://sharepoint/sites/portal' `
-TermSetId '6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc' `
# Deinstallation -TermSetName 'Global Navigation' `
.\src\deployment\remove-megamenu.ps1 -CacheMinutes 15
``` ```
## 🎨 Anpassungen Abwärtskompatibel nur mit Namen:
### CSS-Customization ```powershell
.\deployment\add-megamenu.ps1 `
-SiteUrl 'https://sharepoint/sites/portal' `
-TermSetName 'Global Navigation'
```
Das MegaMenu kann über externe CSS-Dateien angepasst werden. Wichtige CSS-Klassen: Das Skript speichert die Konfiguration zentral unter `PortalSettings.MegaMenu.Configuration`, entfernt alte beziehungsweise doppelte Component IDs und synchronisiert die neue web-scoped Runtime-Bindung idempotent auf alle vorhandenen Webs. Fuer spaeter angelegte Unterwebs kann es ohne Nebenwirkungen erneut ausgefuehrt werden.
## Individuelles Branding
Zusätzliche Stylesheets werden zentral über die Solution Custom Branding eingebunden. Das MegaMenu besitzt bewusst keine eigene `cssUrl`-Property.
Die wichtigsten CSS-Klassen sind:
```css ```css
/* Haupt-Container */ #CustomNavigation { }
#Mega-Menu { } #Mega-Menu { }
.mega-menu-top-level { }
/* Top-Level Menüpunkte */ .mega-menu-top-item { }
#Mega-Menu > ul > li > a { }
/* Mega-Menü Dropdown */
.mega-menu { } .mega-menu { }
.mega-menu-grid { }
/* Kategorien im Dropdown */
.mega-menu-category { } .mega-menu-category { }
.mega-menu-links { }
/* Links in Kategorien */
.mega-menu-category ul li a { }
/* Einstellungs-Panel */
.mm-settings-panel { }
``` ```
### Accessibility Features Viele Farben und Abstände können über CSS Custom Properties überschrieben werden, beispielsweise:
- **Skip-Links** für Screenreader ```css
- **ARIA-Rollen** und Labels :root {
- **Fokus-Management** mit sichtbaren Fokus-Indikatoren --megaMenuNavBackground: #5f6f74;
- **Tastaturnavigation** mit Tab/Shift+Tab/Escape --megaMenuNavTextColor: #ffffff;
- **High Contrast Modus** Unterstützung --megaMenuPanelWidth: 1280px;
--megaMenuZIndex: 6000;
## 🔧 Build-Befehle }
| Befehl | Beschreibung |
|--------|-------------|
| `gulp serve` | Entwicklungsserver mit Live-Reload |
| `gulp build` | Entwicklungs-Build (Debug-Modus) |
| `gulp bundle --ship` | Produktions-Build (Optimiert) |
| `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:**
- `.sppkg` Datei aus `sharepoint/solution/` hochladen
- App genehmigen und für alle Sites verfügbar machen
3. **Site-spezifische Aktivierung:**
- PowerShell-Scripts verwenden
## 🐛 Troubleshooting
### 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 ## Cache
### v1.0.2 (Aktuell) Das geladene Termset wird im `sessionStorage` des Browsers gespeichert. Der Schlüssel ist mit `MegaMenu:TermSet:` namensräumlich getrennt. Nach Ablauf von `cacheMinutes` wird das Termset automatisch neu geladen.
- Einstellungs-Panel implementiert
- Fokus-Management verbessert
- CSS-Optimierungen für SharePoint-Konsistenz
- Barrierefreiheit-Verbesserungen
### v1.0.1 Für einen sofortigen administrativen Test kann der Session-Cache in den Browser-Entwicklertools gelöscht oder ein neues privates Browserfenster verwendet werden.
- Responsive Design hinzugefügt
- Performance-Optimierungen
### v1.0.0 ## Barrierefreiheit
- Initiale Release
- 3-stufige Navigation
- Grundlegende Accessibility
## 👥 Mitwirkende - semantisches `<nav aria-label="Hauptnavigation">`
- native Listen- und Linksemantik
- `aria-expanded` und `aria-haspopup` für aufklappbare Einträge
- `aria-current="page"` für den aktuellen Navigationspfad
- Öffnen über Leertaste oder Pfeil nach unten
- Schließen über Escape
- sichtbare Fokusindikatoren
- Unterstützung von `prefers-reduced-motion` und kontrastreichen Darstellungen
- Entwicklung: Matthias Glubrecht Bewusst wird kein `role="menubar"` verwendet: Eine Portalnavigation ist eine normale Navigation und kein Desktop-Menü-Widget. Dadurch bleiben native Browser- und Screenreader-Erwartungen erhalten.
- Framework: SharePoint Framework (Microsoft)
- UI: Office UI Fabric (Microsoft)
## 📄 Lizenz ## Fehlerdiagnose
tbd ### „Die Navigation ist nicht konfiguriert“
## 🆘 Support Weder eine gültige `termSetId` noch ein `termSetName` ist gesetzt. UserCustomAction beziehungsweise PortalSettings-Konfiguration prüfen.
Bei Fragen oder Problemen: ### „Die Navigation konnte nicht geladen werden“
1. Issues im Repository erstellen
2. Dokumentation und Troubleshooting-Bereich prüfen - Existiert das Termset im Standard-Term-Store der Site Collection?
- Ist die GUID korrekt?
- Haben Benutzer Zugriff auf den Managed-Metadata-Service?
- Enthält das Termset mindestens einen sichtbaren Term?
- Ist der App-Catalog-Eintrag aktuell?
Für weitere technische Details kann vorübergehend `debug: true` gesetzt werden.
Bei aktiviertem Debug-Modus protokolliert MegaMenu den kompletten Startpfad mit den Quellen
`MegaMenuApplicationCustomizer`, `TaxonomyNavigationService`, `SPTermStorePickerService` und
`MegaMenuRenderer`. Dazu gehören die übergebenen Properties, Placeholder-Erkennung,
Taxonomy-Anfrage, Cache-Nutzung, Term-Anzahl, Containerwahl und vollständige Fehlerdetails.
Wenn keine dieser Meldungen erscheint, wurde das MegaMenu-Bundle nicht durch den SPFx-Loader
ausgeführt.
### Änderungen am Termset erscheinen verzögert
Die Navigation wird bis zu `cacheMinutes` im Session-Cache gehalten. Cache leeren oder Ablaufzeit abwarten.
### Zusätzliches CSS wird nicht geladen
- URL und Dateiberechtigungen prüfen.
- Für externe Quellen HTTPS verwenden.
- Browser-Konsole bei aktiviertem Debug-Modus prüfen.
## Upgrade von 1.0.x
- Bestehende `termSetName`-Konfigurationen funktionieren unverändert.
- Der alte unversionierte Session-Cache wird nicht weiterverwendet.
- `cacheMinutes` verwendet ohne Konfiguration den Wert 15.
- Die eigene, nicht verwendete MegaMenu-Einstellungsseite wurde entfernt; PortalSettings ist der zentrale Konfigurationsweg.
- Globale Event Listener werden beim Dispose der Extension entfernt.
- Paket-, npm- und Dokumentationsversion wurden auf 1.1.0 vereinheitlicht.
## Classic Pages
Unter `classic/` befindet sich eine separate Kompatibilitätsimplementierung für klassische SharePoint-Seiten. Sie wird nicht vom SPFx-Bundle ausgeführt und besitzt einen eigenen Deploymentweg. Neue Funktionen sollten zuerst in der modernen SPFx-Variante umgesetzt und anschließend bewusst in die Classic-Variante übernommen werden.
## Projektstruktur
```text
MegaMenu/
├── config/ SPFx-Build- und Paketkonfiguration
├── deployment/ Farm-/Site-Registrierung
├── src/
│ ├── extensions/megaMenu/ Application Customizer und Renderer
│ └── services/ Taxonomy- und Datenmodelle
├── classic/ optionale Classic-Kompatibilität
├── build-classic.ps1 Classic-Build
├── gulpfile.js
└── package.json
```
## Versionshistorie
### 2.0.1
- Zentrale SPFx-Bereitstellung mit `skipFeatureDeployment: true`.
- Das MegaMenu-Paket muss nicht mehr als App in jedem Unterweb installiert werden.
- Feature-Framework-Registrierung aus dem Paket entfernt; die Aktivierung erfolgt zentral über PortalSettings oder das Deploymentskript.
### 2.0.0
- Neue Application-Customizer-ID `c0abbffb-355d-4d4e-bd38-9e15fb811506`, um alte On-Premises-Manifest- und Loader-Zuordnungen sicher zu umgehen.
- Eine zentrale MegaMenu-Konfiguration pro Site Collection im Property Bag des Root Webs.
- Web-scoped Runtime-Bindungen werden auf Root Web und alle vorhandenen Unterwebs synchronisiert.
- Aktivierungsskript entfernt alte Component IDs und kann fuer neue Unterwebs idempotent erneut ausgefuehrt werden.
### 1.1.4
- Unnötige externe Abhängigkeit zu `@microsoft/sp-lodash-subset` entfernt.
- Terms werden beim Sortieren über eine lokale, SPFx-1.4.1-kompatible Pfadsuche zugeordnet.
- Verhindert, dass der Application Customizer bereits bei der Systemkomponenten-Auflösung übersprungen wird.
### 1.1.3
- Ausführliches, ausschließlich über `debug: true` aktiviertes Lifecycle-Logging ergänzt.
- Taxonomy-Anfragen, Session-Cache, Term-Filterung und Renderer werden nachvollziehbar protokolliert.
- Fehlerausgaben enthalten Name, Meldung und Stacktrace.
### 1.1.2
- `cssUrl` zugunsten der zentralen Custom-Branding-Solution entfernt
- deaktivierte, veraltete oder nicht zum Tagging verfügbare Terms werden ausgeblendet
- Cache-Namespace aktualisiert, damit alte deaktivierte Einträge nicht weiter angezeigt werden
### 1.1.1
- automatische Registrierung des MegaMenu Application Customizers bei der App-Installation
- direkte Erkennung durch PortalSettings ohne vorherigen Skriptaufruf
### 1.1.0
- konsistente Verwendung des Default Site Collection Term Store
- optionale Konfiguration per Termset-GUID
- Cache-TTL und namensräumlich getrennter Session-Cache
- korrekte Anwendung der Deprecated-/Availability-Filter
- Prüfung der Taxonomy-HTTP-Antworten
- Lifecycle-Cleanup für globale Event Listener
- korrigierte Navigationssemantik und aktive Screenreader-Statusmeldungen
- Validierung externer CSS-URLs
- sichtbare Konfigurations- und Ladefehler
- Entfernung des ungenutzten lokalen Settings-Panels
- vereinheitlichte Projektversion und neue Build-/Deployment-Dokumentation
+2 -2
View File
@@ -113,7 +113,7 @@ Use one of these methods:
````powershell ````powershell
# Connect to SharePoint site # Connect to SharePoint site
Add-PnPCustomAction -Name "MegaMenuConfig" -Location "ClientSideExtension.ApplicationCustomizer" ` Add-PnPCustomAction -Name "MegaMenuConfig" -Location "ClientSideExtension.ApplicationCustomizer" `
-ClientSideComponentId "abc3361f-bb2d-491f-aba3-cd51c19a299b" ` -ClientSideComponentId "c0abbffb-355d-4d4e-bd38-9e15fb811506" `
-ClientSideComponentProperties '{"termSetName":"Your Navigation Termset","cssUrl":"/SiteAssets/custom-menu.css"}' -ClientSideComponentProperties '{"termSetName":"Your Navigation Termset","cssUrl":"/SiteAssets/custom-menu.css"}'
```` ````
@@ -269,4 +269,4 @@ Write-Host "✓ Same CSS as SPFx version" -ForegroundColor Green
Write-Host "✓ Same service logic (extracted to standalone)" -ForegroundColor Green Write-Host "✓ Same service logic (extracted to standalone)" -ForegroundColor Green
Write-Host "✓ Same configuration system" -ForegroundColor Green Write-Host "✓ Same configuration system" -ForegroundColor Green
Write-Host "✓ Same menu rendering output" -ForegroundColor Green Write-Host "✓ Same menu rendering output" -ForegroundColor Green
Write-Host "✓ No SPFx dependencies in classic version" -ForegroundColor Green Write-Host "✓ No SPFx dependencies in classic version" -ForegroundColor Green
+2 -2
View File
@@ -19,7 +19,7 @@
(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', containerId: 's4-titlerow',
@@ -262,4 +262,4 @@
log('MegaMenu Classic wrapper loaded. Waiting for dependencies...'); log('MegaMenu Classic wrapper loaded. Waiting for dependencies...');
})(); })();
+2 -2
View File
@@ -3,11 +3,11 @@
"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.0.1.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
"paths": { "paths": {
"zippedPackage": "solution/mega-menu.sppkg" "zippedPackage": "solution/mega-menu.sppkg"
} }
} }
+2 -2
View File
@@ -6,7 +6,7 @@
"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",
@@ -18,7 +18,7 @@
"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" "testMessage": "Test message"
+101 -70
View File
@@ -5,100 +5,131 @@ param(
[string]$TermSetName = '', [string]$TermSetName = '',
[string]$CssUrl = '', [string]$TermSetId = '',
[ValidateRange(1, 1440)]
[int]$CacheMinutes = 15,
[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, $location = 'ClientSideExtension.ApplicationCustomizer'
[string]$CurrentComponentId, $configurationProperty = 'PortalSettings.MegaMenu.Configuration'
[string]$CurrentLocation
$properties = [string](@{
termSetName = $TermSetName
termSetId = $TermSetId
cacheMinutes = $CacheMinutes
debug = [bool]$EnableDebug.IsPresent
} | ConvertTo-Json -Depth 10 -Compress)
$site = Get-SPSite -Identity $SiteUrl
try {
$rootWeb = $site.RootWeb
$rootWeb.AllProperties[$configurationProperty] = [string]$properties
$rootWeb.Update()
$siteActions = @(
$site.UserCustomActions |
Where-Object {
$_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -eq $legacyComponentId
}
) )
$removedCount = 0 foreach ($siteAction in $siteActions) {
$siteAction.Delete()
}
foreach ($web in $CurrentSite.AllWebs) { $created = 0
$updated = 0
$removedLegacy = 0
foreach ($web in $site.AllWebs) {
try { try {
$webActions = @($web.UserCustomActions | Where-Object { $webUrl = $web.Url
$_.Location -eq $CurrentLocation -and $userCustomActions = $web.UserCustomActions
$_.ClientSideComponentId -and
$_.ClientSideComponentId.ToString().ToLower() -eq $CurrentComponentId
})
foreach ($webAction in $webActions) { if ($null -eq $userCustomActions) {
$web.UserCustomActions.Delete($webAction.Id) throw ('UserCustomActions konnten fuer das Web nicht geladen werden: ' + $webUrl)
$removedCount++
} }
if ($webActions.Count -gt 0) { $actions = @(
$web.Update() $userCustomActions |
Where-Object {
$_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -eq $legacyComponentId
}
)
$currentAction = $actions |
Where-Object {
$_.ClientSideComponentId -eq $componentId
} |
Select-Object -First 1
foreach ($existingAction in $actions) {
if ($currentAction -and $existingAction.Id -eq $currentAction.Id) {
continue
}
$existingAction.Delete()
$removedLegacy++
} }
if (-not $currentAction) {
$currentAction = $userCustomActions.Add()
$currentAction.Name = [Guid]::NewGuid().ToString('B')
$created++
}
else {
$updated++
}
$currentAction.Title = 'MegaMenu Application Customizer'
$currentAction.Description = $Description
$currentAction.Location = $location
$currentAction.ClientSideComponentId = $componentId
$currentAction.ClientSideComponentProperties = $properties
$currentAction.Sequence = 100
$currentAction.Update()
$web.Update()
Write-Host ('MegaMenu synchronisiert: ' + $webUrl) -ForegroundColor Green
} }
finally { finally {
$web.Dispose() $web.Dispose()
} }
} }
return $removedCount Write-Host ''
} Write-Host 'MegaMenu wurde fuer die gesamte Site Collection konfiguriert.' -ForegroundColor Green
Write-Host ('Zentrale Konfiguration: ' + $configurationProperty)
$site = Get-SPSite -Identity $SiteUrl Write-Host ('Neu registrierte Webs: ' + $created)
try { Write-Host ('Aktualisierte Webs: ' + $updated)
$removedWebScopedActions = Remove-WebScopedComponentActions -CurrentSite $site -CurrentComponentId $componentId -CurrentLocation $location Write-Host ('Entfernte alte oder doppelte Registrierungen: ' + $removedLegacy)
Write-Host ('Properties: ' + $properties)
$existingAction = $site.UserCustomActions | Where-Object { Write-Host 'Neue Unterwebs koennen durch erneutes Ausfuehren idempotent synchronisiert werden.' -ForegroundColor Yellow
$_.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()
Write-Host 'Erstelle neue site-scoped Mega Menu UserCustomAction...' -ForegroundColor Yellow
}
$action.Name = $name
$action.Title = $title
$action.Description = $Description
$action.Location = $location
$action.ClientSideComponentId = [Guid]$componentId
$action.ClientSideComponentProperties = Get-MegaMenuPropertiesJson -CurrentTermSetName $TermSetName -CurrentCssUrl $CssUrl -CurrentDebug ([bool]$EnableDebug.IsPresent)
$action.Update()
Write-Host 'Mega Menu wurde site-scoped registriert.' -ForegroundColor Green
Write-Host ('Entfernte web-scoped Eintraege: ' + $removedWebScopedActions)
Write-Host ('Beschreibung: ' + $Description)
Write-Host ('Properties: ' + $action.ClientSideComponentProperties)
} }
finally { finally {
if ($site) { if ($site) {
$site.Dispose() $site.Dispose()
} }
} }
+136
View File
@@ -0,0 +1,136 @@
param(
[Parameter(Mandatory = $true)]
[string]$SiteUrl,
[string]$Description = 'MSFT-Custom-Solution:MegaMenu'
)
if (-not (Get-Command -Name Get-SPSite -ErrorAction SilentlyContinue)) {
if (Get-Module -ListAvailable -Name Microsoft.SharePoint.PowerShell) {
Import-Module Microsoft.SharePoint.PowerShell -DisableNameChecking
}
elseif (Get-PSSnapin -Registered -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) {
Add-PSSnapin Microsoft.SharePoint.PowerShell
}
else {
throw 'Die SharePoint PowerShell-Komponenten wurden nicht gefunden. Bitte in der SharePoint Management Shell ausfuehren.'
}
}
$componentId = [Guid]'c0abbffb-355d-4d4e-bd38-9e15fb811506'
$legacyComponentId = [Guid]'abc3361f-bb2d-491f-aba3-cd51c19a299b'
$location = 'ClientSideExtension.ApplicationCustomizer'
$configurationProperty = 'PortalSettings.MegaMenu.Configuration'
$site = Get-SPSite -Identity $SiteUrl
try {
$rootWeb = $site.RootWeb
$properties = [string]$rootWeb.AllProperties[$configurationProperty]
if ([string]::IsNullOrWhiteSpace($properties)) {
$existingAction = $null
foreach ($web in $site.AllWebs) {
try {
$existingAction = @(
$web.UserCustomActions |
Where-Object {
$_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -eq $legacyComponentId
}
) | Select-Object -First 1
if ($existingAction) {
$properties = [string]$existingAction.ClientSideComponentProperties
break
}
}
finally {
$web.Dispose()
}
}
}
if ([string]::IsNullOrWhiteSpace($properties)) {
throw ('Keine MegaMenu-Konfiguration im Property Bag oder in einer bestehenden ' +
'Web-Registrierung gefunden. Bitte zuerst add-megamenu.ps1 ausfuehren.')
}
try {
$null = $properties | ConvertFrom-Json
}
catch {
throw ('Die gefundene MegaMenu-Konfiguration ist kein gueltiges JSON: ' + $_.Exception.Message)
}
$removedSiteActions = 0
@(
$site.UserCustomActions |
Where-Object {
$_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -eq $legacyComponentId
}
) | ForEach-Object {
$_.Delete()
$removedSiteActions++
}
$siteAction = $site.UserCustomActions.Add()
$siteAction.Name = [Guid]::NewGuid().ToString('B')
$siteAction.Title = 'MegaMenu Application Customizer'
$siteAction.Description = $Description
$siteAction.Location = $location
$siteAction.ClientSideComponentId = $componentId
$siteAction.ClientSideComponentProperties = $properties
$siteAction.Sequence = 100
$siteAction.Update()
$removedWebActions = 0
$testedWebUrls = @()
foreach ($web in $site.AllWebs) {
try {
$testedWebUrls += $web.Url
@(
$web.UserCustomActions |
Where-Object {
$_.ClientSideComponentId -eq $componentId -or
$_.ClientSideComponentId -eq $legacyComponentId
}
) | ForEach-Object {
$_.Delete()
$removedWebActions++
}
}
finally {
$web.Dispose()
}
}
$verification = @(
$site.UserCustomActions |
Where-Object { $_.ClientSideComponentId -eq $componentId }
)
Write-Host ''
Write-Host '=== MegaMenu Site-Scope-Test vorbereitet ===' -ForegroundColor Green
Write-Host ('Site-Action-ID: ' + $siteAction.Id)
Write-Host ('Component-ID: ' + $componentId)
Write-Host ('Entfernte vorherige Site-Actions: ' + $removedSiteActions)
Write-Host ('Entfernte Web-Actions: ' + $removedWebActions)
Write-Host ('Verifizierte Site-Actions: ' + $verification.Count)
Write-Host ('Properties: ' + $properties)
Write-Host ''
Write-Host 'Bitte jeweils mit Strg+F5 testen:' -ForegroundColor Cyan
$testedWebUrls | ForEach-Object { Write-Host (' ' + $_) }
Write-Host ''
Write-Host 'Rueckkehr zum bisherigen Web-Scope:' -ForegroundColor Yellow
Write-Host ('.\deployment\add-megamenu.ps1 -SiteUrl ''' + $site.Url + '''')
}
finally {
if ($site) {
$site.Dispose()
}
}
+5 -12
View File
@@ -3,7 +3,7 @@
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
"version": "1.0.4", "version": "1.1.2",
"manifestVersion": 2, "manifestVersion": 2,
"requiresCustomScript": false, "requiresCustomScript": false,
"loaderConfig": { "loaderConfig": {
@@ -19,18 +19,16 @@
"MegaMenuApplicationCustomizerStrings": { "MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js", "defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath", "type": "localizedPath",
"paths": {} "paths": {
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
}
}, },
"@microsoft/decorators": { "@microsoft/decorators": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3" "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": { "@microsoft/sp-application-base": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
@@ -45,11 +43,6 @@
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a" "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
File diff suppressed because one or more lines are too long
+1 -4
View File
@@ -1,5 +1,2 @@
declare const styles: { declare const styles: {};
mmSlideIn: string;
mmFadeIn: string;
};
export default styles; export default styles;
@@ -1,9 +1,6 @@
/* tslint:disable */ /* tslint:disable */
require('./MegaMenu.module.css'); require('./MegaMenu.module.css');
var styles = { var styles = {};
mmSlideIn: 'mmSlideIn_09a8e1a7',
mmFadeIn: 'mmFadeIn_09a8e1a7',
};
export default styles; export default styles;
/* tslint:enable */ /* tslint:enable */
@@ -1 +1 @@
{"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"} {"version":3,"sources":["extensions/megaMenu/MegaMenu.module.scss.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,CAAC,uBAAuB,CAAC,CAAC;AACjC,IAAM,MAAM,GAAG,EACd,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};\r\n\r\nexport default styles;\r\n/* tslint:enable */"],"sourceRoot":"..\\..\\..\\src"}
+13 -16
View File
@@ -1,27 +1,24 @@
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base'; import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
import './MegaMenu.module.scss'; import './MegaMenu.module.scss';
export declare const UserCustomActionMegaMenuId: string; export declare const UserCustomActionMegaMenuId: string;
/**
* Properties for the MegaMenu Application Customizer
*/
export interface IMegaMenuApplicationCustomizerProperties { export interface IMegaMenuApplicationCustomizerProperties {
/** termSetName?: string;
* The name of the termset to load menu items from termSetId?: string;
*/ cacheMinutes?: number;
termSetName: string; debug?: boolean;
/**
* 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> { export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
private _topPlaceholder; private _topPlaceholder;
private _menuContainer;
private _renderer;
onInit(): Promise<void>; onInit(): Promise<void>;
private _renderPlaceHolders(); private _renderPlaceHolders();
private _updateCallback; private _renderMegaMenu(termSetIdentifier, debug?);
private _renderMegaMenu(termSetName);
private _getOrCreateContainer(id, placeholder); private _getOrCreateContainer(id, placeholder);
private _loadExternalCss(cssUrl); private _onDispose;
private _onDispose(); private _getTermSetIdentifier();
private _getCacheMinutes();
private _renderStatus(message);
private _isGuid(value);
private _isDebugEnabled(debugOverride?);
} }
@@ -54,59 +54,60 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
} }
}; };
import { override } from '@microsoft/decorators'; import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base'; import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base';
import * as strings from 'MegaMenuApplicationCustomizerStrings'; import * as strings from 'MegaMenuApplicationCustomizerStrings';
import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService'; import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService';
import { MegaMenuRenderer } from './MegaMenuRenderer'; import { MegaMenuRenderer } from './MegaMenuRenderer';
// Import SCSS module import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
import './MegaMenu.module.scss'; import './MegaMenu.module.scss';
var LOG_SOURCE = 'MegaMenuApplicationCustomizer'; var LOG_SOURCE = 'MegaMenuApplicationCustomizer';
export var UserCustomActionMegaMenuId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; 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) { var MegaMenuApplicationCustomizer = (function (_super) {
__extends(MegaMenuApplicationCustomizer, _super); __extends(MegaMenuApplicationCustomizer, _super);
function MegaMenuApplicationCustomizer() { function MegaMenuApplicationCustomizer() {
var _this = _super !== null && _super.apply(this, arguments) || this; var _this = _super !== null && _super.apply(this, arguments) || this;
_this._updateCallback = function (data) { _this._onDispose = function () {
_this._loadExternalCss(data.cssUrl); _this.context.placeholderProvider.changedEvent.remove(_this, _this._renderPlaceHolders);
_this._renderMegaMenu(data.termSetName); if (_this._renderer) {
_this._renderer.dispose();
_this._renderer = undefined;
}
_this._menuContainer = undefined;
_this._topPlaceholder = undefined;
debugLog(_this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
}; };
return _this; return _this;
} }
MegaMenuApplicationCustomizer.prototype.onInit = function () { MegaMenuApplicationCustomizer.prototype.onInit = function () {
Log.info(LOG_SOURCE, "Initialized " + strings.Title); debugLog(this._isDebugEnabled(), 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); this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
// Initial render
this._renderPlaceHolders(); this._renderPlaceHolders();
return Promise.resolve(); return Promise.resolve();
}; };
MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () { MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () {
console.log('Available placeholders: ', this.context.placeholderProvider.placeholderNames.map(function (name) { return PlaceholderName[name]; }).join(', ')); var availablePlaceholders = this.context.placeholderProvider.placeholderNames
// Handling the top placeholder .map(function (name) { return PlaceholderName[name]; })
.join(', ');
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders);
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose }); 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) { if (!this._topPlaceholder) {
console.error('The expected placeholder (Top) was not found.'); debugError(this._isDebugEnabled(), LOG_SOURCE, 'The expected placeholder (Top) was not found.');
return; return;
} }
if (!this.properties.termSetName) { var termSetIdentifier = this._getTermSetIdentifier();
console.error('TermSetName property is required but not provided.'); if (!termSetIdentifier) {
1; debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.');
this._renderStatus(strings.ConfigurationMissing);
return; return;
} }
this._renderMegaMenu(this.properties.termSetName); this._renderMegaMenu(termSetIdentifier);
} }
}; };
MegaMenuApplicationCustomizer.prototype._renderMegaMenu = function (termSetName) { MegaMenuApplicationCustomizer.prototype._renderMegaMenu = function (termSetIdentifier, debug) {
if (debug === void 0) { debug = this._isDebugEnabled(); }
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var taxonomyService, menuItems, renderer, container, error_1; var taxonomyService, menuItems, error_1;
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
@@ -116,23 +117,27 @@ var MegaMenuApplicationCustomizer = (function (_super) {
_a.label = 1; _a.label = 1;
case 1: case 1:
_a.trys.push([1, 3, , 4]); _a.trys.push([1, 3, , 4]);
taxonomyService = new TaxonomyNavigationService(this.context, termSetName); taxonomyService = new TaxonomyNavigationService(this.context, termSetIdentifier, debug, this._getCacheMinutes());
return [4 /*yield*/, taxonomyService.getMenuItems()]; return [4 /*yield*/, taxonomyService.getMenuItems()];
case 2: case 2:
menuItems = _a.sent(); menuItems = _a.sent();
renderer = new MegaMenuRenderer(this.context, menuItems, this._updateCallback); if (this._renderer) {
container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder); this._renderer.dispose();
if (container) { }
renderer.render(container); this._renderer = new MegaMenuRenderer(menuItems, debug);
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
if (this._menuContainer) {
this._renderer.render(this._menuContainer);
} }
else { else {
renderer.render(this._topPlaceholder.domElement); this._renderer.render(this._topPlaceholder.domElement);
} }
Log.info(LOG_SOURCE, "MegaMenu rendered successfully with " + menuItems.length + " top-level items"); debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
return [3 /*break*/, 4]; return [3 /*break*/, 4];
case 3: case 3:
error_1 = _a.sent(); error_1 = _a.sent();
console.error('Error rendering MegaMenu:', error_1); debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error_1);
this._renderStatus(strings.LoadError);
return [3 /*break*/, 4]; return [3 /*break*/, 4];
case 4: return [2 /*return*/]; case 4: return [2 /*return*/];
} }
@@ -142,41 +147,52 @@ var MegaMenuApplicationCustomizer = (function (_super) {
MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) { MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) {
var container = document.getElementById(id); var container = document.getElementById(id);
if (container) { if (container) {
var div = document.createElement('div'); var host = document.getElementById('MegaMenuHost');
container.appendChild(div); if (!host) {
return div; host = document.createElement('div');
} host.id = 'MegaMenuHost';
else { container.appendChild(host);
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; return host;
}
else if (link) {
link.remove();
} }
return placeholder.domElement;
}; };
MegaMenuApplicationCustomizer.prototype._onDispose = function () { MegaMenuApplicationCustomizer.prototype._getTermSetIdentifier = function () {
console.log('[MegaMenuApplicationCustomizer._onDispose] Disposed custom top placeholder.'); var termSetId = 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()
: '';
};
MegaMenuApplicationCustomizer.prototype._getCacheMinutes = function () {
var value = this.properties ? Number(this.properties.cacheMinutes) : 15;
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
};
MegaMenuApplicationCustomizer.prototype._renderStatus = function (message) {
if (!this._topPlaceholder) {
return;
}
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
var container = this._menuContainer || this._topPlaceholder.domElement;
container.innerHTML = '';
var status = document.createElement('div');
status.className = 'mega-menu-status';
status.setAttribute('role', 'status');
status.textContent = message;
container.appendChild(status);
};
MegaMenuApplicationCustomizer.prototype._isGuid = function (value) {
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);
};
MegaMenuApplicationCustomizer.prototype._isDebugEnabled = function (debugOverride) {
if (typeof debugOverride === 'boolean') {
return debugOverride;
}
return !!(this.properties && this.properties.debug === true);
}; };
__decorate([ __decorate([
override override
File diff suppressed because one or more lines are too long
+18 -10
View File
@@ -1,17 +1,18 @@
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { IMenuItem } from '../../services/IMenuItem'; import { IMenuItem } from '../../services/IMenuItem';
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
export declare class MegaMenuRenderer { export declare class MegaMenuRenderer {
private context;
private menuItems; private menuItems;
private updateCallback; private debug;
private _settingsPanel?; private static readonly hoverOpenDelayMs;
constructor(context: ApplicationCustomizerContext, menuItems: IMenuItem[], updateCallback: (data: IMegaMenuApplicationCustomizerProperties) => void); private static readonly hoverCloseDelayMs;
private _container;
private _documentKeydownHandler;
private _documentClickHandler;
private _announcer;
constructor(menuItems: IMenuItem[], debug?: boolean);
render(container: HTMLElement): void; render(container: HTMLElement): void;
private createSettingsItem(); dispose(): void;
private openSettings();
private createTopLevelItem(item); private createTopLevelItem(item);
private createTopLevelElement(item); private createTopLevelElement(item, hasChildren, isActive, isCurrent);
private createMegaMenu(parentItem); private createMegaMenu(parentItem);
private createCategorySection(item); private createCategorySection(item);
private attachEventListeners(); private attachEventListeners();
@@ -19,10 +20,17 @@ export declare class MegaMenuRenderer {
private attachMouseEvents(heading, megaMenu); private attachMouseEvents(heading, megaMenu);
private attachFocusManagement(heading, megaMenu); private attachFocusManagement(heading, megaMenu);
private attachGlobalKeyboardNavigation(); private attachGlobalKeyboardNavigation();
private attachGlobalClickOutsideHandler();
private openMegaMenu(trigger, menu); private openMegaMenu(trigger, menu);
private closeMegaMenu(trigger, menu); private closeMegaMenu(trigger, menu);
private toggleMegaMenu(trigger, menu); private toggleMegaMenu(trigger, menu);
private closeAllMegaMenus(); private closeAllMegaMenus(exceptMenu?);
private focusFirstLink(megaMenu); private focusFirstLink(megaMenu);
private isItemActive(item);
private isCurrentUrl(url?);
private normalizePath(url?);
private getColumnCount(items?);
private joinClasses(...classNames);
private createScreenReaderAnnouncer(); private createScreenReaderAnnouncer();
private announce(message);
} }
+211 -113
View File
@@ -2,109 +2,88 @@
// tslint:disable:match-default-export-name // tslint:disable:match-default-export-name
// tslint:disable:typedef // tslint:disable:typedef
// tslint:disable:variable-name // tslint:disable:variable-name
import { SPPermission } from '@microsoft/sp-page-context'; import { debugLog } from '../../services/MegaMenuDebug';
import { MegaMenuSettingsPanel } from './MegaMenuSettings'; var LOG_SOURCE = 'MegaMenuRenderer';
var MegaMenuRenderer = (function () { var MegaMenuRenderer = (function () {
function MegaMenuRenderer(context, menuItems, updateCallback) { function MegaMenuRenderer(menuItems, debug) {
this.context = context; if (debug === void 0) { debug = false; }
this.menuItems = menuItems; this.menuItems = menuItems;
this.updateCallback = updateCallback; this.debug = debug;
} }
MegaMenuRenderer.prototype.render = function (container) { MegaMenuRenderer.prototype.render = function (container) {
var _this = this; var _this = this;
// Clear any existing content this.dispose();
this._container = container;
container.innerHTML = ''; container.innerHTML = '';
container.id = 'CustomNavigation'; container.id = 'CustomNavigation';
// Create the main nav element
var nav = document.createElement('nav'); var nav = document.createElement('nav');
nav.id = 'Mega-Menu'; nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main'; nav.className = 'mega-menu-main';
nav.setAttribute('role', 'navigation');
nav.setAttribute('aria-label', 'Hauptnavigation'); nav.setAttribute('aria-label', 'Hauptnavigation');
// Create the top-level menubar
var topLevelUl = document.createElement('ul'); var topLevelUl = document.createElement('ul');
topLevelUl.setAttribute('role', 'menubar'); topLevelUl.className = 'mega-menu-top-level';
// Process each top-level menu item
this.menuItems.forEach(function (topLevelItem) { this.menuItems.forEach(function (topLevelItem) {
var topLevelLi = _this.createTopLevelItem(topLevelItem); var topLevelLi = _this.createTopLevelItem(topLevelItem);
topLevelUl.appendChild(topLevelLi); 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); nav.appendChild(topLevelUl);
container.appendChild(nav); container.appendChild(nav);
// Attach accessibility event listeners after rendering
this.attachEventListeners(); this.attachEventListeners();
this.createScreenReaderAnnouncer(); this.createScreenReaderAnnouncer();
}; };
MegaMenuRenderer.prototype.createSettingsItem = function () { MegaMenuRenderer.prototype.dispose = function () {
var _this = this; if (this._documentKeydownHandler) {
var li = document.createElement('li'); document.removeEventListener('keydown', this._documentKeydownHandler);
li.setAttribute('role', 'none'); this._documentKeydownHandler = undefined;
// 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(); 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;
if (this._container) {
this._container.innerHTML = '';
}
this._container = undefined;
}; };
// Panel logic moved to MegaMenuSettingsPanel
MegaMenuRenderer.prototype.createTopLevelItem = function (item) { MegaMenuRenderer.prototype.createTopLevelItem = function (item) {
var li = document.createElement('li'); var li = document.createElement('li');
li.setAttribute('role', 'none'); var hasChildren = item.hasChildren() && item.items && item.items.length > 0;
// Create the top-level element (link or span) var isActive = this.isItemActive(item);
var topElement = this.createTopLevelElement(item); var isCurrent = this.isCurrentUrl(item.url);
li.className = this.joinClasses('mega-menu-top-item', hasChildren ? 'has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
var topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent);
li.appendChild(topElement); li.appendChild(topElement);
// If the item has children, create the mega menu if (hasChildren) {
if (item.hasChildren() && item.items && item.items.length > 0) {
var megaMenu = this.createMegaMenu(item); var megaMenu = this.createMegaMenu(item);
li.appendChild(megaMenu); li.appendChild(megaMenu);
} }
return li; return li;
}; };
MegaMenuRenderer.prototype.createTopLevelElement = function (item) { MegaMenuRenderer.prototype.createTopLevelElement = function (item, hasChildren, isActive, isCurrent) {
var element; var element;
if (item.url) { if (item.url) {
// Create as link
element = document.createElement('a'); element = document.createElement('a');
element.href = item.url; element.href = item.url;
element.className = 'menu-item-link'; element.className = 'menu-item-link';
} }
else { else {
// Create as span (no link)
element = document.createElement('span'); element = document.createElement('span');
element.className = 'menu-item-text'; element.className = 'menu-item-text';
element.setAttribute('tabindex', '0'); element.setAttribute('tabindex', '0');
} }
element.setAttribute('role', 'menuitem');
element.setAttribute('aria-haspopup', 'true');
element.setAttribute('aria-expanded', 'false');
element.textContent = item.label; element.textContent = item.label;
element.className = this.joinClasses(element.className, hasChildren ? 'menu-item-has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
if (hasChildren) {
element.setAttribute('aria-haspopup', 'true');
element.setAttribute('aria-expanded', 'false');
}
if (isCurrent && item.url) {
element.setAttribute('aria-current', 'page');
}
if (item.hoverText) { if (item.hoverText) {
element.title = item.hoverText; element.title = item.hoverText;
} }
@@ -114,11 +93,11 @@ var MegaMenuRenderer = (function () {
var _this = this; var _this = this;
var megaMenuDiv = document.createElement('div'); var megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu'; megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('role', 'menu'); megaMenuDiv.setAttribute('aria-expanded', 'false');
megaMenuDiv.setAttribute('aria-label', parentItem.label + " Unterkategorien"); megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien');
var gridDiv = document.createElement('div'); var gridDiv = document.createElement('div');
gridDiv.className = 'mega-menu-grid'; gridDiv.className = 'mega-menu-grid';
// Process second-level items (categories) gridDiv.style.setProperty('--megaMenuColumnCount', this.getColumnCount(parentItem.items));
if (parentItem.items) { if (parentItem.items) {
parentItem.items.forEach(function (secondLevelItem) { parentItem.items.forEach(function (secondLevelItem) {
var categoryDiv = _this.createCategorySection(secondLevelItem); var categoryDiv = _this.createCategorySection(secondLevelItem);
@@ -129,38 +108,51 @@ var MegaMenuRenderer = (function () {
return megaMenuDiv; return megaMenuDiv;
}; };
MegaMenuRenderer.prototype.createCategorySection = function (item) { MegaMenuRenderer.prototype.createCategorySection = function (item) {
var _this = this;
var categoryDiv = document.createElement('div'); var categoryDiv = document.createElement('div');
categoryDiv.className = 'mega-menu-category'; var isActive = this.isItemActive(item);
// Create the category header (h3) var isCurrent = this.isCurrentUrl(item.url);
categoryDiv.className = this.joinClasses('mega-menu-category', isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
var h3 = document.createElement('h3'); var h3 = document.createElement('h3');
h3.className = 'mega-menu-category-title';
if (item.url) { if (item.url) {
// Category header as link
var link = document.createElement('a'); var link = document.createElement('a');
link.href = item.url; link.href = item.url;
link.textContent = item.label; link.textContent = item.label;
link.className = this.joinClasses(isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
if (isCurrent) {
link.setAttribute('aria-current', 'page');
}
if (item.hoverText) { if (item.hoverText) {
link.title = item.hoverText; link.title = item.hoverText;
} }
h3.appendChild(link); h3.appendChild(link);
} }
else { else {
// Category header as span (no link)
var span = document.createElement('span'); var span = document.createElement('span');
span.textContent = item.label; span.textContent = item.label;
span.className = this.joinClasses(isActive ? 'is-active' : undefined);
if (item.hoverText) { if (item.hoverText) {
span.title = item.hoverText; span.title = item.hoverText;
} }
h3.appendChild(span); h3.appendChild(span);
} }
categoryDiv.appendChild(h3); categoryDiv.appendChild(h3);
// Create the third-level items list if they exist
if (item.hasChildren() && item.items && item.items.length > 0) { if (item.hasChildren() && item.items && item.items.length > 0) {
var ul_1 = document.createElement('ul'); var ul_1 = document.createElement('ul');
ul_1.className = 'mega-menu-links';
item.items.forEach(function (thirdLevelItem) { item.items.forEach(function (thirdLevelItem) {
var li = document.createElement('li'); var li = document.createElement('li');
var link = document.createElement('a'); var link = document.createElement('a');
var isLinkActive = _this.isItemActive(thirdLevelItem);
var isLinkCurrent = _this.isCurrentUrl(thirdLevelItem.url);
li.className = _this.joinClasses(isLinkActive ? 'is-active' : undefined, isLinkCurrent ? 'is-current' : undefined);
link.href = thirdLevelItem.url || '#'; link.href = thirdLevelItem.url || '#';
link.textContent = thirdLevelItem.label; link.textContent = thirdLevelItem.label;
link.className = _this.joinClasses(isLinkActive ? 'is-active' : undefined, isLinkCurrent ? 'is-current' : undefined);
if (isLinkCurrent && thirdLevelItem.url) {
link.setAttribute('aria-current', 'page');
}
if (thirdLevelItem.hoverText) { if (thirdLevelItem.hoverText) {
link.title = thirdLevelItem.hoverText; link.title = thirdLevelItem.hoverText;
} }
@@ -172,7 +164,7 @@ var MegaMenuRenderer = (function () {
return categoryDiv; return categoryDiv;
}; };
MegaMenuRenderer.prototype.attachEventListeners = function () { MegaMenuRenderer.prototype.attachEventListeners = function () {
var headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span[role="menuitem"]'); var headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > .menu-item-text');
for (var i = 0; i < headings.length; i++) { for (var i = 0; i < headings.length; i++) {
var heading = headings[i]; var heading = headings[i];
var megaMenu = heading.nextElementSibling; var megaMenu = heading.nextElementSibling;
@@ -182,86 +174,101 @@ var MegaMenuRenderer = (function () {
this.attachFocusManagement(heading, megaMenu); this.attachFocusManagement(heading, megaMenu);
} }
} }
// Global keyboard navigation
this.attachGlobalKeyboardNavigation(); this.attachGlobalKeyboardNavigation();
this.attachGlobalClickOutsideHandler();
}; };
MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) { MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) {
var _this = this; var _this = this;
heading.addEventListener('keydown', function (e) { heading.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
// Enter: Navigation (nur bei Links ohne Mega-Menu-Override)
if (heading.tagName === 'A') { if (heading.tagName === 'A') {
// Lasse normale Link-Navigation zu (KEIN preventDefault!)
return; return;
} }
else { e.preventDefault();
// Bei span: Toggle Menu _this.toggleMegaMenu(heading, megaMenu);
e.preventDefault();
_this.toggleMegaMenu(heading, megaMenu);
}
} }
else if (e.key === ' ') { else if (e.key === ' ') {
// Space: Toggle Dropdown (immer)
e.preventDefault(); e.preventDefault();
_this.toggleMegaMenu(heading, megaMenu); _this.toggleMegaMenu(heading, megaMenu);
} }
else if (e.key === 'ArrowDown') { else if (e.key === 'ArrowDown') {
// Pfeil runter: Menü öffnen + erster Link
e.preventDefault(); e.preventDefault();
_this.openMegaMenu(heading, megaMenu); _this.openMegaMenu(heading, megaMenu);
_this.focusFirstLink(megaMenu); _this.focusFirstLink(megaMenu);
} }
else if (e.key === 'ArrowUp') { else if (e.key === 'ArrowUp') {
// Pfeil hoch: Menü schließen
e.preventDefault(); e.preventDefault();
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
} }
else if (e.key === 'Escape') { else if (e.key === 'Escape') {
// Escape: Menü schließen
e.preventDefault(); e.preventDefault();
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
heading.focus(); heading.focus();
} }
}); });
// Click Event für Top-Level Links - NORMALE Navigation erlauben
if (heading.tagName === 'A') { if (heading.tagName === 'A') {
heading.addEventListener('click', function (e) { heading.addEventListener('click', function () {
// Links sollen normal navigieren, nicht das Mega-Menu toglen debugLog(_this.debug, LOG_SOURCE, 'Link clicked:', heading.href);
// 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 () { heading.addEventListener('focus', function () {
console.log('Focus auf:', heading.textContent); debugLog(_this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
// Einfach nur fokussiert - keine automatischen Aktionen!
}); });
}; };
MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) { MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) {
var _this = this; var _this = this;
var parentLi = heading.parentElement; var parentLi = heading.parentElement;
var openTimeout;
var closeTimeout;
var clearOpenTimeout = function () {
if (typeof openTimeout === 'number') {
window.clearTimeout(openTimeout);
openTimeout = undefined;
}
};
var clearCloseTimeout = function () {
if (typeof closeTimeout === 'number') {
window.clearTimeout(closeTimeout);
closeTimeout = undefined;
}
};
parentLi.addEventListener('mouseenter', function () { parentLi.addEventListener('mouseenter', function () {
_this.openMegaMenu(heading, megaMenu); clearCloseTimeout();
if (megaMenu.classList.contains('js-open')) {
return;
}
clearOpenTimeout();
openTimeout = window.setTimeout(function () {
_this.openMegaMenu(heading, megaMenu);
openTimeout = undefined;
}, MegaMenuRenderer.hoverOpenDelayMs);
}); });
parentLi.addEventListener('mouseleave', function () { parentLi.addEventListener('mouseleave', function () {
_this.closeMegaMenu(heading, megaMenu); clearOpenTimeout();
clearCloseTimeout();
closeTimeout = window.setTimeout(function () {
_this.closeMegaMenu(heading, megaMenu);
closeTimeout = undefined;
}, MegaMenuRenderer.hoverCloseDelayMs);
});
heading.addEventListener('focus', function () {
clearCloseTimeout();
});
megaMenu.addEventListener('mouseenter', function () {
clearCloseTimeout();
}); });
}; };
MegaMenuRenderer.prototype.attachFocusManagement = function (heading, megaMenu) { MegaMenuRenderer.prototype.attachFocusManagement = function (heading, megaMenu) {
var _this = this; var _this = this;
// Focus-Verlust-Behandlung - vereinfacht megaMenu.addEventListener('focusout', function () {
megaMenu.addEventListener('focusout', function (e) {
// Kurz warten, um zu prüfen ob Focus innerhalb des Mega-Menus bleibt
setTimeout(function () { setTimeout(function () {
var focusedElement = document.activeElement; var focusedElement = document.activeElement;
var isInsideThisMenu = megaMenu.contains(focusedElement); var isInsideThisMenu = megaMenu.contains(focusedElement);
var isOnThisTrigger = focusedElement === heading; var isOnThisTrigger = focusedElement === heading;
var isInAnyMegaMenu = focusedElement.closest('.mega-menu'); var isInAnyMegaMenu = focusedElement.closest('.mega-menu');
var isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span'); 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) { if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
console.log('Schließe Menu wegen Focus-Verlust'); debugLog(_this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.');
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
} }
}, 150); }, 150);
@@ -269,7 +276,7 @@ var MegaMenuRenderer = (function () {
}; };
MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () { MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () {
var _this = this; var _this = this;
document.addEventListener('keydown', function (e) { this._documentKeydownHandler = function (e) {
var activeElement = document.activeElement; var activeElement = document.activeElement;
if (e.key === 'Escape') { if (e.key === 'Escape') {
var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]'); var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]');
@@ -279,16 +286,13 @@ var MegaMenuRenderer = (function () {
triggerLink.focus(); triggerLink.focus();
} }
} }
// Tab-Navigation: Nur bei spezifischen Übergängen eingreifen
if (e.key === 'Tab') { if (e.key === 'Tab') {
// Von Top-Level zum ersten Link im offenen Menu
if (!e.shiftKey) { if (!e.shiftKey) {
var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span'); var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
if (currentTopLevel) { if (currentTopLevel) {
var parentLi = currentTopLevel.closest('li'); var parentLi = currentTopLevel.closest('li');
var megaMenu = parentLi.querySelector('.mega-menu.js-open'); var megaMenu = parentLi.querySelector('.mega-menu.js-open');
if (megaMenu) { if (megaMenu) {
// Nur eingreifen wenn wir vom Trigger weg-tabben
e.preventDefault(); e.preventDefault();
var firstLink = megaMenu.querySelector('a'); var firstLink = megaMenu.querySelector('a');
if (firstLink) { if (firstLink) {
@@ -298,13 +302,11 @@ var MegaMenuRenderer = (function () {
} }
} }
} }
// Shift+Tab: Vom ersten Link im Menu zurück zum Trigger
if (e.shiftKey) { if (e.shiftKey) {
var megaMenu = activeElement.closest('.mega-menu'); var megaMenu = activeElement.closest('.mega-menu');
if (megaMenu && megaMenu.classList.contains('js-open')) { if (megaMenu && megaMenu.classList.contains('js-open')) {
var allLinksInMenu = megaMenu.querySelectorAll('a'); var allLinksInMenu = megaMenu.querySelectorAll('a');
var firstLinkInMenu = allLinksInMenu[0]; var firstLinkInMenu = allLinksInMenu[0];
// Nur eingreifen wenn wir beim ersten Link sind
if (activeElement === firstLinkInMenu) { if (activeElement === firstLinkInMenu) {
e.preventDefault(); e.preventDefault();
var triggerElement = megaMenu.previousElementSibling; var triggerElement = megaMenu.previousElementSibling;
@@ -313,21 +315,41 @@ var MegaMenuRenderer = (function () {
} }
} }
} }
// Ansonsten: Normale Tab-Navigation nicht unterbrechen!
} }
}); };
document.addEventListener('keydown', this._documentKeydownHandler);
};
MegaMenuRenderer.prototype.attachGlobalClickOutsideHandler = function () {
var _this = this;
this._documentClickHandler = function (e) {
var eventTarget = e.target;
var isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' &&
eventTarget.closest('#CustomNavigation'));
if (isInsideNavigation) {
return;
}
var openMenu = document.querySelector('.mega-menu.js-open');
if (openMenu) {
debugLog(_this.debug, LOG_SOURCE, 'Closing menu because of outside click.');
_this.closeAllMegaMenus();
}
};
document.addEventListener('click', this._documentClickHandler);
}; };
MegaMenuRenderer.prototype.openMegaMenu = function (trigger, menu) { MegaMenuRenderer.prototype.openMegaMenu = function (trigger, menu) {
this.closeAllMegaMenus(menu);
trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-expanded', 'true'); menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open'); menu.classList.add('js-open');
console.log('Menu geöffnet:', trigger.textContent); this.announce((trigger.textContent || '') + ' geöffnet');
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
}; };
MegaMenuRenderer.prototype.closeMegaMenu = function (trigger, menu) { MegaMenuRenderer.prototype.closeMegaMenu = function (trigger, menu) {
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-expanded', 'false'); menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open'); menu.classList.remove('js-open');
console.log('Menu geschlossen:', trigger.textContent); this.announce((trigger.textContent || '') + ' geschlossen');
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
}; };
MegaMenuRenderer.prototype.toggleMegaMenu = function (trigger, menu) { MegaMenuRenderer.prototype.toggleMegaMenu = function (trigger, menu) {
var isOpen = trigger.getAttribute('aria-expanded') === 'true'; var isOpen = trigger.getAttribute('aria-expanded') === 'true';
@@ -335,36 +357,112 @@ var MegaMenuRenderer = (function () {
this.closeMegaMenu(trigger, menu); this.closeMegaMenu(trigger, menu);
} }
else { else {
this.closeAllMegaMenus();
this.openMegaMenu(trigger, menu); this.openMegaMenu(trigger, menu);
} }
}; };
MegaMenuRenderer.prototype.closeAllMegaMenus = function () { MegaMenuRenderer.prototype.closeAllMegaMenus = function (exceptMenu) {
var allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]'); 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++) { for (var i = 0; i < allTriggers.length; i++) {
var trigger = allTriggers[i]; var trigger = allTriggers[i];
var menu = trigger.nextElementSibling; var menu = trigger.nextElementSibling;
if (menu) { if (menu && menu !== exceptMenu) {
this.closeMegaMenu(trigger, menu); this.closeMegaMenu(trigger, menu);
} }
} }
}; };
// removed unused closeOtherMegaMenus (was previously declared but not used)
MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) { MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) {
var firstLink = megaMenu.querySelector('a'); var firstLink = megaMenu.querySelector('a');
if (firstLink) { if (firstLink) {
firstLink.focus(); firstLink.focus();
} }
}; };
MegaMenuRenderer.prototype.isItemActive = function (item) {
if (this.isCurrentUrl(item.url)) {
return true;
}
if (!item.items || item.items.length === 0) {
return false;
}
for (var i = 0; i < item.items.length; i++) {
if (this.isItemActive(item.items[i])) {
return true;
}
}
return false;
};
MegaMenuRenderer.prototype.isCurrentUrl = function (url) {
var currentPath = this.normalizePath(window.location.href);
var itemPath = this.normalizePath(url);
if (!currentPath || !itemPath) {
return false;
}
if (itemPath === '/') {
return currentPath === '/';
}
if (currentPath === itemPath) {
return true;
}
return currentPath.indexOf(itemPath + '/') === 0;
};
MegaMenuRenderer.prototype.normalizePath = function (url) {
if (!url) {
return '';
}
try {
var parsedUrl = new URL(url, window.location.origin);
var pathname = (parsedUrl.pathname || '/').replace(/\/+$/, '');
return (pathname || '/').toLowerCase();
}
catch (error) {
return '';
}
};
MegaMenuRenderer.prototype.getColumnCount = function (items) {
var itemCount = items ? items.length : 0;
var columnCount = Math.min(Math.max(itemCount, 1), 4);
return columnCount.toString();
};
MegaMenuRenderer.prototype.joinClasses = function () {
var classNames = [];
for (var _i = 0; _i < arguments.length; _i++) {
classNames[_i] = arguments[_i];
}
var filteredClassNames = [];
for (var i = 0; i < classNames.length; i++) {
if (classNames[i]) {
filteredClassNames.push(classNames[i]);
}
}
return filteredClassNames.join(' ');
};
MegaMenuRenderer.prototype.createScreenReaderAnnouncer = function () { MegaMenuRenderer.prototype.createScreenReaderAnnouncer = function () {
// Screenreader-Ankündigungen var existingAnnouncer = document.getElementById('mega-menu-sr-announcer');
if (existingAnnouncer) {
this._announcer = existingAnnouncer;
return;
}
var srAnnouncer = document.createElement('div'); var srAnnouncer = document.createElement('div');
srAnnouncer.id = 'mega-menu-sr-announcer';
srAnnouncer.setAttribute('aria-live', 'polite'); srAnnouncer.setAttribute('aria-live', 'polite');
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);
console.log('Screenreader-Ankündigungen sind jetzt bereit'); this._announcer = srAnnouncer;
debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.');
}; };
MegaMenuRenderer.prototype.announce = function (message) {
var _this = this;
if (this._announcer) {
this._announcer.textContent = '';
window.setTimeout(function () {
if (_this._announcer) {
_this._announcer.textContent = message;
}
}, 20);
}
};
MegaMenuRenderer.hoverOpenDelayMs = 140;
MegaMenuRenderer.hoverCloseDelayMs = 180;
return MegaMenuRenderer; return MegaMenuRenderer;
}()); }());
export { MegaMenuRenderer }; export { MegaMenuRenderer };
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
+4 -2
View File
@@ -1,5 +1,7 @@
define([], function() { define([], function() {
return { return {
"Title": "MegaMenuApplicationCustomizer" "Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Navigation is not configured.",
"LoadError": "Navigation could not be loaded."
} }
}); });
+1
View File
@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
hideDeprecatedTags: boolean; hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean; hideTagsNotAvailableForTagging: boolean;
anchorId: string; anchorId: string;
cacheMinutes: number;
} }
+7 -3
View File
@@ -8,12 +8,13 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export default class SPTermStorePickerService { export default class SPTermStorePickerService {
private props; private props;
private context; private context;
private debug;
private clientServiceUrl; private clientServiceUrl;
private suggestionServiceUrl; private suggestionServiceUrl;
/** /**
* Service constructor * Service constructor
*/ */
constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext); constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext, debug?: boolean);
getTermLabels(termId: string): Promise<string[]>; getTermLabels(termId: string): Promise<string[]>;
/** /**
* Gets the collection of term stores in the current SharePoint env * Gets the collection of term stores in the current SharePoint env
@@ -27,7 +28,7 @@ export default class SPTermStorePickerService {
* Retrieve all terms for the given term set * Retrieve all terms for the given term set
* @param termset * @param termset
*/ */
getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean): Promise<ITermSet>; getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean, cacheMinutes?: number): Promise<ITermSet>;
/** /**
* Retrieve all terms that starts with the searchText * Retrieve all terms that starts with the searchText
* @param searchText * @param searchText
@@ -49,7 +50,10 @@ export default class SPTermStorePickerService {
* @param termset * @param termset
*/ */
private getTermSetId(termstore, termsetName); private getTermSetId(termstore, termsetName);
private getTermsById(termId, useSessionStorage?); private getCachedTermSet(termId, useSessionStorage?);
private getCachedPickerTerms(termId, useSessionStorage?);
private normalizeCacheMinutes(value);
private ensureSuccessfulResponse(response, operation);
private searchTermsBySearchText(terms, searchText); private searchTermsBySearchText(terms, searchText);
/** /**
* Searches terms for the given term set * Searches terms for the given term set
+76 -18
View File
@@ -39,7 +39,10 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
}; };
import { SPHttpClient } from '@microsoft/sp-http'; import { SPHttpClient } from '@microsoft/sp-http';
import { findIndex } from '@microsoft/sp-lodash-subset'; import { findIndex } from '@microsoft/sp-lodash-subset';
import { debugError } from './MegaMenuDebug';
var EmptyGuid = '00000000-0000-0000-0000-000000000000'; var EmptyGuid = '00000000-0000-0000-0000-000000000000';
var TermCachePrefix = 'MegaMenu:TermSet:v2:';
var PickerCachePrefix = 'MegaMenu:PickerTerms:v2:';
/** /**
* Service implementation to manage term stores in SharePoint * Service implementation to manage term stores in SharePoint
*/ */
@@ -47,9 +50,11 @@ var SPTermStorePickerService = (function () {
/** /**
* Service constructor * Service constructor
*/ */
function SPTermStorePickerService(props, context) { function SPTermStorePickerService(props, context, debug) {
if (debug === void 0) { debug = false; }
this.props = props; this.props = props;
this.context = context; this.context = context;
this.debug = debug;
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';
} }
@@ -63,7 +68,7 @@ var SPTermStorePickerService = (function () {
_a.label = 1; _a.label = 1;
case 1: case 1:
_a.trys.push([1, 4, , 5]); _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>"; 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>";
reqHeaders = new Headers(); reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml'); reqHeaders.append('content-type', 'application/xml');
@@ -74,6 +79,7 @@ var SPTermStorePickerService = (function () {
return [4 /*yield*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions)]; return [4 /*yield*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions)];
case 2: case 2:
callResult = _a.sent(); callResult = _a.sent();
this.ensureSuccessfulResponse(callResult, 'Term labels');
return [4 /*yield*/, callResult.json()]; return [4 /*yield*/, callResult.json()];
case 3: case 3:
jsonResult = _a.sent(); jsonResult = _a.sent();
@@ -85,7 +91,7 @@ var SPTermStorePickerService = (function () {
case 4: case 4:
error_1 = _a.sent(); error_1 = _a.sent();
result = null; result = null;
console.log(error_1.message); debugError(this.debug, 'SPTermStorePickerService', 'Error reading term labels.', error_1);
return [3 /*break*/, 5]; return [3 /*break*/, 5];
case 5: return [2 /*return*/, result]; case 5: return [2 /*return*/, result];
} }
@@ -107,6 +113,7 @@ var SPTermStorePickerService = (function () {
body: data body: data
}; };
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
_this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
return serviceResponse.json().then(function (serviceJSONResponse) { return serviceResponse.json().then(function (serviceJSONResponse) {
// Construct results // Construct results
var termStoreResult = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermStore'; }); var termStoreResult = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermStore'; });
@@ -160,8 +167,9 @@ var SPTermStorePickerService = (function () {
* Retrieve all terms for the given term set * Retrieve all terms for the given term set
* @param termset * @param termset
*/ */
SPTermStorePickerService.prototype.getAllTerms = function (termset, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage) { SPTermStorePickerService.prototype.getAllTerms = function (termset, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage, cacheMinutes) {
if (useSessionStorage === void 0) { useSessionStorage = true; } if (useSessionStorage === void 0) { useSessionStorage = true; }
if (cacheMinutes === void 0) { cacheMinutes = 15; }
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var _this = this; var _this = this;
var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions; var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions;
@@ -182,11 +190,11 @@ var SPTermStorePickerService = (function () {
} }
_a.label = 2; _a.label = 2;
case 2: case 2:
childTerms = this.getTermsById(termsetId, useSessionStorage); childTerms = this.getCachedTermSet(termsetId, useSessionStorage);
if (childTerms) { if (childTerms) {
return [2 /*return*/, 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>"; 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>";
reqHeaders = new Headers(); reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml'); reqHeaders.append('content-type', 'application/xml');
@@ -195,7 +203,11 @@ var SPTermStorePickerService = (function () {
body: data body: data
}; };
return [2 /*return*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) { return [2 /*return*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
_this.ensureSuccessfulResponse(serviceResponse, 'Terms');
return serviceResponse.json().then(function (serviceJSONResponse) { return serviceResponse.json().then(function (serviceJSONResponse) {
if (!Array.isArray(serviceJSONResponse)) {
throw new Error('The taxonomy service returned an unexpected response.');
}
var termStoreResultTermSets = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermSet'; }); var termStoreResultTermSets = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermSet'; });
if (termStoreResultTermSets.length > 0) { if (termStoreResultTermSets.length > 0) {
var termStoreResultTermSet_1 = termStoreResultTermSets[0]; var termStoreResultTermSet_1 = termStoreResultTermSets[0];
@@ -236,7 +248,12 @@ var SPTermStorePickerService = (function () {
} }
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet_1)); var ttlMinutes = _this.normalizeCacheMinutes(cacheMinutes);
var cacheEntry = {
expiresAt: Date.now() + ttlMinutes * 60 * 1000,
value: termStoreResultTermSet_1
};
window.sessionStorage.setItem(TermCachePrefix + termsetId, JSON.stringify(cacheEntry));
} }
} }
catch (error) { catch (error) {
@@ -265,7 +282,7 @@ var SPTermStorePickerService = (function () {
switch (_b.label) { switch (_b.label) {
case 0: case 0:
useSessionStorage = this.props.useSessionStorage; useSessionStorage = this.props.useSessionStorage;
childTerms = this.getTermsById(termId, useSessionStorage); childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
if (!childTerms) return [3 /*break*/, 1]; if (!childTerms) return [3 /*break*/, 1];
return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)]; return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)];
case 1: case 1:
@@ -289,16 +306,16 @@ var SPTermStorePickerService = (function () {
if (useSessionStorage === void 0) { useSessionStorage = true; } if (useSessionStorage === void 0) { useSessionStorage = true; }
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var _this = this; var _this = this;
var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms; var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms, cacheEntry;
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
returnTerms = []; returnTerms = [];
childTerms = this.getTermsById(anchorId, useSessionStorage); childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
if (childTerms) { if (childTerms) {
return [2 /*return*/, childTerms]; return [2 /*return*/, childTerms];
} }
return [4 /*yield*/, this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging)]; return [4 /*yield*/, this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage, this.props.cacheMinutes)];
case 1: case 1:
termSet = _a.sent(); termSet = _a.sent();
terms = termSet.Terms; terms = termSet.Terms;
@@ -312,7 +329,11 @@ var SPTermStorePickerService = (function () {
}); });
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms)); cacheEntry = {
expiresAt: Date.now() + this.normalizeCacheMinutes(this.props.cacheMinutes) * 60 * 1000,
value: returnTerms
};
window.sessionStorage.setItem(PickerCachePrefix + anchorId, JSON.stringify(cacheEntry));
} }
} }
catch (error) { catch (error) {
@@ -370,17 +391,21 @@ var SPTermStorePickerService = (function () {
} }
return null; return null;
}; };
SPTermStorePickerService.prototype.getTermsById = function (termId, useSessionStorage) { SPTermStorePickerService.prototype.getCachedTermSet = function (termId, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; } if (useSessionStorage === void 0) { useSessionStorage = true; }
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
var terms = window.sessionStorage.getItem(termId); var key = TermCachePrefix + termId;
if (terms) { var terms = window.sessionStorage.getItem(key);
return JSON.parse(terms); if (!terms) {
}
else {
return null; return null;
} }
var cacheEntry = JSON.parse(terms);
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} }
else { else {
return null; return null;
@@ -390,6 +415,39 @@ var SPTermStorePickerService = (function () {
return null; return null;
} }
}; };
SPTermStorePickerService.prototype.getCachedPickerTerms = function (termId, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; }
try {
if (!useSessionStorage || !window.sessionStorage) {
return null;
}
var key = PickerCachePrefix + termId;
var serialized = window.sessionStorage.getItem(key);
if (!serialized) {
return null;
}
var cacheEntry = JSON.parse(serialized);
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
}
catch (error) {
return null;
}
};
SPTermStorePickerService.prototype.normalizeCacheMinutes = function (value) {
if (typeof value !== 'number' || !isFinite(value)) {
return 15;
}
return Math.min(Math.max(Math.floor(value), 1), 1440);
};
SPTermStorePickerService.prototype.ensureSuccessfulResponse = function (response, operation) {
if (!response.ok) {
throw new Error(operation + ' request failed (' + response.status + ' ' + response.statusText + ').');
}
};
SPTermStorePickerService.prototype.searchTermsBySearchText = function (terms, searchText) { SPTermStorePickerService.prototype.searchTermsBySearchText = function (terms, searchText) {
if (terms) { if (terms) {
return terms.filter(function (t) { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; }); return terms.filter(function (t) { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -3,9 +3,10 @@ import { IMenuItem } from './IMenuItem';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export declare class TaxonomyNavigationService implements ITaxonomyNavigationService { export declare class TaxonomyNavigationService implements ITaxonomyNavigationService {
private context; private context;
private termSetName; private termSetNameOrId;
private debug;
private cacheMinutes;
private _taxonomyPickerService; private _taxonomyPickerService;
private _noTerm; constructor(context: ApplicationCustomizerContext, termSetNameOrId: string, debug?: boolean, cacheMinutes?: number);
constructor(context: ApplicationCustomizerContext, termSetName: string);
getMenuItems(): Promise<IMenuItem[]>; getMenuItems(): Promise<IMenuItem[]>;
} }
+30 -46
View File
@@ -37,40 +37,28 @@ import { sp } from '@pnp/sp';
import { MenuItem } from './MenuItem'; import { MenuItem } from './MenuItem';
import SPTermStorePickerService from './SPTermStorePickerService'; import SPTermStorePickerService from './SPTermStorePickerService';
import { ItemDictionary } from './ItemDictionary'; import { ItemDictionary } from './ItemDictionary';
import { debugWarn } from './MegaMenuDebug';
var LOG_SOURCE = 'TaxonomyNavigationService';
var TaxonomyNavigationService = (function () { var TaxonomyNavigationService = (function () {
function TaxonomyNavigationService(context /*, private lcid: number*/, termSetName) { function TaxonomyNavigationService(context, termSetNameOrId, debug, cacheMinutes) {
this.context = context; /*, private lcid: number*/ if (debug === void 0) { debug = false; }
this.termSetName = termSetName; if (cacheMinutes === void 0) { cacheMinutes = 15; }
this._noTerm = { this.context = context;
_ObjectType_: '', this.termSetNameOrId = termSetNameOrId;
_ObjectIdentity_: '', this.debug = debug;
CustomSortOrderIndex: 0, this.cacheMinutes = cacheMinutes;
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({ sp.setup({
spfxContext: context spfxContext: context
}); });
this._taxonomyPickerService = new SPTermStorePickerService({ this._taxonomyPickerService = new SPTermStorePickerService({
anchorId: '', anchorId: '',
termsetNameOrID: termSetName, termsetNameOrID: termSetNameOrId,
useSessionStorage: true, useSessionStorage: true,
hideDeprecatedTags: true, hideDeprecatedTags: true,
hideTagsNotAvailableForTagging: false hideTagsNotAvailableForTagging: true,
}, this.context); cacheMinutes: cacheMinutes
}, this.context, this.debug);
} }
// Implement the methods from ITaxonomyNavigationService interface here
TaxonomyNavigationService.prototype.getMenuItems = function () { TaxonomyNavigationService.prototype.getMenuItems = function () {
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var _this = this; var _this = this;
@@ -79,35 +67,31 @@ var TaxonomyNavigationService = (function () {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
siteCollectionUrl = this.context.pageContext.site.absoluteUrl; siteCollectionUrl = this.context.pageContext.site.absoluteUrl;
return [4 /*yield*/, this._taxonomyPickerService.getAllTerms(this.termSetName)]; return [4 /*yield*/, this._taxonomyPickerService.getAllTerms(this.termSetNameOrId, true, true, true, this.cacheMinutes)];
case 1: case 1:
termset = _a.sent(); termset = _a.sent();
itemsDict = new ItemDictionary(); itemsDict = new ItemDictionary();
menuItems = []; menuItems = [];
if (!termset || !termset.Terms) { if (!termset || !termset.Terms || termset.Terms.length === 0) {
console.warn('No terms found in the term set'); debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.');
return [2 /*return*/, [new MenuItem(this._noTerm, 0, siteCollectionUrl)]]; throw new Error('The configured navigation term set was not found or contains no terms.');
} }
termset.Terms.forEach(function (term) { return __awaiter(_this, void 0, void 0, function () { termset.Terms.forEach(function (term) {
var menuItem, parentItem; var menuItem = new MenuItem(term, 0, siteCollectionUrl);
return __generator(this, function (_a) { itemsDict.Add(term.Id, menuItem);
menuItem = new MenuItem(term, 0, siteCollectionUrl); if (menuItem.pathDepth === 1) {
itemsDict.Add(term.Id, menuItem); menuItems.push(menuItem);
if (menuItem.pathDepth === 1) { }
menuItems.push(menuItem); else {
var parentItem = itemsDict.Get(term.ParentId);
if (parentItem) {
parentItem.items.push(menuItem);
} }
else { else {
parentItem = itemsDict.Get(term.ParentId); debugWarn(_this.debug, LOG_SOURCE, 'Item without parent:', term.PathOfTerm);
if (parentItem) {
parentItem.items.push(menuItem);
}
else {
console.warn("Item without parent: " + term.PathOfTerm);
}
} }
return [2 /*return*/]; }
}); });
}); });
return [2 /*return*/, menuItems]; return [2 /*return*/, menuItems];
} }
}); });
File diff suppressed because one or more lines are too long
@@ -4,7 +4,8 @@ import { UserCustomActionScope } from './UserCustomActionScope';
import { IUserCustomActionProps } from './IUserCustomActionProps'; import { IUserCustomActionProps } from './IUserCustomActionProps';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export declare class UserCustomActionService implements IUserCustomActionService { export declare class UserCustomActionService implements IUserCustomActionService {
constructor(context: ApplicationCustomizerContext); private debug;
constructor(context: ApplicationCustomizerContext, debug?: boolean);
getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>; getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>;
getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>; getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>;
addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>; addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>;
@@ -35,8 +35,12 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
}; };
import { sp } from '@pnp/sp'; import { sp } from '@pnp/sp';
import { UserCustomActionScope } from './UserCustomActionScope'; import { UserCustomActionScope } from './UserCustomActionScope';
import { debugError } from '../MegaMenuDebug';
var LOG_SOURCE = 'UserCustomActionService';
var UserCustomActionService = (function () { var UserCustomActionService = (function () {
function UserCustomActionService(context) { function UserCustomActionService(context, debug) {
if (debug === void 0) { debug = false; }
this.debug = debug;
sp.setup({ sp.setup({
spfxContext: context spfxContext: context
}); });
@@ -76,7 +80,7 @@ var UserCustomActionService = (function () {
case 8: return [2 /*return*/, actions]; case 8: return [2 /*return*/, actions];
case 9: case 9:
error_1 = _b.sent(); error_1 = _b.sent();
console.error('Error getting user custom actions: ', error_1); debugError(this.debug, LOG_SOURCE, 'Error getting user custom actions.', error_1);
throw error_1; throw error_1;
case 10: return [2 /*return*/]; case 10: return [2 /*return*/];
} }
@@ -102,7 +106,7 @@ var UserCustomActionService = (function () {
} }
} }
catch (error) { catch (error) {
console.error('Error getting user custom action by ID: ', error); debugError(this.debug, LOG_SOURCE, 'Error getting user custom action by ID.', error);
throw error; throw error;
} }
return [2 /*return*/]; return [2 /*return*/];
@@ -128,7 +132,7 @@ var UserCustomActionService = (function () {
} }
} }
catch (error) { catch (error) {
console.error('Error adding user custom action: ', error); debugError(this.debug, LOG_SOURCE, 'Error adding user custom action.', error);
throw error; throw error;
} }
return [2 /*return*/]; return [2 /*return*/];
@@ -170,7 +174,7 @@ var UserCustomActionService = (function () {
case 8: return [2 /*return*/, result]; case 8: return [2 /*return*/, result];
case 9: case 9:
error_2 = _b.sent(); error_2 = _b.sent();
console.error('Error updating user custom action: ', error_2); debugError(this.debug, LOG_SOURCE, 'Error updating user custom action.', error_2);
throw error_2; throw error_2;
case 10: return [2 /*return*/]; case 10: return [2 /*return*/];
} }
@@ -196,7 +200,7 @@ var UserCustomActionService = (function () {
} }
} }
catch (error) { catch (error) {
console.error('Error deleting user custom action: ', error); debugError(this.debug, LOG_SOURCE, 'Error deleting user custom action.', error);
throw error; throw error;
} }
return [2 /*return*/]; return [2 /*return*/];
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.0.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+4 -3
View File
@@ -1,15 +1,16 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "1.0.4", "version": "2.0.1",
"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": "gulp bundle",
"clean": "gulp clean", "clean": "gulp clean",
"test": "gulp test" "test": "gulp test",
"package": "gulp clean && gulp bundle --ship && gulp package-solution --ship"
}, },
"dependencies": { "dependencies": {
"@microsoft/decorators": "~1.4.1", "@microsoft/decorators": "~1.4.1",
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Title="MegaMenu Application Customizer"
Location="ClientSideExtension.ApplicationCustomizer"
ClientSideComponentId="c0abbffb-355d-4d4e-bd38-9e15fb811506"
ClientSideComponentProperties="{}" />
</Elements>
+14 -6
View File
@@ -103,6 +103,14 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
z-index: $mm-layer-index; z-index: $mm-layer-index;
} }
.mega-menu-status {
padding: 8px 14px;
border-bottom: 1px solid $mm-panel-border;
background: $mm-focus-soft;
color: $mm-text;
font-size: 13px;
}
.mega-menu-main { .mega-menu-main {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -145,7 +153,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li > a, .mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"], .mega-menu-main > ul > li > .menu-item-text,
.menu-item-link, .menu-item-link,
.menu-item-text { .menu-item-text {
position: relative; position: relative;
@@ -199,7 +207,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li:hover > a, .mega-menu-main > ul > li:hover > a,
.mega-menu-main > ul > li:hover > span[role="menuitem"], .mega-menu-main > ul > li:hover > .menu-item-text,
.mega-menu-main > ul > li > a[aria-expanded="true"], .mega-menu-main > ul > li > a[aria-expanded="true"],
.mega-menu-main > ul > li > span[aria-expanded="true"], .mega-menu-main > ul > li > span[aria-expanded="true"],
.menu-item-link:hover, .menu-item-link:hover,
@@ -214,7 +222,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li > a:focus, .mega-menu-main > ul > li > a:focus,
.mega-menu-main > ul > li > span[role="menuitem"]:focus, .mega-menu-main > ul > li > .menu-item-text:focus,
.menu-item-link:focus, .menu-item-link:focus,
.menu-item-text:focus { .menu-item-text:focus {
color: $mm-nav-text; color: $mm-nav-text;
@@ -416,7 +424,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li > a, .mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"], .mega-menu-main > ul > li > .menu-item-text,
.menu-item-link, .menu-item-link,
.menu-item-text { .menu-item-text {
min-height: 36px; min-height: 36px;
@@ -448,7 +456,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
.skip-link, .skip-link,
.menu-item-has-children::after, .menu-item-has-children::after,
.mega-menu-main > ul > li > a, .mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"], .mega-menu-main > ul > li > .menu-item-text,
.menu-item-link, .menu-item-link,
.menu-item-text, .menu-item-text,
.mega-menu, .mega-menu,
@@ -460,7 +468,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
@media (prefers-contrast: high) { @media (prefers-contrast: high) {
.mega-menu-main > ul > li > a:focus, .mega-menu-main > ul > li > a:focus,
.mega-menu-main > ul > li > span[role="menuitem"]:focus, .mega-menu-main > ul > li > .menu-item-text: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 > h3 > a:focus,
@@ -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",
@@ -19,11 +19,12 @@ import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
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;
} }
@@ -31,78 +32,121 @@ 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;
@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.0.1',
} 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()
});
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;
} }
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()
); );
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 (this._renderer) {
debugLog(debug, LOG_SOURCE, 'Disposing the previous renderer instance.');
this._renderer.dispose();
}
this._renderer = new MegaMenuRenderer(
menuItems, menuItems,
debug debug
); );
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);
} }
} }
@@ -110,42 +154,81 @@ export default class MegaMenuApplicationCustomizer
const container = document.getElementById(id); const container = document.getElementById(id);
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 || ''
});
let host: HTMLElement = document.getElementById('MegaMenuHost');
if (!host) {
host = document.createElement('div');
host.id = 'MegaMenuHost';
container.appendChild(host);
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'MegaMenuHost was created inside the existing host container.');
} else {
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing MegaMenuHost will be reused.');
}
return host;
} }
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'No external host container found; the Top placeholder will be used.', {
requestedId: id
});
return placeholder.domElement; return placeholder.domElement;
} }
private _loadExternalCss(cssUrl?: string, debug: boolean = this._isDebugEnabled()): void { private _onDispose = (): void => {
const externalCssLinkId: string = 'mega-menu-additional-css-34FAB720'; this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders);
let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement; if (this._renderer) {
this._renderer.dispose();
if (cssUrl && cssUrl.trim() !== '') { this._renderer = undefined;
if (!link) {
const head: HTMLHeadElement = document.getElementsByTagName('head')[0];
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.id = externalCssLinkId;
link.onload = () => {
debugLog(debug, LOG_SOURCE, 'External CSS loaded successfully from:', cssUrl);
};
link.onerror = () => {
debugWarn(debug, LOG_SOURCE, 'Failed to load external CSS from:', cssUrl);
};
head.appendChild(link);
}
link.href = cssUrl;
} else if (link) {
link.remove();
} }
this._menuContainer = undefined;
this._topPlaceholder = undefined;
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
} }
private _onDispose = (): void => { private _getTermSetIdentifier(): string {
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.'); 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 _renderStatus(message: string): void {
if (!this._topPlaceholder) {
return;
}
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);
}
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 {
@@ -155,4 +238,4 @@ export default class MegaMenuApplicationCustomizer
return !!(this.properties && this.properties.debug === true); return !!(this.properties && this.properties.debug === true);
} }
} }
+64 -12
View File
@@ -13,25 +13,34 @@ const LOG_SOURCE: string = 'MegaMenuRenderer';
export class MegaMenuRenderer { export class MegaMenuRenderer {
private static readonly hoverOpenDelayMs: number = 140; private static readonly hoverOpenDelayMs: number = 140;
private static readonly hoverCloseDelayMs: number = 180; private static readonly hoverCloseDelayMs: number = 180;
private _container: HTMLElement | undefined;
private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined;
private _documentClickHandler: ((event: MouseEvent) => void) | undefined;
private _announcer: HTMLElement | undefined;
constructor( constructor(
private menuItems: IMenuItem[], private menuItems: IMenuItem[],
private debug: boolean = false private debug: boolean = false
) { } ) { }
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 = 'mega-menu-main';
nav.setAttribute('role', 'navigation');
nav.setAttribute('aria-label', 'Hauptnavigation'); nav.setAttribute('aria-label', 'Hauptnavigation');
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');
this.menuItems.forEach(topLevelItem => { this.menuItems.forEach(topLevelItem => {
const topLevelLi = this.createTopLevelItem(topLevelItem); const topLevelLi = this.createTopLevelItem(topLevelItem);
@@ -43,6 +52,34 @@ 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;
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,7 +94,6 @@ 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);
@@ -83,7 +119,6 @@ export class MegaMenuRenderer {
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,
@@ -111,7 +146,6 @@ export class MegaMenuRenderer {
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 + ' Unterkategorien');
@@ -213,7 +247,7 @@ export class MegaMenuRenderer {
} }
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 > a, #Mega-Menu > ul > li > .menu-item-text');
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;
@@ -336,7 +370,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') {
@@ -380,11 +414,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,7 +434,8 @@ 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 openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
@@ -407,6 +443,7 @@ export class MegaMenuRenderer {
trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-expanded', 'true'); menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open'); menu.classList.add('js-open');
this.announce((trigger.textContent || '') + ' geöffnet');
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent); debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
} }
@@ -414,6 +451,7 @@ export class MegaMenuRenderer {
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-expanded', 'false'); menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open'); menu.classList.remove('js-open');
this.announce((trigger.textContent || '') + ' geschlossen');
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent); debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
} }
@@ -514,7 +552,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 +564,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 '';
}
}
+7
View File
@@ -0,0 +1,7 @@
define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Die Navigation ist nicht konfiguriert.",
"LoadError": "Die Navigation konnte nicht geladen werden."
};
});
+4 -2
View File
@@ -1,5 +1,7 @@
define([], function() { define([], function() {
return { return {
"Title": "MegaMenuApplicationCustomizer" "Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Navigation is not configured.",
"LoadError": "Navigation could not be loaded."
} }
}); });
+2
View File
@@ -1,5 +1,7 @@
declare interface IMegaMenuApplicationCustomizerStrings { declare interface IMegaMenuApplicationCustomizerStrings {
Title: string; Title: string;
ConfigurationMissing: string;
LoadError: string;
} }
declare module 'MegaMenuApplicationCustomizerStrings' { declare module 'MegaMenuApplicationCustomizerStrings' {
@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
hideDeprecatedTags: boolean; hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean; hideTagsNotAvailableForTagging: boolean;
anchorId: string; anchorId: string;
} cacheMinutes: number;
}
+3 -2
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;
} }
@@ -23,4 +24,4 @@ export function debugError(enabled: boolean, source: string, message: string, ..
const output: any[] = ['[' + source + '] ' + message].concat(args || []); const output: any[] = ['[' + source + '] ' + message].concat(args || []);
console.error.apply(console, output); console.error.apply(console, output);
} }
+158 -18
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';
const EmptyGuid: string = '00000000-0000-0000-0000-000000000000'; const EmptyGuid: string = '00000000-0000-0000-0000-000000000000';
const TermCachePrefix: string = 'MegaMenu:TermSet:v2:';
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,18 @@ 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
});
} }
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 +60,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 +91,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 +143,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 +166,32 @@ 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);
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 +203,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 +225,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,13 +267,27 @@ 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
};
window.sessionStorage.setItem(TermCachePrefix + termsetId, JSON.stringify(cacheEntry));
debugLog(this.debug, 'SPTermStorePickerService', 'Term set stored in session cache.', {
key: TermCachePrefix + termsetId,
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;
}); });
}); });
@@ -225,7 +303,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 +335,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 +361,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 +420,21 @@ export default class SPTermStorePickerService {
return null; return null;
} }
private getTermsById(termId, useSessionStorage: boolean = true) { private getCachedTermSet(termId: string, useSessionStorage: boolean = true): ITermSet {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
const terms = window.sessionStorage.getItem(termId); const key: string = TermCachePrefix + termId;
if (terms) { const terms: string = window.sessionStorage.getItem(key);
return JSON.parse(terms); if (!terms) {
} else {
return null; return null;
} }
const cacheEntry: ITermCacheEntry = JSON.parse(terms) as ITermCacheEntry;
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} else { } else {
return null; return null;
} }
@@ -349,6 +443,42 @@ 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 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 +586,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 +606,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
* *
+35 -28
View File
@@ -6,46 +6,35 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import SPTermStorePickerService from './SPTermStorePickerService'; import SPTermStorePickerService from './SPTermStorePickerService';
import { ITerm, ITermSet } from './ISPTermStorePickerService'; import { ITerm, ITermSet } from './ISPTermStorePickerService';
import { ItemDictionary } from './ItemDictionary'; import { ItemDictionary } from './ItemDictionary';
import { debugWarn } from './MegaMenuDebug'; import { debugLog, debugWarn } from './MegaMenuDebug';
const LOG_SOURCE: string = 'TaxonomyNavigationService'; const LOG_SOURCE: string = 'TaxonomyNavigationService';
export class TaxonomyNavigationService implements ITaxonomyNavigationService { export class TaxonomyNavigationService implements ITaxonomyNavigationService {
private _taxonomyPickerService: SPTermStorePickerService; private _taxonomyPickerService: SPTermStorePickerService;
private _noTerm: ITerm = {
_ObjectType_: '',
_ObjectIdentity_: '',
CustomSortOrderIndex: 0,
Description: '',
Id: '',
IsAvailableForTagging: false,
IsDeprecated: false,
IsRoot: true,
LocalCustomProperties: {
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte ueberpruefen Sie Ihre Einstellungen.',
_Sys_Nav_ExcludedProviders: undefined,
_Sys_Nav_SimpleLinkUrl: undefined
},
Name: 'Es wurden keine Terms gefunden. Bitte ueberpruefen Sie Ihre Einstellungen.',
PathOfTerm: '',
TermSet: undefined
};
constructor( constructor(
private context: ApplicationCustomizerContext, private context: ApplicationCustomizerContext,
private termSetName: string, private termSetNameOrId: string,
private debug: boolean = false private debug: boolean = false,
private cacheMinutes: number = 15
) { ) {
debugLog(this.debug, LOG_SOURCE, 'Initializing taxonomy navigation service.', {
termSetNameOrId: this.termSetNameOrId,
cacheMinutes: this.cacheMinutes,
webUrl: this.context.pageContext.web.absoluteUrl,
siteUrl: this.context.pageContext.site.absoluteUrl
});
sp.setup({ sp.setup({
spfxContext: context spfxContext: context
}); });
this._taxonomyPickerService = new SPTermStorePickerService( this._taxonomyPickerService = new SPTermStorePickerService(
{ {
anchorId: '', anchorId: '',
termsetNameOrID: termSetName, termsetNameOrID: termSetNameOrId,
useSessionStorage: true, useSessionStorage: true,
hideDeprecatedTags: true, hideDeprecatedTags: true,
hideTagsNotAvailableForTagging: false hideTagsNotAvailableForTagging: true,
cacheMinutes: cacheMinutes
}, },
this.context, this.context,
this.debug this.debug
@@ -54,13 +43,27 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
public async getMenuItems(): Promise<IMenuItem[]> { public async getMenuItems(): Promise<IMenuItem[]> {
const siteCollectionUrl: string = this.context.pageContext.site.absoluteUrl; const siteCollectionUrl: string = this.context.pageContext.site.absoluteUrl;
const termset: ITermSet = await this._taxonomyPickerService.getAllTerms(this.termSetName); debugLog(this.debug, LOG_SOURCE, 'Reading all terms.', {
termSetNameOrId: this.termSetNameOrId,
cacheMinutes: this.cacheMinutes
});
const termset: ITermSet = await this._taxonomyPickerService.getAllTerms(
this.termSetNameOrId,
true,
true,
true,
this.cacheMinutes
);
debugLog(this.debug, LOG_SOURCE, 'Term set response received.', {
termSetFound: !!termset,
termCount: termset && termset.Terms ? termset.Terms.length : 0
});
const itemsDict: ItemDictionary<IMenuItem> = new ItemDictionary<IMenuItem>(); const itemsDict: ItemDictionary<IMenuItem> = new ItemDictionary<IMenuItem>();
const menuItems: IMenuItem[] = []; const menuItems: IMenuItem[] = [];
if (!termset || !termset.Terms) { if (!termset || !termset.Terms || termset.Terms.length === 0) {
debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.'); debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.');
return [new MenuItem(this._noTerm, 0, siteCollectionUrl)]; throw new Error('The configured navigation term set was not found or contains no terms.');
} }
termset.Terms.forEach((term: ITerm) => { termset.Terms.forEach((term: ITerm) => {
@@ -78,6 +81,10 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
} }
}); });
debugLog(this.debug, LOG_SOURCE, 'Navigation hierarchy built.', {
totalTerms: termset.Terms.length,
topLevelItems: menuItems.length
});
return menuItems; return menuItems;
} }
} }
@@ -1 +1 @@
{"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":["<!-- PATH TO CDN -->/"],"scriptResources":{"mega-menu-application-customizer":{"type":"path","path":"mega-menu-application-customizer_a8c5b6b10667569479c9117e39176bd7.js"},"MegaMenuApplicationCustomizerStrings":{"defaultPath":"megamenu-megamenuapplicationcustomizerstrings_en-us_6adac6d498d12e2a464a299b650578f1.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"}}}} {"id":"abc3361f-bb2d-491f-aba3-cd51c19a299b","alias":"MegaMenuApplicationCustomizer","componentType":"Extension","extensionType":"ApplicationCustomizer","version":"1.1.2","manifestVersion":2,"requiresCustomScript":false,"loaderConfig":{"entryModuleId":"mega-menu-application-customizer","internalModuleBaseUrls":["<!-- PATH TO CDN -->/"],"scriptResources":{"mega-menu-application-customizer":{"type":"path","path":"mega-menu-application-customizer_4b71d735107c7ad3a5ddc7b19a2c8ce9.js"},"MegaMenuApplicationCustomizerStrings":{"defaultPath":"megamenu-megamenuapplicationcustomizerstrings_en-us_c5b741e5b77398ea44801b5b5c0a11a3.js","type":"localizedPath","paths":{"de-DE":"megamenu-megamenuapplicationcustomizerstrings_de-de_b346aef029646a3a296989aa5e40a5ae.js","en-US":"megamenu-megamenuapplicationcustomizerstrings_en-us_c5b741e5b77398ea44801b5b5c0a11a3.js"}},"@microsoft/decorators":{"type":"component","version":"1.4.1","id":"f97266fb-ccb7-430e-9384-4124d05295d3"},"@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"}}}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer"
}
});
+5 -12
View File
@@ -2300,7 +2300,7 @@ function getManifests() {
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
"version": "1.0.4", "version": "1.1.2",
"manifestVersion": 2, "manifestVersion": 2,
"requiresCustomScript": false, "requiresCustomScript": false,
"loaderConfig": { "loaderConfig": {
@@ -2316,18 +2316,16 @@ function getManifests() {
"MegaMenuApplicationCustomizerStrings": { "MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js", "defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath", "type": "localizedPath",
"paths": {} "paths": {
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
}
}, },
"@microsoft/decorators": { "@microsoft/decorators": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3" "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": { "@microsoft/sp-application-base": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
@@ -2342,11 +2340,6 @@ function getManifests() {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a" "id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"
},
"@microsoft/sp-page-context": {
"type": "component",
"version": "1.4.1",
"id": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8"
} }
} }
} }
+5 -12
View File
@@ -695,7 +695,7 @@
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
"version": "1.0.4", "version": "1.1.2",
"manifestVersion": 2, "manifestVersion": 2,
"requiresCustomScript": false, "requiresCustomScript": false,
"loaderConfig": { "loaderConfig": {
@@ -711,18 +711,16 @@
"MegaMenuApplicationCustomizerStrings": { "MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js", "defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath", "type": "localizedPath",
"paths": {} "paths": {
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
}
}, },
"@microsoft/decorators": { "@microsoft/decorators": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3" "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": { "@microsoft/sp-application-base": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
@@ -737,11 +735,6 @@
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a" "id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"
},
"@microsoft/sp-page-context": {
"type": "component",
"version": "1.4.1",
"id": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8"
} }
} }
} }