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.
This commit is contained in:
Torsten Brendgen
2026-07-16 22:43:31 +02:00
parent a2b8165262
commit 97d2b6b5d2
53 changed files with 1071 additions and 1065 deletions
+10
View File
@@ -0,0 +1,10 @@
node_modules/
.tools/
lib/
dist/
temp/
release/
sharepoint/solution/
*.sppkg
*.tgz
npm-debug.log*
+226 -147
View File
@@ -1,199 +1,278 @@
# SharePoint MegaMenu Extension # SharePoint MegaMenu
**Version:** 1.0.2 Barrierearme, dreistufige Portalnavigation auf Basis von SharePoint Managed Metadata für SharePoint Server Subscription Edition und SharePoint Server 2019.
**Framework:** SharePoint Framework (SPFx) 1.4.1
**Node.js:** v8.17.0
Eine moderne, barrierefreie Mega-Menü-Lösung für SharePoint 2019 oder SharePoint SE als ApplicationCustomizer Extension. | Eigenschaft | Wert |
|---|---|
| Version | 1.1.0 |
| SharePoint Framework | SPFx 1.4.1 |
| Node.js | 8.17.0 |
| Zieloberfläche | Moderne SharePoint-Seiten |
| Paket | `sharepoint/solution/mega-menu.sppkg` |
## 🌟 Features ## Funktionen
- **3-stufige Navigationshierarchie** basierend auf SharePoint Managed Metadata (Taxonomy) - Dreistufige Navigation aus einem Managed-Metadata-Termset
- **Responsive Design** mit automatischer Anpassung an verschiedene Bildschirmgrößen - Auswahl des Termsets per GUID oder, abwärtskompatibel, per Name
- **Barrierefreiheit** mit vollständiger Tastaturnavigation und Screenreader-Unterstützung - Benutzerdefinierte Sortierung aus dem Term Store
- **Einstellungs-Panel** für Administratoren zur Konfiguration - Unterstützung der Navigations-Eigenschaften `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText`
- **Externe CSS-Unterstützung** für individuelle Anpassungen - Auflösung von `~sitecollection` in Navigations-URLs
- **Office UI Fabric Integration** für konsistente SharePoint-Optik - Markierung des aktuellen Menüpunktes und seiner übergeordneten Einträge
- Bedienung mit Maus, Tastatur und Touch
- Responsive Darstellung
- Optionales zusätzliches Stylesheet
- Taxonomy-Cache mit konfigurierbarer Ablaufzeit
- Bereinigter SPFx-Lifecycle ohne verbleibende globale Event Listener
- Deutsche und englische Statusmeldungen
## 📋 Voraussetzungen ## Architektur
- SharePoint 2019 oder SharePoint SE (Subscription Edition) ```text
- Node.js v8.17.0 (empfohlen) UserCustomAction.ClientSideComponentProperties
- SharePoint Framework Development Tools (SPFx 1.4.1) │
▼
MegaMenuApplicationCustomizer
│
├── TaxonomyNavigationService
│ └── SPTermStorePickerService
│ └── Default Site Collection Term Store
│
└── MegaMenuRenderer
├── semantische Navigation
├── Fokus- und Tastatursteuerung
└── responsive Darstellung
```
## 🚀 Installation & Entwicklung Das MegaMenu liest seine Laufzeitkonfiguration aus der site-scoped UserCustomAction. Es enthält keine eigene Administrationsoberfläche. Die zentrale Pflege soll über die separate Solution **PortalSettings** erfolgen.
### 1. Repository klonen und Abhängigkeiten installieren ## Voraussetzungen
```bash - SharePoint Server Subscription Edition oder SharePoint Server 2019
git clone <repository-url> - konfigurierter App Catalog
cd MegaMenu - konfigurierter App Management Service
- ein Termset im Standard-Term-Store der Site Collection
- Leseberechtigung der Benutzer auf den Managed-Metadata-Service
- für Entwicklung und Build: Node.js 8.17.0 und eine zu SPFx 1.4.1 passende npm-Version
## Konfiguration
Die UserCustomAction unterstützt folgende `ClientSideComponentProperties`:
```json
{
"termSetId": "6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc",
"termSetName": "Global Navigation",
"cacheMinutes": 15,
"cssUrl": "/SiteAssets/megamenu/megamenu-custom.css",
"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 |
| `cssUrl` | String | leer | Relatives/same-origin Stylesheet oder externe HTTPS-Adresse |
| `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
Die bisherige PortalSettings-Version verwaltet `termSetName`, `cssUrl` und `debug`. Das MegaMenu 1.1.0 bleibt damit vollständig lauffähig und verwendet für nicht vorhandene `cacheMinutes` automatisch 15 Minuten.
Damit auch `termSetId` und `cacheMinutes` zentral bearbeitet werden können, muss das MegaMenu-Formular in PortalSettings um diese beiden Eigenschaften ergänzt werden. Bis dahin können sie über das Deployment-Skript gesetzt werden. Speichert eine ältere PortalSettings-Version anschließend erneut, werden die beiden noch unbekannten Eigenschaften entfernt; das MegaMenu fällt dann kontrolliert auf `termSetName` und den Standard-Cache zurück.
## Termset-Aufbau
Die Darstellung unterstützt drei Ebenen:
```text
Ebene 1: Hauptnavigation
├── Ebene 2: Kategorie
│ ├── Ebene 3: Link
│ └── Ebene 3: Link
└── Ebene 2: Kategorie
```
Für Zieladressen werden die lokalen benutzerdefinierten Eigenschaften des Terms ausgewertet:
1. `_Sys_Nav_SimpleLinkUrl`
2. `_Sys_Nav_TargetUrl`
Optional kann `_Sys_Nav_HoverText` als ergänzender Hilfetext verwendet werden. Veraltete Terms werden nicht angezeigt. Terms, die nicht für Tagging verfügbar sind, bleiben für Navigationsszenarien sichtbar.
## Build
SPFx 1.4.1 verwendet die Legacy-Gulp-Toolchain. Neuere Node-Versionen sind nicht kompatibel.
```powershell
npm install npm install
npm run package
``` ```
### 2. Entwicklungsserver starten Alternativ einzeln:
```bash ```powershell
gulp serve
```
### 3. Produktionspaket erstellen
```bash
gulp clean gulp clean
gulp bundle --ship gulp bundle --ship
gulp package-solution --ship gulp package-solution --ship
``` ```
## 📁 Projektstruktur Das fertige Paket befindet sich anschließend unter:
``` ```text
src/ sharepoint/solution/mega-menu.sppkg
├── extensions/
│ └── megaMenu/
│ ├── MegaMenuApplicationCustomizer.ts # Haupteinstiegspunkt
│ ├── MegaMenuRenderer.ts # Menü-Rendering-Logik
│ ├── MegaMenuSettings.ts # Einstellungs-Panel
│ └── MegaMenu.css # Styling
├── services/
│ ├── TaxonomyNavigationService.ts # Taxonomy-Datenabfrage
│ ├── UserCustomActionService/ # UserCustomAction-Verwaltung
│ └── ...
└── deployment/
├── add-megamenu.ps1 # Installations-Script
└── remove-megamenu.ps1 # Deinstallations-Script
``` ```
## ⚙️ Konfiguration ## Deployment
### Einstellungs-Panel (Admin-Bereich) 1. `mega-menu.sppkg` in den App Catalog laden.
2. Die Solution freigeben.
3. Die App in der gewünschten Site Collection installieren.
4. Das MegaMenu site-scoped registrieren.
Das MegaMenu verfügt über ein Einstellungs-Panel, das über das Zahnrad-Icon (⚙️) im Menü zugänglich ist: Empfohlen mit Termset-GUID:
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
```powershell ```powershell
# Installation .\deployment\add-megamenu.ps1 `
.\src\deployment\add-megamenu.ps1 -SiteUrl 'https://sharepoint/sites/portal' `
-TermSetId '6f36a1a8-7bd8-4ed0-a22f-e219f4d2eacc' `
# Deinstallation -TermSetName 'Global Navigation' `
.\src\deployment\remove-megamenu.ps1 -CacheMinutes 15 `
-CssUrl '/SiteAssets/megamenu/megamenu-custom.css'
``` ```
## 🎨 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 entfernt ältere web-scoped Registrierungen derselben Komponente und erstellt beziehungsweise aktualisiert eine site-scoped UserCustomAction mit dem Description-Tag `MSFT-Custom-Solution:MegaMenu`. Dieses Tag wird von PortalSettings zur Erkennung verwendet.
## Zusätzliches CSS
Zulässig sind:
- serverrelative oder relative Pfade derselben SharePoint-Origin,
- absolute URLs derselben Origin,
- externe URLs ausschließlich über HTTPS.
Nicht-HTTP(S)-Schemes und unverschlüsselte externe HTTP-Adressen werden zur Laufzeit verworfen.
Die wichtigsten CSS-Klassen sind:
```css ```css
/* Haupt-Container */ #CustomNavigation { }
#Mega-Menu { } #Mega-Menu { }
.mega-menu-top-level { }
/* Top-Level Menüpunkte */ .mega-menu-top-item { }
#Mega-Menu > ul > li > a { }
/* Mega-Menü Dropdown */
.mega-menu { } .mega-menu { }
.mega-menu-grid { }
/* Kategorien im Dropdown */
.mega-menu-category { } .mega-menu-category { }
.mega-menu-links { }
/* Links in Kategorien */
.mega-menu-category ul li a { }
/* Einstellungs-Panel */
.mm-settings-panel { }
``` ```
### Accessibility Features Viele Farben und Abstände können über CSS Custom Properties überschrieben werden, beispielsweise:
- **Skip-Links** für Screenreader ```css
- **ARIA-Rollen** und Labels :root {
- **Fokus-Management** mit sichtbaren Fokus-Indikatoren --megaMenuNavBackground: #5f6f74;
- **Tastaturnavigation** mit Tab/Shift+Tab/Escape --megaMenuNavTextColor: #ffffff;
- **High Contrast Modus** Unterstützung --megaMenuPanelWidth: 1280px;
--megaMenuZIndex: 6000;
## 🔧 Build-Befehle }
| Befehl | Beschreibung |
|--------|-------------|
| `gulp serve` | Entwicklungsserver mit Live-Reload |
| `gulp build` | Entwicklungs-Build (Debug-Modus) |
| `gulp bundle --ship` | Produktions-Build (Optimiert) |
| `gulp package-solution --ship` | SharePoint Package (.sppkg) erstellen |
| `gulp clean` | Build-Artifacts löschen |
| `gulp test` | Unit-Tests ausführen |
## 📦 Deployment
1. **Paket erstellen:**
```bash
gulp clean && gulp bundle --ship && gulp package-solution --ship
``` ```
2. **SharePoint App Catalog:** ## Cache
- `.sppkg` Datei aus `sharepoint/solution/` hochladen
- App genehmigen und für alle Sites verfügbar machen
3. **Site-spezifische Aktivierung:** 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.
- PowerShell-Scripts verwenden
## 🐛 Troubleshooting Für einen sofortigen administrativen Test kann der Session-Cache in den Browser-Entwicklertools gelöscht oder ein neues privates Browserfenster verwendet werden.
### Häufige Probleme ## Barrierefreiheit
**Problem:** Menü wird nicht angezeigt - semantisches `<nav aria-label="Hauptnavigation">`
**Lösung:** Termset-Name in den Einstellungen prüfen - native Listen- und Linksemantik
- `aria-expanded` und `aria-haspopup` für aufklappbare Einträge
- `aria-current="page"` für den aktuellen Navigationspfad
- Öffnen über Leertaste oder Pfeil nach unten
- Schließen über Escape
- sichtbare Fokusindikatoren
- Unterstützung von `prefers-reduced-motion` und kontrastreichen Darstellungen
**Problem:** CSS-Styling funktioniert nicht 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.
**Lösung:** Externe CSS-URL und CORS-Einstellungen überprüfen
**Problem:** Build-Fehler ## Fehlerdiagnose
**Lösung:** Node.js Version prüfen (v8.17.0 empfohlen)
### Debug-Modus ### „Die Navigation ist nicht konfiguriert“
```bash Weder eine gültige `termSetId` noch ein `termSetName` ist gesetzt. UserCustomAction beziehungsweise PortalSettings-Konfiguration prüfen.
# Detaillierte Build-Ausgabe
gulp bundle --verbose
# TypeScript-Fehler anzeigen ### „Die Navigation konnte nicht geladen werden“
gulp build
- 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.
### Änderungen am Termset erscheinen verzögert
Die Navigation wird bis zu `cacheMinutes` im Session-Cache gehalten. Cache leeren oder Ablaufzeit abwarten.
### Zusätzliches CSS wird nicht geladen
- URL und Dateiberechtigungen prüfen.
- Für externe Quellen HTTPS verwenden.
- Browser-Konsole bei aktiviertem Debug-Modus prüfen.
## Upgrade von 1.0.x
- Bestehende `termSetName`-Konfigurationen funktionieren unverändert.
- Der alte unversionierte Session-Cache wird nicht weiterverwendet.
- `cacheMinutes` verwendet ohne Konfiguration den Wert 15.
- Die eigene, nicht verwendete MegaMenu-Einstellungsseite wurde entfernt; PortalSettings ist der zentrale Konfigurationsweg.
- Globale Event Listener werden beim Dispose der Extension entfernt.
- Paket-, npm- und Dokumentationsversion wurden auf 1.1.0 vereinheitlicht.
## Classic Pages
Unter `classic/` befindet sich eine separate Kompatibilitätsimplementierung für klassische SharePoint-Seiten. Sie wird nicht vom SPFx-Bundle ausgeführt und besitzt einen eigenen Deploymentweg. Neue Funktionen sollten zuerst in der modernen SPFx-Variante umgesetzt und anschließend bewusst in die Classic-Variante übernommen werden.
## Projektstruktur
```text
MegaMenu/
├── config/ SPFx-Build- und Paketkonfiguration
├── deployment/ Farm-/Site-Registrierung
├── src/
│ ├── extensions/megaMenu/ Application Customizer und Renderer
│ └── services/ Taxonomy- und Datenmodelle
├── classic/ optionale Classic-Kompatibilität
├── build-classic.ps1 Classic-Build
├── gulpfile.js
└── package.json
``` ```
## 🔄 Versionshistorie ## Versionshistorie
### v1.0.2 (Aktuell) ### 1.1.0
- Einstellungs-Panel implementiert
- Fokus-Management verbessert
- CSS-Optimierungen für SharePoint-Konsistenz
- Barrierefreiheit-Verbesserungen
### v1.0.1 - konsistente Verwendung des Default Site Collection Term Store
- Responsive Design hinzugefügt - optionale Konfiguration per Termset-GUID
- Performance-Optimierungen - Cache-TTL und namensräumlich getrennter Session-Cache
- korrekte Anwendung der Deprecated-/Availability-Filter
### v1.0.0 - Prüfung der Taxonomy-HTTP-Antworten
- Initiale Release - Lifecycle-Cleanup für globale Event Listener
- 3-stufige Navigation - korrigierte Navigationssemantik und aktive Screenreader-Statusmeldungen
- Grundlegende Accessibility - Validierung externer CSS-URLs
- sichtbare Konfigurations- und Ladefehler
## 👥 Mitwirkende - Entfernung des ungenutzten lokalen Settings-Panels
- vereinheitlichte Projektversion und neue Build-/Deployment-Dokumentation
- Entwicklung: Matthias Glubrecht
- Framework: SharePoint Framework (Microsoft)
- UI: Office UI Fabric (Microsoft)
## 📄 Lizenz
tbd
## 🆘 Support
Bei Fragen oder Problemen:
1. Issues im Repository erstellen
2. Dokumentation und Troubleshooting-Bereich prüfen
+2 -2
View File
@@ -3,9 +3,9 @@
"solution": { "solution": {
"name": "mega-menu-client-side-solution", "name": "mega-menu-client-side-solution",
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133", "id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
"version": "1.0.0", "version": "1.1.0.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": false
}, },
"paths": { "paths": {
"zippedPackage": "solution/mega-menu.sppkg" "zippedPackage": "solution/mega-menu.sppkg"
+10 -1
View File
@@ -5,6 +5,11 @@ param(
[string]$TermSetName = '', [string]$TermSetName = '',
[string]$TermSetId = '',
[ValidateRange(1, 1440)]
[int]$CacheMinutes = 15,
[string]$CssUrl = '', [string]$CssUrl = '',
[switch]$EnableDebug, [switch]$EnableDebug,
@@ -22,12 +27,16 @@ $title = 'Mega Menu'
function Get-MegaMenuPropertiesJson { function Get-MegaMenuPropertiesJson {
param( param(
[string]$CurrentTermSetName, [string]$CurrentTermSetName,
[string]$CurrentTermSetId,
[int]$CurrentCacheMinutes,
[string]$CurrentCssUrl, [string]$CurrentCssUrl,
[bool]$CurrentDebug [bool]$CurrentDebug
) )
return (@{ return (@{
termSetName = $CurrentTermSetName termSetName = $CurrentTermSetName
termSetId = $CurrentTermSetId
cacheMinutes = $CurrentCacheMinutes
cssUrl = $CurrentCssUrl cssUrl = $CurrentCssUrl
debug = $CurrentDebug debug = $CurrentDebug
} | ConvertTo-Json -Depth 10 -Compress) } | ConvertTo-Json -Depth 10 -Compress)
@@ -89,7 +98,7 @@ try {
$action.Description = $Description $action.Description = $Description
$action.Location = $location $action.Location = $location
$action.ClientSideComponentId = [Guid]$componentId $action.ClientSideComponentId = [Guid]$componentId
$action.ClientSideComponentProperties = Get-MegaMenuPropertiesJson -CurrentTermSetName $TermSetName -CurrentCssUrl $CssUrl -CurrentDebug ([bool]$EnableDebug.IsPresent) $action.ClientSideComponentProperties = Get-MegaMenuPropertiesJson -CurrentTermSetName $TermSetName -CurrentTermSetId $TermSetId -CurrentCacheMinutes $CacheMinutes -CurrentCssUrl $CssUrl -CurrentDebug ([bool]$EnableDebug.IsPresent)
$action.Update() $action.Update()
Write-Host 'Mega Menu wurde site-scoped registriert.' -ForegroundColor Green Write-Host 'Mega Menu wurde site-scoped registriert.' -ForegroundColor Green
+5 -12
View File
@@ -3,7 +3,7 @@
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
"version": "1.0.4", "version": "1.1.0",
"manifestVersion": 2, "manifestVersion": 2,
"requiresCustomScript": false, "requiresCustomScript": false,
"loaderConfig": { "loaderConfig": {
@@ -19,18 +19,16 @@
"MegaMenuApplicationCustomizerStrings": { "MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js", "defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath", "type": "localizedPath",
"paths": {} "paths": {
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
}
}, },
"@microsoft/decorators": { "@microsoft/decorators": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3" "id": "f97266fb-ccb7-430e-9384-4124d05295d3"
}, },
"@microsoft/sp-core-library": {
"type": "component",
"version": "1.4.1",
"id": "7263c7d0-1d6a-45ec-8d85-d4d1d234171b"
},
"@microsoft/sp-application-base": { "@microsoft/sp-application-base": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
@@ -45,11 +43,6 @@
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a" "id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"
},
"@microsoft/sp-page-context": {
"type": "component",
"version": "1.4.1",
"id": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8"
} }
} }
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -4
View File
@@ -1,5 +1,2 @@
declare const styles: { declare const styles: {};
mmSlideIn: string;
mmFadeIn: string;
};
export default styles; export default styles;
@@ -1,9 +1,6 @@
/* tslint:disable */ /* tslint:disable */
require('./MegaMenu.module.css'); require('./MegaMenu.module.css');
var styles = { var styles = {};
mmSlideIn: 'mmSlideIn_09a8e1a7',
mmFadeIn: 'mmFadeIn_09a8e1a7',
};
export default styles; export default styles;
/* tslint:enable */ /* tslint:enable */
@@ -1 +1 @@
{"version":3,"sources":["extensions/megaMenu/MegaMenu.module.scss.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,CAAC,uBAAuB,CAAC,CAAC;AACjC,IAAM,MAAM,GAAG;IACb,SAAS,EAAE,oBAAoB;IAC/B,QAAQ,EAAE,mBAAmB;CAC9B,CAAC;AAEF,eAAe,MAAM,CAAC;AACtB,mBAAmB","file":"extensions/megaMenu/MegaMenu.module.scss.js","sourcesContent":["/* tslint:disable */\r\nrequire('./MegaMenu.module.css');\r\nconst styles = {\r\n mmSlideIn: 'mmSlideIn_09a8e1a7',\r\n mmFadeIn: 'mmFadeIn_09a8e1a7',\r\n};\r\n\r\nexport default styles;\r\n/* tslint:enable */"],"sourceRoot":"..\\..\\..\\src"} {"version":3,"sources":["extensions/megaMenu/MegaMenu.module.scss.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,CAAC,uBAAuB,CAAC,CAAC;AACjC,IAAM,MAAM,GAAG,EACd,CAAC;AAEF,eAAe,MAAM,CAAC;AACtB,mBAAmB","file":"extensions/megaMenu/MegaMenu.module.scss.js","sourcesContent":["/* tslint:disable */\r\nrequire('./MegaMenu.module.css');\r\nconst styles = {\r\n};\r\n\r\nexport default styles;\r\n/* tslint:enable */"],"sourceRoot":"..\\..\\..\\src"}
+15 -15
View File
@@ -1,27 +1,27 @@
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base'; import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
import './MegaMenu.module.scss'; import './MegaMenu.module.scss';
export declare const UserCustomActionMegaMenuId: string; export declare const UserCustomActionMegaMenuId: string;
/**
* Properties for the MegaMenu Application Customizer
*/
export interface IMegaMenuApplicationCustomizerProperties { export interface IMegaMenuApplicationCustomizerProperties {
/** termSetName?: string;
* The name of the termset to load menu items from termSetId?: string;
*/ cacheMinutes?: number;
termSetName: string;
/**
* Optional URL to an external CSS file
*/
cssUrl?: string; cssUrl?: string;
debug?: boolean;
} }
/** A Custom Action which can be run during execution of a Client Side Application */
export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> { export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
private _topPlaceholder; private _topPlaceholder;
private _menuContainer;
private _renderer;
onInit(): Promise<void>; onInit(): Promise<void>;
private _renderPlaceHolders(); private _renderPlaceHolders();
private _updateCallback; private _renderMegaMenu(termSetIdentifier, debug?);
private _renderMegaMenu(termSetName);
private _getOrCreateContainer(id, placeholder); private _getOrCreateContainer(id, placeholder);
private _loadExternalCss(cssUrl); private _loadExternalCss(cssUrl?, debug?);
private _onDispose(); private _onDispose;
private _getTermSetIdentifier();
private _getCacheMinutes();
private _validateCssUrl(cssUrl);
private _renderStatus(message);
private _isGuid(value);
private _isDebugEnabled(debugOverride?);
} }
@@ -54,59 +54,67 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
} }
}; };
import { override } from '@microsoft/decorators'; import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base'; import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base';
import * as strings from 'MegaMenuApplicationCustomizerStrings'; import * as strings from 'MegaMenuApplicationCustomizerStrings';
import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService'; import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService';
import { MegaMenuRenderer } from './MegaMenuRenderer'; import { MegaMenuRenderer } from './MegaMenuRenderer';
// Import SCSS module import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
import './MegaMenu.module.scss'; import './MegaMenu.module.scss';
var LOG_SOURCE = 'MegaMenuApplicationCustomizer'; var LOG_SOURCE = 'MegaMenuApplicationCustomizer';
export var UserCustomActionMegaMenuId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; export var UserCustomActionMegaMenuId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
/** A Custom Action which can be run during execution of a Client Side Application */
var MegaMenuApplicationCustomizer = (function (_super) { var MegaMenuApplicationCustomizer = (function (_super) {
__extends(MegaMenuApplicationCustomizer, _super); __extends(MegaMenuApplicationCustomizer, _super);
function MegaMenuApplicationCustomizer() { function MegaMenuApplicationCustomizer() {
var _this = _super !== null && _super.apply(this, arguments) || this; var _this = _super !== null && _super.apply(this, arguments) || this;
_this._updateCallback = function (data) { _this._onDispose = function () {
_this._loadExternalCss(data.cssUrl); _this.context.placeholderProvider.changedEvent.remove(_this, _this._renderPlaceHolders);
_this._renderMegaMenu(data.termSetName); if (_this._renderer) {
_this._renderer.dispose();
_this._renderer = undefined;
}
var externalCssLink = document.getElementById('mega-menu-additional-css-34FAB720');
if (externalCssLink) {
externalCssLink.remove();
}
_this._menuContainer = undefined;
_this._topPlaceholder = undefined;
debugLog(_this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
}; };
return _this; return _this;
} }
MegaMenuApplicationCustomizer.prototype.onInit = function () { MegaMenuApplicationCustomizer.prototype.onInit = function () {
Log.info(LOG_SOURCE, "Initialized " + strings.Title); debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Initialized ' + strings.Title);
// Load external CSS if provided
if (this.properties.cssUrl) { if (this.properties.cssUrl) {
this._loadExternalCss(this.properties.cssUrl); this._loadExternalCss(this.properties.cssUrl);
} }
// Wait for the page to be ready
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders); this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
// Initial render
this._renderPlaceHolders(); this._renderPlaceHolders();
return Promise.resolve(); return Promise.resolve();
}; };
MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () { MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () {
console.log('Available placeholders: ', this.context.placeholderProvider.placeholderNames.map(function (name) { return PlaceholderName[name]; }).join(', ')); var availablePlaceholders = this.context.placeholderProvider.placeholderNames
// Handling the top placeholder .map(function (name) { return PlaceholderName[name]; })
.join(', ');
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders);
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose }); this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose });
// The extension should not assume that the expected placeholder is available.
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
console.error('The expected placeholder (Top) was not found.'); debugError(this._isDebugEnabled(), LOG_SOURCE, 'The expected placeholder (Top) was not found.');
return; return;
} }
if (!this.properties.termSetName) { var termSetIdentifier = this._getTermSetIdentifier();
console.error('TermSetName property is required but not provided.'); if (!termSetIdentifier) {
1; debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.');
this._renderStatus(strings.ConfigurationMissing);
return; return;
} }
this._renderMegaMenu(this.properties.termSetName); this._renderMegaMenu(termSetIdentifier);
} }
}; };
MegaMenuApplicationCustomizer.prototype._renderMegaMenu = function (termSetName) { MegaMenuApplicationCustomizer.prototype._renderMegaMenu = function (termSetIdentifier, debug) {
if (debug === void 0) { debug = this._isDebugEnabled(); }
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var taxonomyService, menuItems, renderer, container, error_1; var taxonomyService, menuItems, error_1;
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
@@ -116,23 +124,27 @@ var MegaMenuApplicationCustomizer = (function (_super) {
_a.label = 1; _a.label = 1;
case 1: case 1:
_a.trys.push([1, 3, , 4]); _a.trys.push([1, 3, , 4]);
taxonomyService = new TaxonomyNavigationService(this.context, termSetName); taxonomyService = new TaxonomyNavigationService(this.context, termSetIdentifier, debug, this._getCacheMinutes());
return [4 /*yield*/, taxonomyService.getMenuItems()]; return [4 /*yield*/, taxonomyService.getMenuItems()];
case 2: case 2:
menuItems = _a.sent(); menuItems = _a.sent();
renderer = new MegaMenuRenderer(this.context, menuItems, this._updateCallback); if (this._renderer) {
container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder); this._renderer.dispose();
if (container) { }
renderer.render(container); this._renderer = new MegaMenuRenderer(menuItems, debug);
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
if (this._menuContainer) {
this._renderer.render(this._menuContainer);
} }
else { else {
renderer.render(this._topPlaceholder.domElement); this._renderer.render(this._topPlaceholder.domElement);
} }
Log.info(LOG_SOURCE, "MegaMenu rendered successfully with " + menuItems.length + " top-level items"); debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
return [3 /*break*/, 4]; return [3 /*break*/, 4];
case 3: case 3:
error_1 = _a.sent(); error_1 = _a.sent();
console.error('Error rendering MegaMenu:', error_1); debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error_1);
this._renderStatus(strings.LoadError);
return [3 /*break*/, 4]; return [3 /*break*/, 4];
case 4: return [2 /*return*/]; case 4: return [2 /*return*/];
} }
@@ -142,19 +154,29 @@ var MegaMenuApplicationCustomizer = (function (_super) {
MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) { MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) {
var container = document.getElementById(id); var container = document.getElementById(id);
if (container) { if (container) {
var div = document.createElement('div'); var host = document.getElementById('MegaMenuHost');
container.appendChild(div); if (!host) {
return div; host = document.createElement('div');
host.id = 'MegaMenuHost';
container.appendChild(host);
}
return host;
} }
else {
return placeholder.domElement; return placeholder.domElement;
}
}; };
MegaMenuApplicationCustomizer.prototype._loadExternalCss = function (cssUrl) { MegaMenuApplicationCustomizer.prototype._loadExternalCss = function (cssUrl, debug) {
if (debug === void 0) { debug = this._isDebugEnabled(); }
var externalCssLinkId = 'mega-menu-additional-css-34FAB720'; var externalCssLinkId = 'mega-menu-additional-css-34FAB720';
var link = document.getElementById(externalCssLinkId); var link = document.getElementById(externalCssLinkId);
if (cssUrl && cssUrl.trim() !== '') { if (cssUrl && cssUrl.trim() !== '') {
// update previous link, if present var validatedUrl = this._validateCssUrl(cssUrl);
if (!validatedUrl) {
debugWarn(debug, LOG_SOURCE, 'Rejected unsafe external CSS URL:', cssUrl);
if (link) {
link.remove();
}
return;
}
if (!link) { if (!link) {
var head = document.getElementsByTagName('head')[0]; var head = document.getElementsByTagName('head')[0];
link = document.createElement('link'); link = document.createElement('link');
@@ -162,21 +184,69 @@ var MegaMenuApplicationCustomizer = (function (_super) {
link.type = 'text/css'; link.type = 'text/css';
link.id = externalCssLinkId; link.id = externalCssLinkId;
link.onload = function () { link.onload = function () {
Log.info(LOG_SOURCE, "External CSS loaded successfully from: " + cssUrl); debugLog(debug, LOG_SOURCE, 'External CSS loaded successfully from:', cssUrl);
}; };
link.onerror = function () { link.onerror = function () {
console.warn("Failed to load external CSS from: " + cssUrl); debugWarn(debug, LOG_SOURCE, 'Failed to load external CSS from:', cssUrl);
}; };
head.appendChild(link); head.appendChild(link);
} }
link.href = cssUrl; link.href = validatedUrl;
} }
else if (link) { else if (link) {
link.remove(); link.remove();
} }
}; };
MegaMenuApplicationCustomizer.prototype._onDispose = function () { MegaMenuApplicationCustomizer.prototype._getTermSetIdentifier = function () {
console.log('[MegaMenuApplicationCustomizer._onDispose] Disposed custom top placeholder.'); var termSetId = this.properties && this.properties.termSetId
? this.properties.termSetId.trim()
: '';
if (termSetId && this._isGuid(termSetId)) {
return termSetId;
}
return this.properties && this.properties.termSetName
? this.properties.termSetName.trim()
: '';
};
MegaMenuApplicationCustomizer.prototype._getCacheMinutes = function () {
var value = this.properties ? Number(this.properties.cacheMinutes) : 15;
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
};
MegaMenuApplicationCustomizer.prototype._validateCssUrl = function (cssUrl) {
try {
var parsed = new URL(cssUrl.trim(), window.location.origin);
var isHttp = parsed.protocol === 'http:' || parsed.protocol === 'https:';
var isSameOrigin = parsed.origin === window.location.origin;
if (!isHttp || (!isSameOrigin && parsed.protocol !== 'https:')) {
return undefined;
}
return parsed.href;
}
catch (error) {
return undefined;
}
};
MegaMenuApplicationCustomizer.prototype._renderStatus = function (message) {
if (!this._topPlaceholder) {
return;
}
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
var container = this._menuContainer || this._topPlaceholder.domElement;
container.innerHTML = '';
var status = document.createElement('div');
status.className = 'mega-menu-status';
status.setAttribute('role', 'status');
status.textContent = message;
container.appendChild(status);
};
MegaMenuApplicationCustomizer.prototype._isGuid = function (value) {
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
};
MegaMenuApplicationCustomizer.prototype._isDebugEnabled = function (debugOverride) {
if (typeof debugOverride === 'boolean') {
return debugOverride;
}
return !!(this.properties && this.properties.debug === true);
}; };
__decorate([ __decorate([
override override
File diff suppressed because one or more lines are too long
+18 -10
View File
@@ -1,17 +1,18 @@
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { IMenuItem } from '../../services/IMenuItem'; import { IMenuItem } from '../../services/IMenuItem';
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
export declare class MegaMenuRenderer { export declare class MegaMenuRenderer {
private context;
private menuItems; private menuItems;
private updateCallback; private debug;
private _settingsPanel?; private static readonly hoverOpenDelayMs;
constructor(context: ApplicationCustomizerContext, menuItems: IMenuItem[], updateCallback: (data: IMegaMenuApplicationCustomizerProperties) => void); private static readonly hoverCloseDelayMs;
private _container;
private _documentKeydownHandler;
private _documentClickHandler;
private _announcer;
constructor(menuItems: IMenuItem[], debug?: boolean);
render(container: HTMLElement): void; render(container: HTMLElement): void;
private createSettingsItem(); dispose(): void;
private openSettings();
private createTopLevelItem(item); private createTopLevelItem(item);
private createTopLevelElement(item); private createTopLevelElement(item, hasChildren, isActive, isCurrent);
private createMegaMenu(parentItem); private createMegaMenu(parentItem);
private createCategorySection(item); private createCategorySection(item);
private attachEventListeners(); private attachEventListeners();
@@ -19,10 +20,17 @@ export declare class MegaMenuRenderer {
private attachMouseEvents(heading, megaMenu); private attachMouseEvents(heading, megaMenu);
private attachFocusManagement(heading, megaMenu); private attachFocusManagement(heading, megaMenu);
private attachGlobalKeyboardNavigation(); private attachGlobalKeyboardNavigation();
private attachGlobalClickOutsideHandler();
private openMegaMenu(trigger, menu); private openMegaMenu(trigger, menu);
private closeMegaMenu(trigger, menu); private closeMegaMenu(trigger, menu);
private toggleMegaMenu(trigger, menu); private toggleMegaMenu(trigger, menu);
private closeAllMegaMenus(); private closeAllMegaMenus(exceptMenu?);
private focusFirstLink(megaMenu); private focusFirstLink(megaMenu);
private isItemActive(item);
private isCurrentUrl(url?);
private normalizePath(url?);
private getColumnCount(items?);
private joinClasses(...classNames);
private createScreenReaderAnnouncer(); private createScreenReaderAnnouncer();
private announce(message);
} }
+205 -107
View File
@@ -2,109 +2,88 @@
// tslint:disable:match-default-export-name // tslint:disable:match-default-export-name
// tslint:disable:typedef // tslint:disable:typedef
// tslint:disable:variable-name // tslint:disable:variable-name
import { SPPermission } from '@microsoft/sp-page-context'; import { debugLog } from '../../services/MegaMenuDebug';
import { MegaMenuSettingsPanel } from './MegaMenuSettings'; var LOG_SOURCE = 'MegaMenuRenderer';
var MegaMenuRenderer = (function () { var MegaMenuRenderer = (function () {
function MegaMenuRenderer(context, menuItems, updateCallback) { function MegaMenuRenderer(menuItems, debug) {
this.context = context; if (debug === void 0) { debug = false; }
this.menuItems = menuItems; this.menuItems = menuItems;
this.updateCallback = updateCallback; this.debug = debug;
} }
MegaMenuRenderer.prototype.render = function (container) { MegaMenuRenderer.prototype.render = function (container) {
var _this = this; var _this = this;
// Clear any existing content this.dispose();
this._container = container;
container.innerHTML = ''; container.innerHTML = '';
container.id = 'CustomNavigation'; container.id = 'CustomNavigation';
// Create the main nav element
var nav = document.createElement('nav'); var nav = document.createElement('nav');
nav.id = 'Mega-Menu'; nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main'; nav.className = 'mega-menu-main';
nav.setAttribute('role', 'navigation');
nav.setAttribute('aria-label', 'Hauptnavigation'); nav.setAttribute('aria-label', 'Hauptnavigation');
// Create the top-level menubar
var topLevelUl = document.createElement('ul'); var topLevelUl = document.createElement('ul');
topLevelUl.setAttribute('role', 'menubar'); topLevelUl.className = 'mega-menu-top-level';
// Process each top-level menu item
this.menuItems.forEach(function (topLevelItem) { this.menuItems.forEach(function (topLevelItem) {
var topLevelLi = _this.createTopLevelItem(topLevelItem); var topLevelLi = _this.createTopLevelItem(topLevelItem);
topLevelUl.appendChild(topLevelLi); topLevelUl.appendChild(topLevelLi);
}); });
// Only if current user has ManageWeb permissions in this website can they access the settings
if (this.context.pageContext.web.permissions.hasPermission(SPPermission.manageWeb)) {
topLevelUl.appendChild(this.createSettingsItem());
}
nav.appendChild(topLevelUl); nav.appendChild(topLevelUl);
container.appendChild(nav); container.appendChild(nav);
// Attach accessibility event listeners after rendering
this.attachEventListeners(); this.attachEventListeners();
this.createScreenReaderAnnouncer(); this.createScreenReaderAnnouncer();
}; };
MegaMenuRenderer.prototype.createSettingsItem = function () { MegaMenuRenderer.prototype.dispose = function () {
var _this = this; if (this._documentKeydownHandler) {
var li = document.createElement('li'); document.removeEventListener('keydown', this._documentKeydownHandler);
li.setAttribute('role', 'none'); this._documentKeydownHandler = undefined;
// Verwenden eines Links-ähnlichen Buttons damit Styling identisch zu Top-Level Items ist
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'menu-item-link menu-item-settings'; // reuse same base styles
btn.setAttribute('role', 'menuitem');
btn.setAttribute('tabindex', '0');
btn.setAttribute('aria-haspopup', 'false');
btn.setAttribute('aria-label', 'Einstellungen');
btn.title = 'Einstellungen';
// Nur EIN Icon: Wenn Fabric Icons geladen sind, zeigt ms-Icon das Symbol. Fallback via aria-label für Screenreader
var icon = document.createElement('span');
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
icon.setAttribute('aria-hidden', 'true');
btn.appendChild(icon);
btn.addEventListener('click', function () { return _this.openSettings(); });
btn.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
_this.openSettings();
} }
}); if (this._documentClickHandler) {
li.appendChild(btn); document.removeEventListener('click', this._documentClickHandler);
return li; this._documentClickHandler = undefined;
};
MegaMenuRenderer.prototype.openSettings = function () {
if (!this._settingsPanel) {
this._settingsPanel = new MegaMenuSettingsPanel(this.context, this.updateCallback);
} }
this._settingsPanel.open(); if (this._announcer && this._announcer.parentElement) {
this._announcer.parentElement.removeChild(this._announcer);
}
this._announcer = undefined;
if (this._container) {
this._container.innerHTML = '';
}
this._container = undefined;
}; };
// Panel logic moved to MegaMenuSettingsPanel
MegaMenuRenderer.prototype.createTopLevelItem = function (item) { MegaMenuRenderer.prototype.createTopLevelItem = function (item) {
var li = document.createElement('li'); var li = document.createElement('li');
li.setAttribute('role', 'none'); var hasChildren = item.hasChildren() && item.items && item.items.length > 0;
// Create the top-level element (link or span) var isActive = this.isItemActive(item);
var topElement = this.createTopLevelElement(item); var isCurrent = this.isCurrentUrl(item.url);
li.className = this.joinClasses('mega-menu-top-item', hasChildren ? 'has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
var topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent);
li.appendChild(topElement); li.appendChild(topElement);
// If the item has children, create the mega menu if (hasChildren) {
if (item.hasChildren() && item.items && item.items.length > 0) {
var megaMenu = this.createMegaMenu(item); var megaMenu = this.createMegaMenu(item);
li.appendChild(megaMenu); li.appendChild(megaMenu);
} }
return li; return li;
}; };
MegaMenuRenderer.prototype.createTopLevelElement = function (item) { MegaMenuRenderer.prototype.createTopLevelElement = function (item, hasChildren, isActive, isCurrent) {
var element; var element;
if (item.url) { if (item.url) {
// Create as link
element = document.createElement('a'); element = document.createElement('a');
element.href = item.url; element.href = item.url;
element.className = 'menu-item-link'; element.className = 'menu-item-link';
} }
else { else {
// Create as span (no link)
element = document.createElement('span'); element = document.createElement('span');
element.className = 'menu-item-text'; element.className = 'menu-item-text';
element.setAttribute('tabindex', '0'); element.setAttribute('tabindex', '0');
} }
element.setAttribute('role', 'menuitem'); element.textContent = item.label;
element.className = this.joinClasses(element.className, hasChildren ? 'menu-item-has-children' : undefined, isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
if (hasChildren) {
element.setAttribute('aria-haspopup', 'true'); element.setAttribute('aria-haspopup', 'true');
element.setAttribute('aria-expanded', 'false'); element.setAttribute('aria-expanded', 'false');
element.textContent = item.label; }
if (isCurrent && item.url) {
element.setAttribute('aria-current', 'page');
}
if (item.hoverText) { if (item.hoverText) {
element.title = item.hoverText; element.title = item.hoverText;
} }
@@ -114,11 +93,11 @@ var MegaMenuRenderer = (function () {
var _this = this; var _this = this;
var megaMenuDiv = document.createElement('div'); var megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu'; megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('role', 'menu'); megaMenuDiv.setAttribute('aria-expanded', 'false');
megaMenuDiv.setAttribute('aria-label', parentItem.label + " Unterkategorien"); megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien');
var gridDiv = document.createElement('div'); var gridDiv = document.createElement('div');
gridDiv.className = 'mega-menu-grid'; gridDiv.className = 'mega-menu-grid';
// Process second-level items (categories) gridDiv.style.setProperty('--megaMenuColumnCount', this.getColumnCount(parentItem.items));
if (parentItem.items) { if (parentItem.items) {
parentItem.items.forEach(function (secondLevelItem) { parentItem.items.forEach(function (secondLevelItem) {
var categoryDiv = _this.createCategorySection(secondLevelItem); var categoryDiv = _this.createCategorySection(secondLevelItem);
@@ -129,38 +108,51 @@ var MegaMenuRenderer = (function () {
return megaMenuDiv; return megaMenuDiv;
}; };
MegaMenuRenderer.prototype.createCategorySection = function (item) { MegaMenuRenderer.prototype.createCategorySection = function (item) {
var _this = this;
var categoryDiv = document.createElement('div'); var categoryDiv = document.createElement('div');
categoryDiv.className = 'mega-menu-category'; var isActive = this.isItemActive(item);
// Create the category header (h3) var isCurrent = this.isCurrentUrl(item.url);
categoryDiv.className = this.joinClasses('mega-menu-category', isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
var h3 = document.createElement('h3'); var h3 = document.createElement('h3');
h3.className = 'mega-menu-category-title';
if (item.url) { if (item.url) {
// Category header as link
var link = document.createElement('a'); var link = document.createElement('a');
link.href = item.url; link.href = item.url;
link.textContent = item.label; link.textContent = item.label;
link.className = this.joinClasses(isActive ? 'is-active' : undefined, isCurrent ? 'is-current' : undefined);
if (isCurrent) {
link.setAttribute('aria-current', 'page');
}
if (item.hoverText) { if (item.hoverText) {
link.title = item.hoverText; link.title = item.hoverText;
} }
h3.appendChild(link); h3.appendChild(link);
} }
else { else {
// Category header as span (no link)
var span = document.createElement('span'); var span = document.createElement('span');
span.textContent = item.label; span.textContent = item.label;
span.className = this.joinClasses(isActive ? 'is-active' : undefined);
if (item.hoverText) { if (item.hoverText) {
span.title = item.hoverText; span.title = item.hoverText;
} }
h3.appendChild(span); h3.appendChild(span);
} }
categoryDiv.appendChild(h3); categoryDiv.appendChild(h3);
// Create the third-level items list if they exist
if (item.hasChildren() && item.items && item.items.length > 0) { if (item.hasChildren() && item.items && item.items.length > 0) {
var ul_1 = document.createElement('ul'); var ul_1 = document.createElement('ul');
ul_1.className = 'mega-menu-links';
item.items.forEach(function (thirdLevelItem) { item.items.forEach(function (thirdLevelItem) {
var li = document.createElement('li'); var li = document.createElement('li');
var link = document.createElement('a'); var link = document.createElement('a');
var isLinkActive = _this.isItemActive(thirdLevelItem);
var isLinkCurrent = _this.isCurrentUrl(thirdLevelItem.url);
li.className = _this.joinClasses(isLinkActive ? 'is-active' : undefined, isLinkCurrent ? 'is-current' : undefined);
link.href = thirdLevelItem.url || '#'; link.href = thirdLevelItem.url || '#';
link.textContent = thirdLevelItem.label; link.textContent = thirdLevelItem.label;
link.className = _this.joinClasses(isLinkActive ? 'is-active' : undefined, isLinkCurrent ? 'is-current' : undefined);
if (isLinkCurrent && thirdLevelItem.url) {
link.setAttribute('aria-current', 'page');
}
if (thirdLevelItem.hoverText) { if (thirdLevelItem.hoverText) {
link.title = thirdLevelItem.hoverText; link.title = thirdLevelItem.hoverText;
} }
@@ -172,7 +164,7 @@ var MegaMenuRenderer = (function () {
return categoryDiv; return categoryDiv;
}; };
MegaMenuRenderer.prototype.attachEventListeners = function () { MegaMenuRenderer.prototype.attachEventListeners = function () {
var headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span[role="menuitem"]'); var headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > .menu-item-text');
for (var i = 0; i < headings.length; i++) { for (var i = 0; i < headings.length; i++) {
var heading = headings[i]; var heading = headings[i];
var megaMenu = heading.nextElementSibling; var megaMenu = heading.nextElementSibling;
@@ -182,86 +174,101 @@ var MegaMenuRenderer = (function () {
this.attachFocusManagement(heading, megaMenu); this.attachFocusManagement(heading, megaMenu);
} }
} }
// Global keyboard navigation
this.attachGlobalKeyboardNavigation(); this.attachGlobalKeyboardNavigation();
this.attachGlobalClickOutsideHandler();
}; };
MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) { MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) {
var _this = this; var _this = this;
heading.addEventListener('keydown', function (e) { heading.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
// Enter: Navigation (nur bei Links ohne Mega-Menu-Override)
if (heading.tagName === 'A') { if (heading.tagName === 'A') {
// Lasse normale Link-Navigation zu (KEIN preventDefault!)
return; return;
} }
else {
// Bei span: Toggle Menu
e.preventDefault(); e.preventDefault();
_this.toggleMegaMenu(heading, megaMenu); _this.toggleMegaMenu(heading, megaMenu);
} }
}
else if (e.key === ' ') { else if (e.key === ' ') {
// Space: Toggle Dropdown (immer)
e.preventDefault(); e.preventDefault();
_this.toggleMegaMenu(heading, megaMenu); _this.toggleMegaMenu(heading, megaMenu);
} }
else if (e.key === 'ArrowDown') { else if (e.key === 'ArrowDown') {
// Pfeil runter: Menü öffnen + erster Link
e.preventDefault(); e.preventDefault();
_this.openMegaMenu(heading, megaMenu); _this.openMegaMenu(heading, megaMenu);
_this.focusFirstLink(megaMenu); _this.focusFirstLink(megaMenu);
} }
else if (e.key === 'ArrowUp') { else if (e.key === 'ArrowUp') {
// Pfeil hoch: Menü schließen
e.preventDefault(); e.preventDefault();
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
} }
else if (e.key === 'Escape') { else if (e.key === 'Escape') {
// Escape: Menü schließen
e.preventDefault(); e.preventDefault();
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
heading.focus(); heading.focus();
} }
}); });
// Click Event für Top-Level Links - NORMALE Navigation erlauben
if (heading.tagName === 'A') { if (heading.tagName === 'A') {
heading.addEventListener('click', function (e) { heading.addEventListener('click', function () {
// Links sollen normal navigieren, nicht das Mega-Menu toglen debugLog(_this.debug, LOG_SOURCE, 'Link clicked:', heading.href);
// Wenn der User das Menu öffnen will, soll er Space oder Pfeil↓ nutzen
console.log('Link geklickt:', heading.href);
// KEIN preventDefault() - normale Link-Navigation
}); });
} }
// Focus Management - KEIN automatisches Öffnen oder Schließen
heading.addEventListener('focus', function () { heading.addEventListener('focus', function () {
console.log('Focus auf:', heading.textContent); debugLog(_this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
// Einfach nur fokussiert - keine automatischen Aktionen!
}); });
}; };
MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) { MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) {
var _this = this; var _this = this;
var parentLi = heading.parentElement; var parentLi = heading.parentElement;
var openTimeout;
var closeTimeout;
var clearOpenTimeout = function () {
if (typeof openTimeout === 'number') {
window.clearTimeout(openTimeout);
openTimeout = undefined;
}
};
var clearCloseTimeout = function () {
if (typeof closeTimeout === 'number') {
window.clearTimeout(closeTimeout);
closeTimeout = undefined;
}
};
parentLi.addEventListener('mouseenter', function () { parentLi.addEventListener('mouseenter', function () {
clearCloseTimeout();
if (megaMenu.classList.contains('js-open')) {
return;
}
clearOpenTimeout();
openTimeout = window.setTimeout(function () {
_this.openMegaMenu(heading, megaMenu); _this.openMegaMenu(heading, megaMenu);
openTimeout = undefined;
}, MegaMenuRenderer.hoverOpenDelayMs);
}); });
parentLi.addEventListener('mouseleave', function () { parentLi.addEventListener('mouseleave', function () {
clearOpenTimeout();
clearCloseTimeout();
closeTimeout = window.setTimeout(function () {
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
closeTimeout = undefined;
}, MegaMenuRenderer.hoverCloseDelayMs);
});
heading.addEventListener('focus', function () {
clearCloseTimeout();
});
megaMenu.addEventListener('mouseenter', function () {
clearCloseTimeout();
}); });
}; };
MegaMenuRenderer.prototype.attachFocusManagement = function (heading, megaMenu) { MegaMenuRenderer.prototype.attachFocusManagement = function (heading, megaMenu) {
var _this = this; var _this = this;
// Focus-Verlust-Behandlung - vereinfacht megaMenu.addEventListener('focusout', function () {
megaMenu.addEventListener('focusout', function (e) {
// Kurz warten, um zu prüfen ob Focus innerhalb des Mega-Menus bleibt
setTimeout(function () { setTimeout(function () {
var focusedElement = document.activeElement; var focusedElement = document.activeElement;
var isInsideThisMenu = megaMenu.contains(focusedElement); var isInsideThisMenu = megaMenu.contains(focusedElement);
var isOnThisTrigger = focusedElement === heading; var isOnThisTrigger = focusedElement === heading;
var isInAnyMegaMenu = focusedElement.closest('.mega-menu'); var isInAnyMegaMenu = focusedElement.closest('.mega-menu');
var isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span'); var isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
// Nur schließen wenn Focus komplett außerhalb der Navigation ist
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) { if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
console.log('Schließe Menu wegen Focus-Verlust'); debugLog(_this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.');
_this.closeMegaMenu(heading, megaMenu); _this.closeMegaMenu(heading, megaMenu);
} }
}, 150); }, 150);
@@ -269,7 +276,7 @@ var MegaMenuRenderer = (function () {
}; };
MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () { MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () {
var _this = this; var _this = this;
document.addEventListener('keydown', function (e) { this._documentKeydownHandler = function (e) {
var activeElement = document.activeElement; var activeElement = document.activeElement;
if (e.key === 'Escape') { if (e.key === 'Escape') {
var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]'); var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]');
@@ -279,16 +286,13 @@ var MegaMenuRenderer = (function () {
triggerLink.focus(); triggerLink.focus();
} }
} }
// Tab-Navigation: Nur bei spezifischen Übergängen eingreifen
if (e.key === 'Tab') { if (e.key === 'Tab') {
// Von Top-Level zum ersten Link im offenen Menu
if (!e.shiftKey) { if (!e.shiftKey) {
var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span'); var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
if (currentTopLevel) { if (currentTopLevel) {
var parentLi = currentTopLevel.closest('li'); var parentLi = currentTopLevel.closest('li');
var megaMenu = parentLi.querySelector('.mega-menu.js-open'); var megaMenu = parentLi.querySelector('.mega-menu.js-open');
if (megaMenu) { if (megaMenu) {
// Nur eingreifen wenn wir vom Trigger weg-tabben
e.preventDefault(); e.preventDefault();
var firstLink = megaMenu.querySelector('a'); var firstLink = megaMenu.querySelector('a');
if (firstLink) { if (firstLink) {
@@ -298,13 +302,11 @@ var MegaMenuRenderer = (function () {
} }
} }
} }
// Shift+Tab: Vom ersten Link im Menu zurück zum Trigger
if (e.shiftKey) { if (e.shiftKey) {
var megaMenu = activeElement.closest('.mega-menu'); var megaMenu = activeElement.closest('.mega-menu');
if (megaMenu && megaMenu.classList.contains('js-open')) { if (megaMenu && megaMenu.classList.contains('js-open')) {
var allLinksInMenu = megaMenu.querySelectorAll('a'); var allLinksInMenu = megaMenu.querySelectorAll('a');
var firstLinkInMenu = allLinksInMenu[0]; var firstLinkInMenu = allLinksInMenu[0];
// Nur eingreifen wenn wir beim ersten Link sind
if (activeElement === firstLinkInMenu) { if (activeElement === firstLinkInMenu) {
e.preventDefault(); e.preventDefault();
var triggerElement = megaMenu.previousElementSibling; var triggerElement = megaMenu.previousElementSibling;
@@ -313,21 +315,41 @@ var MegaMenuRenderer = (function () {
} }
} }
} }
// Ansonsten: Normale Tab-Navigation nicht unterbrechen!
} }
}); };
document.addEventListener('keydown', this._documentKeydownHandler);
};
MegaMenuRenderer.prototype.attachGlobalClickOutsideHandler = function () {
var _this = this;
this._documentClickHandler = function (e) {
var eventTarget = e.target;
var isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' &&
eventTarget.closest('#CustomNavigation'));
if (isInsideNavigation) {
return;
}
var openMenu = document.querySelector('.mega-menu.js-open');
if (openMenu) {
debugLog(_this.debug, LOG_SOURCE, 'Closing menu because of outside click.');
_this.closeAllMegaMenus();
}
};
document.addEventListener('click', this._documentClickHandler);
}; };
MegaMenuRenderer.prototype.openMegaMenu = function (trigger, menu) { MegaMenuRenderer.prototype.openMegaMenu = function (trigger, menu) {
this.closeAllMegaMenus(menu);
trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-expanded', 'true'); menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open'); menu.classList.add('js-open');
console.log('Menu geöffnet:', trigger.textContent); this.announce((trigger.textContent || '') + ' geöffnet');
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
}; };
MegaMenuRenderer.prototype.closeMegaMenu = function (trigger, menu) { MegaMenuRenderer.prototype.closeMegaMenu = function (trigger, menu) {
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-expanded', 'false'); menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open'); menu.classList.remove('js-open');
console.log('Menu geschlossen:', trigger.textContent); this.announce((trigger.textContent || '') + ' geschlossen');
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
}; };
MegaMenuRenderer.prototype.toggleMegaMenu = function (trigger, menu) { MegaMenuRenderer.prototype.toggleMegaMenu = function (trigger, menu) {
var isOpen = trigger.getAttribute('aria-expanded') === 'true'; var isOpen = trigger.getAttribute('aria-expanded') === 'true';
@@ -335,36 +357,112 @@ var MegaMenuRenderer = (function () {
this.closeMegaMenu(trigger, menu); this.closeMegaMenu(trigger, menu);
} }
else { else {
this.closeAllMegaMenus();
this.openMegaMenu(trigger, menu); this.openMegaMenu(trigger, menu);
} }
}; };
MegaMenuRenderer.prototype.closeAllMegaMenus = function () { MegaMenuRenderer.prototype.closeAllMegaMenus = function (exceptMenu) {
var allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]'); var allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]');
for (var i = 0; i < allTriggers.length; i++) { for (var i = 0; i < allTriggers.length; i++) {
var trigger = allTriggers[i]; var trigger = allTriggers[i];
var menu = trigger.nextElementSibling; var menu = trigger.nextElementSibling;
if (menu) { if (menu && menu !== exceptMenu) {
this.closeMegaMenu(trigger, menu); this.closeMegaMenu(trigger, menu);
} }
} }
}; };
// removed unused closeOtherMegaMenus (was previously declared but not used)
MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) { MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) {
var firstLink = megaMenu.querySelector('a'); var firstLink = megaMenu.querySelector('a');
if (firstLink) { if (firstLink) {
firstLink.focus(); firstLink.focus();
} }
}; };
MegaMenuRenderer.prototype.isItemActive = function (item) {
if (this.isCurrentUrl(item.url)) {
return true;
}
if (!item.items || item.items.length === 0) {
return false;
}
for (var i = 0; i < item.items.length; i++) {
if (this.isItemActive(item.items[i])) {
return true;
}
}
return false;
};
MegaMenuRenderer.prototype.isCurrentUrl = function (url) {
var currentPath = this.normalizePath(window.location.href);
var itemPath = this.normalizePath(url);
if (!currentPath || !itemPath) {
return false;
}
if (itemPath === '/') {
return currentPath === '/';
}
if (currentPath === itemPath) {
return true;
}
return currentPath.indexOf(itemPath + '/') === 0;
};
MegaMenuRenderer.prototype.normalizePath = function (url) {
if (!url) {
return '';
}
try {
var parsedUrl = new URL(url, window.location.origin);
var pathname = (parsedUrl.pathname || '/').replace(/\/+$/, '');
return (pathname || '/').toLowerCase();
}
catch (error) {
return '';
}
};
MegaMenuRenderer.prototype.getColumnCount = function (items) {
var itemCount = items ? items.length : 0;
var columnCount = Math.min(Math.max(itemCount, 1), 4);
return columnCount.toString();
};
MegaMenuRenderer.prototype.joinClasses = function () {
var classNames = [];
for (var _i = 0; _i < arguments.length; _i++) {
classNames[_i] = arguments[_i];
}
var filteredClassNames = [];
for (var i = 0; i < classNames.length; i++) {
if (classNames[i]) {
filteredClassNames.push(classNames[i]);
}
}
return filteredClassNames.join(' ');
};
MegaMenuRenderer.prototype.createScreenReaderAnnouncer = function () { MegaMenuRenderer.prototype.createScreenReaderAnnouncer = function () {
// Screenreader-Ankündigungen var existingAnnouncer = document.getElementById('mega-menu-sr-announcer');
if (existingAnnouncer) {
this._announcer = existingAnnouncer;
return;
}
var srAnnouncer = document.createElement('div'); var srAnnouncer = document.createElement('div');
srAnnouncer.id = 'mega-menu-sr-announcer';
srAnnouncer.setAttribute('aria-live', 'polite'); srAnnouncer.setAttribute('aria-live', 'polite');
srAnnouncer.setAttribute('aria-atomic', 'true'); srAnnouncer.setAttribute('aria-atomic', 'true');
srAnnouncer.className = 'sr-only'; srAnnouncer.className = 'sr-only';
document.body.appendChild(srAnnouncer); document.body.appendChild(srAnnouncer);
console.log('Screenreader-Ankündigungen sind jetzt bereit'); this._announcer = srAnnouncer;
debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.');
}; };
MegaMenuRenderer.prototype.announce = function (message) {
var _this = this;
if (this._announcer) {
this._announcer.textContent = '';
window.setTimeout(function () {
if (_this._announcer) {
_this._announcer.textContent = message;
}
}, 20);
}
};
MegaMenuRenderer.hoverOpenDelayMs = 140;
MegaMenuRenderer.hoverCloseDelayMs = 180;
return MegaMenuRenderer; return MegaMenuRenderer;
}()); }());
export { MegaMenuRenderer }; export { MegaMenuRenderer };
File diff suppressed because one or more lines are too long
-25
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
+3 -1
View File
@@ -1,5 +1,7 @@
define([], function() { define([], function() {
return { return {
"Title": "MegaMenuApplicationCustomizer" "Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Navigation is not configured.",
"LoadError": "Navigation could not be loaded."
} }
}); });
+1
View File
@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
hideDeprecatedTags: boolean; hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean; hideTagsNotAvailableForTagging: boolean;
anchorId: string; anchorId: string;
cacheMinutes: number;
} }
+7 -3
View File
@@ -8,12 +8,13 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export default class SPTermStorePickerService { export default class SPTermStorePickerService {
private props; private props;
private context; private context;
private debug;
private clientServiceUrl; private clientServiceUrl;
private suggestionServiceUrl; private suggestionServiceUrl;
/** /**
* Service constructor * Service constructor
*/ */
constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext); constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext, debug?: boolean);
getTermLabels(termId: string): Promise<string[]>; getTermLabels(termId: string): Promise<string[]>;
/** /**
* Gets the collection of term stores in the current SharePoint env * Gets the collection of term stores in the current SharePoint env
@@ -27,7 +28,7 @@ export default class SPTermStorePickerService {
* Retrieve all terms for the given term set * Retrieve all terms for the given term set
* @param termset * @param termset
*/ */
getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean): Promise<ITermSet>; getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean, cacheMinutes?: number): Promise<ITermSet>;
/** /**
* Retrieve all terms that starts with the searchText * Retrieve all terms that starts with the searchText
* @param searchText * @param searchText
@@ -49,7 +50,10 @@ export default class SPTermStorePickerService {
* @param termset * @param termset
*/ */
private getTermSetId(termstore, termsetName); private getTermSetId(termstore, termsetName);
private getTermsById(termId, useSessionStorage?); private getCachedTermSet(termId, useSessionStorage?);
private getCachedPickerTerms(termId, useSessionStorage?);
private normalizeCacheMinutes(value);
private ensureSuccessfulResponse(response, operation);
private searchTermsBySearchText(terms, searchText); private searchTermsBySearchText(terms, searchText);
/** /**
* Searches terms for the given term set * Searches terms for the given term set
+76 -18
View File
@@ -39,7 +39,10 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
}; };
import { SPHttpClient } from '@microsoft/sp-http'; import { SPHttpClient } from '@microsoft/sp-http';
import { findIndex } from '@microsoft/sp-lodash-subset'; import { findIndex } from '@microsoft/sp-lodash-subset';
import { debugError } from './MegaMenuDebug';
var EmptyGuid = '00000000-0000-0000-0000-000000000000'; var EmptyGuid = '00000000-0000-0000-0000-000000000000';
var TermCachePrefix = 'MegaMenu:TermSet:';
var PickerCachePrefix = 'MegaMenu:PickerTerms:';
/** /**
* Service implementation to manage term stores in SharePoint * Service implementation to manage term stores in SharePoint
*/ */
@@ -47,9 +50,11 @@ var SPTermStorePickerService = (function () {
/** /**
* Service constructor * Service constructor
*/ */
function SPTermStorePickerService(props, context) { function SPTermStorePickerService(props, context, debug) {
if (debug === void 0) { debug = false; }
this.props = props; this.props = props;
this.context = context; this.context = context;
this.debug = debug;
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery'; this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions'; this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
} }
@@ -63,7 +68,7 @@ var SPTermStorePickerService = (function () {
_a.label = 1; _a.label = 1;
case 1: case 1:
_a.trys.push([1, 4, , 5]); _a.trys.push([1, 4, , 5]);
data = "<Request AddExpandoFieldTypeSuffix=\"true\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\".NET Library\" xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\"><Actions><ObjectPath Id=\"8\" ObjectPathId=\"7\" /><ObjectIdentityQuery Id=\"9\" ObjectPathId=\"7\" /><ObjectPath Id=\"11\" ObjectPathId=\"10\" /><ObjectIdentityQuery Id=\"12\" ObjectPathId=\"10\" /><ObjectPath Id=\"14\" ObjectPathId=\"13\" /><ObjectIdentityQuery Id=\"15\" ObjectPathId=\"13\" /><Query Id=\"16\" ObjectPathId=\"13\"><Query SelectAllProperties=\"false\"><Properties><Property Name=\"Labels\" SelectAll=\"true\"><Query SelectAllProperties=\"false\"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id=\"7\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"10\" ParentId=\"7\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"13\" ParentId=\"10\" Name=\"GetTerm\"><Parameters><Parameter Type=\"Guid\">" + termId + "</Parameter></Parameters></Method></ObjectPaths></Request>"; data = "<Request AddExpandoFieldTypeSuffix=\"true\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\".NET Library\" xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\"><Actions><ObjectPath Id=\"8\" ObjectPathId=\"7\" /><ObjectIdentityQuery Id=\"9\" ObjectPathId=\"7\" /><ObjectPath Id=\"11\" ObjectPathId=\"10\" /><ObjectIdentityQuery Id=\"12\" ObjectPathId=\"10\" /><ObjectPath Id=\"14\" ObjectPathId=\"13\" /><ObjectIdentityQuery Id=\"15\" ObjectPathId=\"13\" /><Query Id=\"16\" ObjectPathId=\"13\"><Query SelectAllProperties=\"false\"><Properties><Property Name=\"Labels\" SelectAll=\"true\"><Query SelectAllProperties=\"false\"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id=\"7\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"10\" ParentId=\"7\" Name=\"GetDefaultSiteCollectionTermStore\" /><Method Id=\"13\" ParentId=\"10\" Name=\"GetTerm\"><Parameters><Parameter Type=\"Guid\">" + termId + "</Parameter></Parameters></Method></ObjectPaths></Request>";
reqHeaders = new Headers(); reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml'); reqHeaders.append('content-type', 'application/xml');
@@ -74,6 +79,7 @@ var SPTermStorePickerService = (function () {
return [4 /*yield*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions)]; return [4 /*yield*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions)];
case 2: case 2:
callResult = _a.sent(); callResult = _a.sent();
this.ensureSuccessfulResponse(callResult, 'Term labels');
return [4 /*yield*/, callResult.json()]; return [4 /*yield*/, callResult.json()];
case 3: case 3:
jsonResult = _a.sent(); jsonResult = _a.sent();
@@ -85,7 +91,7 @@ var SPTermStorePickerService = (function () {
case 4: case 4:
error_1 = _a.sent(); error_1 = _a.sent();
result = null; result = null;
console.log(error_1.message); debugError(this.debug, 'SPTermStorePickerService', 'Error reading term labels.', error_1);
return [3 /*break*/, 5]; return [3 /*break*/, 5];
case 5: return [2 /*return*/, result]; case 5: return [2 /*return*/, result];
} }
@@ -107,6 +113,7 @@ var SPTermStorePickerService = (function () {
body: data body: data
}; };
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
_this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
return serviceResponse.json().then(function (serviceJSONResponse) { return serviceResponse.json().then(function (serviceJSONResponse) {
// Construct results // Construct results
var termStoreResult = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermStore'; }); var termStoreResult = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermStore'; });
@@ -160,8 +167,9 @@ var SPTermStorePickerService = (function () {
* Retrieve all terms for the given term set * Retrieve all terms for the given term set
* @param termset * @param termset
*/ */
SPTermStorePickerService.prototype.getAllTerms = function (termset, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage) { SPTermStorePickerService.prototype.getAllTerms = function (termset, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage, cacheMinutes) {
if (useSessionStorage === void 0) { useSessionStorage = true; } if (useSessionStorage === void 0) { useSessionStorage = true; }
if (cacheMinutes === void 0) { cacheMinutes = 15; }
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var _this = this; var _this = this;
var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions; var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions;
@@ -182,11 +190,11 @@ var SPTermStorePickerService = (function () {
} }
_a.label = 2; _a.label = 2;
case 2: case 2:
childTerms = this.getTermsById(termsetId, useSessionStorage); childTerms = this.getCachedTermSet(termsetId, useSessionStorage);
if (childTerms) { if (childTerms) {
return [2 /*return*/, childTerms]; return [2 /*return*/, childTerms];
} }
data = "<Request xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\"Javascript Library\"><Actions><ObjectPath Id=\"1\" ObjectPathId=\"0\" /><ObjectIdentityQuery Id=\"2\" ObjectPathId=\"0\" /><ObjectPath Id=\"4\" ObjectPathId=\"3\" /><ObjectIdentityQuery Id=\"5\" ObjectPathId=\"3\" /><ObjectPath Id=\"7\" ObjectPathId=\"6\" /><ObjectIdentityQuery Id=\"8\" ObjectPathId=\"6\" /><ObjectPath Id=\"10\" ObjectPathId=\"9\" /><Query Id=\"11\" ObjectPathId=\"6\"><Query SelectAllProperties=\"true\"><Properties /></Query></Query><Query Id=\"12\" ObjectPathId=\"9\"><Query SelectAllProperties=\"false\"><Properties /></Query><ChildItemQuery SelectAllProperties=\"false\"><Properties><Property Name=\"IsRoot\" SelectAll=\"true\" /><Property Name=\"Labels\" SelectAll=\"true\" /><Property Name=\"TermsCount\" SelectAll=\"true\" /><Property Name=\"CustomSortOrder\" SelectAll=\"true\" /><Property Name=\"Id\" SelectAll=\"true\" /><Property Name=\"Name\" SelectAll=\"true\" /><Property Name=\"PathOfTerm\" SelectAll=\"true\" /><Property Name=\"Parent\" SelectAll=\"true\" /><Property Name=\"LocalCustomProperties\" SelectAll=\"true\" /><Property Name=\"IsDeprecated\" ScalarProperty=\"true\" /><Property Name=\"IsAvailableForTagging\" ScalarProperty=\"true\" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id=\"0\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"3\" ParentId=\"0\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"6\" ParentId=\"3\" Name=\"GetTermSet\"><Parameters><Parameter Type=\"Guid\">" + termsetId + "</Parameter></Parameters></Method><Method Id=\"9\" ParentId=\"6\" Name=\"GetAllTerms\" /></ObjectPaths></Request>"; data = "<Request xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\"Javascript Library\"><Actions><ObjectPath Id=\"1\" ObjectPathId=\"0\" /><ObjectIdentityQuery Id=\"2\" ObjectPathId=\"0\" /><ObjectPath Id=\"4\" ObjectPathId=\"3\" /><ObjectIdentityQuery Id=\"5\" ObjectPathId=\"3\" /><ObjectPath Id=\"7\" ObjectPathId=\"6\" /><ObjectIdentityQuery Id=\"8\" ObjectPathId=\"6\" /><ObjectPath Id=\"10\" ObjectPathId=\"9\" /><Query Id=\"11\" ObjectPathId=\"6\"><Query SelectAllProperties=\"true\"><Properties /></Query></Query><Query Id=\"12\" ObjectPathId=\"9\"><Query SelectAllProperties=\"false\"><Properties /></Query><ChildItemQuery SelectAllProperties=\"false\"><Properties><Property Name=\"IsRoot\" SelectAll=\"true\" /><Property Name=\"Labels\" SelectAll=\"true\" /><Property Name=\"TermsCount\" SelectAll=\"true\" /><Property Name=\"CustomSortOrder\" SelectAll=\"true\" /><Property Name=\"Id\" SelectAll=\"true\" /><Property Name=\"Name\" SelectAll=\"true\" /><Property Name=\"PathOfTerm\" SelectAll=\"true\" /><Property Name=\"Parent\" SelectAll=\"true\" /><Property Name=\"LocalCustomProperties\" SelectAll=\"true\" /><Property Name=\"IsDeprecated\" ScalarProperty=\"true\" /><Property Name=\"IsAvailableForTagging\" ScalarProperty=\"true\" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id=\"0\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"3\" ParentId=\"0\" Name=\"GetDefaultSiteCollectionTermStore\" /><Method Id=\"6\" ParentId=\"3\" Name=\"GetTermSet\"><Parameters><Parameter Type=\"Guid\">" + termsetId + "</Parameter></Parameters></Method><Method Id=\"9\" ParentId=\"6\" Name=\"GetAllTerms\" /></ObjectPaths></Request>";
reqHeaders = new Headers(); reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
reqHeaders.append('content-type', 'application/xml'); reqHeaders.append('content-type', 'application/xml');
@@ -195,7 +203,11 @@ var SPTermStorePickerService = (function () {
body: data body: data
}; };
return [2 /*return*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) { return [2 /*return*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
_this.ensureSuccessfulResponse(serviceResponse, 'Terms');
return serviceResponse.json().then(function (serviceJSONResponse) { return serviceResponse.json().then(function (serviceJSONResponse) {
if (!Array.isArray(serviceJSONResponse)) {
throw new Error('The taxonomy service returned an unexpected response.');
}
var termStoreResultTermSets = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermSet'; }); var termStoreResultTermSets = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermSet'; });
if (termStoreResultTermSets.length > 0) { if (termStoreResultTermSets.length > 0) {
var termStoreResultTermSet_1 = termStoreResultTermSets[0]; var termStoreResultTermSet_1 = termStoreResultTermSets[0];
@@ -236,7 +248,12 @@ var SPTermStorePickerService = (function () {
} }
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet_1)); var ttlMinutes = _this.normalizeCacheMinutes(cacheMinutes);
var cacheEntry = {
expiresAt: Date.now() + ttlMinutes * 60 * 1000,
value: termStoreResultTermSet_1
};
window.sessionStorage.setItem(TermCachePrefix + termsetId, JSON.stringify(cacheEntry));
} }
} }
catch (error) { catch (error) {
@@ -265,7 +282,7 @@ var SPTermStorePickerService = (function () {
switch (_b.label) { switch (_b.label) {
case 0: case 0:
useSessionStorage = this.props.useSessionStorage; useSessionStorage = this.props.useSessionStorage;
childTerms = this.getTermsById(termId, useSessionStorage); childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
if (!childTerms) return [3 /*break*/, 1]; if (!childTerms) return [3 /*break*/, 1];
return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)]; return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)];
case 1: case 1:
@@ -289,16 +306,16 @@ var SPTermStorePickerService = (function () {
if (useSessionStorage === void 0) { useSessionStorage = true; } if (useSessionStorage === void 0) { useSessionStorage = true; }
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var _this = this; var _this = this;
var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms; var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms, cacheEntry;
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
returnTerms = []; returnTerms = [];
childTerms = this.getTermsById(anchorId, useSessionStorage); childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
if (childTerms) { if (childTerms) {
return [2 /*return*/, childTerms]; return [2 /*return*/, childTerms];
} }
return [4 /*yield*/, this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging)]; return [4 /*yield*/, this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage, this.props.cacheMinutes)];
case 1: case 1:
termSet = _a.sent(); termSet = _a.sent();
terms = termSet.Terms; terms = termSet.Terms;
@@ -312,7 +329,11 @@ var SPTermStorePickerService = (function () {
}); });
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms)); cacheEntry = {
expiresAt: Date.now() + this.normalizeCacheMinutes(this.props.cacheMinutes) * 60 * 1000,
value: returnTerms
};
window.sessionStorage.setItem(PickerCachePrefix + anchorId, JSON.stringify(cacheEntry));
} }
} }
catch (error) { catch (error) {
@@ -370,17 +391,21 @@ var SPTermStorePickerService = (function () {
} }
return null; return null;
}; };
SPTermStorePickerService.prototype.getTermsById = function (termId, useSessionStorage) { SPTermStorePickerService.prototype.getCachedTermSet = function (termId, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; } if (useSessionStorage === void 0) { useSessionStorage = true; }
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
var terms = window.sessionStorage.getItem(termId); var key = TermCachePrefix + termId;
if (terms) { var terms = window.sessionStorage.getItem(key);
return JSON.parse(terms); if (!terms) {
}
else {
return null; return null;
} }
var cacheEntry = JSON.parse(terms);
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} }
else { else {
return null; return null;
@@ -390,6 +415,39 @@ var SPTermStorePickerService = (function () {
return null; return null;
} }
}; };
SPTermStorePickerService.prototype.getCachedPickerTerms = function (termId, useSessionStorage) {
if (useSessionStorage === void 0) { useSessionStorage = true; }
try {
if (!useSessionStorage || !window.sessionStorage) {
return null;
}
var key = PickerCachePrefix + termId;
var serialized = window.sessionStorage.getItem(key);
if (!serialized) {
return null;
}
var cacheEntry = JSON.parse(serialized);
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
}
catch (error) {
return null;
}
};
SPTermStorePickerService.prototype.normalizeCacheMinutes = function (value) {
if (typeof value !== 'number' || !isFinite(value)) {
return 15;
}
return Math.min(Math.max(Math.floor(value), 1), 1440);
};
SPTermStorePickerService.prototype.ensureSuccessfulResponse = function (response, operation) {
if (!response.ok) {
throw new Error(operation + ' request failed (' + response.status + ' ' + response.statusText + ').');
}
};
SPTermStorePickerService.prototype.searchTermsBySearchText = function (terms, searchText) { SPTermStorePickerService.prototype.searchTermsBySearchText = function (terms, searchText) {
if (terms) { if (terms) {
return terms.filter(function (t) { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; }); return terms.filter(function (t) { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -3,9 +3,10 @@ import { IMenuItem } from './IMenuItem';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export declare class TaxonomyNavigationService implements ITaxonomyNavigationService { export declare class TaxonomyNavigationService implements ITaxonomyNavigationService {
private context; private context;
private termSetName; private termSetNameOrId;
private debug;
private cacheMinutes;
private _taxonomyPickerService; private _taxonomyPickerService;
private _noTerm; constructor(context: ApplicationCustomizerContext, termSetNameOrId: string, debug?: boolean, cacheMinutes?: number);
constructor(context: ApplicationCustomizerContext, termSetName: string);
getMenuItems(): Promise<IMenuItem[]>; getMenuItems(): Promise<IMenuItem[]>;
} }
+21 -37
View File
@@ -37,40 +37,28 @@ import { sp } from '@pnp/sp';
import { MenuItem } from './MenuItem'; import { MenuItem } from './MenuItem';
import SPTermStorePickerService from './SPTermStorePickerService'; import SPTermStorePickerService from './SPTermStorePickerService';
import { ItemDictionary } from './ItemDictionary'; import { ItemDictionary } from './ItemDictionary';
import { debugWarn } from './MegaMenuDebug';
var LOG_SOURCE = 'TaxonomyNavigationService';
var TaxonomyNavigationService = (function () { var TaxonomyNavigationService = (function () {
function TaxonomyNavigationService(context /*, private lcid: number*/, termSetName) { function TaxonomyNavigationService(context, termSetNameOrId, debug, cacheMinutes) {
this.context = context; /*, private lcid: number*/ if (debug === void 0) { debug = false; }
this.termSetName = termSetName; if (cacheMinutes === void 0) { cacheMinutes = 15; }
this._noTerm = { this.context = context;
_ObjectType_: '', this.termSetNameOrId = termSetNameOrId;
_ObjectIdentity_: '', this.debug = debug;
CustomSortOrderIndex: 0, this.cacheMinutes = cacheMinutes;
Description: '',
Id: '',
IsAvailableForTagging: false,
IsDeprecated: false,
IsRoot: true,
LocalCustomProperties: {
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
_Sys_Nav_ExcludedProviders: undefined,
_Sys_Nav_SimpleLinkUrl: undefined
},
Name: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
PathOfTerm: '',
TermSet: undefined
};
sp.setup({ sp.setup({
spfxContext: context spfxContext: context
}); });
this._taxonomyPickerService = new SPTermStorePickerService({ this._taxonomyPickerService = new SPTermStorePickerService({
anchorId: '', anchorId: '',
termsetNameOrID: termSetName, termsetNameOrID: termSetNameOrId,
useSessionStorage: true, useSessionStorage: true,
hideDeprecatedTags: true, hideDeprecatedTags: true,
hideTagsNotAvailableForTagging: false hideTagsNotAvailableForTagging: false,
}, this.context); cacheMinutes: cacheMinutes
}, this.context, this.debug);
} }
// Implement the methods from ITaxonomyNavigationService interface here
TaxonomyNavigationService.prototype.getMenuItems = function () { TaxonomyNavigationService.prototype.getMenuItems = function () {
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var _this = this; var _this = this;
@@ -79,35 +67,31 @@ var TaxonomyNavigationService = (function () {
switch (_a.label) { switch (_a.label) {
case 0: case 0:
siteCollectionUrl = this.context.pageContext.site.absoluteUrl; siteCollectionUrl = this.context.pageContext.site.absoluteUrl;
return [4 /*yield*/, this._taxonomyPickerService.getAllTerms(this.termSetName)]; return [4 /*yield*/, this._taxonomyPickerService.getAllTerms(this.termSetNameOrId, true, false, true, this.cacheMinutes)];
case 1: case 1:
termset = _a.sent(); termset = _a.sent();
itemsDict = new ItemDictionary(); itemsDict = new ItemDictionary();
menuItems = []; menuItems = [];
if (!termset || !termset.Terms) { if (!termset || !termset.Terms || termset.Terms.length === 0) {
console.warn('No terms found in the term set'); debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.');
return [2 /*return*/, [new MenuItem(this._noTerm, 0, siteCollectionUrl)]]; throw new Error('The configured navigation term set was not found or contains no terms.');
} }
termset.Terms.forEach(function (term) { return __awaiter(_this, void 0, void 0, function () { termset.Terms.forEach(function (term) {
var menuItem, parentItem; var menuItem = new MenuItem(term, 0, siteCollectionUrl);
return __generator(this, function (_a) {
menuItem = new MenuItem(term, 0, siteCollectionUrl);
itemsDict.Add(term.Id, menuItem); itemsDict.Add(term.Id, menuItem);
if (menuItem.pathDepth === 1) { if (menuItem.pathDepth === 1) {
menuItems.push(menuItem); menuItems.push(menuItem);
} }
else { else {
parentItem = itemsDict.Get(term.ParentId); var parentItem = itemsDict.Get(term.ParentId);
if (parentItem) { if (parentItem) {
parentItem.items.push(menuItem); parentItem.items.push(menuItem);
} }
else { else {
console.warn("Item without parent: " + term.PathOfTerm); debugWarn(_this.debug, LOG_SOURCE, 'Item without parent:', term.PathOfTerm);
} }
} }
return [2 /*return*/];
}); });
}); });
return [2 /*return*/, menuItems]; return [2 /*return*/, menuItems];
} }
}); });
File diff suppressed because one or more lines are too long
@@ -4,7 +4,8 @@ import { UserCustomActionScope } from './UserCustomActionScope';
import { IUserCustomActionProps } from './IUserCustomActionProps'; import { IUserCustomActionProps } from './IUserCustomActionProps';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export declare class UserCustomActionService implements IUserCustomActionService { export declare class UserCustomActionService implements IUserCustomActionService {
constructor(context: ApplicationCustomizerContext); private debug;
constructor(context: ApplicationCustomizerContext, debug?: boolean);
getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>; getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>;
getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>; getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>;
addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>; addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>;
@@ -35,8 +35,12 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
}; };
import { sp } from '@pnp/sp'; import { sp } from '@pnp/sp';
import { UserCustomActionScope } from './UserCustomActionScope'; import { UserCustomActionScope } from './UserCustomActionScope';
import { debugError } from '../MegaMenuDebug';
var LOG_SOURCE = 'UserCustomActionService';
var UserCustomActionService = (function () { var UserCustomActionService = (function () {
function UserCustomActionService(context) { function UserCustomActionService(context, debug) {
if (debug === void 0) { debug = false; }
this.debug = debug;
sp.setup({ sp.setup({
spfxContext: context spfxContext: context
}); });
@@ -76,7 +80,7 @@ var UserCustomActionService = (function () {
case 8: return [2 /*return*/, actions]; case 8: return [2 /*return*/, actions];
case 9: case 9:
error_1 = _b.sent(); error_1 = _b.sent();
console.error('Error getting user custom actions: ', error_1); debugError(this.debug, LOG_SOURCE, 'Error getting user custom actions.', error_1);
throw error_1; throw error_1;
case 10: return [2 /*return*/]; case 10: return [2 /*return*/];
} }
@@ -102,7 +106,7 @@ var UserCustomActionService = (function () {
} }
} }
catch (error) { catch (error) {
console.error('Error getting user custom action by ID: ', error); debugError(this.debug, LOG_SOURCE, 'Error getting user custom action by ID.', error);
throw error; throw error;
} }
return [2 /*return*/]; return [2 /*return*/];
@@ -128,7 +132,7 @@ var UserCustomActionService = (function () {
} }
} }
catch (error) { catch (error) {
console.error('Error adding user custom action: ', error); debugError(this.debug, LOG_SOURCE, 'Error adding user custom action.', error);
throw error; throw error;
} }
return [2 /*return*/]; return [2 /*return*/];
@@ -170,7 +174,7 @@ var UserCustomActionService = (function () {
case 8: return [2 /*return*/, result]; case 8: return [2 /*return*/, result];
case 9: case 9:
error_2 = _b.sent(); error_2 = _b.sent();
console.error('Error updating user custom action: ', error_2); debugError(this.debug, LOG_SOURCE, 'Error updating user custom action.', error_2);
throw error_2; throw error_2;
case 10: return [2 /*return*/]; case 10: return [2 /*return*/];
} }
@@ -196,7 +200,7 @@ var UserCustomActionService = (function () {
} }
} }
catch (error) { catch (error) {
console.error('Error deleting user custom action: ', error); debugError(this.debug, LOG_SOURCE, 'Error deleting user custom action.', error);
throw error; throw error;
} }
return [2 /*return*/]; return [2 /*return*/];
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "1.0.4", "version": "1.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+4 -3
View File
@@ -1,15 +1,16 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "1.0.4", "version": "1.1.0",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=6.9.0 <9.0.0"
}, },
"scripts": { "scripts": {
"build": "gulp bundle", "build": "gulp bundle",
"clean": "gulp clean", "clean": "gulp clean",
"test": "gulp test" "test": "gulp test",
"package": "gulp clean && gulp bundle --ship && gulp package-solution --ship"
}, },
"dependencies": { "dependencies": {
"@microsoft/decorators": "~1.4.1", "@microsoft/decorators": "~1.4.1",
+14 -6
View File
@@ -103,6 +103,14 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
z-index: $mm-layer-index; z-index: $mm-layer-index;
} }
.mega-menu-status {
padding: 8px 14px;
border-bottom: 1px solid $mm-panel-border;
background: $mm-focus-soft;
color: $mm-text;
font-size: 13px;
}
.mega-menu-main { .mega-menu-main {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -145,7 +153,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li > a, .mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"], .mega-menu-main > ul > li > .menu-item-text,
.menu-item-link, .menu-item-link,
.menu-item-text { .menu-item-text {
position: relative; position: relative;
@@ -199,7 +207,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li:hover > a, .mega-menu-main > ul > li:hover > a,
.mega-menu-main > ul > li:hover > span[role="menuitem"], .mega-menu-main > ul > li:hover > .menu-item-text,
.mega-menu-main > ul > li > a[aria-expanded="true"], .mega-menu-main > ul > li > a[aria-expanded="true"],
.mega-menu-main > ul > li > span[aria-expanded="true"], .mega-menu-main > ul > li > span[aria-expanded="true"],
.menu-item-link:hover, .menu-item-link:hover,
@@ -214,7 +222,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li > a:focus, .mega-menu-main > ul > li > a:focus,
.mega-menu-main > ul > li > span[role="menuitem"]:focus, .mega-menu-main > ul > li > .menu-item-text:focus,
.menu-item-link:focus, .menu-item-link:focus,
.menu-item-text:focus { .menu-item-text:focus {
color: $mm-nav-text; color: $mm-nav-text;
@@ -416,7 +424,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
} }
.mega-menu-main > ul > li > a, .mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"], .mega-menu-main > ul > li > .menu-item-text,
.menu-item-link, .menu-item-link,
.menu-item-text { .menu-item-text {
min-height: 36px; min-height: 36px;
@@ -448,7 +456,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
.skip-link, .skip-link,
.menu-item-has-children::after, .menu-item-has-children::after,
.mega-menu-main > ul > li > a, .mega-menu-main > ul > li > a,
.mega-menu-main > ul > li > span[role="menuitem"], .mega-menu-main > ul > li > .menu-item-text,
.menu-item-link, .menu-item-link,
.menu-item-text, .menu-item-text,
.mega-menu, .mega-menu,
@@ -460,7 +468,7 @@ $mm-layer-index: var(--megaMenuZIndex, 6000);
@media (prefers-contrast: high) { @media (prefers-contrast: high) {
.mega-menu-main > ul > li > a:focus, .mega-menu-main > ul > li > a:focus,
.mega-menu-main > ul > li > span[role="menuitem"]:focus, .mega-menu-main > ul > li > .menu-item-text:focus,
.menu-item-link:focus, .menu-item-link:focus,
.menu-item-text:focus, .menu-item-text:focus,
.mega-menu-category > h3 > a:focus, .mega-menu-category > h3 > a:focus,
@@ -1,8 +1,6 @@
/* tslint:disable */ /* tslint:disable */
require('./MegaMenu.module.css'); require('./MegaMenu.module.css');
const styles = { const styles = {
mmSlideIn: 'mmSlideIn_09a8e1a7',
mmFadeIn: 'mmFadeIn_09a8e1a7',
}; };
export default styles; export default styles;
@@ -22,7 +22,9 @@ const LOG_SOURCE: string = 'MegaMenuApplicationCustomizer';
export const UserCustomActionMegaMenuId: string = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; export const UserCustomActionMegaMenuId: string = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
export interface IMegaMenuApplicationCustomizerProperties { export interface IMegaMenuApplicationCustomizerProperties {
termSetName: string; termSetName?: string;
termSetId?: string;
cacheMinutes?: number;
cssUrl?: string; cssUrl?: string;
debug?: boolean; debug?: boolean;
} }
@@ -31,6 +33,8 @@ export default class MegaMenuApplicationCustomizer
extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> { extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined; private _topPlaceholder: PlaceholderContent | undefined;
private _menuContainer: HTMLElement | undefined;
private _renderer: MegaMenuRenderer | undefined;
@override @override
public onInit(): Promise<void> { public onInit(): Promise<void> {
@@ -64,16 +68,18 @@ export default class MegaMenuApplicationCustomizer
return; return;
} }
if (!this.properties.termSetName) { const termSetIdentifier: string = this._getTermSetIdentifier();
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetName configured. Mega Menu rendering is skipped.'); if (!termSetIdentifier) {
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.');
this._renderStatus(strings.ConfigurationMissing);
return; return;
} }
this._renderMegaMenu(this.properties.termSetName); this._renderMegaMenu(termSetIdentifier);
} }
} }
private async _renderMegaMenu(termSetName: string, debug: boolean = this._isDebugEnabled()): Promise<void> { private async _renderMegaMenu(termSetIdentifier: string, debug: boolean = this._isDebugEnabled()): Promise<void> {
if (!this._topPlaceholder) { if (!this._topPlaceholder) {
return; return;
} }
@@ -81,28 +87,34 @@ export default class MegaMenuApplicationCustomizer
try { try {
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService( const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
this.context, this.context,
termSetName, termSetIdentifier,
debug debug,
this._getCacheMinutes()
); );
const menuItems: IMenuItem[] = await taxonomyService.getMenuItems(); const menuItems: IMenuItem[] = await taxonomyService.getMenuItems();
const renderer: MegaMenuRenderer = new MegaMenuRenderer( if (this._renderer) {
this._renderer.dispose();
}
this._renderer = new MegaMenuRenderer(
menuItems, menuItems,
debug debug
); );
const container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder); this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
if (container) { if (this._menuContainer) {
renderer.render(container); this._renderer.render(this._menuContainer);
} else { } else {
renderer.render(this._topPlaceholder.domElement); this._renderer.render(this._topPlaceholder.domElement);
} }
debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.'); debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
} catch (error) { } catch (error) {
debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error); debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error);
this._renderStatus(strings.LoadError);
} }
} }
@@ -110,9 +122,13 @@ export default class MegaMenuApplicationCustomizer
const container = document.getElementById(id); const container = document.getElementById(id);
if (container) { if (container) {
const div = document.createElement('div'); let host: HTMLElement = document.getElementById('MegaMenuHost');
container.appendChild(div); if (!host) {
return div; host = document.createElement('div');
host.id = 'MegaMenuHost';
container.appendChild(host);
}
return host;
} }
return placeholder.domElement; return placeholder.domElement;
@@ -123,6 +139,15 @@ export default class MegaMenuApplicationCustomizer
let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement; let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement;
if (cssUrl && cssUrl.trim() !== '') { if (cssUrl && cssUrl.trim() !== '') {
const validatedUrl: string | undefined = this._validateCssUrl(cssUrl);
if (!validatedUrl) {
debugWarn(debug, LOG_SOURCE, 'Rejected unsafe external CSS URL:', cssUrl);
if (link) {
link.remove();
}
return;
}
if (!link) { if (!link) {
const head: HTMLHeadElement = document.getElementsByTagName('head')[0]; const head: HTMLHeadElement = document.getElementsByTagName('head')[0];
link = document.createElement('link'); link = document.createElement('link');
@@ -138,16 +163,76 @@ export default class MegaMenuApplicationCustomizer
head.appendChild(link); head.appendChild(link);
} }
link.href = cssUrl; link.href = validatedUrl;
} else if (link) { } else if (link) {
link.remove(); link.remove();
} }
} }
private _onDispose = (): void => { private _onDispose = (): void => {
this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders);
if (this._renderer) {
this._renderer.dispose();
this._renderer = undefined;
}
const externalCssLink: HTMLElement = document.getElementById('mega-menu-additional-css-34FAB720');
if (externalCssLink) {
externalCssLink.remove();
}
this._menuContainer = undefined;
this._topPlaceholder = undefined;
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.'); debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
} }
private _getTermSetIdentifier(): string {
const termSetId: string = this.properties && this.properties.termSetId
? this.properties.termSetId.trim()
: '';
if (termSetId && this._isGuid(termSetId)) {
return termSetId;
}
return this.properties && this.properties.termSetName
? this.properties.termSetName.trim()
: '';
}
private _getCacheMinutes(): number {
const value: number = this.properties ? Number(this.properties.cacheMinutes) : 15;
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
}
private _validateCssUrl(cssUrl: string): string | undefined {
try {
const parsed: URL = new URL(cssUrl.trim(), window.location.origin);
const isHttp: boolean = parsed.protocol === 'http:' || parsed.protocol === 'https:';
const isSameOrigin: boolean = parsed.origin === window.location.origin;
if (!isHttp || (!isSameOrigin && parsed.protocol !== 'https:')) {
return undefined;
}
return parsed.href;
} catch (error) {
return undefined;
}
}
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 _isDebugEnabled(debugOverride?: boolean): boolean { private _isDebugEnabled(debugOverride?: boolean): boolean {
if (typeof debugOverride === 'boolean') { if (typeof debugOverride === 'boolean') {
return debugOverride; return debugOverride;
+50 -12
View File
@@ -13,25 +13,29 @@ const LOG_SOURCE: string = 'MegaMenuRenderer';
export class MegaMenuRenderer { export class MegaMenuRenderer {
private static readonly hoverOpenDelayMs: number = 140; private static readonly hoverOpenDelayMs: number = 140;
private static readonly hoverCloseDelayMs: number = 180; private static readonly hoverCloseDelayMs: number = 180;
private _container: HTMLElement | undefined;
private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined;
private _documentClickHandler: ((event: MouseEvent) => void) | undefined;
private _announcer: HTMLElement | undefined;
constructor( constructor(
private menuItems: IMenuItem[], private menuItems: IMenuItem[],
private debug: boolean = false private debug: boolean = false
) { } ) { }
public render(container: HTMLElement) { public render(container: HTMLElement): void {
this.dispose();
this._container = container;
container.innerHTML = ''; container.innerHTML = '';
container.id = 'CustomNavigation'; container.id = 'CustomNavigation';
const nav = document.createElement('nav'); const nav = document.createElement('nav');
nav.id = 'Mega-Menu'; nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main'; nav.className = 'mega-menu-main';
nav.setAttribute('role', 'navigation');
nav.setAttribute('aria-label', 'Hauptnavigation'); nav.setAttribute('aria-label', 'Hauptnavigation');
const topLevelUl = document.createElement('ul'); const topLevelUl = document.createElement('ul');
topLevelUl.className = 'mega-menu-top-level'; topLevelUl.className = 'mega-menu-top-level';
topLevelUl.setAttribute('role', 'menubar');
this.menuItems.forEach(topLevelItem => { this.menuItems.forEach(topLevelItem => {
const topLevelLi = this.createTopLevelItem(topLevelItem); const topLevelLi = this.createTopLevelItem(topLevelItem);
@@ -45,6 +49,25 @@ export class MegaMenuRenderer {
this.createScreenReaderAnnouncer(); this.createScreenReaderAnnouncer();
} }
public dispose(): void {
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;
}
private createTopLevelItem(item: IMenuItem): HTMLLIElement { private createTopLevelItem(item: IMenuItem): HTMLLIElement {
const li = document.createElement('li'); const li = document.createElement('li');
const hasChildren = item.hasChildren() && item.items && item.items.length > 0; const hasChildren = item.hasChildren() && item.items && item.items.length > 0;
@@ -57,7 +80,6 @@ export class MegaMenuRenderer {
isActive ? 'is-active' : undefined, isActive ? 'is-active' : undefined,
isCurrent ? 'is-current' : undefined isCurrent ? 'is-current' : undefined
); );
li.setAttribute('role', 'none');
const topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent); const topElement = this.createTopLevelElement(item, hasChildren, isActive, isCurrent);
li.appendChild(topElement); li.appendChild(topElement);
@@ -83,7 +105,6 @@ export class MegaMenuRenderer {
element.setAttribute('tabindex', '0'); element.setAttribute('tabindex', '0');
} }
element.setAttribute('role', 'menuitem');
element.textContent = item.label; element.textContent = item.label;
element.className = this.joinClasses( element.className = this.joinClasses(
element.className, element.className,
@@ -111,7 +132,6 @@ export class MegaMenuRenderer {
private createMegaMenu(parentItem: IMenuItem): HTMLDivElement { private createMegaMenu(parentItem: IMenuItem): HTMLDivElement {
const megaMenuDiv = document.createElement('div'); const megaMenuDiv = document.createElement('div');
megaMenuDiv.className = 'mega-menu'; megaMenuDiv.className = 'mega-menu';
megaMenuDiv.setAttribute('role', 'menu');
megaMenuDiv.setAttribute('aria-expanded', 'false'); megaMenuDiv.setAttribute('aria-expanded', 'false');
megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien'); megaMenuDiv.setAttribute('aria-label', parentItem.label + ' Unterkategorien');
@@ -213,7 +233,7 @@ export class MegaMenuRenderer {
} }
private attachEventListeners(): void { private attachEventListeners(): void {
const headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span[role="menuitem"]'); const headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > .menu-item-text');
for (let i = 0; i < headings.length; i++) { for (let i = 0; i < headings.length; i++) {
const heading = headings[i] as HTMLElement; const heading = headings[i] as HTMLElement;
@@ -336,7 +356,7 @@ export class MegaMenuRenderer {
} }
private attachGlobalKeyboardNavigation(): void { private attachGlobalKeyboardNavigation(): void {
document.addEventListener('keydown', (e: KeyboardEvent) => { this._documentKeydownHandler = (e: KeyboardEvent): void => {
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;
if (e.key === 'Escape') { if (e.key === 'Escape') {
@@ -380,11 +400,12 @@ export class MegaMenuRenderer {
} }
} }
} }
}); };
document.addEventListener('keydown', this._documentKeydownHandler);
} }
private attachGlobalClickOutsideHandler(): void { private attachGlobalClickOutsideHandler(): void {
document.addEventListener('click', (e: MouseEvent) => { this._documentClickHandler = (e: MouseEvent): void => {
const eventTarget = e.target as Element; const eventTarget = e.target as Element;
const isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' && const isInsideNavigation = !!(eventTarget && typeof eventTarget.closest === 'function' &&
eventTarget.closest('#CustomNavigation')); eventTarget.closest('#CustomNavigation'));
@@ -399,7 +420,8 @@ export class MegaMenuRenderer {
debugLog(this.debug, LOG_SOURCE, 'Closing menu because of outside click.'); debugLog(this.debug, LOG_SOURCE, 'Closing menu because of outside click.');
this.closeAllMegaMenus(); this.closeAllMegaMenus();
} }
}); };
document.addEventListener('click', this._documentClickHandler);
} }
private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void { private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
@@ -407,6 +429,7 @@ export class MegaMenuRenderer {
trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-expanded', 'true'); menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open'); menu.classList.add('js-open');
this.announce((trigger.textContent || '') + ' geöffnet');
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent); debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
} }
@@ -414,6 +437,7 @@ export class MegaMenuRenderer {
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-expanded', 'false'); menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open'); menu.classList.remove('js-open');
this.announce((trigger.textContent || '') + ' geschlossen');
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent); debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
} }
@@ -514,7 +538,9 @@ export class MegaMenuRenderer {
} }
private createScreenReaderAnnouncer(): void { private createScreenReaderAnnouncer(): void {
if (document.getElementById('mega-menu-sr-announcer')) { const existingAnnouncer: HTMLElement = document.getElementById('mega-menu-sr-announcer');
if (existingAnnouncer) {
this._announcer = existingAnnouncer;
return; return;
} }
@@ -524,7 +550,19 @@ export class MegaMenuRenderer {
srAnnouncer.setAttribute('aria-atomic', 'true'); srAnnouncer.setAttribute('aria-atomic', 'true');
srAnnouncer.className = 'sr-only'; srAnnouncer.className = 'sr-only';
document.body.appendChild(srAnnouncer); document.body.appendChild(srAnnouncer);
this._announcer = srAnnouncer;
debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.'); debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.');
} }
private announce(message: string): void {
if (this._announcer) {
this._announcer.textContent = '';
window.setTimeout((): void => {
if (this._announcer) {
this._announcer.textContent = message;
}
}, 20);
}
}
} }
-210
View File
@@ -1,210 +0,0 @@
// tslint:disable:max-line-length export-name
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { IMegaMenuApplicationCustomizerProperties, UserCustomActionMegaMenuId } from './MegaMenuApplicationCustomizer';
import { UserCustomActionService } from '../../services/UserCustomActionService/UserCustomActionService';
import { UserCustomActionScope } from '../../services/UserCustomActionService/UserCustomActionScope';
import { IUserCustomActionProps } from '../../services/UserCustomActionService/IUserCustomActionProps';
import { debugError } from '../../services/MegaMenuDebug';
const LOG_SOURCE: string = 'MegaMenuSettingsPanel';
export class MegaMenuSettingsPanel {
private _service: UserCustomActionService;
private _ucaId: string;
private _panelElement: HTMLElement | undefined = undefined;
private _overlayElement: HTMLElement | undefined = undefined;
constructor(
private context: ApplicationCustomizerContext,
private dataUpdated: (data: IMegaMenuApplicationCustomizerProperties) => void,
private debug: boolean = false
) {
this._service = new UserCustomActionService(this.context, this.debug);
}
public async open(): Promise<void> {
if (this._panelElement) {
return;
}
const currentProps: IMegaMenuApplicationCustomizerProperties = await this.readApplicationCustomizerProps();
this._createPanel(currentProps);
}
public close(): void {
if (this._panelElement) {
this._panelElement.remove();
this._panelElement = undefined;
}
if (this._overlayElement) {
this._overlayElement.remove();
this._overlayElement = undefined;
}
document.body.focus();
document.body.blur();
}
private async readApplicationCustomizerProps(): Promise<IMegaMenuApplicationCustomizerProperties> {
const ucas: IUserCustomActionProps[] = await this._service.getUserCustomActions(UserCustomActionScope.Site);
const candidates: IUserCustomActionProps[] = ucas.filter(uca => uca.ClientSideComponentId === UserCustomActionMegaMenuId);
if (candidates.length) {
const uca: IUserCustomActionProps = candidates[0];
this._ucaId = uca.Id;
if (uca.ClientSideComponentProperties) {
return JSON.parse(uca.ClientSideComponentProperties) as IMegaMenuApplicationCustomizerProperties;
}
return {
termSetName: '',
cssUrl: '',
debug: false
};
}
debugError(this.debug, LOG_SOURCE, 'UserCustomAction for the MegaMenu was not found.');
return {
termSetName: '',
cssUrl: '',
debug: false
};
}
private async saveApplicationCustomizerProps(componentProps: IMegaMenuApplicationCustomizerProperties): Promise<void> {
try {
const newUserCustomActionsProperty: {} = {
ClientSideComponentProperties: JSON.stringify(componentProps)
};
await this._service.updateUserCustomAction(UserCustomActionScope.Site, this._ucaId, newUserCustomActionsProperty);
this.debug = componentProps.debug === true;
this._service = new UserCustomActionService(this.context, this.debug);
this.dataUpdated(componentProps);
} catch (e) {
debugError(this.debug, LOG_SOURCE, 'Error saving MegaMenu settings.', e);
}
}
private _createPanel(props: IMegaMenuApplicationCustomizerProperties): void {
const overlay: HTMLElement = document.createElement('div');
overlay.className = 'mm-settings-overlay';
overlay.tabIndex = -1;
overlay.onclick = () => this.close();
this._overlayElement = overlay;
const panel: HTMLElement = document.createElement('div');
panel.className = 'mm-settings-panel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-modal', 'true');
panel.setAttribute('aria-label', 'MegaMenu Einstellungen');
panel.innerHTML = this._getMarkup(props.termSetName, props.cssUrl || '', props.debug === true);
this._panelElement = panel;
document.body.appendChild(overlay);
document.body.appendChild(panel);
const closeBtn: HTMLButtonElement = panel.querySelector('.mm-settings-close') as HTMLButtonElement;
const cancelBtn: HTMLButtonElement = panel.querySelector('.mm-settings-cancel') as HTMLButtonElement;
const saveBtn: HTMLButtonElement = panel.querySelector('.mm-settings-save') as HTMLButtonElement;
const firstInput: HTMLInputElement = panel.querySelector('#mm-setting-termset') as HTMLInputElement;
if (closeBtn) { closeBtn.onclick = () => this.close(); }
if (cancelBtn) { cancelBtn.onclick = () => this.close(); }
if (saveBtn) { saveBtn.onclick = () => this._save(); }
panel.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
this.close();
} else if (e.key === 'Tab') {
this._trapFocus(e);
}
});
setTimeout(() => {
if (firstInput) {
firstInput.focus();
}
}, 0);
}
private _getMarkup(termSet: string, cssUrl: string, debug: boolean): string {
return `<div class='mm-settings-header'>
<h2 class='mm-settings-title'>Einstellungen</h2>
<button type='button' class='mm-settings-close' aria-label='Schliessen'>&times;</button>
</div>
<div class='mm-settings-body'>
<div class='mm-settings-field'>
<label for='mm-setting-termset'>Name des Navigations-Termsets</label>
<input id='mm-setting-termset' type='text' value='${this._escape(termSet)}' />
</div>
<div class='mm-settings-field'>
<label for='mm-setting-css'>Pfad zu zusaetzlicher CSS-Datei</label>
<input id='mm-setting-css' type='text' value='${this._escape(cssUrl)}' />
</div>
<div class='mm-settings-field'>
<label for='mm-setting-debug'>
<input id='mm-setting-debug' type='checkbox' ${debug ? 'checked' : ''} />
Debug-Ausgaben in der Browser-Konsole aktivieren
</label>
</div>
</div>
<div class='mm-settings-footer'>
<button type='button' class='mm-settings-save ms-Button ms-Button--primary'><span>Speichern</span></button>
<button type='button' class='mm-settings-cancel ms-Button'><span>Abbrechen</span></button>
</div>`;
}
private _save(): void {
if (!this._panelElement) {
return;
}
const termSetInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-termset') as HTMLInputElement;
const cssInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-css') as HTMLInputElement;
const debugInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-debug') as HTMLInputElement;
this.saveApplicationCustomizerProps({
termSetName: termSetInput && termSetInput.value ? termSetInput.value : '',
cssUrl: cssInput && cssInput.value ? cssInput.value : '',
debug: !!(debugInput && debugInput.checked)
});
this.close();
}
private _trapFocus(e: KeyboardEvent): void {
if (!this._panelElement) {
return;
}
const focusable: NodeListOf<Element> = this._panelElement.querySelectorAll('button, input');
if (!focusable || focusable.length === 0) {
return;
}
const first: HTMLElement = focusable[0] as HTMLElement;
const last: HTMLElement = focusable[focusable.length - 1] as HTMLElement;
const active: HTMLElement = document.activeElement as HTMLElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
}
private _escape(value: string): string {
if (value) {
return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
return '';
}
}
+7
View File
@@ -0,0 +1,7 @@
define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Die Navigation ist nicht konfiguriert.",
"LoadError": "Die Navigation konnte nicht geladen werden."
};
});
+3 -1
View File
@@ -1,5 +1,7 @@
define([], function() { define([], function() {
return { return {
"Title": "MegaMenuApplicationCustomizer" "Title": "MegaMenuApplicationCustomizer",
"ConfigurationMissing": "Navigation is not configured.",
"LoadError": "Navigation could not be loaded."
} }
}); });
+2
View File
@@ -1,5 +1,7 @@
declare interface IMegaMenuApplicationCustomizerStrings { declare interface IMegaMenuApplicationCustomizerStrings {
Title: string; Title: string;
ConfigurationMissing: string;
LoadError: string;
} }
declare module 'MegaMenuApplicationCustomizerStrings' { declare module 'MegaMenuApplicationCustomizerStrings' {
@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
hideDeprecatedTags: boolean; hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean; hideTagsNotAvailableForTagging: boolean;
anchorId: string; anchorId: string;
cacheMinutes: number;
} }
+2 -1
View File
@@ -1,4 +1,5 @@
export function debugLog(enabled: boolean, source: string, message: string, ...args: any[]): void { // tslint:disable:no-any
export function debugLog(enabled: boolean, source: string, message: string, ...args: any[]): void {
if (!enabled) { if (!enabled) {
return; return;
} }
+95 -14
View File
@@ -12,6 +12,18 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { debugError } from './MegaMenuDebug'; import { debugError } from './MegaMenuDebug';
const EmptyGuid: string = '00000000-0000-0000-0000-000000000000'; const EmptyGuid: string = '00000000-0000-0000-0000-000000000000';
const TermCachePrefix: string = 'MegaMenu:TermSet:';
const PickerCachePrefix: string = 'MegaMenu:PickerTerms:';
interface ITermCacheEntry {
expiresAt: number;
value: ITermSet;
}
interface IPickerTermCacheEntry {
expiresAt: number;
value: IPickerTerm[];
}
/** /**
* Service implementation to manage term stores in SharePoint * Service implementation to manage term stores in SharePoint
@@ -31,7 +43,7 @@ export default class SPTermStorePickerService {
public async getTermLabels(termId: string): Promise<string[]> { public async getTermLabels(termId: string): Promise<string[]> {
let result: string[] = null; let result: string[] = null;
try { try {
const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultKeywordsTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${termId}</Parameter></Parameters></Method></ObjectPaths></Request>`; const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultSiteCollectionTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${termId}</Parameter></Parameters></Method></ObjectPaths></Request>`;
const reqHeaders = new Headers(); const reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
@@ -43,6 +55,7 @@ export default class SPTermStorePickerService {
}; };
const callResult = await this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions); const callResult = await this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions);
this.ensureSuccessfulResponse(callResult, 'Term labels');
const jsonResult = await callResult.json(); const jsonResult = await callResult.json();
const node = jsonResult.find(x => x._ObjectType_ === 'SP.Taxonomy.Term'); const node = jsonResult.find(x => x._ObjectType_ === 'SP.Taxonomy.Term');
@@ -73,6 +86,7 @@ export default class SPTermStorePickerService {
}; };
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
return serviceResponse.json().then((serviceJSONResponse: any) => { return serviceResponse.json().then((serviceJSONResponse: any) => {
// Construct results // Construct results
const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore'); const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore');
@@ -124,7 +138,13 @@ export default class SPTermStorePickerService {
* Retrieve all terms for the given term set * Retrieve all terms for the given term set
* @param termset * @param termset
*/ */
public async getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage: boolean = true): Promise<ITermSet> { public async getAllTerms(
termset: string,
hideDeprecatedTags?: boolean,
hideTagsNotAvailableForTagging?: boolean,
useSessionStorage: boolean = true,
cacheMinutes: number = 15
): Promise<ITermSet> {
let termsetId: string = termset; let termsetId: string = termset;
// Check if the provided term set property is a GUID or string // Check if the provided term set property is a GUID or string
if (!this.isGuid(termset)) { if (!this.isGuid(termset)) {
@@ -139,14 +159,14 @@ export default class SPTermStorePickerService {
} }
} }
const childTerms = this.getTermsById(termsetId, useSessionStorage); const childTerms = this.getCachedTermSet(termsetId, useSessionStorage);
if (childTerms) { if (childTerms) {
return childTerms; return childTerms;
} }
// Request body to retrieve all terms for the given term set // Request body to retrieve all terms for the given term set
const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultKeywordsTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`; const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultSiteCollectionTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`;
const reqHeaders = new Headers(); const reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json'); reqHeaders.append('accept', 'application/json');
@@ -158,7 +178,11 @@ export default class SPTermStorePickerService {
}; };
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
this.ensureSuccessfulResponse(serviceResponse, 'Terms');
return serviceResponse.json().then((serviceJSONResponse: any) => { return serviceResponse.json().then((serviceJSONResponse: any) => {
if (!Array.isArray(serviceJSONResponse)) {
throw new Error('The taxonomy service returned an unexpected response.');
}
const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet'); const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet');
if (termStoreResultTermSets.length > 0) { if (termStoreResultTermSets.length > 0) {
@@ -203,7 +227,12 @@ export default class SPTermStorePickerService {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet)); const ttlMinutes: number = this.normalizeCacheMinutes(cacheMinutes);
const cacheEntry: ITermCacheEntry = {
expiresAt: Date.now() + ttlMinutes * 60 * 1000,
value: termStoreResultTermSet
};
window.sessionStorage.setItem(TermCachePrefix + termsetId, JSON.stringify(cacheEntry));
} }
} catch (error) { } catch (error) {
// Do nothing, sometimes "storage quota exceeded" error if too many items // Do nothing, sometimes "storage quota exceeded" error if too many items
@@ -225,7 +254,7 @@ export default class SPTermStorePickerService {
public async searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]> { public async searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]> {
const { useSessionStorage } = this.props; const { useSessionStorage } = this.props;
const childTerms = this.getTermsById(termId, useSessionStorage); const childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
if (childTerms) { if (childTerms) {
return this.searchTermsBySearchText(childTerms, searchText); return this.searchTermsBySearchText(childTerms, searchText);
} else { } else {
@@ -257,12 +286,18 @@ export default class SPTermStorePickerService {
const returnTerms: IPickerTerm[] = []; const returnTerms: IPickerTerm[] = [];
const childTerms = this.getTermsById(anchorId, useSessionStorage); const childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
if (childTerms) { if (childTerms) {
return childTerms; return childTerms;
} }
const termSet = await this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging); const termSet = await this.getAllTerms(
termsetNameOrID,
hideDeprecatedTags,
hideTagsNotAvailableForTagging,
useSessionStorage,
this.props.cacheMinutes
);
const terms = termSet.Terms; const terms = termSet.Terms;
if (anchorId) { if (anchorId) {
const anchorTerm = terms.filter(t => t.Id.toLowerCase() === anchorId.toLowerCase()).shift(); const anchorTerm = terms.filter(t => t.Id.toLowerCase() === anchorId.toLowerCase()).shift();
@@ -277,7 +312,11 @@ export default class SPTermStorePickerService {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms)); const cacheEntry: IPickerTermCacheEntry = {
expiresAt: Date.now() + this.normalizeCacheMinutes(this.props.cacheMinutes) * 60 * 1000,
value: returnTerms
};
window.sessionStorage.setItem(PickerCachePrefix + anchorId, JSON.stringify(cacheEntry));
} }
} catch (error) { } catch (error) {
// Do nothing // Do nothing
@@ -332,15 +371,21 @@ export default class SPTermStorePickerService {
return null; return null;
} }
private getTermsById(termId, useSessionStorage: boolean = true) { private getCachedTermSet(termId: string, useSessionStorage: boolean = true): ITermSet {
try { try {
if (useSessionStorage && window.sessionStorage) { if (useSessionStorage && window.sessionStorage) {
const terms = window.sessionStorage.getItem(termId); const key: string = TermCachePrefix + termId;
if (terms) { const terms: string = window.sessionStorage.getItem(key);
return JSON.parse(terms); if (!terms) {
} else {
return null; return null;
} }
const cacheEntry: ITermCacheEntry = JSON.parse(terms) as ITermCacheEntry;
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} else { } else {
return null; return null;
} }
@@ -349,6 +394,42 @@ export default class SPTermStorePickerService {
} }
} }
private getCachedPickerTerms(termId: string, useSessionStorage: boolean = true): IPickerTerm[] {
try {
if (!useSessionStorage || !window.sessionStorage) {
return null;
}
const key: string = PickerCachePrefix + termId;
const serialized: string = window.sessionStorage.getItem(key);
if (!serialized) {
return null;
}
const cacheEntry: IPickerTermCacheEntry = JSON.parse(serialized) as IPickerTermCacheEntry;
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} catch (error) {
return null;
}
}
private normalizeCacheMinutes(value: number): number {
if (typeof value !== 'number' || !isFinite(value)) {
return 15;
}
return Math.min(Math.max(Math.floor(value), 1), 1440);
}
private ensureSuccessfulResponse(response: SPHttpClientResponse, operation: string): void {
if (!response.ok) {
throw new Error(operation + ' request failed (' + response.status + ' ' + response.statusText + ').');
}
}
private searchTermsBySearchText(terms, searchText) { private searchTermsBySearchText(terms, searchText) {
if (terms) { if (terms) {
return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; }); return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
+15 -26
View File
@@ -12,29 +12,11 @@ const LOG_SOURCE: string = 'TaxonomyNavigationService';
export class TaxonomyNavigationService implements ITaxonomyNavigationService { export class TaxonomyNavigationService implements ITaxonomyNavigationService {
private _taxonomyPickerService: SPTermStorePickerService; private _taxonomyPickerService: SPTermStorePickerService;
private _noTerm: ITerm = {
_ObjectType_: '',
_ObjectIdentity_: '',
CustomSortOrderIndex: 0,
Description: '',
Id: '',
IsAvailableForTagging: false,
IsDeprecated: false,
IsRoot: true,
LocalCustomProperties: {
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte ueberpruefen Sie Ihre Einstellungen.',
_Sys_Nav_ExcludedProviders: undefined,
_Sys_Nav_SimpleLinkUrl: undefined
},
Name: 'Es wurden keine Terms gefunden. Bitte ueberpruefen Sie Ihre Einstellungen.',
PathOfTerm: '',
TermSet: undefined
};
constructor( constructor(
private context: ApplicationCustomizerContext, private context: ApplicationCustomizerContext,
private termSetName: string, private termSetNameOrId: string,
private debug: boolean = false private debug: boolean = false,
private cacheMinutes: number = 15
) { ) {
sp.setup({ sp.setup({
spfxContext: context spfxContext: context
@@ -42,10 +24,11 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
this._taxonomyPickerService = new SPTermStorePickerService( this._taxonomyPickerService = new SPTermStorePickerService(
{ {
anchorId: '', anchorId: '',
termsetNameOrID: termSetName, termsetNameOrID: termSetNameOrId,
useSessionStorage: true, useSessionStorage: true,
hideDeprecatedTags: true, hideDeprecatedTags: true,
hideTagsNotAvailableForTagging: false hideTagsNotAvailableForTagging: false,
cacheMinutes: cacheMinutes
}, },
this.context, this.context,
this.debug this.debug
@@ -54,13 +37,19 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
public async getMenuItems(): Promise<IMenuItem[]> { public async getMenuItems(): Promise<IMenuItem[]> {
const siteCollectionUrl: string = this.context.pageContext.site.absoluteUrl; const siteCollectionUrl: string = this.context.pageContext.site.absoluteUrl;
const termset: ITermSet = await this._taxonomyPickerService.getAllTerms(this.termSetName); const termset: ITermSet = await this._taxonomyPickerService.getAllTerms(
this.termSetNameOrId,
true,
false,
true,
this.cacheMinutes
);
const itemsDict: ItemDictionary<IMenuItem> = new ItemDictionary<IMenuItem>(); const itemsDict: ItemDictionary<IMenuItem> = new ItemDictionary<IMenuItem>();
const menuItems: IMenuItem[] = []; const menuItems: IMenuItem[] = [];
if (!termset || !termset.Terms) { if (!termset || !termset.Terms || termset.Terms.length === 0) {
debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.'); debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.');
return [new MenuItem(this._noTerm, 0, siteCollectionUrl)]; throw new Error('The configured navigation term set was not found or contains no terms.');
} }
termset.Terms.forEach((term: ITerm) => { termset.Terms.forEach((term: ITerm) => {
@@ -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.0","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_0280cb624fe09d9608e12e4ea730705f.js"},"MegaMenuApplicationCustomizerStrings":{"defaultPath":"megamenu-megamenuapplicationcustomizerstrings_en-us_c5b741e5b77398ea44801b5b5c0a11a3.js","type":"localizedPath","paths":{"de-DE":"megamenu-megamenuapplicationcustomizerstrings_de-de_b346aef029646a3a296989aa5e40a5ae.js","en-US":"megamenu-megamenuapplicationcustomizerstrings_en-us_c5b741e5b77398ea44801b5b5c0a11a3.js"}},"@microsoft/decorators":{"type":"component","version":"1.4.1","id":"f97266fb-ccb7-430e-9384-4124d05295d3"},"@microsoft/sp-application-base":{"type":"component","version":"1.4.1","id":"4df9bb86-ab0a-4aab-ab5f-48bf167048fb"},"@microsoft/sp-http":{"type":"component","version":"1.4.1","id":"c07208f0-ea3b-4c1a-9965-ac1b825211a6"},"@microsoft/sp-lodash-subset":{"type":"component","version":"1.4.1","id":"73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"}}}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
define([], function() {
return {
"Title": "MegaMenuApplicationCustomizer"
}
});
+5 -12
View File
@@ -2300,7 +2300,7 @@ function getManifests() {
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
"version": "1.0.4", "version": "1.1.0",
"manifestVersion": 2, "manifestVersion": 2,
"requiresCustomScript": false, "requiresCustomScript": false,
"loaderConfig": { "loaderConfig": {
@@ -2316,18 +2316,16 @@ function getManifests() {
"MegaMenuApplicationCustomizerStrings": { "MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js", "defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath", "type": "localizedPath",
"paths": {} "paths": {
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
}
}, },
"@microsoft/decorators": { "@microsoft/decorators": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3" "id": "f97266fb-ccb7-430e-9384-4124d05295d3"
}, },
"@microsoft/sp-core-library": {
"type": "component",
"version": "1.4.1",
"id": "7263c7d0-1d6a-45ec-8d85-d4d1d234171b"
},
"@microsoft/sp-application-base": { "@microsoft/sp-application-base": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
@@ -2342,11 +2340,6 @@ function getManifests() {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a" "id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"
},
"@microsoft/sp-page-context": {
"type": "component",
"version": "1.4.1",
"id": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8"
} }
} }
} }
+5 -12
View File
@@ -695,7 +695,7 @@
"alias": "MegaMenuApplicationCustomizer", "alias": "MegaMenuApplicationCustomizer",
"componentType": "Extension", "componentType": "Extension",
"extensionType": "ApplicationCustomizer", "extensionType": "ApplicationCustomizer",
"version": "1.0.4", "version": "1.1.0",
"manifestVersion": 2, "manifestVersion": 2,
"requiresCustomScript": false, "requiresCustomScript": false,
"loaderConfig": { "loaderConfig": {
@@ -711,18 +711,16 @@
"MegaMenuApplicationCustomizerStrings": { "MegaMenuApplicationCustomizerStrings": {
"defaultPath": "lib/extensions/megaMenu/loc/en-us.js", "defaultPath": "lib/extensions/megaMenu/loc/en-us.js",
"type": "localizedPath", "type": "localizedPath",
"paths": {} "paths": {
"de-DE": "lib/extensions/megaMenu/loc/de-de.js",
"en-US": "lib/extensions/megaMenu/loc/en-us.js"
}
}, },
"@microsoft/decorators": { "@microsoft/decorators": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "f97266fb-ccb7-430e-9384-4124d05295d3" "id": "f97266fb-ccb7-430e-9384-4124d05295d3"
}, },
"@microsoft/sp-core-library": {
"type": "component",
"version": "1.4.1",
"id": "7263c7d0-1d6a-45ec-8d85-d4d1d234171b"
},
"@microsoft/sp-application-base": { "@microsoft/sp-application-base": {
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
@@ -737,11 +735,6 @@
"type": "component", "type": "component",
"version": "1.4.1", "version": "1.4.1",
"id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a" "id": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a"
},
"@microsoft/sp-page-context": {
"type": "component",
"version": "1.4.1",
"id": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8"
} }
} }
} }