Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d91c4ba97 | ||
|
|
e75b11117f | ||
|
|
dd79e85725 | ||
|
|
e119573b5e | ||
|
|
85c56a1153 | ||
|
|
07583b8122 | ||
|
|
2d723da5a2 | ||
|
|
5f1d26c7f6 | ||
|
|
0ab514ca21 | ||
|
|
5cc88a83ba | ||
|
|
97d2b6b5d2 |
+47
@@ -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
|
||||
|
||||
@@ -1,199 +1,312 @@
|
||||
# SharePoint MegaMenu Extension
|
||||
# SharePoint MegaMenu
|
||||
|
||||
**Version:** 1.0.2
|
||||
**Framework:** SharePoint Framework (SPFx) 1.4.1
|
||||
**Node.js:** v8.17.0
|
||||
Barrierearme, dreistufige Portalnavigation auf Basis von SharePoint Managed Metadata für SharePoint Server Subscription Edition und SharePoint Server 2019.
|
||||
|
||||
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)
|
||||
- **Responsive Design** mit automatischer Anpassung an verschiedene Bildschirmgrößen
|
||||
- **Barrierefreiheit** mit vollständiger Tastaturnavigation und Screenreader-Unterstützung
|
||||
- **Einstellungs-Panel** für Administratoren zur Konfiguration
|
||||
- **Externe CSS-Unterstützung** für individuelle Anpassungen
|
||||
- **Office UI Fabric Integration** für konsistente SharePoint-Optik
|
||||
- Dreistufige Navigation aus einem Managed-Metadata-Termset
|
||||
- Auswahl des Termsets per GUID oder, abwärtskompatibel, per Name
|
||||
- Benutzerdefinierte Sortierung aus dem Term Store
|
||||
- Unterstützung der Navigations-Eigenschaften `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText`
|
||||
- Auflösung von `~sitecollection` in Navigations-URLs
|
||||
- Markierung des aktuellen Menüpunktes und seiner übergeordneten Einträge
|
||||
- Bedienung mit Maus, Tastatur und Touch
|
||||
- Responsive Darstellung
|
||||
- Optionales zusätzliches Stylesheet
|
||||
- Taxonomy-Cache mit konfigurierbarer Ablaufzeit
|
||||
- Bereinigter SPFx-Lifecycle ohne verbleibende globale Event Listener
|
||||
- Deutsche und englische Statusmeldungen
|
||||
|
||||
## 📋 Voraussetzungen
|
||||
## Architektur
|
||||
|
||||
- SharePoint 2019 oder SharePoint SE (Subscription Edition)
|
||||
- Node.js v8.17.0 (empfohlen)
|
||||
- SharePoint Framework Development Tools (SPFx 1.4.1)
|
||||
```text
|
||||
UserCustomAction.ClientSideComponentProperties
|
||||
│
|
||||
▼
|
||||
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
|
||||
git clone <repository-url>
|
||||
cd MegaMenu
|
||||
- SharePoint Server Subscription Edition oder SharePoint Server 2019
|
||||
- konfigurierter App Catalog
|
||||
- konfigurierter App Management Service
|
||||
- ein Termset im Standard-Term-Store der Site Collection
|
||||
- Leseberechtigung der Benutzer auf den Managed-Metadata-Service
|
||||
- für Entwicklung und Build: Node.js 8.17.0 und eine zu SPFx 1.4.1 passende npm-Version
|
||||
|
||||
## 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 run package
|
||||
```
|
||||
|
||||
### 2. Entwicklungsserver starten
|
||||
Alternativ einzeln:
|
||||
|
||||
```bash
|
||||
gulp serve
|
||||
```
|
||||
|
||||
### 3. Produktionspaket erstellen
|
||||
|
||||
```bash
|
||||
```powershell
|
||||
gulp clean
|
||||
gulp bundle --ship
|
||||
gulp package-solution --ship
|
||||
```
|
||||
|
||||
## 📁 Projektstruktur
|
||||
Das fertige Paket befindet sich anschließend unter:
|
||||
|
||||
```
|
||||
src/
|
||||
├── extensions/
|
||||
│ └── megaMenu/
|
||||
│ ├── MegaMenuApplicationCustomizer.ts # Haupteinstiegspunkt
|
||||
│ ├── MegaMenuRenderer.ts # Menü-Rendering-Logik
|
||||
│ ├── MegaMenuSettings.ts # Einstellungs-Panel
|
||||
│ └── MegaMenu.css # Styling
|
||||
├── services/
|
||||
│ ├── TaxonomyNavigationService.ts # Taxonomy-Datenabfrage
|
||||
│ ├── UserCustomActionService/ # UserCustomAction-Verwaltung
|
||||
│ └── ...
|
||||
└── deployment/
|
||||
├── add-megamenu.ps1 # Installations-Script
|
||||
└── remove-megamenu.ps1 # Deinstallations-Script
|
||||
```text
|
||||
sharepoint/solution/mega-menu.sppkg
|
||||
```
|
||||
|
||||
## ⚙️ 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
|
||||
2. **Pfad zu zusätzlicher CSS-Datei**: Optional - URL zu einer benutzerdefinierten CSS-Datei
|
||||
|
||||
### PowerShell-Deployment
|
||||
Empfohlen mit Termset-GUID:
|
||||
|
||||
```powershell
|
||||
# Installation
|
||||
.\src\deployment\add-megamenu.ps1
|
||||
|
||||
# Deinstallation
|
||||
.\src\deployment\remove-megamenu.ps1
|
||||
.\deployment\add-megamenu.ps1 `
|
||||
-SiteUrl 'https://sharepoint/sites/portal' `
|
||||
-TermSetId '6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc' `
|
||||
-TermSetName 'Global Navigation' `
|
||||
-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
|
||||
/* Haupt-Container */
|
||||
#CustomNavigation { }
|
||||
#Mega-Menu { }
|
||||
|
||||
/* Top-Level Menüpunkte */
|
||||
#Mega-Menu > ul > li > a { }
|
||||
|
||||
/* Mega-Menü Dropdown */
|
||||
.mega-menu-top-level { }
|
||||
.mega-menu-top-item { }
|
||||
.mega-menu { }
|
||||
|
||||
/* Kategorien im Dropdown */
|
||||
.mega-menu-grid { }
|
||||
.mega-menu-category { }
|
||||
|
||||
/* Links in Kategorien */
|
||||
.mega-menu-category ul li a { }
|
||||
|
||||
/* Einstellungs-Panel */
|
||||
.mm-settings-panel { }
|
||||
.mega-menu-links { }
|
||||
```
|
||||
|
||||
### Accessibility Features
|
||||
Viele Farben und Abstände können über CSS Custom Properties überschrieben werden, beispielsweise:
|
||||
|
||||
- **Skip-Links** für Screenreader
|
||||
- **ARIA-Rollen** und Labels
|
||||
- **Fokus-Management** mit sichtbaren Fokus-Indikatoren
|
||||
- **Tastaturnavigation** mit Tab/Shift+Tab/Escape
|
||||
- **High Contrast Modus** Unterstützung
|
||||
|
||||
## 🔧 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
|
||||
```css
|
||||
:root {
|
||||
--megaMenuNavBackground: #5f6f74;
|
||||
--megaMenuNavTextColor: #ffffff;
|
||||
--megaMenuPanelWidth: 1280px;
|
||||
--megaMenuZIndex: 6000;
|
||||
}
|
||||
```
|
||||
|
||||
2. **SharePoint App Catalog:**
|
||||
- `.sppkg` Datei aus `sharepoint/solution/` hochladen
|
||||
- App genehmigen und für alle Sites verfügbar machen
|
||||
## Cache
|
||||
|
||||
3. **Site-spezifische Aktivierung:**
|
||||
- PowerShell-Scripts verwenden
|
||||
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.
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
Für einen sofortigen administrativen Test kann der Session-Cache in den Browser-Entwicklertools gelöscht oder ein neues privates Browserfenster verwendet werden.
|
||||
|
||||
### Häufige Probleme
|
||||
## Barrierefreiheit
|
||||
|
||||
**Problem:** Menü wird nicht angezeigt
|
||||
**Lösung:** Termset-Name in den Einstellungen prüfen
|
||||
- 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
|
||||
|
||||
**Problem:** CSS-Styling funktioniert nicht
|
||||
**Lösung:** Externe CSS-URL und CORS-Einstellungen überprüfen
|
||||
Bewusst wird kein `role="menubar"` verwendet: Eine Portalnavigation ist eine normale Navigation und kein Desktop-Menü-Widget. Dadurch bleiben native Browser- und Screenreader-Erwartungen erhalten.
|
||||
|
||||
**Problem:** Build-Fehler
|
||||
**Lösung:** Node.js Version prüfen (v8.17.0 empfohlen)
|
||||
## Fehlerdiagnose
|
||||
|
||||
### Debug-Modus
|
||||
### „Die Navigation ist nicht konfiguriert“
|
||||
|
||||
```bash
|
||||
# Detaillierte Build-Ausgabe
|
||||
gulp bundle --verbose
|
||||
Weder eine gültige `termSetId` noch ein `termSetName` ist gesetzt. UserCustomAction beziehungsweise PortalSettings-Konfiguration prüfen.
|
||||
|
||||
# TypeScript-Fehler anzeigen
|
||||
gulp build
|
||||
### „Die Navigation konnte nicht geladen werden“
|
||||
|
||||
- Existiert das Termset im Standard-Term-Store der Site Collection?
|
||||
- Ist die GUID korrekt?
|
||||
- Haben Benutzer Zugriff auf den Managed-Metadata-Service?
|
||||
- Enthält das Termset mindestens einen sichtbaren Term?
|
||||
- Ist der App-Catalog-Eintrag aktuell?
|
||||
|
||||
Für weitere technische Details kann vorübergehend `debug: true` gesetzt werden.
|
||||
|
||||
Bei aktiviertem Debug-Modus protokolliert MegaMenu den kompletten Startpfad mit den Quellen
|
||||
`MegaMenuApplicationCustomizer`, `TaxonomyNavigationService`, `SPTermStorePickerService` und
|
||||
`MegaMenuRenderer`. Dazu gehören die übergebenen Properties, Placeholder-Erkennung,
|
||||
Taxonomy-Anfrage, Cache-Nutzung, Term-Anzahl, Containerwahl und vollständige Fehlerdetails.
|
||||
Wenn keine dieser Meldungen erscheint, wurde das MegaMenu-Bundle nicht durch den SPFx-Loader
|
||||
ausgeführt.
|
||||
|
||||
### Änderungen am Termset erscheinen verzögert
|
||||
|
||||
Die Navigation wird bis zu `cacheMinutes` im Session-Cache gehalten. Cache leeren oder Ablaufzeit abwarten.
|
||||
|
||||
### Zusätzliches CSS wird nicht geladen
|
||||
|
||||
- URL und Dateiberechtigungen prüfen.
|
||||
- Für externe Quellen HTTPS verwenden.
|
||||
- Browser-Konsole bei aktiviertem Debug-Modus prüfen.
|
||||
|
||||
## Upgrade von 1.0.x
|
||||
|
||||
- Bestehende `termSetName`-Konfigurationen funktionieren unverändert.
|
||||
- Der alte unversionierte Session-Cache wird nicht weiterverwendet.
|
||||
- `cacheMinutes` verwendet ohne Konfiguration den Wert 15.
|
||||
- Die eigene, nicht verwendete MegaMenu-Einstellungsseite wurde entfernt; PortalSettings ist der zentrale Konfigurationsweg.
|
||||
- Globale Event Listener werden beim Dispose der Extension entfernt.
|
||||
- Paket-, npm- und Dokumentationsversion wurden auf 1.1.0 vereinheitlicht.
|
||||
|
||||
## Classic Pages
|
||||
|
||||
Unter `classic/` befindet sich eine separate Kompatibilitätsimplementierung für klassische SharePoint-Seiten. Sie wird nicht vom SPFx-Bundle ausgeführt und besitzt einen eigenen Deploymentweg. Neue Funktionen sollten zuerst in der modernen SPFx-Variante umgesetzt und anschließend bewusst in die Classic-Variante übernommen werden.
|
||||
|
||||
## 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
|
||||
## Versionshistorie
|
||||
|
||||
### v1.0.2 (Aktuell)
|
||||
- Einstellungs-Panel implementiert
|
||||
- Fokus-Management verbessert
|
||||
- CSS-Optimierungen für SharePoint-Konsistenz
|
||||
- Barrierefreiheit-Verbesserungen
|
||||
### 2.0.1
|
||||
|
||||
### v1.0.1
|
||||
- Responsive Design hinzugefügt
|
||||
- Performance-Optimierungen
|
||||
- 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.
|
||||
|
||||
### v1.0.0
|
||||
- Initiale Release
|
||||
- 3-stufige Navigation
|
||||
- Grundlegende Accessibility
|
||||
### 2.0.0
|
||||
|
||||
## 👥 Mitwirkende
|
||||
- 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.
|
||||
|
||||
- Entwicklung: Matthias Glubrecht
|
||||
- Framework: SharePoint Framework (Microsoft)
|
||||
- UI: Office UI Fabric (Microsoft)
|
||||
### 1.1.4
|
||||
|
||||
## 📄 Lizenz
|
||||
- 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.
|
||||
|
||||
tbd
|
||||
### 1.1.3
|
||||
|
||||
## 🆘 Support
|
||||
- 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.
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
1. Issues im Repository erstellen
|
||||
2. Dokumentation und Troubleshooting-Bereich prüfen
|
||||
### 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
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ Use one of these methods:
|
||||
````powershell
|
||||
# Connect to SharePoint site
|
||||
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"}'
|
||||
````
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
(function() {
|
||||
'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 || {
|
||||
containerId: 's4-titlerow',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"solution": {
|
||||
"name": "mega-menu-client-side-solution",
|
||||
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.1.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true
|
||||
},
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
"default": {
|
||||
"pageUrl": "https://sharepoint.contoso.local/SitePages/Neues-Zuhause.aspx",
|
||||
"customActions": {
|
||||
"abc3361f-bb2d-491f-aba3-cd51c19a299b": {
|
||||
"c0abbffb-355d-4d4e-bd38-9e15fb811506": {
|
||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||
"properties": {
|
||||
"termSetName": "Horizontale Navigation",
|
||||
@@ -18,7 +18,7 @@
|
||||
"megaMenu": {
|
||||
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
|
||||
"customActions": {
|
||||
"abc3361f-bb2d-491f-aba3-cd51c19a299b": {
|
||||
"c0abbffb-355d-4d4e-bd38-9e15fb811506": {
|
||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||
"properties": {
|
||||
"testMessage": "Test message"
|
||||
|
||||
+97
-66
@@ -5,97 +5,128 @@ param(
|
||||
|
||||
[string]$TermSetName = '',
|
||||
|
||||
[string]$CssUrl = '',
|
||||
[string]$TermSetId = '',
|
||||
|
||||
[ValidateRange(1, 1440)]
|
||||
[int]$CacheMinutes = 15,
|
||||
|
||||
[switch]$EnableDebug,
|
||||
|
||||
[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) {
|
||||
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 = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
||||
$componentId = [Guid]'c0abbffb-355d-4d4e-bd38-9e15fb811506'
|
||||
$legacyComponentId = [Guid]'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
||||
$location = 'ClientSideExtension.ApplicationCustomizer'
|
||||
$name = 'MegaMenu'
|
||||
$title = 'Mega Menu'
|
||||
$configurationProperty = 'PortalSettings.MegaMenu.Configuration'
|
||||
|
||||
function Get-MegaMenuPropertiesJson {
|
||||
param(
|
||||
[string]$CurrentTermSetName,
|
||||
[string]$CurrentCssUrl,
|
||||
[bool]$CurrentDebug
|
||||
)
|
||||
|
||||
return (@{
|
||||
termSetName = $CurrentTermSetName
|
||||
cssUrl = $CurrentCssUrl
|
||||
debug = $CurrentDebug
|
||||
$properties = [string](@{
|
||||
termSetName = $TermSetName
|
||||
termSetId = $TermSetId
|
||||
cacheMinutes = $CacheMinutes
|
||||
debug = [bool]$EnableDebug.IsPresent
|
||||
} | ConvertTo-Json -Depth 10 -Compress)
|
||||
}
|
||||
|
||||
function Remove-WebScopedComponentActions {
|
||||
param(
|
||||
[Microsoft.SharePoint.SPSite]$CurrentSite,
|
||||
[string]$CurrentComponentId,
|
||||
[string]$CurrentLocation
|
||||
$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 {
|
||||
$webActions = @($web.UserCustomActions | Where-Object {
|
||||
$_.Location -eq $CurrentLocation -and
|
||||
$_.ClientSideComponentId -and
|
||||
$_.ClientSideComponentId.ToString().ToLower() -eq $CurrentComponentId
|
||||
})
|
||||
$webUrl = $web.Url
|
||||
$userCustomActions = $web.UserCustomActions
|
||||
|
||||
foreach ($webAction in $webActions) {
|
||||
$web.UserCustomActions.Delete($webAction.Id)
|
||||
$removedCount++
|
||||
if ($null -eq $userCustomActions) {
|
||||
throw ('UserCustomActions konnten fuer das Web nicht geladen werden: ' + $webUrl)
|
||||
}
|
||||
|
||||
if ($webActions.Count -gt 0) {
|
||||
$actions = @(
|
||||
$userCustomActions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId -or
|
||||
$_.ClientSideComponentId -eq $legacyComponentId
|
||||
}
|
||||
)
|
||||
|
||||
$currentAction = $actions |
|
||||
Where-Object {
|
||||
$_.ClientSideComponentId -eq $componentId
|
||||
} |
|
||||
Select-Object -First 1
|
||||
|
||||
foreach ($existingAction in $actions) {
|
||||
if ($currentAction -and $existingAction.Id -eq $currentAction.Id) {
|
||||
continue
|
||||
}
|
||||
|
||||
$existingAction.Delete()
|
||||
$removedLegacy++
|
||||
}
|
||||
|
||||
if (-not $currentAction) {
|
||||
$currentAction = $userCustomActions.Add()
|
||||
$currentAction.Name = [Guid]::NewGuid().ToString('B')
|
||||
$created++
|
||||
}
|
||||
else {
|
||||
$updated++
|
||||
}
|
||||
|
||||
$currentAction.Title = 'MegaMenu Application Customizer'
|
||||
$currentAction.Description = $Description
|
||||
$currentAction.Location = $location
|
||||
$currentAction.ClientSideComponentId = $componentId
|
||||
$currentAction.ClientSideComponentProperties = $properties
|
||||
$currentAction.Sequence = 100
|
||||
$currentAction.Update()
|
||||
$web.Update()
|
||||
}
|
||||
|
||||
Write-Host ('MegaMenu synchronisiert: ' + $webUrl) -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
$web.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return $removedCount
|
||||
}
|
||||
|
||||
$site = Get-SPSite -Identity $SiteUrl
|
||||
try {
|
||||
$removedWebScopedActions = Remove-WebScopedComponentActions -CurrentSite $site -CurrentComponentId $componentId -CurrentLocation $location
|
||||
|
||||
$existingAction = $site.UserCustomActions | Where-Object {
|
||||
$_.Location -eq $location -and $_.ClientSideComponentId -and $_.ClientSideComponentId.ToString().ToLower() -eq $componentId
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($existingAction) {
|
||||
$action = $existingAction
|
||||
Write-Host 'Aktualisiere vorhandene site-scoped Mega Menu UserCustomAction...' -ForegroundColor Yellow
|
||||
}
|
||||
else {
|
||||
$action = $site.UserCustomActions.Add()
|
||||
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)
|
||||
Write-Host ''
|
||||
Write-Host 'MegaMenu wurde fuer die gesamte Site Collection konfiguriert.' -ForegroundColor Green
|
||||
Write-Host ('Zentrale Konfiguration: ' + $configurationProperty)
|
||||
Write-Host ('Neu registrierte Webs: ' + $created)
|
||||
Write-Host ('Aktualisierte Webs: ' + $updated)
|
||||
Write-Host ('Entfernte alte oder doppelte Registrierungen: ' + $removedLegacy)
|
||||
Write-Host ('Properties: ' + $properties)
|
||||
Write-Host 'Neue Unterwebs koennen durch erneutes Ausfuehren idempotent synchronisiert werden.' -ForegroundColor Yellow
|
||||
}
|
||||
finally {
|
||||
if ($site) {
|
||||
|
||||
@@ -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
@@ -3,7 +3,7 @@
|
||||
"alias": "MegaMenuApplicationCustomizer",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.2",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"loaderConfig": {
|
||||
@@ -19,18 +19,16 @@
|
||||
"MegaMenuApplicationCustomizerStrings": {
|
||||
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
|
||||
"type": "localizedPath",
|
||||
"paths": {}
|
||||
"paths": {
|
||||
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
|
||||
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
@@ -45,11 +43,6 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
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
@@ -1,5 +1,2 @@
|
||||
declare const styles: {
|
||||
mmSlideIn: string;
|
||||
mmFadeIn: string;
|
||||
};
|
||||
declare const styles: {};
|
||||
export default styles;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/* tslint:disable */
|
||||
require('./MegaMenu.module.css');
|
||||
var styles = {
|
||||
mmSlideIn: 'mmSlideIn_09a8e1a7',
|
||||
mmFadeIn: 'mmFadeIn_09a8e1a7',
|
||||
};
|
||||
var styles = {};
|
||||
export default styles;
|
||||
/* 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
@@ -1,27 +1,24 @@
|
||||
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
|
||||
import './MegaMenu.module.scss';
|
||||
export declare const UserCustomActionMegaMenuId: string;
|
||||
/**
|
||||
* Properties for the MegaMenu Application Customizer
|
||||
*/
|
||||
export interface IMegaMenuApplicationCustomizerProperties {
|
||||
/**
|
||||
* The name of the termset to load menu items from
|
||||
*/
|
||||
termSetName: string;
|
||||
/**
|
||||
* Optional URL to an external CSS file
|
||||
*/
|
||||
cssUrl?: string;
|
||||
termSetName?: string;
|
||||
termSetId?: string;
|
||||
cacheMinutes?: number;
|
||||
debug?: boolean;
|
||||
}
|
||||
/** A Custom Action which can be run during execution of a Client Side Application */
|
||||
export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
|
||||
private _topPlaceholder;
|
||||
private _menuContainer;
|
||||
private _renderer;
|
||||
onInit(): Promise<void>;
|
||||
private _renderPlaceHolders();
|
||||
private _updateCallback;
|
||||
private _renderMegaMenu(termSetName);
|
||||
private _renderMegaMenu(termSetIdentifier, debug?);
|
||||
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 { Log } from '@microsoft/sp-core-library';
|
||||
import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base';
|
||||
import * as strings from 'MegaMenuApplicationCustomizerStrings';
|
||||
import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService';
|
||||
import { MegaMenuRenderer } from './MegaMenuRenderer';
|
||||
// Import SCSS module
|
||||
import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
|
||||
import './MegaMenu.module.scss';
|
||||
var LOG_SOURCE = 'MegaMenuApplicationCustomizer';
|
||||
export var UserCustomActionMegaMenuId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
|
||||
/** A Custom Action which can be run during execution of a Client Side Application */
|
||||
var MegaMenuApplicationCustomizer = (function (_super) {
|
||||
__extends(MegaMenuApplicationCustomizer, _super);
|
||||
function MegaMenuApplicationCustomizer() {
|
||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
_this._updateCallback = function (data) {
|
||||
_this._loadExternalCss(data.cssUrl);
|
||||
_this._renderMegaMenu(data.termSetName);
|
||||
_this._onDispose = function () {
|
||||
_this.context.placeholderProvider.changedEvent.remove(_this, _this._renderPlaceHolders);
|
||||
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;
|
||||
}
|
||||
MegaMenuApplicationCustomizer.prototype.onInit = function () {
|
||||
Log.info(LOG_SOURCE, "Initialized " + strings.Title);
|
||||
// Load external CSS if provided
|
||||
if (this.properties.cssUrl) {
|
||||
this._loadExternalCss(this.properties.cssUrl);
|
||||
}
|
||||
// Wait for the page to be ready
|
||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Initialized ' + strings.Title);
|
||||
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
|
||||
// Initial render
|
||||
this._renderPlaceHolders();
|
||||
return Promise.resolve();
|
||||
};
|
||||
MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () {
|
||||
console.log('Available placeholders: ', this.context.placeholderProvider.placeholderNames.map(function (name) { return PlaceholderName[name]; }).join(', '));
|
||||
// Handling the top placeholder
|
||||
var availablePlaceholders = this.context.placeholderProvider.placeholderNames
|
||||
.map(function (name) { return PlaceholderName[name]; })
|
||||
.join(', ');
|
||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders);
|
||||
if (!this._topPlaceholder) {
|
||||
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose });
|
||||
// The extension should not assume that the expected placeholder is available.
|
||||
if (!this._topPlaceholder) {
|
||||
console.error('The expected placeholder (Top) was not found.');
|
||||
debugError(this._isDebugEnabled(), LOG_SOURCE, 'The expected placeholder (Top) was not found.');
|
||||
return;
|
||||
}
|
||||
if (!this.properties.termSetName) {
|
||||
console.error('TermSetName property is required but not provided.');
|
||||
1;
|
||||
var termSetIdentifier = this._getTermSetIdentifier();
|
||||
if (!termSetIdentifier) {
|
||||
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.');
|
||||
this._renderStatus(strings.ConfigurationMissing);
|
||||
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 () {
|
||||
var taxonomyService, menuItems, renderer, container, error_1;
|
||||
var taxonomyService, menuItems, error_1;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
@@ -116,23 +117,27 @@ var MegaMenuApplicationCustomizer = (function (_super) {
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_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()];
|
||||
case 2:
|
||||
menuItems = _a.sent();
|
||||
renderer = new MegaMenuRenderer(this.context, menuItems, this._updateCallback);
|
||||
container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
||||
if (container) {
|
||||
renderer.render(container);
|
||||
if (this._renderer) {
|
||||
this._renderer.dispose();
|
||||
}
|
||||
this._renderer = new MegaMenuRenderer(menuItems, debug);
|
||||
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
||||
if (this._menuContainer) {
|
||||
this._renderer.render(this._menuContainer);
|
||||
}
|
||||
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];
|
||||
case 3:
|
||||
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];
|
||||
case 4: return [2 /*return*/];
|
||||
}
|
||||
@@ -142,41 +147,52 @@ var MegaMenuApplicationCustomizer = (function (_super) {
|
||||
MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) {
|
||||
var container = document.getElementById(id);
|
||||
if (container) {
|
||||
var div = document.createElement('div');
|
||||
container.appendChild(div);
|
||||
return div;
|
||||
var host = document.getElementById('MegaMenuHost');
|
||||
if (!host) {
|
||||
host = document.createElement('div');
|
||||
host.id = 'MegaMenuHost';
|
||||
container.appendChild(host);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
else {
|
||||
return placeholder.domElement;
|
||||
}
|
||||
};
|
||||
MegaMenuApplicationCustomizer.prototype._loadExternalCss = function (cssUrl) {
|
||||
var externalCssLinkId = 'mega-menu-additional-css-34FAB720';
|
||||
var link = document.getElementById(externalCssLinkId);
|
||||
if (cssUrl && cssUrl.trim() !== '') {
|
||||
// update previous link, if present
|
||||
if (!link) {
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.id = externalCssLinkId;
|
||||
link.onload = function () {
|
||||
Log.info(LOG_SOURCE, "External CSS loaded successfully from: " + cssUrl);
|
||||
};
|
||||
link.onerror = function () {
|
||||
console.warn("Failed to load external CSS from: " + cssUrl);
|
||||
};
|
||||
head.appendChild(link);
|
||||
}
|
||||
link.href = cssUrl;
|
||||
}
|
||||
else if (link) {
|
||||
link.remove();
|
||||
MegaMenuApplicationCustomizer.prototype._getTermSetIdentifier = function () {
|
||||
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._onDispose = function () {
|
||||
console.log('[MegaMenuApplicationCustomizer._onDispose] Disposed custom top placeholder.');
|
||||
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([
|
||||
override
|
||||
|
||||
File diff suppressed because one or more lines are too long
+18
-10
@@ -1,17 +1,18 @@
|
||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||
import { IMenuItem } from '../../services/IMenuItem';
|
||||
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
|
||||
export declare class MegaMenuRenderer {
|
||||
private context;
|
||||
private menuItems;
|
||||
private updateCallback;
|
||||
private _settingsPanel?;
|
||||
constructor(context: ApplicationCustomizerContext, menuItems: IMenuItem[], updateCallback: (data: IMegaMenuApplicationCustomizerProperties) => void);
|
||||
private debug;
|
||||
private static readonly hoverOpenDelayMs;
|
||||
private static readonly hoverCloseDelayMs;
|
||||
private _container;
|
||||
private _documentKeydownHandler;
|
||||
private _documentClickHandler;
|
||||
private _announcer;
|
||||
constructor(menuItems: IMenuItem[], debug?: boolean);
|
||||
render(container: HTMLElement): void;
|
||||
private createSettingsItem();
|
||||
private openSettings();
|
||||
dispose(): void;
|
||||
private createTopLevelItem(item);
|
||||
private createTopLevelElement(item);
|
||||
private createTopLevelElement(item, hasChildren, isActive, isCurrent);
|
||||
private createMegaMenu(parentItem);
|
||||
private createCategorySection(item);
|
||||
private attachEventListeners();
|
||||
@@ -19,10 +20,17 @@ export declare class MegaMenuRenderer {
|
||||
private attachMouseEvents(heading, megaMenu);
|
||||
private attachFocusManagement(heading, megaMenu);
|
||||
private attachGlobalKeyboardNavigation();
|
||||
private attachGlobalClickOutsideHandler();
|
||||
private openMegaMenu(trigger, menu);
|
||||
private closeMegaMenu(trigger, menu);
|
||||
private toggleMegaMenu(trigger, menu);
|
||||
private closeAllMegaMenus();
|
||||
private closeAllMegaMenus(exceptMenu?);
|
||||
private focusFirstLink(megaMenu);
|
||||
private isItemActive(item);
|
||||
private isCurrentUrl(url?);
|
||||
private normalizePath(url?);
|
||||
private getColumnCount(items?);
|
||||
private joinClasses(...classNames);
|
||||
private createScreenReaderAnnouncer();
|
||||
private announce(message);
|
||||
}
|
||||
|
||||
@@ -2,109 +2,88 @@
|
||||
// tslint:disable:match-default-export-name
|
||||
// tslint:disable:typedef
|
||||
// tslint:disable:variable-name
|
||||
import { SPPermission } from '@microsoft/sp-page-context';
|
||||
import { MegaMenuSettingsPanel } from './MegaMenuSettings';
|
||||
import { debugLog } from '../../services/MegaMenuDebug';
|
||||
var LOG_SOURCE = 'MegaMenuRenderer';
|
||||
var MegaMenuRenderer = (function () {
|
||||
function MegaMenuRenderer(context, menuItems, updateCallback) {
|
||||
this.context = context;
|
||||
function MegaMenuRenderer(menuItems, debug) {
|
||||
if (debug === void 0) { debug = false; }
|
||||
this.menuItems = menuItems;
|
||||
this.updateCallback = updateCallback;
|
||||
this.debug = debug;
|
||||
}
|
||||
MegaMenuRenderer.prototype.render = function (container) {
|
||||
var _this = this;
|
||||
// Clear any existing content
|
||||
this.dispose();
|
||||
this._container = container;
|
||||
container.innerHTML = '';
|
||||
container.id = 'CustomNavigation';
|
||||
// Create the main nav element
|
||||
var nav = document.createElement('nav');
|
||||
nav.id = 'Mega-Menu';
|
||||
nav.className = 'mega-menu-main';
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
||||
// Create the top-level menubar
|
||||
var topLevelUl = document.createElement('ul');
|
||||
topLevelUl.setAttribute('role', 'menubar');
|
||||
// Process each top-level menu item
|
||||
topLevelUl.className = 'mega-menu-top-level';
|
||||
this.menuItems.forEach(function (topLevelItem) {
|
||||
var topLevelLi = _this.createTopLevelItem(topLevelItem);
|
||||
topLevelUl.appendChild(topLevelLi);
|
||||
});
|
||||
// Only if current user has ManageWeb permissions in this website can they access the settings
|
||||
if (this.context.pageContext.web.permissions.hasPermission(SPPermission.manageWeb)) {
|
||||
topLevelUl.appendChild(this.createSettingsItem());
|
||||
}
|
||||
nav.appendChild(topLevelUl);
|
||||
container.appendChild(nav);
|
||||
// Attach accessibility event listeners after rendering
|
||||
this.attachEventListeners();
|
||||
this.createScreenReaderAnnouncer();
|
||||
};
|
||||
MegaMenuRenderer.prototype.createSettingsItem = function () {
|
||||
var _this = this;
|
||||
var li = document.createElement('li');
|
||||
li.setAttribute('role', 'none');
|
||||
// Verwenden eines Links-ähnlichen Buttons damit Styling identisch zu Top-Level Items ist
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'menu-item-link menu-item-settings'; // reuse same base styles
|
||||
btn.setAttribute('role', 'menuitem');
|
||||
btn.setAttribute('tabindex', '0');
|
||||
btn.setAttribute('aria-haspopup', 'false');
|
||||
btn.setAttribute('aria-label', 'Einstellungen');
|
||||
btn.title = 'Einstellungen';
|
||||
// Nur EIN Icon: Wenn Fabric Icons geladen sind, zeigt ms-Icon das Symbol. Fallback via aria-label für Screenreader
|
||||
var icon = document.createElement('span');
|
||||
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
btn.appendChild(icon);
|
||||
btn.addEventListener('click', function () { return _this.openSettings(); });
|
||||
btn.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
_this.openSettings();
|
||||
MegaMenuRenderer.prototype.dispose = function () {
|
||||
if (this._documentKeydownHandler) {
|
||||
document.removeEventListener('keydown', this._documentKeydownHandler);
|
||||
this._documentKeydownHandler = undefined;
|
||||
}
|
||||
});
|
||||
li.appendChild(btn);
|
||||
return li;
|
||||
};
|
||||
MegaMenuRenderer.prototype.openSettings = function () {
|
||||
if (!this._settingsPanel) {
|
||||
this._settingsPanel = new MegaMenuSettingsPanel(this.context, this.updateCallback);
|
||||
if (this._documentClickHandler) {
|
||||
document.removeEventListener('click', this._documentClickHandler);
|
||||
this._documentClickHandler = undefined;
|
||||
}
|
||||
this._settingsPanel.open();
|
||||
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) {
|
||||
var li = document.createElement('li');
|
||||
li.setAttribute('role', 'none');
|
||||
// Create the top-level element (link or span)
|
||||
var topElement = this.createTopLevelElement(item);
|
||||
var hasChildren = item.hasChildren() && item.items && item.items.length > 0;
|
||||
var isActive = this.isItemActive(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);
|
||||
// If the item has children, create the mega menu
|
||||
if (item.hasChildren() && item.items && item.items.length > 0) {
|
||||
if (hasChildren) {
|
||||
var megaMenu = this.createMegaMenu(item);
|
||||
li.appendChild(megaMenu);
|
||||
}
|
||||
return li;
|
||||
};
|
||||
MegaMenuRenderer.prototype.createTopLevelElement = function (item) {
|
||||
MegaMenuRenderer.prototype.createTopLevelElement = function (item, hasChildren, isActive, isCurrent) {
|
||||
var element;
|
||||
if (item.url) {
|
||||
// Create as link
|
||||
element = document.createElement('a');
|
||||
element.href = item.url;
|
||||
element.className = 'menu-item-link';
|
||||
}
|
||||
else {
|
||||
// Create as span (no link)
|
||||
element = document.createElement('span');
|
||||
element.className = 'menu-item-text';
|
||||
element.setAttribute('tabindex', '0');
|
||||
}
|
||||
element.setAttribute('role', 'menuitem');
|
||||
element.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');
|
||||
element.textContent = item.label;
|
||||
}
|
||||
if (isCurrent && item.url) {
|
||||
element.setAttribute('aria-current', 'page');
|
||||
}
|
||||
if (item.hoverText) {
|
||||
element.title = item.hoverText;
|
||||
}
|
||||
@@ -114,11 +93,11 @@ var MegaMenuRenderer = (function () {
|
||||
var _this = this;
|
||||
var megaMenuDiv = document.createElement('div');
|
||||
megaMenuDiv.className = 'mega-menu';
|
||||
megaMenuDiv.setAttribute('role', 'menu');
|
||||
megaMenuDiv.setAttribute('aria-label', parentItem.label + " Unterkategorien");
|
||||
megaMenuDiv.setAttribute('aria-expanded', 'false');
|
||||
megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien');
|
||||
var gridDiv = document.createElement('div');
|
||||
gridDiv.className = 'mega-menu-grid';
|
||||
// Process second-level items (categories)
|
||||
gridDiv.style.setProperty('--megaMenuColumnCount', this.getColumnCount(parentItem.items));
|
||||
if (parentItem.items) {
|
||||
parentItem.items.forEach(function (secondLevelItem) {
|
||||
var categoryDiv = _this.createCategorySection(secondLevelItem);
|
||||
@@ -129,38 +108,51 @@ var MegaMenuRenderer = (function () {
|
||||
return megaMenuDiv;
|
||||
};
|
||||
MegaMenuRenderer.prototype.createCategorySection = function (item) {
|
||||
var _this = this;
|
||||
var categoryDiv = document.createElement('div');
|
||||
categoryDiv.className = 'mega-menu-category';
|
||||
// Create the category header (h3)
|
||||
var isActive = this.isItemActive(item);
|
||||
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');
|
||||
h3.className = 'mega-menu-category-title';
|
||||
if (item.url) {
|
||||
// Category header as link
|
||||
var link = document.createElement('a');
|
||||
link.href = item.url;
|
||||
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) {
|
||||
link.title = item.hoverText;
|
||||
}
|
||||
h3.appendChild(link);
|
||||
}
|
||||
else {
|
||||
// Category header as span (no link)
|
||||
var span = document.createElement('span');
|
||||
span.textContent = item.label;
|
||||
span.className = this.joinClasses(isActive ? 'is-active' : undefined);
|
||||
if (item.hoverText) {
|
||||
span.title = item.hoverText;
|
||||
}
|
||||
h3.appendChild(span);
|
||||
}
|
||||
categoryDiv.appendChild(h3);
|
||||
// Create the third-level items list if they exist
|
||||
if (item.hasChildren() && item.items && item.items.length > 0) {
|
||||
var ul_1 = document.createElement('ul');
|
||||
ul_1.className = 'mega-menu-links';
|
||||
item.items.forEach(function (thirdLevelItem) {
|
||||
var li = document.createElement('li');
|
||||
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.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) {
|
||||
link.title = thirdLevelItem.hoverText;
|
||||
}
|
||||
@@ -172,7 +164,7 @@ var MegaMenuRenderer = (function () {
|
||||
return categoryDiv;
|
||||
};
|
||||
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++) {
|
||||
var heading = headings[i];
|
||||
var megaMenu = heading.nextElementSibling;
|
||||
@@ -182,86 +174,101 @@ var MegaMenuRenderer = (function () {
|
||||
this.attachFocusManagement(heading, megaMenu);
|
||||
}
|
||||
}
|
||||
// Global keyboard navigation
|
||||
this.attachGlobalKeyboardNavigation();
|
||||
this.attachGlobalClickOutsideHandler();
|
||||
};
|
||||
MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) {
|
||||
var _this = this;
|
||||
heading.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
// Enter: Navigation (nur bei Links ohne Mega-Menu-Override)
|
||||
if (heading.tagName === 'A') {
|
||||
// Lasse normale Link-Navigation zu (KEIN preventDefault!)
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// Bei span: Toggle Menu
|
||||
e.preventDefault();
|
||||
_this.toggleMegaMenu(heading, megaMenu);
|
||||
}
|
||||
}
|
||||
else if (e.key === ' ') {
|
||||
// Space: Toggle Dropdown (immer)
|
||||
e.preventDefault();
|
||||
_this.toggleMegaMenu(heading, megaMenu);
|
||||
}
|
||||
else if (e.key === 'ArrowDown') {
|
||||
// Pfeil runter: Menü öffnen + erster Link
|
||||
e.preventDefault();
|
||||
_this.openMegaMenu(heading, megaMenu);
|
||||
_this.focusFirstLink(megaMenu);
|
||||
}
|
||||
else if (e.key === 'ArrowUp') {
|
||||
// Pfeil hoch: Menü schließen
|
||||
e.preventDefault();
|
||||
_this.closeMegaMenu(heading, megaMenu);
|
||||
}
|
||||
else if (e.key === 'Escape') {
|
||||
// Escape: Menü schließen
|
||||
e.preventDefault();
|
||||
_this.closeMegaMenu(heading, megaMenu);
|
||||
heading.focus();
|
||||
}
|
||||
});
|
||||
// Click Event für Top-Level Links - NORMALE Navigation erlauben
|
||||
if (heading.tagName === 'A') {
|
||||
heading.addEventListener('click', function (e) {
|
||||
// Links sollen normal navigieren, nicht das Mega-Menu toglen
|
||||
// Wenn der User das Menu öffnen will, soll er Space oder Pfeil↓ nutzen
|
||||
console.log('Link geklickt:', heading.href);
|
||||
// KEIN preventDefault() - normale Link-Navigation
|
||||
heading.addEventListener('click', function () {
|
||||
debugLog(_this.debug, LOG_SOURCE, 'Link clicked:', heading.href);
|
||||
});
|
||||
}
|
||||
// Focus Management - KEIN automatisches Öffnen oder Schließen
|
||||
heading.addEventListener('focus', function () {
|
||||
console.log('Focus auf:', heading.textContent);
|
||||
// Einfach nur fokussiert - keine automatischen Aktionen!
|
||||
debugLog(_this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
|
||||
});
|
||||
};
|
||||
MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) {
|
||||
var _this = this;
|
||||
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 () {
|
||||
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 () {
|
||||
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) {
|
||||
var _this = this;
|
||||
// Focus-Verlust-Behandlung - vereinfacht
|
||||
megaMenu.addEventListener('focusout', function (e) {
|
||||
// Kurz warten, um zu prüfen ob Focus innerhalb des Mega-Menus bleibt
|
||||
megaMenu.addEventListener('focusout', function () {
|
||||
setTimeout(function () {
|
||||
var focusedElement = document.activeElement;
|
||||
var isInsideThisMenu = megaMenu.contains(focusedElement);
|
||||
var isOnThisTrigger = focusedElement === heading;
|
||||
var isInAnyMegaMenu = focusedElement.closest('.mega-menu');
|
||||
var isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
|
||||
// Nur schließen wenn Focus komplett außerhalb der Navigation ist
|
||||
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
|
||||
console.log('Schließe Menu wegen Focus-Verlust');
|
||||
debugLog(_this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.');
|
||||
_this.closeMegaMenu(heading, megaMenu);
|
||||
}
|
||||
}, 150);
|
||||
@@ -269,7 +276,7 @@ var MegaMenuRenderer = (function () {
|
||||
};
|
||||
MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () {
|
||||
var _this = this;
|
||||
document.addEventListener('keydown', function (e) {
|
||||
this._documentKeydownHandler = function (e) {
|
||||
var activeElement = document.activeElement;
|
||||
if (e.key === 'Escape') {
|
||||
var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]');
|
||||
@@ -279,16 +286,13 @@ var MegaMenuRenderer = (function () {
|
||||
triggerLink.focus();
|
||||
}
|
||||
}
|
||||
// Tab-Navigation: Nur bei spezifischen Übergängen eingreifen
|
||||
if (e.key === 'Tab') {
|
||||
// Von Top-Level zum ersten Link im offenen Menu
|
||||
if (!e.shiftKey) {
|
||||
var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
|
||||
if (currentTopLevel) {
|
||||
var parentLi = currentTopLevel.closest('li');
|
||||
var megaMenu = parentLi.querySelector('.mega-menu.js-open');
|
||||
if (megaMenu) {
|
||||
// Nur eingreifen wenn wir vom Trigger weg-tabben
|
||||
e.preventDefault();
|
||||
var firstLink = megaMenu.querySelector('a');
|
||||
if (firstLink) {
|
||||
@@ -298,13 +302,11 @@ var MegaMenuRenderer = (function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Shift+Tab: Vom ersten Link im Menu zurück zum Trigger
|
||||
if (e.shiftKey) {
|
||||
var megaMenu = activeElement.closest('.mega-menu');
|
||||
if (megaMenu && megaMenu.classList.contains('js-open')) {
|
||||
var allLinksInMenu = megaMenu.querySelectorAll('a');
|
||||
var firstLinkInMenu = allLinksInMenu[0];
|
||||
// Nur eingreifen wenn wir beim ersten Link sind
|
||||
if (activeElement === firstLinkInMenu) {
|
||||
e.preventDefault();
|
||||
var triggerElement = megaMenu.previousElementSibling;
|
||||
@@ -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) {
|
||||
this.closeAllMegaMenus(menu);
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
menu.setAttribute('aria-expanded', 'true');
|
||||
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) {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
menu.setAttribute('aria-expanded', 'false');
|
||||
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) {
|
||||
var isOpen = trigger.getAttribute('aria-expanded') === 'true';
|
||||
@@ -335,36 +357,112 @@ var MegaMenuRenderer = (function () {
|
||||
this.closeMegaMenu(trigger, menu);
|
||||
}
|
||||
else {
|
||||
this.closeAllMegaMenus();
|
||||
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"]');
|
||||
for (var i = 0; i < allTriggers.length; i++) {
|
||||
var trigger = allTriggers[i];
|
||||
var menu = trigger.nextElementSibling;
|
||||
if (menu) {
|
||||
if (menu && menu !== exceptMenu) {
|
||||
this.closeMegaMenu(trigger, menu);
|
||||
}
|
||||
}
|
||||
};
|
||||
// removed unused closeOtherMegaMenus (was previously declared but not used)
|
||||
MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) {
|
||||
var firstLink = megaMenu.querySelector('a');
|
||||
if (firstLink) {
|
||||
firstLink.focus();
|
||||
}
|
||||
};
|
||||
MegaMenuRenderer.prototype.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 () {
|
||||
// Screenreader-Ankündigungen
|
||||
var existingAnnouncer = document.getElementById('mega-menu-sr-announcer');
|
||||
if (existingAnnouncer) {
|
||||
this._announcer = existingAnnouncer;
|
||||
return;
|
||||
}
|
||||
var srAnnouncer = document.createElement('div');
|
||||
srAnnouncer.id = 'mega-menu-sr-announcer';
|
||||
srAnnouncer.setAttribute('aria-live', 'polite');
|
||||
srAnnouncer.setAttribute('aria-atomic', 'true');
|
||||
srAnnouncer.className = 'sr-only';
|
||||
document.body.appendChild(srAnnouncer);
|
||||
console.log('Screenreader-Ankündigungen sind jetzt bereit');
|
||||
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;
|
||||
}());
|
||||
export { MegaMenuRenderer };
|
||||
|
||||
File diff suppressed because one or more lines are too long
-25
@@ -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);
|
||||
}
|
||||
@@ -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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
return MegaMenuSettingsPanel;
|
||||
}());
|
||||
export { MegaMenuSettingsPanel };
|
||||
|
||||
//# sourceMappingURL=MegaMenuSettings.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"Title": "MegaMenuApplicationCustomizer"
|
||||
"Title": "MegaMenuApplicationCustomizer",
|
||||
"ConfigurationMissing": "Navigation is not configured.",
|
||||
"LoadError": "Navigation could not be loaded."
|
||||
}
|
||||
});
|
||||
@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
|
||||
hideDeprecatedTags: boolean;
|
||||
hideTagsNotAvailableForTagging: boolean;
|
||||
anchorId: string;
|
||||
cacheMinutes: number;
|
||||
}
|
||||
|
||||
+7
-3
@@ -8,12 +8,13 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||
export default class SPTermStorePickerService {
|
||||
private props;
|
||||
private context;
|
||||
private debug;
|
||||
private clientServiceUrl;
|
||||
private suggestionServiceUrl;
|
||||
/**
|
||||
* Service constructor
|
||||
*/
|
||||
constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext);
|
||||
constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext, debug?: boolean);
|
||||
getTermLabels(termId: string): Promise<string[]>;
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
* @param searchText
|
||||
@@ -49,7 +50,10 @@ export default class SPTermStorePickerService {
|
||||
* @param termset
|
||||
*/
|
||||
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);
|
||||
/**
|
||||
* Searches terms for the given term set
|
||||
|
||||
@@ -39,7 +39,10 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
};
|
||||
import { SPHttpClient } from '@microsoft/sp-http';
|
||||
import { findIndex } from '@microsoft/sp-lodash-subset';
|
||||
import { debugError } from './MegaMenuDebug';
|
||||
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
|
||||
*/
|
||||
@@ -47,9 +50,11 @@ var SPTermStorePickerService = (function () {
|
||||
/**
|
||||
* Service constructor
|
||||
*/
|
||||
function SPTermStorePickerService(props, context) {
|
||||
function SPTermStorePickerService(props, context, debug) {
|
||||
if (debug === void 0) { debug = false; }
|
||||
this.props = props;
|
||||
this.context = context;
|
||||
this.debug = debug;
|
||||
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
|
||||
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
|
||||
}
|
||||
@@ -63,7 +68,7 @@ var SPTermStorePickerService = (function () {
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_a.trys.push([1, 4, , 5]);
|
||||
data = "<Request AddExpandoFieldTypeSuffix=\"true\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\".NET Library\" xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\"><Actions><ObjectPath Id=\"8\" ObjectPathId=\"7\" /><ObjectIdentityQuery Id=\"9\" ObjectPathId=\"7\" /><ObjectPath Id=\"11\" ObjectPathId=\"10\" /><ObjectIdentityQuery Id=\"12\" ObjectPathId=\"10\" /><ObjectPath Id=\"14\" ObjectPathId=\"13\" /><ObjectIdentityQuery Id=\"15\" ObjectPathId=\"13\" /><Query Id=\"16\" ObjectPathId=\"13\"><Query SelectAllProperties=\"false\"><Properties><Property Name=\"Labels\" SelectAll=\"true\"><Query SelectAllProperties=\"false\"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id=\"7\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"10\" ParentId=\"7\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"13\" ParentId=\"10\" Name=\"GetTerm\"><Parameters><Parameter Type=\"Guid\">" + termId + "</Parameter></Parameters></Method></ObjectPaths></Request>";
|
||||
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.append('accept', 'application/json');
|
||||
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)];
|
||||
case 2:
|
||||
callResult = _a.sent();
|
||||
this.ensureSuccessfulResponse(callResult, 'Term labels');
|
||||
return [4 /*yield*/, callResult.json()];
|
||||
case 3:
|
||||
jsonResult = _a.sent();
|
||||
@@ -85,7 +91,7 @@ var SPTermStorePickerService = (function () {
|
||||
case 4:
|
||||
error_1 = _a.sent();
|
||||
result = null;
|
||||
console.log(error_1.message);
|
||||
debugError(this.debug, 'SPTermStorePickerService', 'Error reading term labels.', error_1);
|
||||
return [3 /*break*/, 5];
|
||||
case 5: return [2 /*return*/, result];
|
||||
}
|
||||
@@ -107,6 +113,7 @@ var SPTermStorePickerService = (function () {
|
||||
body: data
|
||||
};
|
||||
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) {
|
||||
// Construct results
|
||||
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
|
||||
* @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 (cacheMinutes === void 0) { cacheMinutes = 15; }
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions;
|
||||
@@ -182,11 +190,11 @@ var SPTermStorePickerService = (function () {
|
||||
}
|
||||
_a.label = 2;
|
||||
case 2:
|
||||
childTerms = this.getTermsById(termsetId, useSessionStorage);
|
||||
childTerms = this.getCachedTermSet(termsetId, useSessionStorage);
|
||||
if (childTerms) {
|
||||
return [2 /*return*/, childTerms];
|
||||
}
|
||||
data = "<Request xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\"Javascript Library\"><Actions><ObjectPath Id=\"1\" ObjectPathId=\"0\" /><ObjectIdentityQuery Id=\"2\" ObjectPathId=\"0\" /><ObjectPath Id=\"4\" ObjectPathId=\"3\" /><ObjectIdentityQuery Id=\"5\" ObjectPathId=\"3\" /><ObjectPath Id=\"7\" ObjectPathId=\"6\" /><ObjectIdentityQuery Id=\"8\" ObjectPathId=\"6\" /><ObjectPath Id=\"10\" ObjectPathId=\"9\" /><Query Id=\"11\" ObjectPathId=\"6\"><Query SelectAllProperties=\"true\"><Properties /></Query></Query><Query Id=\"12\" ObjectPathId=\"9\"><Query SelectAllProperties=\"false\"><Properties /></Query><ChildItemQuery SelectAllProperties=\"false\"><Properties><Property Name=\"IsRoot\" SelectAll=\"true\" /><Property Name=\"Labels\" SelectAll=\"true\" /><Property Name=\"TermsCount\" SelectAll=\"true\" /><Property Name=\"CustomSortOrder\" SelectAll=\"true\" /><Property Name=\"Id\" SelectAll=\"true\" /><Property Name=\"Name\" SelectAll=\"true\" /><Property Name=\"PathOfTerm\" SelectAll=\"true\" /><Property Name=\"Parent\" SelectAll=\"true\" /><Property Name=\"LocalCustomProperties\" SelectAll=\"true\" /><Property Name=\"IsDeprecated\" ScalarProperty=\"true\" /><Property Name=\"IsAvailableForTagging\" ScalarProperty=\"true\" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id=\"0\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"3\" ParentId=\"0\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"6\" ParentId=\"3\" Name=\"GetTermSet\"><Parameters><Parameter Type=\"Guid\">" + termsetId + "</Parameter></Parameters></Method><Method Id=\"9\" ParentId=\"6\" Name=\"GetAllTerms\" /></ObjectPaths></Request>";
|
||||
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.append('accept', 'application/json');
|
||||
reqHeaders.append('content-type', 'application/xml');
|
||||
@@ -195,7 +203,11 @@ var SPTermStorePickerService = (function () {
|
||||
body: data
|
||||
};
|
||||
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) {
|
||||
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'; });
|
||||
if (termStoreResultTermSets.length > 0) {
|
||||
var termStoreResultTermSet_1 = termStoreResultTermSets[0];
|
||||
@@ -236,7 +248,12 @@ var SPTermStorePickerService = (function () {
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
@@ -265,7 +282,7 @@ var SPTermStorePickerService = (function () {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
useSessionStorage = this.props.useSessionStorage;
|
||||
childTerms = this.getTermsById(termId, useSessionStorage);
|
||||
childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
|
||||
if (!childTerms) return [3 /*break*/, 1];
|
||||
return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)];
|
||||
case 1:
|
||||
@@ -289,16 +306,16 @@ var SPTermStorePickerService = (function () {
|
||||
if (useSessionStorage === void 0) { useSessionStorage = true; }
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms;
|
||||
var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms, cacheEntry;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
returnTerms = [];
|
||||
childTerms = this.getTermsById(anchorId, useSessionStorage);
|
||||
childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
|
||||
if (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:
|
||||
termSet = _a.sent();
|
||||
terms = termSet.Terms;
|
||||
@@ -312,7 +329,11 @@ var SPTermStorePickerService = (function () {
|
||||
});
|
||||
try {
|
||||
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) {
|
||||
@@ -370,17 +391,21 @@ var SPTermStorePickerService = (function () {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
SPTermStorePickerService.prototype.getTermsById = function (termId, useSessionStorage) {
|
||||
SPTermStorePickerService.prototype.getCachedTermSet = function (termId, useSessionStorage) {
|
||||
if (useSessionStorage === void 0) { useSessionStorage = true; }
|
||||
try {
|
||||
if (useSessionStorage && window.sessionStorage) {
|
||||
var terms = window.sessionStorage.getItem(termId);
|
||||
if (terms) {
|
||||
return JSON.parse(terms);
|
||||
}
|
||||
else {
|
||||
var key = TermCachePrefix + termId;
|
||||
var terms = window.sessionStorage.getItem(key);
|
||||
if (!terms) {
|
||||
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 {
|
||||
return null;
|
||||
@@ -390,6 +415,39 @@ var SPTermStorePickerService = (function () {
|
||||
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) {
|
||||
if (terms) {
|
||||
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
@@ -3,9 +3,10 @@ import { IMenuItem } from './IMenuItem';
|
||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||
export declare class TaxonomyNavigationService implements ITaxonomyNavigationService {
|
||||
private context;
|
||||
private termSetName;
|
||||
private termSetNameOrId;
|
||||
private debug;
|
||||
private cacheMinutes;
|
||||
private _taxonomyPickerService;
|
||||
private _noTerm;
|
||||
constructor(context: ApplicationCustomizerContext, termSetName: string);
|
||||
constructor(context: ApplicationCustomizerContext, termSetNameOrId: string, debug?: boolean, cacheMinutes?: number);
|
||||
getMenuItems(): Promise<IMenuItem[]>;
|
||||
}
|
||||
|
||||
@@ -37,40 +37,28 @@ import { sp } from '@pnp/sp';
|
||||
import { MenuItem } from './MenuItem';
|
||||
import SPTermStorePickerService from './SPTermStorePickerService';
|
||||
import { ItemDictionary } from './ItemDictionary';
|
||||
import { debugWarn } from './MegaMenuDebug';
|
||||
var LOG_SOURCE = 'TaxonomyNavigationService';
|
||||
var TaxonomyNavigationService = (function () {
|
||||
function TaxonomyNavigationService(context /*, private lcid: number*/, termSetName) {
|
||||
this.context = context; /*, private lcid: number*/
|
||||
this.termSetName = termSetName;
|
||||
this._noTerm = {
|
||||
_ObjectType_: '',
|
||||
_ObjectIdentity_: '',
|
||||
CustomSortOrderIndex: 0,
|
||||
Description: '',
|
||||
Id: '',
|
||||
IsAvailableForTagging: false,
|
||||
IsDeprecated: false,
|
||||
IsRoot: true,
|
||||
LocalCustomProperties: {
|
||||
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
|
||||
_Sys_Nav_ExcludedProviders: undefined,
|
||||
_Sys_Nav_SimpleLinkUrl: undefined
|
||||
},
|
||||
Name: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
|
||||
PathOfTerm: '',
|
||||
TermSet: undefined
|
||||
};
|
||||
function TaxonomyNavigationService(context, termSetNameOrId, debug, cacheMinutes) {
|
||||
if (debug === void 0) { debug = false; }
|
||||
if (cacheMinutes === void 0) { cacheMinutes = 15; }
|
||||
this.context = context;
|
||||
this.termSetNameOrId = termSetNameOrId;
|
||||
this.debug = debug;
|
||||
this.cacheMinutes = cacheMinutes;
|
||||
sp.setup({
|
||||
spfxContext: context
|
||||
});
|
||||
this._taxonomyPickerService = new SPTermStorePickerService({
|
||||
anchorId: '',
|
||||
termsetNameOrID: termSetName,
|
||||
termsetNameOrID: termSetNameOrId,
|
||||
useSessionStorage: true,
|
||||
hideDeprecatedTags: true,
|
||||
hideTagsNotAvailableForTagging: false
|
||||
}, this.context);
|
||||
hideTagsNotAvailableForTagging: true,
|
||||
cacheMinutes: cacheMinutes
|
||||
}, this.context, this.debug);
|
||||
}
|
||||
// Implement the methods from ITaxonomyNavigationService interface here
|
||||
TaxonomyNavigationService.prototype.getMenuItems = function () {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
@@ -79,35 +67,31 @@ var TaxonomyNavigationService = (function () {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
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:
|
||||
termset = _a.sent();
|
||||
itemsDict = new ItemDictionary();
|
||||
menuItems = [];
|
||||
if (!termset || !termset.Terms) {
|
||||
console.warn('No terms found in the term set');
|
||||
return [2 /*return*/, [new MenuItem(this._noTerm, 0, siteCollectionUrl)]];
|
||||
if (!termset || !termset.Terms || termset.Terms.length === 0) {
|
||||
debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.');
|
||||
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 () {
|
||||
var menuItem, parentItem;
|
||||
return __generator(this, function (_a) {
|
||||
menuItem = new MenuItem(term, 0, siteCollectionUrl);
|
||||
termset.Terms.forEach(function (term) {
|
||||
var menuItem = new MenuItem(term, 0, siteCollectionUrl);
|
||||
itemsDict.Add(term.Id, menuItem);
|
||||
if (menuItem.pathDepth === 1) {
|
||||
menuItems.push(menuItem);
|
||||
}
|
||||
else {
|
||||
parentItem = itemsDict.Get(term.ParentId);
|
||||
var parentItem = itemsDict.Get(term.ParentId);
|
||||
if (parentItem) {
|
||||
parentItem.items.push(menuItem);
|
||||
}
|
||||
else {
|
||||
console.warn("Item without parent: " + term.PathOfTerm);
|
||||
debugWarn(_this.debug, LOG_SOURCE, 'Item without parent:', term.PathOfTerm);
|
||||
}
|
||||
}
|
||||
return [2 /*return*/];
|
||||
});
|
||||
}); });
|
||||
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 { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||
export declare class UserCustomActionService implements IUserCustomActionService {
|
||||
constructor(context: ApplicationCustomizerContext);
|
||||
private debug;
|
||||
constructor(context: ApplicationCustomizerContext, debug?: boolean);
|
||||
getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>;
|
||||
getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>;
|
||||
addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>;
|
||||
|
||||
@@ -35,8 +35,12 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
};
|
||||
import { sp } from '@pnp/sp';
|
||||
import { UserCustomActionScope } from './UserCustomActionScope';
|
||||
import { debugError } from '../MegaMenuDebug';
|
||||
var LOG_SOURCE = 'UserCustomActionService';
|
||||
var UserCustomActionService = (function () {
|
||||
function UserCustomActionService(context) {
|
||||
function UserCustomActionService(context, debug) {
|
||||
if (debug === void 0) { debug = false; }
|
||||
this.debug = debug;
|
||||
sp.setup({
|
||||
spfxContext: context
|
||||
});
|
||||
@@ -76,7 +80,7 @@ var UserCustomActionService = (function () {
|
||||
case 8: return [2 /*return*/, actions];
|
||||
case 9:
|
||||
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;
|
||||
case 10: return [2 /*return*/];
|
||||
}
|
||||
@@ -102,7 +106,7 @@ var UserCustomActionService = (function () {
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
return [2 /*return*/];
|
||||
@@ -128,7 +132,7 @@ var UserCustomActionService = (function () {
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error adding user custom action: ', error);
|
||||
debugError(this.debug, LOG_SOURCE, 'Error adding user custom action.', error);
|
||||
throw error;
|
||||
}
|
||||
return [2 /*return*/];
|
||||
@@ -170,7 +174,7 @@ var UserCustomActionService = (function () {
|
||||
case 8: return [2 /*return*/, result];
|
||||
case 9:
|
||||
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;
|
||||
case 10: return [2 /*return*/];
|
||||
}
|
||||
@@ -196,7 +200,7 @@ var UserCustomActionService = (function () {
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error deleting user custom action: ', error);
|
||||
debugError(this.debug, LOG_SOURCE, 'Error deleting user custom action.', error);
|
||||
throw error;
|
||||
}
|
||||
return [2 /*return*/];
|
||||
|
||||
File diff suppressed because one or more lines are too long
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mega-menu",
|
||||
"version": "1.0.4",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+4
-3
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "mega-menu",
|
||||
"version": "1.0.4",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">=6.9.0 <9.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
"test": "gulp test",
|
||||
"package": "gulp clean && gulp bundle --ship && gulp package-solution --ship"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/decorators": "~1.4.1",
|
||||
|
||||
@@ -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>
|
||||
@@ -103,6 +103,14 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
|
||||
z-index: $mm-layer-index;
|
||||
}
|
||||
|
||||
.mega-menu-status {
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid $mm-panel-border;
|
||||
background: $mm-focus-soft;
|
||||
color: $mm-text;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mega-menu-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -145,7 +153,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
|
||||
}
|
||||
|
||||
.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-text {
|
||||
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 > 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 > span[aria-expanded="true"],
|
||||
.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 > span[role="menuitem"]:focus,
|
||||
.mega-menu-main > ul > li > .menu-item-text:focus,
|
||||
.menu-item-link:focus,
|
||||
.menu-item-text:focus {
|
||||
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 > span[role="menuitem"],
|
||||
.mega-menu-main > ul > li > .menu-item-text,
|
||||
.menu-item-link,
|
||||
.menu-item-text {
|
||||
min-height: 36px;
|
||||
@@ -448,7 +456,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
|
||||
.skip-link,
|
||||
.menu-item-has-children::after,
|
||||
.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-text,
|
||||
.mega-menu,
|
||||
@@ -460,7 +468,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.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-text:focus,
|
||||
.mega-menu-category > h3 > a:focus,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* tslint:disable */
|
||||
require('./MegaMenu.module.css');
|
||||
const styles = {
|
||||
mmSlideIn: 'mmSlideIn_09a8e1a7',
|
||||
mmFadeIn: 'mmFadeIn_09a8e1a7',
|
||||
};
|
||||
|
||||
export default styles;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$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",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
|
||||
@@ -19,11 +19,12 @@ import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
|
||||
import './MegaMenu.module.scss';
|
||||
|
||||
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 {
|
||||
termSetName: string;
|
||||
cssUrl?: string;
|
||||
termSetName?: string;
|
||||
termSetId?: string;
|
||||
cacheMinutes?: number;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
@@ -31,78 +32,121 @@ export default class MegaMenuApplicationCustomizer
|
||||
extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
|
||||
|
||||
private _topPlaceholder: PlaceholderContent | undefined;
|
||||
private _menuContainer: HTMLElement | undefined;
|
||||
private _renderer: MegaMenuRenderer | undefined;
|
||||
|
||||
@override
|
||||
public onInit(): Promise<void> {
|
||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Initialized ' + strings.Title);
|
||||
|
||||
if (this.properties.cssUrl) {
|
||||
this._loadExternalCss(this.properties.cssUrl);
|
||||
}
|
||||
const debug: boolean = this._isDebugEnabled();
|
||||
debugLog(debug, LOG_SOURCE, 'onInit started.', {
|
||||
componentId: UserCustomActionMegaMenuId,
|
||||
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);
|
||||
debugLog(debug, LOG_SOURCE, 'Placeholder changedEvent handler registered.');
|
||||
this._renderPlaceHolders();
|
||||
|
||||
debugLog(debug, LOG_SOURCE, 'onInit completed. Asynchronous menu loading may still be running.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private _renderPlaceHolders(): void {
|
||||
const debug: boolean = this._isDebugEnabled();
|
||||
const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames
|
||||
.map(name => PlaceholderName[name])
|
||||
.join(', ');
|
||||
|
||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders);
|
||||
debugLog(debug, LOG_SOURCE, 'Rendering placeholders.', {
|
||||
availablePlaceholders: availablePlaceholders,
|
||||
topPlaceholderAlreadyCreated: !!this._topPlaceholder
|
||||
});
|
||||
|
||||
if (!this._topPlaceholder) {
|
||||
debugLog(debug, LOG_SOURCE, 'Requesting the Top placeholder.');
|
||||
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
|
||||
PlaceholderName.Top,
|
||||
{ onDispose: this._onDispose }
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!this.properties.termSetName) {
|
||||
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetName configured. Mega Menu rendering is skipped.');
|
||||
debugLog(debug, LOG_SOURCE, 'Top placeholder acquired.', {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
debugLog(debug, LOG_SOURCE, 'Creating TaxonomyNavigationService.');
|
||||
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
|
||||
this.context,
|
||||
termSetName,
|
||||
debug
|
||||
termSetIdentifier,
|
||||
debug,
|
||||
this._getCacheMinutes()
|
||||
);
|
||||
|
||||
debugLog(debug, LOG_SOURCE, 'Requesting navigation items from taxonomy.');
|
||||
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,
|
||||
debug
|
||||
);
|
||||
|
||||
const container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
||||
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
||||
|
||||
if (container) {
|
||||
renderer.render(container);
|
||||
if (this._menuContainer) {
|
||||
debugLog(debug, LOG_SOURCE, 'Rendering into the selected container.', {
|
||||
id: this._menuContainer.id || '',
|
||||
className: this._menuContainer.className || ''
|
||||
});
|
||||
this._renderer.render(this._menuContainer);
|
||||
} 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.');
|
||||
} 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,44 +154,83 @@ export default class MegaMenuApplicationCustomizer
|
||||
const container = document.getElementById(id);
|
||||
|
||||
if (container) {
|
||||
const div = document.createElement('div');
|
||||
container.appendChild(div);
|
||||
return div;
|
||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing host container found.', {
|
||||
requestedId: id,
|
||||
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;
|
||||
}
|
||||
|
||||
private _loadExternalCss(cssUrl?: string, debug: boolean = this._isDebugEnabled()): void {
|
||||
const externalCssLinkId: string = 'mega-menu-additional-css-34FAB720';
|
||||
let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement;
|
||||
|
||||
if (cssUrl && cssUrl.trim() !== '') {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private _onDispose = (): void => {
|
||||
this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders);
|
||||
if (this._renderer) {
|
||||
this._renderer.dispose();
|
||||
this._renderer = undefined;
|
||||
}
|
||||
this._menuContainer = undefined;
|
||||
this._topPlaceholder = undefined;
|
||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
|
||||
}
|
||||
|
||||
private _getTermSetIdentifier(): string {
|
||||
const termSetId: string = this.properties && this.properties.termSetId
|
||||
? this.properties.termSetId.trim()
|
||||
: '';
|
||||
if (termSetId && this._isGuid(termSetId)) {
|
||||
return termSetId;
|
||||
}
|
||||
return this.properties && this.properties.termSetName
|
||||
? this.properties.termSetName.trim()
|
||||
: '';
|
||||
}
|
||||
|
||||
private _getCacheMinutes(): number {
|
||||
const value: number = this.properties ? Number(this.properties.cacheMinutes) : 15;
|
||||
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
|
||||
}
|
||||
|
||||
private _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 {
|
||||
if (typeof debugOverride === 'boolean') {
|
||||
return debugOverride;
|
||||
|
||||
@@ -13,25 +13,34 @@ const LOG_SOURCE: string = 'MegaMenuRenderer';
|
||||
export class MegaMenuRenderer {
|
||||
private static readonly hoverOpenDelayMs: number = 140;
|
||||
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(
|
||||
private menuItems: IMenuItem[],
|
||||
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.id = 'CustomNavigation';
|
||||
|
||||
const nav = document.createElement('nav');
|
||||
nav.id = 'Mega-Menu';
|
||||
nav.className = 'mega-menu-main';
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
||||
|
||||
const topLevelUl = document.createElement('ul');
|
||||
topLevelUl.className = 'mega-menu-top-level';
|
||||
topLevelUl.setAttribute('role', 'menubar');
|
||||
|
||||
this.menuItems.forEach(topLevelItem => {
|
||||
const topLevelLi = this.createTopLevelItem(topLevelItem);
|
||||
@@ -43,6 +52,34 @@ export class MegaMenuRenderer {
|
||||
|
||||
this.attachEventListeners();
|
||||
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 {
|
||||
@@ -57,7 +94,6 @@ export class MegaMenuRenderer {
|
||||
isActive ? 'is-active' : undefined,
|
||||
isCurrent ? 'is-current' : undefined
|
||||
);
|
||||
li.setAttribute('role', 'none');
|
||||
|
||||
const topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent);
|
||||
li.appendChild(topElement);
|
||||
@@ -83,7 +119,6 @@ export class MegaMenuRenderer {
|
||||
element.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
element.setAttribute('role', 'menuitem');
|
||||
element.textContent = item.label;
|
||||
element.className = this.joinClasses(
|
||||
element.className,
|
||||
@@ -111,7 +146,6 @@ export class MegaMenuRenderer {
|
||||
private createMegaMenu(parentItem: IMenuItem): HTMLDivElement {
|
||||
const megaMenuDiv = document.createElement('div');
|
||||
megaMenuDiv.className = 'mega-menu';
|
||||
megaMenuDiv.setAttribute('role', 'menu');
|
||||
megaMenuDiv.setAttribute('aria-expanded', 'false');
|
||||
megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien');
|
||||
|
||||
@@ -213,7 +247,7 @@ export class MegaMenuRenderer {
|
||||
}
|
||||
|
||||
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++) {
|
||||
const heading = headings[i] as HTMLElement;
|
||||
@@ -336,7 +370,7 @@ export class MegaMenuRenderer {
|
||||
}
|
||||
|
||||
private attachGlobalKeyboardNavigation(): void {
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
this._documentKeydownHandler = (e: KeyboardEvent): void => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
@@ -380,11 +414,12 @@ export class MegaMenuRenderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
document.addEventListener('keydown', this._documentKeydownHandler);
|
||||
}
|
||||
|
||||
private attachGlobalClickOutsideHandler(): void {
|
||||
document.addEventListener('click', (e: MouseEvent) => {
|
||||
this._documentClickHandler = (e: MouseEvent): void => {
|
||||
const eventTarget = e.target as Element;
|
||||
const isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' &&
|
||||
eventTarget.closest('#CustomNavigation'));
|
||||
@@ -399,7 +434,8 @@ export class MegaMenuRenderer {
|
||||
debugLog(this.debug, LOG_SOURCE, 'Closing menu because of outside click.');
|
||||
this.closeAllMegaMenus();
|
||||
}
|
||||
});
|
||||
};
|
||||
document.addEventListener('click', this._documentClickHandler);
|
||||
}
|
||||
|
||||
private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
||||
@@ -407,6 +443,7 @@ export class MegaMenuRenderer {
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
menu.setAttribute('aria-expanded', 'true');
|
||||
menu.classList.add('js-open');
|
||||
this.announce((trigger.textContent || '') + ' geöffnet');
|
||||
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
|
||||
}
|
||||
|
||||
@@ -414,6 +451,7 @@ export class MegaMenuRenderer {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
menu.setAttribute('aria-expanded', 'false');
|
||||
menu.classList.remove('js-open');
|
||||
this.announce((trigger.textContent || '') + ' geschlossen');
|
||||
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
|
||||
}
|
||||
|
||||
@@ -514,7 +552,9 @@ export class MegaMenuRenderer {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -524,7 +564,19 @@ export class MegaMenuRenderer {
|
||||
srAnnouncer.setAttribute('aria-atomic', 'true');
|
||||
srAnnouncer.className = 'sr-only';
|
||||
document.body.appendChild(srAnnouncer);
|
||||
this._announcer = srAnnouncer;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'>×</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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"Title": "MegaMenuApplicationCustomizer",
|
||||
"ConfigurationMissing": "Die Navigation ist nicht konfiguriert.",
|
||||
"LoadError": "Die Navigation konnte nicht geladen werden."
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"Title": "MegaMenuApplicationCustomizer"
|
||||
"Title": "MegaMenuApplicationCustomizer",
|
||||
"ConfigurationMissing": "Navigation is not configured.",
|
||||
"LoadError": "Navigation could not be loaded."
|
||||
}
|
||||
});
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
declare interface IMegaMenuApplicationCustomizerStrings {
|
||||
Title: string;
|
||||
ConfigurationMissing: string;
|
||||
LoadError: string;
|
||||
}
|
||||
|
||||
declare module 'MegaMenuApplicationCustomizerStrings' {
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
|
||||
hideDeprecatedTags: boolean;
|
||||
hideTagsNotAvailableForTagging: boolean;
|
||||
anchorId: string;
|
||||
cacheMinutes: number;
|
||||
}
|
||||
@@ -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) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,24 @@
|
||||
|
||||
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
|
||||
import { ITermStore, ITerms, ITerm, IGroup, ITermSet, ISuggestTerm } from './ISPTermStorePickerService';
|
||||
import { findIndex } from '@microsoft/sp-lodash-subset';
|
||||
import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps';
|
||||
import { IPickerTerm } from './IPickerTerm';
|
||||
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 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
|
||||
@@ -26,12 +37,18 @@ export default class SPTermStorePickerService {
|
||||
constructor(private props: ISPTermStorePickerServiceProps, private context: ApplicationCustomizerContext, private debug: boolean = false) {
|
||||
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
|
||||
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[]> {
|
||||
let result: string[] = null;
|
||||
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();
|
||||
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);
|
||||
this.ensureSuccessfulResponse(callResult, 'Term labels');
|
||||
const jsonResult = await callResult.json();
|
||||
|
||||
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) => {
|
||||
this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
|
||||
return serviceResponse.json().then((serviceJSONResponse: any) => {
|
||||
// Construct results
|
||||
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
|
||||
* @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;
|
||||
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
|
||||
if (!this.isGuid(termset)) {
|
||||
// Fetch the term store information
|
||||
@@ -134,19 +166,32 @@ export default class SPTermStorePickerService {
|
||||
const crntTermSet = this.getTermSetId(termStore, termset);
|
||||
if (crntTermSet) {
|
||||
termsetId = this.cleanGuid(crntTermSet.Id);
|
||||
debugLog(this.debug, 'SPTermStorePickerService', 'Term set name resolved to an ID.', {
|
||||
termsetId: termsetId
|
||||
});
|
||||
} else {
|
||||
debugWarn(this.debug, 'SPTermStorePickerService', 'Configured term set could not be resolved.', termset);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const childTerms = this.getTermsById(termsetId, useSessionStorage);
|
||||
const childTerms = this.getCachedTermSet(termsetId, useSessionStorage);
|
||||
|
||||
if (childTerms) {
|
||||
debugLog(this.debug, 'SPTermStorePickerService', 'Returning terms from session cache.', {
|
||||
termsetId: termsetId,
|
||||
termCount: childTerms.Terms ? childTerms.Terms.length : 0
|
||||
});
|
||||
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
|
||||
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();
|
||||
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) => {
|
||||
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) => {
|
||||
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');
|
||||
|
||||
if (termStoreResultTermSets.length > 0) {
|
||||
@@ -171,11 +225,21 @@ export default class SPTermStorePickerService {
|
||||
let terms = termStoreResultTerms[0]._Child_Items_;
|
||||
|
||||
if (hideDeprecatedTags === true) {
|
||||
const countBeforeDeprecatedFilter: number = terms.length;
|
||||
terms = terms.filter(d => d.IsDeprecated === false);
|
||||
debugLog(this.debug, 'SPTermStorePickerService', 'Deprecated terms filtered.', {
|
||||
before: countBeforeDeprecatedFilter,
|
||||
after: terms.length
|
||||
});
|
||||
}
|
||||
|
||||
if (hideTagsNotAvailableForTagging === true) {
|
||||
const countBeforeAvailableFilter: number = terms.length;
|
||||
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
|
||||
@@ -203,13 +267,27 @@ export default class SPTermStorePickerService {
|
||||
|
||||
try {
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
debugWarn(this.debug, 'SPTermStorePickerService', 'ProcessQuery response contained no term set.');
|
||||
return null;
|
||||
});
|
||||
});
|
||||
@@ -225,7 +303,7 @@ export default class SPTermStorePickerService {
|
||||
|
||||
public async searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]> {
|
||||
const { useSessionStorage } = this.props;
|
||||
const childTerms = this.getTermsById(termId, useSessionStorage);
|
||||
const childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
|
||||
if (childTerms) {
|
||||
return this.searchTermsBySearchText(childTerms, searchText);
|
||||
} else {
|
||||
@@ -257,12 +335,18 @@ export default class SPTermStorePickerService {
|
||||
|
||||
const returnTerms: IPickerTerm[] = [];
|
||||
|
||||
const childTerms = this.getTermsById(anchorId, useSessionStorage);
|
||||
const childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
|
||||
if (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;
|
||||
if (anchorId) {
|
||||
const anchorTerm = terms.filter(t => t.Id.toLowerCase() === anchorId.toLowerCase()).shift();
|
||||
@@ -277,7 +361,11 @@ export default class SPTermStorePickerService {
|
||||
|
||||
try {
|
||||
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) {
|
||||
// Do nothing
|
||||
@@ -332,15 +420,21 @@ export default class SPTermStorePickerService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private getTermsById(termId, useSessionStorage: boolean = true) {
|
||||
private getCachedTermSet(termId: string, useSessionStorage: boolean = true): ITermSet {
|
||||
try {
|
||||
if (useSessionStorage && window.sessionStorage) {
|
||||
const terms = window.sessionStorage.getItem(termId);
|
||||
if (terms) {
|
||||
return JSON.parse(terms);
|
||||
} else {
|
||||
const key: string = TermCachePrefix + termId;
|
||||
const terms: string = window.sessionStorage.getItem(key);
|
||||
if (!terms) {
|
||||
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 {
|
||||
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) {
|
||||
if (terms) {
|
||||
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
|
||||
pathElms.pop();
|
||||
// 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) {
|
||||
newTermsOrder.splice(idx + 1, 0, crntTerm);
|
||||
} else {
|
||||
@@ -476,6 +606,16 @@ export default class SPTermStorePickerService {
|
||||
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
|
||||
*
|
||||
|
||||
@@ -6,46 +6,35 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||
import SPTermStorePickerService from './SPTermStorePickerService';
|
||||
import { ITerm, ITermSet } from './ISPTermStorePickerService';
|
||||
import { ItemDictionary } from './ItemDictionary';
|
||||
import { debugWarn } from './MegaMenuDebug';
|
||||
import { debugLog, debugWarn } from './MegaMenuDebug';
|
||||
|
||||
const LOG_SOURCE: string = 'TaxonomyNavigationService';
|
||||
|
||||
export class TaxonomyNavigationService implements ITaxonomyNavigationService {
|
||||
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(
|
||||
private context: ApplicationCustomizerContext,
|
||||
private termSetName: string,
|
||||
private debug: boolean = false
|
||||
private termSetNameOrId: string,
|
||||
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({
|
||||
spfxContext: context
|
||||
});
|
||||
this._taxonomyPickerService = new SPTermStorePickerService(
|
||||
{
|
||||
anchorId: '',
|
||||
termsetNameOrID: termSetName,
|
||||
termsetNameOrID: termSetNameOrId,
|
||||
useSessionStorage: true,
|
||||
hideDeprecatedTags: true,
|
||||
hideTagsNotAvailableForTagging: false
|
||||
hideTagsNotAvailableForTagging: true,
|
||||
cacheMinutes: cacheMinutes
|
||||
},
|
||||
this.context,
|
||||
this.debug
|
||||
@@ -54,13 +43,27 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
|
||||
|
||||
public async getMenuItems(): Promise<IMenuItem[]> {
|
||||
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 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.');
|
||||
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) => {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
-5
@@ -1,5 +0,0 @@
|
||||
define([], function() {
|
||||
return {
|
||||
"Title": "MegaMenuApplicationCustomizer"
|
||||
}
|
||||
});
|
||||
+5
-12
@@ -2300,7 +2300,7 @@ function getManifests() {
|
||||
"alias": "MegaMenuApplicationCustomizer",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.2",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"loaderConfig": {
|
||||
@@ -2316,18 +2316,16 @@ function getManifests() {
|
||||
"MegaMenuApplicationCustomizerStrings": {
|
||||
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
|
||||
"type": "localizedPath",
|
||||
"paths": {}
|
||||
"paths": {
|
||||
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
|
||||
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
@@ -2342,11 +2340,6 @@ function getManifests() {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-12
@@ -695,7 +695,7 @@
|
||||
"alias": "MegaMenuApplicationCustomizer",
|
||||
"componentType": "Extension",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.2",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"loaderConfig": {
|
||||
@@ -711,18 +711,16 @@
|
||||
"MegaMenuApplicationCustomizerStrings": {
|
||||
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
|
||||
"type": "localizedPath",
|
||||
"paths": {}
|
||||
"paths": {
|
||||
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
|
||||
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
@@ -737,11 +735,6 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user