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