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