Compare commits
21
Commits
a2b8165262
...
v.2.2.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
767d6bfd10 | ||
|
|
79b4a8a6c4 | ||
|
|
df3df26be9 | ||
|
|
6b4a1293f3 | ||
|
|
ea2e5a6088 | ||
|
|
f95accd1ac | ||
|
|
e977fd7365 | ||
|
|
f58c6f6135 | ||
|
|
50b85468f1 | ||
|
|
bc61166267 | ||
|
|
2d91c4ba97 | ||
|
|
e75b11117f | ||
|
|
dd79e85725 | ||
|
|
e119573b5e | ||
|
|
85c56a1153 | ||
|
|
07583b8122 | ||
|
|
2d723da5a2 | ||
|
|
5f1d26c7f6 | ||
|
|
0ab514ca21 | ||
|
|
5cc88a83ba | ||
|
|
97d2b6b5d2 |
+47
@@ -0,0 +1,47 @@
|
|||||||
|
# Dependencies and local tools
|
||||||
|
node_modules/
|
||||||
|
.tools/
|
||||||
|
|
||||||
|
# SPFx build output
|
||||||
|
lib/
|
||||||
|
dist/
|
||||||
|
temp/
|
||||||
|
release/
|
||||||
|
solution/
|
||||||
|
sharepoint/solution/
|
||||||
|
*.sppkg
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Test output
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Generated TypeScript files
|
||||||
|
*.resx.ts
|
||||||
|
*.scss.ts
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Visual Studio and IDE files
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
# Operating system files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
@@ -1,199 +1,313 @@
|
|||||||
# SharePoint MegaMenu Extension
|
# SharePoint MegaMenu
|
||||||
|
|
||||||
**Version:** 1.0.2
|
Zentrale, barrierearme Portalnavigation aus SharePoint Managed Metadata für SharePoint Server Subscription Edition und SharePoint Server 2019.
|
||||||
**Framework:** SharePoint Framework (SPFx) 1.4.1
|
|
||||||
**Node.js:** v8.17.0
|
|
||||||
|
|
||||||
Eine moderne, barrierefreie Mega-Menü-Lösung für SharePoint 2019 oder SharePoint SE als ApplicationCustomizer Extension.
|
| Eigenschaft | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Version | 2.2.4 |
|
||||||
|
| SharePoint Framework | 1.4.1 |
|
||||||
|
| Node.js für den Build | 8.17.0 |
|
||||||
|
| Moderne Seiten | SPFx Application Customizer |
|
||||||
|
| Klassische Seiten | separate ScriptLink-Variante |
|
||||||
|
| Paket | `sharepoint/solution/mega-menu.sppkg` |
|
||||||
|
|
||||||
## 🌟 Features
|
## Funktionen
|
||||||
|
|
||||||
- **3-stufige Navigationshierarchie** basierend auf SharePoint Managed Metadata (Taxonomy)
|
- zentrale Konfiguration pro Site Collection
|
||||||
- **Responsive Design** mit automatischer Anpassung an verschiedene Bildschirmgrößen
|
- Termset-Auswahl bevorzugt per GUID, alternativ per Name
|
||||||
- **Barrierefreiheit** mit vollständiger Tastaturnavigation und Screenreader-Unterstützung
|
- drei Navigationsebenen
|
||||||
- **Einstellungs-Panel** für Administratoren zur Konfiguration
|
- zwei Darstellungsmodi: SharePoint-ähnliches Mega Menu und strukturelles Flyout
|
||||||
- **Externe CSS-Unterstützung** für individuelle Anpassungen
|
- benutzerdefinierte Sortierung aus dem Term Store
|
||||||
- **Office UI Fabric Integration** für konsistente SharePoint-Optik
|
- Unterstützung für `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText`
|
||||||
|
- Auflösung von `~sitecollection`
|
||||||
|
- Markierung des aktuellen Navigationspfads
|
||||||
|
- Tastatur-, Fokus-, Maus- und Touch-Bedienung
|
||||||
|
- responsive Darstellung und Unterstützung reduzierter Bewegung
|
||||||
|
- Session-Cache mit konfigurierbarer Laufzeit
|
||||||
|
- Debug-Protokollierung ohne Ausgabe im Normalbetrieb
|
||||||
|
- Unterstützung moderner und klassischer SharePoint-Seiten
|
||||||
|
|
||||||
## 📋 Voraussetzungen
|
## Architektur
|
||||||
|
|
||||||
- SharePoint 2019 oder SharePoint SE (Subscription Edition)
|
```text
|
||||||
- Node.js v8.17.0 (empfohlen)
|
App Catalog
|
||||||
- SharePoint Framework Development Tools (SPFx 1.4.1)
|
└── zentral bereitgestelltes MegaMenu-SPFx-Bundle
|
||||||
|
|
||||||
## 🚀 Installation & Entwicklung
|
Site Collection
|
||||||
|
└── SPSite.UserCustomAction
|
||||||
|
├── ComponentId
|
||||||
|
└── ClientSideComponentProperties
|
||||||
|
├── termSetId / termSetName
|
||||||
|
├── cacheMinutes
|
||||||
|
├── menuMode
|
||||||
|
└── debug
|
||||||
|
|
||||||
### 1. Repository klonen und Abhängigkeiten installieren
|
Moderne Seite Klassische Seite
|
||||||
|
└── Application Customizer └── ScriptLink + Classic-Runtime
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd MegaMenu
|
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Entwicklungsserver starten
|
Die Site-Collection-Action ist die einzige Konfigurationsquelle. Es gibt keine versteckte Konfigurationsliste, keine MegaMenu-Konfiguration im Property Bag und keine Web-scoped Kopien. PortalSettings kann diese Action zentral lesen und aktualisieren.
|
||||||
|
|
||||||
```bash
|
Das SPFx-Paket verwendet `skipFeatureDeployment: true`. Es wird im App Catalog zentral bereitgestellt und nicht in Root Web oder Unterwebs über „App hinzufügen“ installiert.
|
||||||
gulp serve
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
|
||||||
|
"termSetName": "Global Navigation",
|
||||||
|
"cacheMinutes": 15,
|
||||||
|
"cacheVersion": "1",
|
||||||
|
"menuMode": "megaMenu",
|
||||||
|
"debug": false
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Produktionspaket erstellen
|
| Property | Typ | Standard | Beschreibung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `termSetId` | String | leer | GUID des Termsets; hat Vorrang vor dem Namen |
|
||||||
|
| `termSetName` | String | leer | Fallback für bestehende Konfigurationen |
|
||||||
|
| `cacheMinutes` | Number | `15` | Cachezeit von 1 bis 1440 Minuten |
|
||||||
|
| `cacheVersion` | String | `1` | Frei wählbare Version zur sofortigen Invalidierung vorhandener Browser-Caches |
|
||||||
|
| `menuMode` | String | `megaMenu` | `megaMenu` oder `flyout` |
|
||||||
|
| `debug` | Boolean | `false` | ausführliche Browser-Konsolenausgaben |
|
||||||
|
|
||||||
```bash
|
### Mega Menu
|
||||||
gulp clean
|
|
||||||
gulp bundle --ship
|
|
||||||
gulp package-solution --ship
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Projektstruktur
|
`"menuMode": "megaMenu"` zeigt die zweite Ebene als breites, spaltenbasiertes Panel. Die dritte Ebene wird als Linkliste unter der jeweiligen Kategorie dargestellt.
|
||||||
|
|
||||||
```
|
### Flyout
|
||||||
src/
|
|
||||||
├── extensions/
|
|
||||||
│ └── megaMenu/
|
|
||||||
│ ├── MegaMenuApplicationCustomizer.ts # Haupteinstiegspunkt
|
|
||||||
│ ├── MegaMenuRenderer.ts # Menü-Rendering-Logik
|
|
||||||
│ ├── MegaMenuSettings.ts # Einstellungs-Panel
|
|
||||||
│ └── MegaMenu.css # Styling
|
|
||||||
├── services/
|
|
||||||
│ ├── TaxonomyNavigationService.ts # Taxonomy-Datenabfrage
|
|
||||||
│ ├── UserCustomActionService/ # UserCustomAction-Verwaltung
|
|
||||||
│ └── ...
|
|
||||||
└── deployment/
|
|
||||||
├── add-megamenu.ps1 # Installations-Script
|
|
||||||
└── remove-megamenu.ps1 # Deinstallations-Script
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Konfiguration
|
`"menuMode": "flyout"` zeigt die zweite Ebene als kompaktes Dropdown. Die dritte Ebene öffnet sich seitlich als strukturelles Flyout.
|
||||||
|
|
||||||
### Einstellungs-Panel (Admin-Bereich)
|
## Lokale Term-Eigenschaften
|
||||||
|
|
||||||
Das MegaMenu verfügt über ein Einstellungs-Panel, das über das Zahnrad-Icon (⚙️) im Menü zugänglich ist:
|
Die folgenden Eigenschaften werden im Term Store als lokale benutzerdefinierte Eigenschaften am jeweiligen Term gepflegt:
|
||||||
|
|
||||||
1. **Name des Navigations-Termsets**: Der Name des Managed Metadata Termsets, das als Datenquelle dient
|
| Eigenschaft | Beispiel | Wirkung |
|
||||||
2. **Pfad zu zusätzlicher CSS-Datei**: Optional - URL zu einer benutzerdefinierten CSS-Datei
|
|---|---|---|
|
||||||
|
| `MegaMenu.Hidden` | `true` | Term und darunterliegender Navigationszweig werden nicht angezeigt |
|
||||||
|
| `MegaMenu.OpenInNewWindow` | `true` | Ziel wird sicher in einem neuen Browserfenster geöffnet |
|
||||||
|
| `MegaMenu.Description` | `Anträge und Vorlagen` | optionale Beschreibung einer Kategorie im Mega-Menu-Modus |
|
||||||
|
|
||||||
### PowerShell-Deployment
|
Zusätzlich werden die SharePoint-Navigationseigenschaften `_Sys_Nav_SimpleLinkUrl`, `_Sys_Nav_TargetUrl` und `_Sys_Nav_HoverText` ausgewertet. Unsichere URL-Protokolle werden verworfen.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Installation
|
npm install
|
||||||
.\src\deployment\add-megamenu.ps1
|
npm run package
|
||||||
|
|
||||||
# Deinstallation
|
|
||||||
.\src\deployment\remove-megamenu.ps1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Anpassungen
|
`npm run package` erstellt automatisch beide Ausgaben:
|
||||||
|
|
||||||
### CSS-Customization
|
- das SPFx-Paket für moderne Seiten
|
||||||
|
- das Classic-Paket unter `classic/dist/`
|
||||||
|
|
||||||
Das MegaMenu kann über externe CSS-Dateien angepasst werden. Wichtige CSS-Klassen:
|
Vor dem Packaging werden automatisch die Kernlogik-, Classic-Syntax- und statischen Asset-Tests ausgeführt. Bei einem Fehler wird kein Paket erzeugt.
|
||||||
|
|
||||||
|
Nur die Classic-Dateien neu erzeugen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build:classic
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartete Umgebung:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Node.js 8.17.0
|
||||||
|
npm 6.x
|
||||||
|
lokales Gulp 3.9.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Ergebnis liegt unter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sharepoint/solution/mega-menu.sppkg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Moderne Bereitstellung
|
||||||
|
|
||||||
|
1. `mega-menu.sppkg` in den Farm App Catalog hochladen beziehungsweise ersetzen.
|
||||||
|
2. Die Lösung zentral für alle Sites verfügbar machen.
|
||||||
|
3. MegaMenu nicht über „App hinzufügen“ in Inhaltswebsites installieren.
|
||||||
|
4. Pro gewünschter Site Collection eine Site-Collection-Action anlegen.
|
||||||
|
|
||||||
|
Beispiel für das Mega Menu:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\deployment\add-megamenu.ps1 `
|
||||||
|
-SiteUrl 'http://clshp001/sites/target' `
|
||||||
|
-TermSetId '7948e6f9-7af4-431e-b6fe-328122f2746c' `
|
||||||
|
-TermSetName 'Global Navigation' `
|
||||||
|
-CacheMinutes 15 `
|
||||||
|
-CacheVersion '1' `
|
||||||
|
-MenuMode megaMenu
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel für das Flyout:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\deployment\add-megamenu.ps1 `
|
||||||
|
-SiteUrl 'http://clshp001/sites/target' `
|
||||||
|
-TermSetId '7948e6f9-7af4-431e-b6fe-328122f2746c' `
|
||||||
|
-MenuMode flyout `
|
||||||
|
-EnableDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript entfernt alte Web- und Site-Registrierungen, bereinigt die frühere Property-Bag-Konfiguration und erstellt genau eine neue Site-Collection-Action.
|
||||||
|
|
||||||
|
## Classic Pages
|
||||||
|
|
||||||
|
Die Classic-Variante bleibt erhalten, weil ein SPFx Application Customizer nur in der modernen Oberfläche ausgeführt wird.
|
||||||
|
|
||||||
|
Das Classic-Paket wird bei `npm run build` und `npm run package` automatisch erzeugt. Es kann bei Bedarf auch separat gebaut werden:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build:classic
|
||||||
|
```
|
||||||
|
|
||||||
|
Ausgabe:
|
||||||
|
|
||||||
|
```text
|
||||||
|
classic/dist/
|
||||||
|
├── megamenu-classic.css
|
||||||
|
├── megamenu-services-standalone.js
|
||||||
|
├── megamenu-classic.js
|
||||||
|
└── classic-deployment.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Classic-Runtime liest dieselben `ClientSideComponentProperties` der zentralen Site-Collection-Action und unterstützt ebenfalls beide Menümodi. Die Installationsdetails stehen in [classic/classic-deployment.md](classic/classic-deployment.md).
|
||||||
|
|
||||||
|
## Mobile Bedienung und Barrierefreiheit
|
||||||
|
|
||||||
|
- Unter 768 Pixeln wird die Navigation als Hamburger-Menü mit Accordion-Unterebenen dargestellt.
|
||||||
|
- Hover bleibt auf Desktop-Geräten als Komfortfunktion erhalten.
|
||||||
|
- Links und Schaltflächen zum Öffnen eines Untermenüs sind getrennt.
|
||||||
|
- Touch, Klick und Tastatur werden gleichwertig unterstützt.
|
||||||
|
- Escape schließt das aktive Menü; Links/Rechts wechselt zwischen Hauptpunkten.
|
||||||
|
- Fokusmarkierungen, Hochkontrast und reduzierte Bewegung werden unterstützt.
|
||||||
|
|
||||||
|
## Cache und Mehrsprachigkeit
|
||||||
|
|
||||||
|
Der Session-Cache ist nach Site Collection, Termset, SharePoint-Oberflächensprache, Cache-Schema und `cacheVersion` getrennt. Nach einem temporären Taxonomy-Fehler kann der letzte abgelaufene, aber strukturell gültige Eintrag als Fallback verwendet werden. Eine geänderte `cacheVersion` erzwingt beim nächsten Laden einen neuen Cache.
|
||||||
|
|
||||||
|
Term-Labels sowie sichtbare Status- und ARIA-Texte werden anhand der aktuellen SharePoint-Sprache auf Deutsch oder Englisch ausgegeben.
|
||||||
|
|
||||||
|
## Automatisierte Tests
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Tests prüfen Term-Eigenschaften, URL-Sicherheit, externe Links, sprachabhängige Labels, Cache-Schlüssel, Hierarchieaufbau und Sortierung.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Das moderne Styling liegt in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/extensions/megaMenu/MegaMenu.module.scss
|
||||||
|
```
|
||||||
|
|
||||||
|
SPFx kompiliert diese Datei in das Bundle-CSS. Die eigenständige Classic-CSS liegt unter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
classic/megamenu-classic.css
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide Varianten orientieren sich an SharePoint mit Segoe UI, neutralen Flächen, Theme-Farbe für den aktiven Pfad, kompakten Abständen und Fabric-kompatiblen Fokusmarkierungen.
|
||||||
|
|
||||||
|
Die wichtigsten CSS-Variablen können durch Custom Branding überschrieben werden:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* Haupt-Container */
|
:root {
|
||||||
#Mega-Menu { }
|
--megaMenuBarHeight: 44px;
|
||||||
|
--megaMenuContentWidth: 1200px;
|
||||||
/* Top-Level Menüpunkte */
|
--megaMenuFlyoutWidth: 288px;
|
||||||
#Mega-Menu > ul > li > a { }
|
--megaMenuZIndex: 6000;
|
||||||
|
}
|
||||||
/* Mega-Menü Dropdown */
|
|
||||||
.mega-menu { }
|
|
||||||
|
|
||||||
/* Kategorien im Dropdown */
|
|
||||||
.mega-menu-category { }
|
|
||||||
|
|
||||||
/* Links in Kategorien */
|
|
||||||
.mega-menu-category ul li a { }
|
|
||||||
|
|
||||||
/* Einstellungs-Panel */
|
|
||||||
.mm-settings-panel { }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Accessibility Features
|
Das MegaMenu lädt bewusst keine externe `cssUrl`-Property. Kundenspezifische Gestaltung gehört in die Solution Custom Branding.
|
||||||
|
|
||||||
- **Skip-Links** für Screenreader
|
## Diagnose
|
||||||
- **ARIA-Rollen** und Labels
|
|
||||||
- **Fokus-Management** mit sichtbaren Fokus-Indikatoren
|
|
||||||
- **Tastaturnavigation** mit Tab/Shift+Tab/Escape
|
|
||||||
- **High Contrast Modus** Unterstützung
|
|
||||||
|
|
||||||
## 🔧 Build-Befehle
|
Mit `"debug": true` schreibt die moderne Extension Meldungen mit diesen Quellen:
|
||||||
|
|
||||||
| Befehl | Beschreibung |
|
```text
|
||||||
|--------|-------------|
|
[MegaMenuApplicationCustomizer]
|
||||||
| `gulp serve` | Entwicklungsserver mit Live-Reload |
|
[TaxonomyNavigationService]
|
||||||
| `gulp build` | Entwicklungs-Build (Debug-Modus) |
|
[SPTermStorePickerService]
|
||||||
| `gulp bundle --ship` | Produktions-Build (Optimiert) |
|
[MegaMenuRenderer]
|
||||||
| `gulp package-solution --ship` | SharePoint Package (.sppkg) erstellen |
|
|
||||||
| `gulp clean` | Build-Artifacts löschen |
|
|
||||||
| `gulp test` | Unit-Tests ausführen |
|
|
||||||
|
|
||||||
## 📦 Deployment
|
|
||||||
|
|
||||||
1. **Paket erstellen:**
|
|
||||||
```bash
|
|
||||||
gulp clean && gulp bundle --ship && gulp package-solution --ship
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **SharePoint App Catalog:**
|
|
||||||
- `.sppkg` Datei aus `sharepoint/solution/` hochladen
|
|
||||||
- App genehmigen und für alle Sites verfügbar machen
|
|
||||||
|
|
||||||
3. **Site-spezifische Aktivierung:**
|
|
||||||
- PowerShell-Scripts verwenden
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Häufige Probleme
|
|
||||||
|
|
||||||
**Problem:** Menü wird nicht angezeigt
|
|
||||||
**Lösung:** Termset-Name in den Einstellungen prüfen
|
|
||||||
|
|
||||||
**Problem:** CSS-Styling funktioniert nicht
|
|
||||||
**Lösung:** Externe CSS-URL und CORS-Einstellungen überprüfen
|
|
||||||
|
|
||||||
**Problem:** Build-Fehler
|
|
||||||
**Lösung:** Node.js Version prüfen (v8.17.0 empfohlen)
|
|
||||||
|
|
||||||
### Debug-Modus
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Detaillierte Build-Ausgabe
|
|
||||||
gulp bundle --verbose
|
|
||||||
|
|
||||||
# TypeScript-Fehler anzeigen
|
|
||||||
gulp build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔄 Versionshistorie
|
Geladenes Bundle prüfen:
|
||||||
|
|
||||||
### v1.0.2 (Aktuell)
|
```javascript
|
||||||
- Einstellungs-Panel implementiert
|
performance.getEntriesByType('resource')
|
||||||
- Fokus-Management verbessert
|
.map(function (entry) { return entry.name; })
|
||||||
- CSS-Optimierungen für SharePoint-Konsistenz
|
.filter(function (url) { return url.toLowerCase().indexOf('mega-menu') >= 0; });
|
||||||
- Barrierefreiheit-Verbesserungen
|
```
|
||||||
|
|
||||||
### v1.0.1
|
Aktiven Modus prüfen:
|
||||||
- Responsive Design hinzugefügt
|
|
||||||
- Performance-Optimierungen
|
|
||||||
|
|
||||||
### v1.0.0
|
```javascript
|
||||||
- Initiale Release
|
document.getElementById('Mega-Menu').getAttribute('data-menu-mode');
|
||||||
- 3-stufige Navigation
|
```
|
||||||
- Grundlegende Accessibility
|
|
||||||
|
|
||||||
## 👥 Mitwirkende
|
## Projektstruktur
|
||||||
|
|
||||||
- Entwicklung: Matthias Glubrecht
|
```text
|
||||||
- Framework: SharePoint Framework (Microsoft)
|
MegaMenu/
|
||||||
- UI: Office UI Fabric (Microsoft)
|
├── classic/ Classic-Runtime und CSS
|
||||||
|
├── config/ SPFx-Buildkonfiguration
|
||||||
|
├── deployment/add-megamenu.ps1 zentrale Site-Registrierung
|
||||||
|
├── src/extensions/megaMenu/ Application Customizer, Renderer und SCSS
|
||||||
|
├── src/services/ Taxonomy- und Navigationsdienste
|
||||||
|
├── build-classic.ps1
|
||||||
|
├── gulpfile.js
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
## 📄 Lizenz
|
## Versionshistorie
|
||||||
|
|
||||||
tbd
|
### 2.2.4
|
||||||
|
|
||||||
## 🆘 Support
|
- Der Ladehinweis wird auch dann zuverlässig entfernt, wenn der Custom-Branding-Host während des asynchronen Ladens erzeugt wird.
|
||||||
|
|
||||||
Bei Fragen oder Problemen:
|
### 2.2.1
|
||||||
1. Issues im Repository erstellen
|
|
||||||
2. Dokumentation und Troubleshooting-Bereich prüfen
|
- Terms mit `_Sys_Nav_ExcludedProviders = GlobalNavigationTaxonomyProvider` werden einschließlich ihres Unterbaums
|
||||||
|
aus der modernen und klassischen globalen Navigation ausgeblendet.
|
||||||
|
- Das Cache-Schema wurde erhöht, damit die neue Sichtbarkeitsregel sofort greift.
|
||||||
|
|
||||||
|
### 2.2.0
|
||||||
|
|
||||||
|
- mobiles Hamburger-/Accordion-Menü für Modern und Classic
|
||||||
|
- getrennte Link- und Untermenü-Schaltflächen für Touch und Tastatur
|
||||||
|
- sichere URL-Prüfung und Unterstützung externer Links
|
||||||
|
- lokale Term-Eigenschaften für Sichtbarkeit, neues Fenster und Beschreibung
|
||||||
|
- sprachabhängige Term-Labels, Status- und ARIA-Texte
|
||||||
|
- versionierter Cache pro Site Collection und Sprache mit Stale-Fallback
|
||||||
|
- automatisierte Kernlogiktests
|
||||||
|
|
||||||
|
### 2.1.0
|
||||||
|
|
||||||
|
- SharePoint-ähnliches, helles Navigationsdesign.
|
||||||
|
- Neue Property `menuMode` mit `megaMenu` und `flyout`.
|
||||||
|
- Eigenständiges Flyout-Rendering für zweite und dritte Ebene.
|
||||||
|
- Classic-Runtime und Classic-CSS auf beide Modi aktualisiert.
|
||||||
|
- Konfiguration auf eine Site-Collection-Action konsolidiert.
|
||||||
|
- Veraltete `elements.xml`, Azure-CDN-Konfiguration, Mock- und UserCustomAction-Service-Dateien entfernt.
|
||||||
|
- Bereitstellungs- und Classic-Buildskripte vereinfacht.
|
||||||
|
|
||||||
|
### 2.0.1
|
||||||
|
|
||||||
|
- Zentrale SPFx-Bereitstellung mit `skipFeatureDeployment: true`.
|
||||||
|
- Keine App-Installation in einzelnen Inhaltswebsites erforderlich.
|
||||||
|
|
||||||
|
### 2.0.0
|
||||||
|
|
||||||
|
- Neue Application-Customizer-ID `c0abbffb-355d-4d4e-bd38-9e15fb811506`.
|
||||||
|
- Alte On-Premises-Manifest- und Loader-Zuordnungen umgangen.
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# MegaMenu – Arbeitsplan Version 2.2.4
|
||||||
|
|
||||||
|
Status: Implementierung abgeschlossen; Ship-Build auf der vorgesehenen Node-8-Buildumgebung ausstehend
|
||||||
|
|
||||||
|
## Funktionsumfang
|
||||||
|
|
||||||
|
- [x] Mobile Navigation als Hamburger-Menü mit aufklappbaren Unterebenen umsetzen.
|
||||||
|
- [x] Hover als Komfortfunktion beibehalten; Bedienung per Klick, Touch und Tastatur gleichwertig unterstützen.
|
||||||
|
- [x] Für Menüpunkte mit Ziel und Unterpunkten eine getrennte Öffnen-Schaltfläche anbieten.
|
||||||
|
- [x] Navigations-URLs validieren und unsichere Protokolle blockieren.
|
||||||
|
- [x] Externe Links und `MegaMenu.OpenInNewWindow` sicher unterstützen.
|
||||||
|
- [x] Lade-, Leer- und Fehlerzustände verständlich und barrierearm darstellen.
|
||||||
|
|
||||||
|
## Lokale Term-Eigenschaften
|
||||||
|
|
||||||
|
- [x] `MegaMenu.Hidden` zum gezielten Ausblenden eines Terms unterstützen.
|
||||||
|
- [x] `MegaMenu.OpenInNewWindow` für ein neues Browserfenster unterstützen.
|
||||||
|
- [x] `MegaMenu.Description` als optionale Linkbeschreibung unterstützen.
|
||||||
|
- [x] Eigenschaften in Modern und Classic identisch auswerten.
|
||||||
|
|
||||||
|
Die Eigenschaften werden im Term Store als lokale benutzerdefinierte Eigenschaften am jeweiligen Term gepflegt. Styling bleibt Aufgabe der Solution Custom Branding.
|
||||||
|
|
||||||
|
## Cache
|
||||||
|
|
||||||
|
- [x] Cache-Schlüssel um Site Collection, Sprache und Schemaversion erweitern.
|
||||||
|
- [x] Konfigurierbare `cacheVersion` zur administrativen Invalidierung ergänzen.
|
||||||
|
- [x] Abgelaufene und ungültige Cache-Einträge sicher entfernen.
|
||||||
|
- [x] Bei einem temporären Taxonomy-Fehler auf den letzten vorhandenen Cache zurückfallen.
|
||||||
|
|
||||||
|
## Mehrsprachigkeit
|
||||||
|
|
||||||
|
- [x] Sämtliche sichtbaren Status- und ARIA-Texte lokalisieren.
|
||||||
|
- [x] Bevorzugtes Term-Label anhand der aktuellen SharePoint-Sprache verwenden.
|
||||||
|
- [x] Cache nach Sprache trennen.
|
||||||
|
- [x] Deutsche und englische Ressourcen pflegen.
|
||||||
|
|
||||||
|
## Barrierefreiheit
|
||||||
|
|
||||||
|
- [x] Semantische Schaltflächen für das Öffnen von Untermenüs verwenden.
|
||||||
|
- [x] Vollständige Tastaturbedienung inklusive Pfeiltasten und Escape sicherstellen.
|
||||||
|
- [x] Fokus beim Öffnen und Schließen nachvollziehbar führen.
|
||||||
|
- [x] Mobile Accordion-Zustände über `aria-expanded` kommunizieren.
|
||||||
|
- [x] Fokusdarstellung, Hochkontrast und reduzierte Bewegung berücksichtigen.
|
||||||
|
|
||||||
|
## Qualitätssicherung
|
||||||
|
|
||||||
|
- [x] Automatisierte Tests für URL-Validierung und externe Links ergänzen.
|
||||||
|
- [x] Automatisierte Tests für lokale Term-Eigenschaften ergänzen.
|
||||||
|
- [x] Automatisierte Tests für Cache-Schlüssel und Cache-Verhalten ergänzen.
|
||||||
|
- [x] Automatisierte Tests für Hierarchie, Sortierung und aktive URLs ergänzen.
|
||||||
|
- [x] TypeScript-, Classic-Syntax-, Classic-Build- und statische Asset-Prüfungen ausführen.
|
||||||
|
- [ ] `npm run package` mit Node.js 8.17.0 auf der vorgesehenen Buildumgebung ausführen.
|
||||||
|
|
||||||
|
Der lokale SPFx-Gulp-Build wurde mit Node.js 24 gestartet und erwartungsgemäß durch die alte Gulp-3-Abhängigkeit mit `primordials is not defined` beendet. Das ist kein TypeScript- oder Projektfehler; SPFx 1.4.1 benötigt für diesen Schritt Node.js 8.17.0.
|
||||||
|
|
||||||
|
## Nicht Bestandteil dieses Durchlaufs
|
||||||
|
|
||||||
|
- [ ] PortalSettings in Version 3 als SPFx-Webpart neu aufbauen.
|
||||||
|
- [ ] MegaMenu-Diagnose und Live-Vorschau in PortalSettings integrieren.
|
||||||
+21
-259
@@ -1,272 +1,34 @@
|
|||||||
# Build script for classic SharePoint version
|
|
||||||
# Creates a standalone distribution with proper service extraction
|
|
||||||
|
|
||||||
Write-Host "Building MegaMenu Classic Distribution..." -ForegroundColor Green
|
|
||||||
|
|
||||||
# Ensure output directory exists
|
|
||||||
$outputPath = ".\classic"
|
|
||||||
if (-not (Test-Path $outputPath)) {
|
|
||||||
New-Item -ItemType Directory -Path $outputPath -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 1: Build the modern SPFx version to ensure CSS is up to date
|
|
||||||
Write-Host "Building SPFx version for CSS..." -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
gulp bundle --ship
|
|
||||||
Write-Host "✓ SPFx build completed" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "⚠ SPFx build failed, using existing CSS" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 2: Copy CSS from the built version (same CSS for both classic and modern)
|
|
||||||
Write-Host "Copying CSS files..." -ForegroundColor Yellow
|
|
||||||
$cssSource = ".\lib\extensions\megaMenu\MegaMenu.css"
|
|
||||||
$cssTarget = ".\classic\MegaMenu.css"
|
|
||||||
|
|
||||||
if (Test-Path $cssSource) {
|
|
||||||
Copy-Item $cssSource $cssTarget -Force
|
|
||||||
Write-Host "✓ CSS copied from SPFx build" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "⚠ Built CSS not found, keeping existing classic CSS" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 3: Verify standalone services exist
|
|
||||||
$servicesFile = ".\classic\megamenu-services-standalone.js"
|
|
||||||
if (Test-Path $servicesFile) {
|
|
||||||
Write-Host "✓ Standalone services found" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "✗ Missing megamenu-services-standalone.js" -ForegroundColor Red
|
|
||||||
Write-Host " Please ensure this file exists in the classic folder" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 4: Verify classic wrapper exists
|
|
||||||
$wrapperFile = ".\classic\megamenu-classic.js"
|
|
||||||
if (Test-Path $wrapperFile) {
|
|
||||||
Write-Host "✓ Classic wrapper found" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "✗ Missing megamenu-classic.js" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# Step 5: Create complete deployment package info
|
|
||||||
Write-Host "Creating deployment documentation..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
$deploymentContent = @"
|
|
||||||
# SharePoint MegaMenu Classic Deployment Guide
|
|
||||||
Version 1.0.2 - Classic SharePoint Support
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
This classic version reuses the exact same service logic as the modern SPFx version:
|
|
||||||
- **Same CSS** (MegaMenu.css from SPFx build)
|
|
||||||
- **Same Services** (TaxonomyNavigationService, MegaMenuRenderer extracted to standalone)
|
|
||||||
- **Same Configuration** (Reads UserCustomAction properties like SPFx version)
|
|
||||||
- **Same Menu Structure** (Identical 3-level navigation rendering)
|
|
||||||
|
|
||||||
## Required Files
|
|
||||||
|
|
||||||
### Core Files (Required)
|
|
||||||
1. **MegaMenu.css** - Main stylesheet (shared with SPFx version)
|
|
||||||
2. **megamenu-services-standalone.js** - Extracted services without SPFx dependencies
|
|
||||||
3. **megamenu-classic.js** - Classic SharePoint integration wrapper
|
|
||||||
|
|
||||||
### Dependencies (SharePoint Built-in)
|
|
||||||
- SP.js (SharePoint JSOM)
|
|
||||||
- SP.Taxonomy.js (Taxonomy/Metadata services)
|
|
||||||
- jQuery (usually available in SharePoint)
|
|
||||||
|
|
||||||
## Deployment Methods
|
|
||||||
|
|
||||||
### Method 1: Site Assets (Recommended)
|
|
||||||
````html
|
|
||||||
<!-- Add to master page before closing </head> tag -->
|
|
||||||
<link rel="stylesheet" href="/SiteAssets/megamenu/MegaMenu.css" />
|
|
||||||
<script src="/SiteAssets/megamenu/megamenu-services-standalone.js"></script>
|
|
||||||
<script src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
|
||||||
````
|
|
||||||
|
|
||||||
### Method 2: Style Library
|
|
||||||
````html
|
|
||||||
<link rel="stylesheet" href="/Style Library/megamenu/MegaMenu.css" />
|
|
||||||
<script src="/Style Library/megamenu/megamenu-services-standalone.js"></script>
|
|
||||||
<script src="/Style Library/megamenu/megamenu-classic.js"></script>
|
|
||||||
````
|
|
||||||
|
|
||||||
### Method 3: CDN or Document Library
|
|
||||||
Upload to any accessible location and reference accordingly.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Automatic Configuration Reading
|
|
||||||
The classic version automatically reads the same UserCustomAction configuration as the SPFx version:
|
|
||||||
- Term Set Name (default: "Navigation")
|
|
||||||
- External CSS URL (optional)
|
|
||||||
|
|
||||||
### Setting Configuration
|
|
||||||
Use one of these methods:
|
|
||||||
|
|
||||||
**Option A: Via Modern SPFx Version**
|
|
||||||
1. Install modern SPFx version on any modern SharePoint site
|
|
||||||
2. Use the settings panel to configure termset and CSS URL
|
|
||||||
3. Configuration is stored site-wide and will be read by classic version
|
|
||||||
|
|
||||||
**Option B: Via PowerShell (Advanced)**
|
|
||||||
````powershell
|
|
||||||
# Connect to SharePoint site
|
|
||||||
Add-PnPCustomAction -Name "MegaMenuConfig" -Location "ClientSideExtension.ApplicationCustomizer" `
|
|
||||||
-ClientSideComponentId "abc3361f-bb2d-491f-aba3-cd51c19a299b" `
|
|
||||||
-ClientSideComponentProperties '{"termSetName":"Your Navigation Termset","cssUrl":"/SiteAssets/custom-menu.css"}'
|
|
||||||
````
|
|
||||||
|
|
||||||
## Testing & Validation
|
|
||||||
|
|
||||||
### 1. Verify Dependencies
|
|
||||||
Open browser console and check:
|
|
||||||
````javascript
|
|
||||||
// These should all return objects/functions:
|
|
||||||
typeof SP
|
|
||||||
typeof SP.Taxonomy
|
|
||||||
typeof window.MegaMenuServices
|
|
||||||
typeof window.MegaMenuClassic
|
|
||||||
````
|
|
||||||
|
|
||||||
### 2. Enable Debug Mode
|
|
||||||
````javascript
|
|
||||||
window.MegaMenuConfig = { debug: true };
|
|
||||||
````
|
|
||||||
Then reload page to see console logging.
|
|
||||||
|
|
||||||
### 3. Manual Initialization
|
|
||||||
If auto-loading fails:
|
|
||||||
````javascript
|
|
||||||
window.MegaMenuClassic.reload();
|
|
||||||
````
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
1. **"SP is undefined"** - SP.js not loaded yet, classic wrapper will retry
|
|
||||||
2. **"MegaMenuServices not found"** - Include megamenu-services-standalone.js first
|
|
||||||
3. **"No menu items found"** - Check term set name and user permissions
|
|
||||||
4. **Menu renders but no styling** - Verify CSS path and inclusion
|
|
||||||
|
|
||||||
### Debug Steps
|
|
||||||
1. Check browser network tab for 404s on CSS/JS files
|
|
||||||
2. Verify SharePoint libraries load: SP.js, SP.Taxonomy.js
|
|
||||||
3. Check term store permissions and term set existence
|
|
||||||
4. Enable debug mode for detailed console logging
|
|
||||||
|
|
||||||
### Permissions Required
|
|
||||||
- Read access to term store
|
|
||||||
- Read access to site UserCustomActions (for configuration)
|
|
||||||
- Basic site user permissions
|
|
||||||
|
|
||||||
## Differences from SPFx Version
|
|
||||||
|
|
||||||
| Feature | SPFx Version | Classic Version |
|
|
||||||
|---------|-------------|----------------|
|
|
||||||
| Deployment | App Catalog | Master Page/Web Part |
|
|
||||||
| Configuration | Settings Panel | PowerShell or via SPFx |
|
|
||||||
| Auto-loading | Yes | Manual inclusion |
|
|
||||||
| Modern Sites | Yes | No |
|
|
||||||
| Classic Sites | No | Yes |
|
|
||||||
| SharePoint Online | Yes | Yes |
|
|
||||||
| SharePoint On-Premises | 2019+ | 2013+ |
|
|
||||||
|
|
||||||
## File Structure After Deployment
|
|
||||||
````
|
|
||||||
/SiteAssets/megamenu/
|
|
||||||
├── MegaMenu.css (from SPFx build)
|
|
||||||
├── megamenu-services-standalone.js (extracted services)
|
|
||||||
└── megamenu-classic.js (classic wrapper)
|
|
||||||
````
|
|
||||||
|
|
||||||
"@
|
|
||||||
|
|
||||||
Set-Content -Path ".\classic\deployment-guide.md" -Value $deploymentContent -Encoding UTF8
|
|
||||||
|
|
||||||
# Step 6: Create a simple deployment script template
|
|
||||||
$deployScript = @"
|
|
||||||
# PowerShell script to deploy MegaMenu Classic to SharePoint
|
|
||||||
# Requires PnP PowerShell module: Install-Module PnP.PowerShell
|
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=`$true)]
|
[string]$OutputPath = ''
|
||||||
[string]`$SiteUrl,
|
|
||||||
|
|
||||||
[string]`$LibraryName = "Site Assets",
|
|
||||||
[string]`$FolderName = "megamenu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Write-Host "Deploying MegaMenu Classic to `$SiteUrl" -ForegroundColor Green
|
$sourcePath = Join-Path $PSScriptRoot 'classic'
|
||||||
|
$resolvedOutputPath = if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||||
# Connect to SharePoint
|
Join-Path $sourcePath 'dist'
|
||||||
Connect-PnPOnline -Url `$SiteUrl -Interactive
|
}
|
||||||
|
else {
|
||||||
# Create folder if it doesn't exist
|
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
|
||||||
try {
|
|
||||||
Get-PnPFolder -Url "`$LibraryName/`$FolderName" -ErrorAction Stop
|
|
||||||
Write-Host "✓ Folder exists: `$LibraryName/`$FolderName" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Add-PnPFolder -Name `$FolderName -Folder `$LibraryName
|
|
||||||
Write-Host "✓ Created folder: `$LibraryName/`$FolderName" -ForegroundColor Green
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Upload files
|
if (-not (Test-Path -LiteralPath $resolvedOutputPath)) {
|
||||||
`$files = @(
|
New-Item -ItemType Directory -Path $resolvedOutputPath -Force | Out-Null
|
||||||
@{Source=".\MegaMenu.css"; Target="`$LibraryName/`$FolderName/MegaMenu.css"},
|
|
||||||
@{Source=".\megamenu-services-standalone.js"; Target="`$LibraryName/`$FolderName/megamenu-services-standalone.js"},
|
|
||||||
@{Source=".\megamenu-classic.js"; Target="`$LibraryName/`$FolderName/megamenu-classic.js"}
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach (`$file in `$files) {
|
|
||||||
if (Test-Path `$file.Source) {
|
|
||||||
Add-PnPFile -Path `$file.Source -Folder "`$LibraryName/`$FolderName"
|
|
||||||
Write-Host "✓ Uploaded: `$(`$file.Source)" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "✗ File not found: `$(`$file.Source)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "`nDeployment completed!" -ForegroundColor Green
|
|
||||||
Write-Host "Add these lines to your master page:" -ForegroundColor Yellow
|
|
||||||
Write-Host "<link rel=`"stylesheet`" href=`"/`$LibraryName/`$FolderName/MegaMenu.css`" />" -ForegroundColor Gray
|
|
||||||
Write-Host "<script src=`"/`$LibraryName/`$FolderName/megamenu-services-standalone.js`"></script>" -ForegroundColor Gray
|
|
||||||
Write-Host "<script src=`"/`$LibraryName/`$FolderName/megamenu-classic.js`"></script>" -ForegroundColor Gray
|
|
||||||
"@
|
|
||||||
|
|
||||||
Set-Content -Path ".\classic\deploy-to-sharepoint.ps1" -Value $deployScript -Encoding UTF8
|
|
||||||
|
|
||||||
# Step 7: Summary
|
|
||||||
Write-Host "`n" + "="*60 -ForegroundColor Cyan
|
|
||||||
Write-Host "MegaMenu Classic Build Completed!" -ForegroundColor Green
|
|
||||||
Write-Host "="*60 -ForegroundColor Cyan
|
|
||||||
|
|
||||||
Write-Host "`nDistribution files created:" -ForegroundColor Yellow
|
|
||||||
$files = @(
|
$files = @(
|
||||||
@{Name="MegaMenu.css"; Desc="Shared stylesheet from SPFx"},
|
'megamenu-classic.css',
|
||||||
@{Name="megamenu-services-standalone.js"; Desc="Extracted services (no SPFx deps)"},
|
'megamenu-services-standalone.js',
|
||||||
@{Name="megamenu-classic.js"; Desc="Classic SharePoint wrapper"},
|
'megamenu-classic.js',
|
||||||
@{Name="deployment-guide.md"; Desc="Complete deployment documentation"},
|
'classic-deployment.md'
|
||||||
@{Name="deploy-to-sharepoint.ps1"; Desc="PowerShell deployment script"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
foreach ($file in $files) {
|
foreach ($file in $files) {
|
||||||
$exists = Test-Path ".\classic\$($file.Name)"
|
$source = Join-Path $sourcePath $file
|
||||||
$status = if ($exists) { "✓" } else { "✗" }
|
if (-not (Test-Path -LiteralPath $source)) {
|
||||||
$color = if ($exists) { "Green" } else { "Red" }
|
throw ('Classic-Datei fehlt: ' + $source)
|
||||||
Write-Host " $status $($file.Name)" -ForegroundColor $color -NoNewline
|
}
|
||||||
Write-Host " - $($file.Desc)" -ForegroundColor Gray
|
|
||||||
|
Copy-Item -LiteralPath $source -Destination (Join-Path $resolvedOutputPath $file) -Force
|
||||||
|
Write-Host ('Kopiert: ' + $file) -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "`nNext steps:" -ForegroundColor Yellow
|
Write-Host ('Classic-Paket erstellt: ' + $resolvedOutputPath) -ForegroundColor Green
|
||||||
Write-Host "1. Review deployment-guide.md for instructions" -ForegroundColor White
|
|
||||||
Write-Host "2. Use deploy-to-sharepoint.ps1 for automated deployment" -ForegroundColor White
|
|
||||||
Write-Host "3. Configure termset via modern SPFx version or PowerShell" -ForegroundColor White
|
|
||||||
Write-Host "4. Test on classic SharePoint pages" -ForegroundColor White
|
|
||||||
|
|
||||||
Write-Host "`nArchitectural Achievement:" -ForegroundColor Cyan
|
|
||||||
Write-Host "✓ Same CSS as SPFx version" -ForegroundColor Green
|
|
||||||
Write-Host "✓ Same service logic (extracted to standalone)" -ForegroundColor Green
|
|
||||||
Write-Host "✓ Same configuration system" -ForegroundColor Green
|
|
||||||
Write-Host "✓ Same menu rendering output" -ForegroundColor Green
|
|
||||||
Write-Host "✓ No SPFx dependencies in classic version" -ForegroundColor Green
|
|
||||||
|
|||||||
+41
-192
@@ -1,216 +1,65 @@
|
|||||||
# MegaMenu für klassische SharePoint-Seiten
|
# MegaMenu für klassische SharePoint-Seiten
|
||||||
|
|
||||||
Diese Dateien ermöglichen die Verwendung des MegaMenus auf klassischen SharePoint-Seiten (2019/SE) ohne SPFx Framework.
|
Die Classic-Variante verwendet dieselbe Site-Collection-Konfiguration wie der moderne Application Customizer. Sie unterstützt `megaMenu` und `flyout`, wird aber unabhängig von SPFx über ScriptLink geladen.
|
||||||
|
|
||||||
## 📁 Dateien
|
## Dateien
|
||||||
|
|
||||||
- `megamenu-classic.js` - Hauptlogik für klassische Seiten
|
- `megamenu-classic.css`
|
||||||
- `megamenu-classic.css` - Styling für klassische Seiten
|
- `megamenu-services-standalone.js`
|
||||||
- `classic-deployment.md` - Diese Dokumentation
|
- `megamenu-classic.js`
|
||||||
|
|
||||||
## 🚀 Installation
|
Mit `build-classic.ps1` werden die Dateien nach `classic/dist` kopiert.
|
||||||
|
|
||||||
### Option 1: Master Page (empfohlen)
|
## Konfiguration
|
||||||
|
|
||||||
1. **Dateien hochladen:**
|
Die Classic-Variante liest die `ClientSideComponentProperties` der zentralen MegaMenu-Site-Action:
|
||||||
```
|
|
||||||
/SiteAssets/megamenu/megamenu-classic.js
|
|
||||||
/SiteAssets/megamenu/megamenu-classic.css
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Master Page bearbeiten:**
|
```json
|
||||||
```html
|
{
|
||||||
<!-- In den <head> Bereich -->
|
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
|
||||||
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
"termSetName": "Global Navigation",
|
||||||
|
"cacheMinutes": 15,
|
||||||
<!-- Vor dem schließenden </body> Tag -->
|
"cacheVersion": "1",
|
||||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
"menuMode": "megaMenu",
|
||||||
```
|
"debug": false
|
||||||
|
}
|
||||||
### Option 2: ScriptLink über PowerShell
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# CSS hinzufügen
|
|
||||||
$web = Get-SPWeb "http://your-site-url"
|
|
||||||
$web.AlternateCSSUrl = "/SiteAssets/megamenu/megamenu-classic.css"
|
|
||||||
$web.Update()
|
|
||||||
|
|
||||||
# JavaScript als Custom Action hinzufügen
|
|
||||||
$customAction = $web.UserCustomActions.Add()
|
|
||||||
$customAction.Location = "ScriptLink"
|
|
||||||
$customAction.ScriptSrc = "/SiteAssets/megamenu/megamenu-classic.js"
|
|
||||||
$customAction.Sequence = 1000
|
|
||||||
$customAction.Update()
|
|
||||||
$web.Update()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 3: Content Editor Web Part
|
`menuMode` erlaubt `megaMenu` oder `flyout`. `termSetId` hat Vorrang vor `termSetName`.
|
||||||
|
|
||||||
|
Die lokalen benutzerdefinierten Term-Eigenschaften `MegaMenu.Hidden`, `MegaMenu.OpenInNewWindow` und `MegaMenu.Description` werden wie in der modernen Variante ausgewertet. Der Cache ist nach Site Collection, Sprache und `cacheVersion` getrennt.
|
||||||
|
|
||||||
|
## Einbindung
|
||||||
|
|
||||||
|
Die drei Dateien beispielsweise nach `/SiteAssets/megamenu/` hochladen und in dieser Reihenfolge laden:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
<link rel="stylesheet" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
||||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
<script src="/SiteAssets/megamenu/megamenu-services-standalone.js"></script>
|
||||||
|
<script src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Konfiguration
|
Die Einbindung kann über die Masterpage oder eine Site-scoped ScriptLink-CustomAction erfolgen. Der moderne SPFx Application Customizer wird auf klassischen Seiten nicht ausgeführt.
|
||||||
|
|
||||||
### Basis-Konfiguration
|
## Optionaler Container
|
||||||
|
|
||||||
|
Vor dem Laden kann ein anderer Zielcontainer gesetzt werden:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
// Konfiguration vor dem Laden des Scripts setzen
|
window.MegaMenuConfig = {
|
||||||
window.MegaMenuConfig = {
|
containerId: 's4-titlerow',
|
||||||
termSetName: 'Navigation', // Name des Taxonomy Term Sets
|
debug: true
|
||||||
cssUrl: '', // Optional: Externe CSS-Datei
|
};
|
||||||
containerId: 's4-titlerow', // SharePoint Container ID
|
|
||||||
debug: true // Debug-Modus aktivieren
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Container IDs für verschiedene SharePoint-Versionen
|
## Diagnose
|
||||||
|
|
||||||
| SharePoint Version | Container ID | Beschreibung |
|
|
||||||
|-------------------|--------------|-------------|
|
|
||||||
| 2019 | `s4-titlerow` | Standard Titel-Bereich |
|
|
||||||
| SE | `suiteBarTop` | Suite Bar Bereich |
|
|
||||||
| Custom | `custom-menu-container` | Eigener Container |
|
|
||||||
|
|
||||||
### Term Set Konfiguration
|
|
||||||
|
|
||||||
Das MegaMenu liest die Navigation aus einem Managed Metadata Term Set:
|
|
||||||
|
|
||||||
```
|
|
||||||
Navigation (Term Set)
|
|
||||||
├── Produkte (Level 1)
|
|
||||||
│ ├── Software (Level 2)
|
|
||||||
│ │ ├── Office 365 (Level 3) → URL
|
|
||||||
│ │ └── SharePoint (Level 3) → URL
|
|
||||||
│ └── Hardware (Level 2)
|
|
||||||
│ ├── Laptops (Level 3) → URL
|
|
||||||
│ └── Server (Level 3) → URL
|
|
||||||
└── Services (Level 1)
|
|
||||||
└── Consulting (Level 2)
|
|
||||||
└── SharePoint Beratung (Level 3) → URL
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎨 Anpassungen
|
|
||||||
|
|
||||||
### CSS Customization
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Eigene Farben */
|
|
||||||
#Mega-Menu-Classic {
|
|
||||||
background: #your-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
#Mega-Menu > ul > li > span[role="menuitem"] {
|
|
||||||
color: #your-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Eigene Schriftarten */
|
|
||||||
#Mega-Menu {
|
|
||||||
font-family: 'Your Font', Arial, sans-serif;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Events
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Nach der Initialisierung eigene Logik hinzufügen
|
typeof window.MegaMenuServices
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
typeof window.MegaMenuClassic
|
||||||
// Warten bis MegaMenu geladen ist
|
window.MegaMenuClassic.reload()
|
||||||
setTimeout(function() {
|
|
||||||
if (window.MegaMenuClassic) {
|
|
||||||
console.log('MegaMenu ist bereit!');
|
|
||||||
// Eigene Anpassungen hier...
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Erweiterte Konfiguration
|
Benutzer benötigen Leserechte auf den verwendeten Managed-Metadata-Termstore.
|
||||||
|
|
||||||
### Custom URL Mapping
|
|
||||||
|
|
||||||
Die URLs für die Navigation-Links können angepasst werden:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Überschreibe die getTermUrl Funktion
|
|
||||||
window.MegaMenuGetTermUrl = function(term) {
|
|
||||||
var termName = term.get_name();
|
|
||||||
// Eigene URL-Logik
|
|
||||||
return '/custom-pages/' + termName.toLowerCase() + '.aspx';
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mehrsprachigkeit
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.MegaMenuConfig = {
|
|
||||||
termSetName: _spPageContextInfo.currentUICultureName === 'de-DE' ? 'Navigation_DE' : 'Navigation_EN',
|
|
||||||
// ... andere Konfigurationen
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Häufige Probleme
|
|
||||||
|
|
||||||
**Problem:** Menü wird nicht angezeigt
|
|
||||||
**Lösung:**
|
|
||||||
- Browser-Konsole auf Fehler prüfen
|
|
||||||
- Taxonomy Term Set Name überprüfen
|
|
||||||
- Berechtigungen für Term Store prüfen
|
|
||||||
|
|
||||||
**Problem:** JavaScript-Fehler "SP is not defined"
|
|
||||||
**Lösung:**
|
|
||||||
- Script erst nach SharePoint-Bibliotheken laden
|
|
||||||
- `SP.SOD.executeFunc` verwenden
|
|
||||||
|
|
||||||
**Problem:** Styling funktioniert nicht
|
|
||||||
**Lösung:**
|
|
||||||
- CSS-Pfad überprüfen
|
|
||||||
- Cache leeren
|
|
||||||
- CSS-Spezifität erhöhen
|
|
||||||
|
|
||||||
### Debug-Modus
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.MegaMenuConfig = {
|
|
||||||
debug: true, // Aktiviert Console-Logging
|
|
||||||
// ... andere Optionen
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Browser-Unterstützung
|
|
||||||
|
|
||||||
- Internet Explorer 11+
|
|
||||||
- Microsoft Edge (alle Versionen)
|
|
||||||
- Chrome 60+
|
|
||||||
- Firefox 55+
|
|
||||||
- Safari 12+
|
|
||||||
|
|
||||||
## ⚠️ Wichtige Hinweise
|
|
||||||
|
|
||||||
1. **Berechtigungen:** Benutzer benötigen Leserechte auf den Term Store
|
|
||||||
2. **Performance:** Bei großen Term Sets kann das Laden länger dauern
|
|
||||||
3. **Caching:** SharePoint cached Taxonomy-Daten - Änderungen können verzögert sichtbar werden
|
|
||||||
4. **Responsive:** Das Menü ist für mobile Geräte optimiert
|
|
||||||
|
|
||||||
## 🔄 Migration von SPFx Version
|
|
||||||
|
|
||||||
Falls Sie von der SPFx-Version migrieren:
|
|
||||||
|
|
||||||
1. SPFx ApplicationCustomizer deaktivieren
|
|
||||||
2. Klassische Dateien hochladen und einbinden
|
|
||||||
3. Konfiguration anpassen (gleiche Term Sets verwendbar)
|
|
||||||
4. Testen und CSS bei Bedarf anpassen
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
Bei Problemen:
|
|
||||||
1. Debug-Modus aktivieren
|
|
||||||
2. Browser-Konsole prüfen
|
|
||||||
3. Term Set Struktur validieren
|
|
||||||
4. Dateipfade und Berechtigungen überprüfen
|
|
||||||
|
|||||||
+386
-43
@@ -1,71 +1,414 @@
|
|||||||
/**
|
/* SharePoint MegaMenu 2.2.3 - Classic pages */
|
||||||
* MegaMenu CSS for Classic SharePoint Pages
|
:root {
|
||||||
* Version: 1.0.2
|
--megaMenuBarHeight: 44px;
|
||||||
*
|
--megaMenuContentWidth: 1200px;
|
||||||
* This file imports the exact same CSS as the modern SPFx version
|
--megaMenuFlyoutWidth: 288px;
|
||||||
* to ensure 100% visual consistency.
|
--megaMenuZIndex: 6000;
|
||||||
*
|
}
|
||||||
* Usage:
|
|
||||||
* 1. Copy the compiled MegaMenu.css from src/extensions/megaMenu/ to your SharePoint assets
|
|
||||||
* 2. Reference it in your master page or via alternate CSS URL:
|
|
||||||
* <link rel="stylesheet" href="/SiteAssets/MegaMenu.css" />
|
|
||||||
*
|
|
||||||
* This file provides additional classic-specific adjustments if needed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The main MegaMenu.css should be loaded first!
|
|
||||||
* This file only contains classic SharePoint specific overrides.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Classic SharePoint specific container adjustments */
|
|
||||||
.megamenu-classic-container {
|
.megamenu-classic-container {
|
||||||
/* Insert after s4-titlerow or other SharePoint containers */
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
border-top: 1px solid #edebe9;
|
||||||
|
border-bottom: 1px solid #edebe9;
|
||||||
|
z-index: var(--megaMenuZIndex);
|
||||||
|
font-family: "Segoe UI", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-main {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--megaMenuBarHeight);
|
||||||
|
overflow: visible;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #323130;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-status {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-bottom: 1px solid #edebe9;
|
||||||
|
background: #f3f9fd;
|
||||||
|
color: #323130;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-toggle { display: none; }
|
||||||
|
|
||||||
|
.mega-menu-top-level {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--megaMenuContentWidth);
|
||||||
|
min-height: var(--megaMenuBarHeight);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-top-item {
|
||||||
|
position: static;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure proper z-index in classic SharePoint context */
|
.mega-menu-top-item:hover,
|
||||||
#Mega-Menu {
|
.mega-menu-top-item:focus-within,
|
||||||
|
.mega-menu-top-item:active,
|
||||||
|
.mega-menu-top-item.is-open,
|
||||||
|
.mega-menu-top-item.is-active {
|
||||||
|
background: #f3f2f1;
|
||||||
|
color: #323130;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mode-flyout .mega-menu-top-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 999; /* Below SharePoint dialogs but above content */
|
}
|
||||||
|
|
||||||
|
.menu-item-link,
|
||||||
|
.menu-item-text {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: var(--megaMenuBarHeight);
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #323130;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 32px;
|
||||||
|
min-height: var(--megaMenuBarHeight);
|
||||||
|
margin: 0 8px 0 -10px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #323130;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle > span::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-right: 1px solid currentColor;
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle[aria-expanded="true"] > span::after { transform: rotate(225deg); }
|
||||||
|
|
||||||
|
.menu-item-link:hover,
|
||||||
|
.menu-item-text:hover,
|
||||||
|
.menu-item-toggle:hover,
|
||||||
|
.menu-item-toggle[aria-expanded="true"] {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-link:focus,
|
||||||
|
.menu-item-text:focus,
|
||||||
|
.menu-item-toggle:focus,
|
||||||
|
.mega-menu-mobile-toggle:focus,
|
||||||
|
.mega-menu-category a:focus,
|
||||||
|
.flyout-link:focus {
|
||||||
|
outline: 2px solid #0078d4;
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu {
|
.mega-menu {
|
||||||
z-index: 1000; /* Above the main menu */
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
display: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #edebe9;
|
||||||
|
border-top: 0;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .16);
|
||||||
|
color: #323130;
|
||||||
|
z-index: calc(var(--megaMenuZIndex) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Classic SharePoint ribbon compatibility */
|
.mega-menu.js-open {
|
||||||
body.ms-backgroundImage #Mega-Menu {
|
display: block;
|
||||||
/* Adjust if SharePoint has background images */
|
}
|
||||||
|
|
||||||
|
.mega-menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--megaMenuContentWidth);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
gap: 24px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category {
|
||||||
|
min-width: 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-description {
|
||||||
|
margin: -4px 0 12px;
|
||||||
|
color: #605e5c;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category h3 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0 0 8px;
|
||||||
|
border-bottom: 1px solid #edebe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category h3 a,
|
||||||
|
.mega-menu-category h3 span {
|
||||||
|
display: block;
|
||||||
|
color: #323130;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category h3 a:hover {
|
||||||
|
color: #0078d4;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category li a {
|
||||||
|
display: block;
|
||||||
|
margin: 0 -8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #605e5c;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-category li a:hover {
|
||||||
|
background: #f3f2f1;
|
||||||
|
color: #323130;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-flyout {
|
||||||
|
right: auto;
|
||||||
|
width: var(--megaMenuFlyoutWidth);
|
||||||
|
min-width: 240px;
|
||||||
|
border-top: 1px solid #edebe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
background: #fff;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure settings panel works in classic mode too */
|
.flyout-link {
|
||||||
.mm-settings-panel {
|
position: relative;
|
||||||
z-index: 4001; /* Above everything else */
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 36px 8px 16px;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #323130;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Classic SharePoint v4.master specific adjustments */
|
.flyout-item:hover > .flyout-link,
|
||||||
.v4master #Mega-Menu {
|
.flyout-item:focus-within > .flyout-link,
|
||||||
/* Any v4.master specific styles if needed */
|
.flyout-item:active > .flyout-link {
|
||||||
|
background: #f3f2f1;
|
||||||
|
color: #323130;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Classic SharePoint seattle.master specific adjustments */
|
.flyout-toggle {
|
||||||
.seattle #Mega-Menu {
|
position: absolute;
|
||||||
/* Any seattle.master specific styles if needed */
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #323130;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments for classic SharePoint layouts */
|
.flyout-toggle > span::after {
|
||||||
@media (max-width: 1024px) {
|
content: "";
|
||||||
.megamenu-classic-container {
|
display: block;
|
||||||
/* Classic SharePoint is often used on older devices */
|
width: 6px;
|
||||||
overflow-x: auto;
|
height: 6px;
|
||||||
|
border-top: 1px solid currentColor;
|
||||||
|
border-right: 1px solid currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-toggle:focus { outline: 2px solid #0078d4; outline-offset: -2px; }
|
||||||
|
|
||||||
|
.flyout-level-3 {
|
||||||
|
position: absolute;
|
||||||
|
top: -9px;
|
||||||
|
left: 100%;
|
||||||
|
display: none;
|
||||||
|
width: var(--megaMenuFlyoutWidth);
|
||||||
|
border: 1px solid #edebe9;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .16);
|
||||||
|
z-index: calc(var(--megaMenuZIndex) + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item:hover > .flyout-level-3,
|
||||||
|
.flyout-item:focus-within > .flyout-level-3,
|
||||||
|
.flyout-item.is-open > .flyout-level-3 {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mega-menu-mobile-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--megaMenuBarHeight);
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #323130;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-icon,
|
||||||
|
.mega-menu-mobile-icon::before,
|
||||||
|
.mega-menu-mobile-icon::after {
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-icon { position: relative; margin-right: 12px; }
|
||||||
|
.mega-menu-mobile-icon::before,
|
||||||
|
.mega-menu-mobile-icon::after { content: ""; position: absolute; left: 0; }
|
||||||
|
.mega-menu-mobile-icon::before { top: -6px; }
|
||||||
|
.mega-menu-mobile-icon::after { top: 6px; }
|
||||||
|
|
||||||
|
.mega-menu-top-level {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid #edebe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-main.is-mobile-open .mega-menu-top-level { display: flex; }
|
||||||
|
|
||||||
|
.mega-menu-top-item,
|
||||||
|
.mega-menu-mode-flyout .mega-menu-top-item {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 44px;
|
||||||
|
border-bottom: 1px solid #edebe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle { width: 44px; margin: 0; border-left: 0; }
|
||||||
|
|
||||||
|
.mega-menu {
|
||||||
|
position: static;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
border-right: 0;
|
||||||
|
border-left: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-level-3 {
|
||||||
|
position: static;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item.is-open > .flyout-level-3 { display: block; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Print styles for classic SharePoint */
|
|
||||||
@media print {
|
@media print {
|
||||||
#Mega-Menu,
|
#Mega-Menu,
|
||||||
.megamenu-classic-container {
|
.megamenu-classic-container {
|
||||||
|
|||||||
+71
-51
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* SharePoint MegaMenu for Classic Pages
|
* SharePoint MegaMenu for Classic Pages
|
||||||
* Version: 1.0.2
|
* Version: 2.2.0
|
||||||
*
|
*
|
||||||
* This version uses standalone services that replicate the SPFx logic
|
* This version uses standalone services that replicate the SPFx logic
|
||||||
* without requiring the SharePoint Framework.
|
* without requiring the SharePoint Framework.
|
||||||
@@ -19,12 +19,46 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var MEGAMENU_UCA_ID = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; // Same as in MegaMenuApplicationCustomizer.ts
|
var MEGAMENU_UCA_ID = 'c0abbffb-355d-4d4e-bd38-9e15fb811506'; // Same as in MegaMenuApplicationCustomizer.ts
|
||||||
|
|
||||||
var config = window.MegaMenuConfig || {
|
var config = window.MegaMenuConfig || {};
|
||||||
containerId: 's4-titlerow',
|
config.containerId = config.containerId || 's4-titlerow';
|
||||||
debug: false
|
config.debug = config.debug === true;
|
||||||
};
|
|
||||||
|
function getLanguageLcid() {
|
||||||
|
return Number(window._spPageContextInfo && _spPageContextInfo.currentLanguage) || 1033;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStrings() {
|
||||||
|
return getLanguageLcid() === 1031 ? {
|
||||||
|
navigationLabel: 'Hauptnavigation', openNavigation: 'Hauptnavigation öffnen', closeNavigation: 'Hauptnavigation schließen',
|
||||||
|
openSubmenu: 'Untermenü öffnen für', closeSubmenu: 'Untermenü schließen für', submenuLabel: 'Untermenü',
|
||||||
|
externalLink: 'externer Link', loading: 'Navigation wird geladen…', missing: 'Die Navigation ist nicht konfiguriert.',
|
||||||
|
empty: 'Die Navigation enthält keine sichtbaren Einträge.', error: 'Die Navigation konnte nicht geladen werden.'
|
||||||
|
} : {
|
||||||
|
navigationLabel: 'Main navigation', openNavigation: 'Open main navigation', closeNavigation: 'Close main navigation',
|
||||||
|
openSubmenu: 'Open submenu for', closeSubmenu: 'Close submenu for', submenuLabel: 'submenu',
|
||||||
|
externalLink: 'external link', loading: 'Loading navigation…', missing: 'Navigation is not configured.',
|
||||||
|
empty: 'The navigation does not contain any visible entries.', error: 'Navigation could not be loaded.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(message) {
|
||||||
|
var existing = document.querySelector('.megamenu-classic-container');
|
||||||
|
var container = existing || document.createElement('div');
|
||||||
|
container.className = 'megamenu-classic-container';
|
||||||
|
container.innerHTML = '';
|
||||||
|
var status = document.createElement('div');
|
||||||
|
status.className = 'mega-menu-status';
|
||||||
|
status.setAttribute('role', 'status');
|
||||||
|
status.textContent = message;
|
||||||
|
container.appendChild(status);
|
||||||
|
if (!existing) {
|
||||||
|
var anchor = document.getElementById(config.containerId) || document.body.firstChild;
|
||||||
|
anchor.parentNode.insertBefore(container, anchor.nextSibling);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
function log(message, data) {
|
function log(message, data) {
|
||||||
if (config.debug && console && console.log) {
|
if (config.debug && console && console.log) {
|
||||||
@@ -84,44 +118,30 @@
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data && data.value && data.value.length > 0) {
|
var actions = data && data.value
|
||||||
var uca = data.value[0];
|
? data.value
|
||||||
|
: (data && data.d && data.d.results ? data.d.results : []);
|
||||||
|
if (actions.length > 0) {
|
||||||
|
var uca = actions[0];
|
||||||
try {
|
try {
|
||||||
var props = JSON.parse(uca.ClientSideComponentProperties);
|
var props = JSON.parse(uca.ClientSideComponentProperties);
|
||||||
log('Configuration found', props);
|
log('Configuration found', props);
|
||||||
callback(props);
|
callback(props);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('Error parsing UserCustomAction properties: ' + e.message);
|
log('Error parsing UserCustomAction properties: ' + e.message);
|
||||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
callback({});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log('No UserCustomAction found - using defaults');
|
log('No UserCustomAction found - using defaults');
|
||||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
callback({});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
log('Error reading configuration: ' + error.message);
|
log('Error reading configuration: ' + error.message);
|
||||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
callback({});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load external CSS (same as SPFx version)
|
|
||||||
function loadExternalCSS(cssUrl) {
|
|
||||||
if (!cssUrl) return;
|
|
||||||
|
|
||||||
var link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.type = 'text/css';
|
|
||||||
link.href = cssUrl;
|
|
||||||
link.onerror = function() {
|
|
||||||
log('Failed to load external CSS: ' + cssUrl);
|
|
||||||
};
|
|
||||||
link.onload = function() {
|
|
||||||
log('External CSS loaded successfully: ' + cssUrl);
|
|
||||||
};
|
|
||||||
document.head.appendChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main initialization
|
// Main initialization
|
||||||
function initMegaMenu() {
|
function initMegaMenu() {
|
||||||
log('Initializing MegaMenu for classic SharePoint...');
|
log('Initializing MegaMenu for classic SharePoint...');
|
||||||
@@ -133,12 +153,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
readMegaMenuConfiguration(function(props) {
|
readMegaMenuConfiguration(function(props) {
|
||||||
|
config.debug = props.debug === true || config.debug === true;
|
||||||
log('Using configuration:', props);
|
log('Using configuration:', props);
|
||||||
|
var strings = getStrings();
|
||||||
// Load external CSS if specified
|
var termSetIdentifier = props.termSetId || props.termSetName;
|
||||||
if (props.cssUrl) {
|
if (!termSetIdentifier) {
|
||||||
loadExternalCSS(props.cssUrl);
|
showStatus(strings.missing);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
var loadingContainer = showStatus(strings.loading);
|
||||||
|
|
||||||
// Create context
|
// Create context
|
||||||
var context = createClassicContext();
|
var context = createClassicContext();
|
||||||
@@ -146,13 +169,25 @@
|
|||||||
// Create taxonomy service using standalone implementation
|
// Create taxonomy service using standalone implementation
|
||||||
var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService(
|
var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService(
|
||||||
context,
|
context,
|
||||||
props.termSetName
|
termSetIdentifier,
|
||||||
|
{
|
||||||
|
cacheMinutes: props.cacheMinutes,
|
||||||
|
cacheVersion: props.cacheVersion,
|
||||||
|
languageLcid: getLanguageLcid()
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load menu items
|
// Load menu items
|
||||||
taxonomyService.getMenuItems()
|
taxonomyService.getMenuItems()
|
||||||
.then(function(menuItems) {
|
.then(function(menuItems) {
|
||||||
log('Menu items loaded:', menuItems);
|
log('Menu items loaded:', menuItems);
|
||||||
|
if (!menuItems || menuItems.length === 0) {
|
||||||
|
showStatus(strings.empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loadingContainer && loadingContainer.parentNode) {
|
||||||
|
loadingContainer.parentNode.removeChild(loadingContainer);
|
||||||
|
}
|
||||||
|
|
||||||
// Find target container
|
// Find target container
|
||||||
var container = document.getElementById(config.containerId);
|
var container = document.getElementById(config.containerId);
|
||||||
@@ -179,9 +214,8 @@
|
|||||||
var renderer = new window.MegaMenuServices.MegaMenuRenderer(
|
var renderer = new window.MegaMenuServices.MegaMenuRenderer(
|
||||||
context,
|
context,
|
||||||
menuItems,
|
menuItems,
|
||||||
function(updatedProps) {
|
props.menuMode === 'flyout' ? 'flyout' : 'megaMenu',
|
||||||
log('Properties updated (not persisted in classic mode):', updatedProps);
|
strings
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the menu
|
// Render the menu
|
||||||
@@ -191,21 +225,7 @@
|
|||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
console.error('[MegaMenu] Error loading menu items:', error);
|
console.error('[MegaMenu] Error loading menu items:', error);
|
||||||
|
showStatus(strings.error);
|
||||||
// Show error message to user
|
|
||||||
var container = document.getElementById(config.containerId);
|
|
||||||
if (container) {
|
|
||||||
var errorDiv = document.createElement('div');
|
|
||||||
errorDiv.style.cssText = 'background:#ffebee;color:#c62828;padding:10px;border:1px solid #ef5350;margin:5px 0;';
|
|
||||||
errorDiv.innerHTML = '<strong>MegaMenu Error:</strong> ' + error.message +
|
|
||||||
'<br><small>Check browser console for details.</small>';
|
|
||||||
|
|
||||||
if (container.nextSibling) {
|
|
||||||
container.parentNode.insertBefore(errorDiv, container.nextSibling);
|
|
||||||
} else {
|
|
||||||
container.parentNode.appendChild(errorDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
constructor(term, depth, siteCollectionUrl) {
|
constructor(term, depth, siteCollectionUrl) {
|
||||||
this.title = term.Name;
|
this.title = term.Name;
|
||||||
this.url = this._getNavigationUrl(term, siteCollectionUrl);
|
this.url = this._getNavigationUrl(term, siteCollectionUrl);
|
||||||
|
const properties = term.LocalCustomProperties || {};
|
||||||
|
this.description = String(properties['MegaMenu.Description'] || '').trim();
|
||||||
|
this.openInNewWindow = this._asBoolean(properties['MegaMenu.OpenInNewWindow']);
|
||||||
|
this.external = this._isExternal(this.url, siteCollectionUrl);
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.pathDepth = this._calculateDepth(term.PathOfTerm);
|
this.pathDepth = this._calculateDepth(term.PathOfTerm);
|
||||||
this.parentId = this._getParentId(term.PathOfTerm);
|
this.parentId = this._getParentId(term.PathOfTerm);
|
||||||
@@ -51,14 +55,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getNavigationUrl(term, siteCollectionUrl) {
|
_getNavigationUrl(term, siteCollectionUrl) {
|
||||||
// Extract URL from term properties
|
const properties = term.LocalCustomProperties || {};
|
||||||
if (term.LocalCustomProperties && term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl) {
|
const rawUrl = properties._Sys_Nav_SimpleLinkUrl || properties._Sys_Nav_TargetUrl;
|
||||||
return term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl;
|
if (!rawUrl) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (/[\u0000-\u001f\u007f]/.test(String(rawUrl))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const resolved = rawUrl.indexOf('~sitecollection') === 0
|
||||||
|
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
|
||||||
|
: rawUrl;
|
||||||
|
const protocol = String(resolved).match(/^([a-z][a-z0-9+.-]*):/i);
|
||||||
|
if (protocol && ['http', 'https', 'mailto'].indexOf(protocol[1].toLowerCase()) < 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: generate URL based on term name
|
_asBoolean(value) {
|
||||||
const termName = term.Name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
return value === true || ['true', '1', 'yes', 'ja'].indexOf(String(value || '').toLowerCase()) >= 0;
|
||||||
return `${siteCollectionUrl}/pages/${termName}.aspx`;
|
}
|
||||||
|
|
||||||
|
_isExternal(url, siteCollectionUrl) {
|
||||||
|
const target = String(url || '').match(/^(?:(https?):)?\/\/([^/]+)/i);
|
||||||
|
const current = String(siteCollectionUrl || '').match(/^(https?):\/\/([^/]+)/i);
|
||||||
|
return !!target && (!current || (target[1] || current[1]).toLowerCase() !== current[1].toLowerCase() || target[2].toLowerCase() !== current[2].toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,49 +89,90 @@
|
|||||||
// ===========================================
|
// ===========================================
|
||||||
|
|
||||||
class TaxonomyNavigationService {
|
class TaxonomyNavigationService {
|
||||||
constructor(context, termSetName) {
|
constructor(context, termSetIdentifier, options) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.termSetName = termSetName;
|
this.termSetIdentifier = termSetIdentifier;
|
||||||
|
this.options = options || {};
|
||||||
this._siteCollectionUrl = context.pageContext.site.absoluteUrl;
|
this._siteCollectionUrl = context.pageContext.site.absoluteUrl;
|
||||||
|
this._languageLcid = Number(this.options.languageLcid) || 1033;
|
||||||
|
this._cacheMinutes = Math.min(Math.max(Number(this.options.cacheMinutes) || 15, 1), 1440);
|
||||||
|
this._cacheVersion = String(this.options.cacheVersion || '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMenuItems() {
|
async getMenuItems() {
|
||||||
console.log('[TaxonomyService] Loading terms for:', this.termSetName);
|
console.log('[TaxonomyService] Loading terms for:', this.termSetIdentifier);
|
||||||
|
const cached = this._getCache(false);
|
||||||
|
if (cached) {
|
||||||
|
return this._processTerms(cached);
|
||||||
|
}
|
||||||
|
const stale = this._getCache(true);
|
||||||
try {
|
try {
|
||||||
const termSet = await this._loadTermSet(this.termSetName);
|
const termSet = await this._loadTermSet(this.termSetIdentifier);
|
||||||
|
this._setCache(termSet);
|
||||||
return this._processTerms(termSet);
|
return this._processTerms(termSet);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[TaxonomyService] Error loading terms:', error);
|
console.error('[TaxonomyService] Error loading terms:', error);
|
||||||
return [this._createNoTermsItem()];
|
if (stale) {
|
||||||
|
console.warn('[TaxonomyService] Using stale cache after taxonomy error.');
|
||||||
|
return this._processTerms(stale);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadTermSet(termSetName) {
|
_cacheKey() {
|
||||||
|
const normalize = value => encodeURIComponent(String(value || '').trim().toLowerCase());
|
||||||
|
return 'MegaMenu:TermSet:v4:' + normalize(this._siteCollectionUrl) + ':' + normalize(this.termSetIdentifier) + ':' + this._languageLcid + ':' + normalize(this._cacheVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCache(allowExpired) {
|
||||||
|
try {
|
||||||
|
const serialized = window.sessionStorage.getItem(this._cacheKey());
|
||||||
|
if (!serialized) return null;
|
||||||
|
const entry = JSON.parse(serialized);
|
||||||
|
if (!entry || !entry.value || !entry.expiresAt) {
|
||||||
|
window.sessionStorage.removeItem(this._cacheKey());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry.expiresAt > Date.now() || allowExpired ? entry.value : null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setCache(value) {
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(this._cacheKey(), JSON.stringify({
|
||||||
|
expiresAt: Date.now() + this._cacheMinutes * 60 * 1000,
|
||||||
|
value: value
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[TaxonomyService] Cache could not be written.', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadTermSet(termSetIdentifier) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const context = SP.ClientContext.get_current();
|
const context = SP.ClientContext.get_current();
|
||||||
const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
|
const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
|
||||||
const termStore = session.getDefaultSiteCollectionTermStore();
|
const termStore = session.getDefaultSiteCollectionTermStore();
|
||||||
const termSets = termStore.getTermSetsByName(termSetName, 1033);
|
|
||||||
|
|
||||||
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(termSetIdentifier)) {
|
||||||
|
const termSet = termStore.getTermSet(new SP.Guid(termSetIdentifier));
|
||||||
|
this._loadAllTerms(context, termSet, resolve, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const termSets = termStore.getTermSetsByName(termSetIdentifier, this._languageLcid);
|
||||||
context.load(termSets);
|
context.load(termSets);
|
||||||
|
|
||||||
context.executeQueryAsync(
|
context.executeQueryAsync(
|
||||||
() => {
|
() => {
|
||||||
if (termSets.get_count() > 0) {
|
if (termSets.get_count() > 0) {
|
||||||
const termSet = termSets.get_item(0);
|
const termSet = termSets.get_item(0);
|
||||||
const terms = termSet.get_terms();
|
this._loadAllTerms(context, termSet, resolve, reject);
|
||||||
context.load(terms);
|
|
||||||
|
|
||||||
context.executeQueryAsync(
|
|
||||||
() => {
|
|
||||||
// Load all terms with their properties and children
|
|
||||||
this._loadAllTermsRecursively(context, terms, resolve, reject);
|
|
||||||
},
|
|
||||||
(sender, args) => reject(new Error(args.get_message()))
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`Term set '${termSetName}' not found`));
|
reject(new Error(`Term set '${termSetIdentifier}' not found`));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(sender, args) => reject(new Error(args.get_message()))
|
(sender, args) => reject(new Error(args.get_message()))
|
||||||
@@ -117,32 +180,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadAllTermsRecursively(context, terms, resolve, reject) {
|
_loadAllTerms(context, termSet, resolve, reject) {
|
||||||
const allTerms = [];
|
const terms = termSet.getAllTerms();
|
||||||
const termsEnum = terms.getEnumerator();
|
context.load(terms, 'Include(Id,Name,PathOfTerm,LocalCustomProperties,IsAvailableForTagging,IsDeprecated)');
|
||||||
|
|
||||||
// First pass: collect all terms
|
|
||||||
while (termsEnum.moveNext()) {
|
|
||||||
const term = termsEnum.get_current();
|
|
||||||
context.load(term, 'Id', 'Name', 'PathOfTerm', 'LocalCustomProperties');
|
|
||||||
allTerms.push(term);
|
|
||||||
|
|
||||||
// Load child terms
|
|
||||||
const childTerms = term.get_terms();
|
|
||||||
context.load(childTerms);
|
|
||||||
this._loadChildTermsRecursively(context, childTerms, allTerms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute query to load all data
|
|
||||||
context.executeQueryAsync(
|
context.executeQueryAsync(
|
||||||
() => {
|
() => {
|
||||||
const processedTerms = allTerms.map(term => ({
|
const processedTerms = [];
|
||||||
Id: term.get_id().toString(),
|
const enumerator = terms.getEnumerator();
|
||||||
Name: term.get_name(),
|
while (enumerator.moveNext()) {
|
||||||
PathOfTerm: term.get_pathOfTerm(),
|
const term = enumerator.get_current();
|
||||||
LocalCustomProperties: this._getCustomProperties(term),
|
const properties = this._getCustomProperties(term);
|
||||||
IsRoot: term.get_pathOfTerm().split(';').length === 1
|
if (!term.get_isAvailableForTagging() || term.get_isDeprecated() ||
|
||||||
}));
|
this._asBoolean(properties['MegaMenu.Hidden']) ||
|
||||||
|
this._isExcludedFromGlobalNavigation(properties)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processedTerms.push({
|
||||||
|
Id: term.get_id().toString(),
|
||||||
|
Name: term.get_name(),
|
||||||
|
PathOfTerm: term.get_pathOfTerm(),
|
||||||
|
LocalCustomProperties: properties,
|
||||||
|
IsRoot: term.get_pathOfTerm().split(';').length === 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
resolve(processedTerms);
|
resolve(processedTerms);
|
||||||
},
|
},
|
||||||
@@ -150,40 +210,48 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadChildTermsRecursively(context, childTerms, allTerms) {
|
|
||||||
const childEnum = childTerms.getEnumerator();
|
|
||||||
while (childEnum.moveNext()) {
|
|
||||||
const childTerm = childEnum.get_current();
|
|
||||||
context.load(childTerm, 'Id', 'Name', 'PathOfTerm', 'LocalCustomProperties');
|
|
||||||
allTerms.push(childTerm);
|
|
||||||
|
|
||||||
// Recursively load grandchildren
|
|
||||||
const grandChildTerms = childTerm.get_terms();
|
|
||||||
context.load(grandChildTerms);
|
|
||||||
this._loadChildTermsRecursively(context, grandChildTerms, allTerms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getCustomProperties(term) {
|
_getCustomProperties(term) {
|
||||||
try {
|
try {
|
||||||
const props = term.get_localCustomProperties();
|
const props = term.get_localCustomProperties();
|
||||||
return {
|
return {
|
||||||
_Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null,
|
_Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null,
|
||||||
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null
|
_Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null,
|
||||||
|
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null,
|
||||||
|
_Sys_Nav_ExcludedProviders: props._Sys_Nav_ExcludedProviders || null,
|
||||||
|
'MegaMenu.Hidden': props['MegaMenu.Hidden'] || null,
|
||||||
|
'MegaMenu.OpenInNewWindow': props['MegaMenu.OpenInNewWindow'] || null,
|
||||||
|
'MegaMenu.Description': props['MegaMenu.Description'] || null
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_asBoolean(value) {
|
||||||
|
return value === true || ['true', '1', 'yes', 'ja'].indexOf(String(value || '').toLowerCase()) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isExcludedFromGlobalNavigation(properties) {
|
||||||
|
const value = String((properties || {})._Sys_Nav_ExcludedProviders || '').replace(/["']/g, '');
|
||||||
|
return value.split(/[;,|]/).some(provider =>
|
||||||
|
provider.trim().toLowerCase() === 'globalnavigationtaxonomyprovider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_processTerms(termsData) {
|
_processTerms(termsData) {
|
||||||
const itemsDict = new ItemDictionary();
|
const itemsDict = new ItemDictionary();
|
||||||
|
const itemsByPath = {};
|
||||||
const menuItems = [];
|
const menuItems = [];
|
||||||
|
const visibleTerms = termsData.filter(termData =>
|
||||||
|
!this._asBoolean((termData.LocalCustomProperties || {})['MegaMenu.Hidden']) &&
|
||||||
|
!this._isExcludedFromGlobalNavigation(termData.LocalCustomProperties)
|
||||||
|
);
|
||||||
|
|
||||||
// Create MenuItem objects
|
// Create MenuItem objects
|
||||||
termsData.forEach(termData => {
|
visibleTerms.forEach(termData => {
|
||||||
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
|
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
|
||||||
itemsDict.Add(termData.Id, menuItem);
|
itemsDict.Add(termData.Id, menuItem);
|
||||||
|
itemsByPath[termData.PathOfTerm] = menuItem;
|
||||||
|
|
||||||
if (menuItem.pathDepth === 1) {
|
if (menuItem.pathDepth === 1) {
|
||||||
menuItems.push(menuItem);
|
menuItems.push(menuItem);
|
||||||
@@ -191,11 +259,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build hierarchy
|
// Build hierarchy
|
||||||
termsData.forEach(termData => {
|
visibleTerms.forEach(termData => {
|
||||||
if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) {
|
if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) {
|
||||||
const menuItem = itemsDict.Get(termData.Id);
|
const menuItem = itemsDict.Get(termData.Id);
|
||||||
const parentId = menuItem.parentId;
|
const pathParts = termData.PathOfTerm.split(';');
|
||||||
const parentItem = itemsDict.Get(parentId);
|
pathParts.pop();
|
||||||
|
const parentItem = itemsByPath[pathParts.join(';')];
|
||||||
|
|
||||||
if (parentItem) {
|
if (parentItem) {
|
||||||
parentItem.items.push(menuItem);
|
parentItem.items.push(menuItem);
|
||||||
@@ -221,10 +290,11 @@
|
|||||||
// ===========================================
|
// ===========================================
|
||||||
|
|
||||||
class MegaMenuRenderer {
|
class MegaMenuRenderer {
|
||||||
constructor(context, menuItems, updateCallback) {
|
constructor(context, menuItems, menuMode, strings) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.menuItems = menuItems;
|
this.menuItems = menuItems;
|
||||||
this.updateCallback = updateCallback;
|
this.menuMode = menuMode === 'flyout' ? 'flyout' : 'megaMenu';
|
||||||
|
this.strings = strings || { navigationLabel: 'Main navigation', openNavigation: 'Open navigation', closeNavigation: 'Close navigation', openSubmenu: 'Open submenu for', closeSubmenu: 'Close submenu for', submenuLabel: 'submenu', externalLink: 'external link' };
|
||||||
}
|
}
|
||||||
|
|
||||||
render(container) {
|
render(container) {
|
||||||
@@ -232,23 +302,29 @@
|
|||||||
|
|
||||||
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 mega-menu-mode-' + this.menuMode;
|
||||||
nav.setAttribute('role', 'navigation');
|
nav.setAttribute('data-menu-mode', this.menuMode);
|
||||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
nav.setAttribute('aria-label', this.strings.navigationLabel);
|
||||||
|
|
||||||
|
const mobileToggle = document.createElement('button');
|
||||||
|
mobileToggle.type = 'button';
|
||||||
|
mobileToggle.className = 'mega-menu-mobile-toggle';
|
||||||
|
mobileToggle.setAttribute('aria-expanded', 'false');
|
||||||
|
mobileToggle.setAttribute('aria-controls', 'Mega-Menu-Items');
|
||||||
|
mobileToggle.setAttribute('aria-label', this.strings.openNavigation);
|
||||||
|
mobileToggle.innerHTML = '<span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label"></span>';
|
||||||
|
mobileToggle.querySelector('.mega-menu-mobile-label').textContent = this.strings.navigationLabel;
|
||||||
|
nav.appendChild(mobileToggle);
|
||||||
|
|
||||||
const topLevelUl = document.createElement('ul');
|
const topLevelUl = document.createElement('ul');
|
||||||
topLevelUl.setAttribute('role', 'menubar');
|
topLevelUl.className = 'mega-menu-top-level';
|
||||||
|
topLevelUl.id = 'Mega-Menu-Items';
|
||||||
|
|
||||||
this.menuItems.forEach(topLevelItem => {
|
this.menuItems.forEach(topLevelItem => {
|
||||||
const topLevelLi = this.createTopLevelItem(topLevelItem);
|
const topLevelLi = this.createTopLevelItem(topLevelItem);
|
||||||
topLevelUl.appendChild(topLevelLi);
|
topLevelUl.appendChild(topLevelLi);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add settings if user has permissions (simplified check)
|
|
||||||
if (this._hasManagePermissions()) {
|
|
||||||
topLevelUl.appendChild(this.createSettingsItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.appendChild(topLevelUl);
|
nav.appendChild(topLevelUl);
|
||||||
container.appendChild(nav);
|
container.appendChild(nav);
|
||||||
|
|
||||||
@@ -258,32 +334,42 @@
|
|||||||
|
|
||||||
createTopLevelItem(item) {
|
createTopLevelItem(item) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.setAttribute('role', 'none');
|
const hasChildren = item.items && item.items.length > 0;
|
||||||
|
li.className = hasChildren ? 'mega-menu-top-item has-children' : 'mega-menu-top-item';
|
||||||
|
|
||||||
if (item.url && item.items.length === 0) {
|
if (item.url) {
|
||||||
// Simple link
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = item.url;
|
a.href = item.url;
|
||||||
a.textContent = item.title;
|
a.textContent = item.title;
|
||||||
a.className = 'menu-item-link';
|
a.className = 'menu-item-link';
|
||||||
a.setAttribute('role', 'menuitem');
|
this.applyLinkBehavior(a, item);
|
||||||
a.setAttribute('tabindex', '0');
|
|
||||||
li.appendChild(a);
|
li.appendChild(a);
|
||||||
} else {
|
} else {
|
||||||
// Menu with submenu
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.textContent = item.title;
|
span.textContent = item.title;
|
||||||
span.className = 'menu-item-text';
|
span.className = 'menu-item-text';
|
||||||
span.setAttribute('role', 'menuitem');
|
if (!hasChildren) span.setAttribute('tabindex', '0');
|
||||||
span.setAttribute('tabindex', '0');
|
|
||||||
span.setAttribute('aria-haspopup', 'true');
|
|
||||||
span.setAttribute('aria-expanded', 'false');
|
|
||||||
li.appendChild(span);
|
li.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.items.length > 0) {
|
if (hasChildren) {
|
||||||
const megaMenu = this.createMegaMenu(item.items);
|
const toggle = document.createElement('button');
|
||||||
li.appendChild(megaMenu);
|
const menuId = 'Mega-Menu-Panel-' + String(item.id).replace(/[^a-z0-9_-]/gi, '');
|
||||||
}
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'menu-item-toggle';
|
||||||
|
toggle.setAttribute('aria-haspopup', 'true');
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
toggle.setAttribute('aria-controls', menuId);
|
||||||
|
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.title);
|
||||||
|
toggle.innerHTML = '<span aria-hidden="true"></span>';
|
||||||
|
li.appendChild(toggle);
|
||||||
|
const megaMenu = this.menuMode === 'flyout'
|
||||||
|
? this.createFlyoutMenu(item.items)
|
||||||
|
: this.createMegaMenu(item.items);
|
||||||
|
megaMenu.id = menuId;
|
||||||
|
megaMenu.setAttribute('aria-expanded', 'false');
|
||||||
|
megaMenu.setAttribute('aria-label', item.title + ' ' + this.strings.submenuLabel);
|
||||||
|
li.appendChild(megaMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
return li;
|
return li;
|
||||||
@@ -292,7 +378,6 @@
|
|||||||
createMegaMenu(categories) {
|
createMegaMenu(categories) {
|
||||||
const megaMenuDiv = document.createElement('div');
|
const megaMenuDiv = document.createElement('div');
|
||||||
megaMenuDiv.className = 'mega-menu';
|
megaMenuDiv.className = 'mega-menu';
|
||||||
megaMenuDiv.setAttribute('role', 'menu');
|
|
||||||
|
|
||||||
const gridDiv = document.createElement('div');
|
const gridDiv = document.createElement('div');
|
||||||
gridDiv.className = 'mega-menu-grid';
|
gridDiv.className = 'mega-menu-grid';
|
||||||
@@ -316,7 +401,7 @@
|
|||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = category.url;
|
a.href = category.url;
|
||||||
a.textContent = category.title;
|
a.textContent = category.title;
|
||||||
a.setAttribute('tabindex', '0');
|
this.applyLinkBehavior(a, category);
|
||||||
h3.appendChild(a);
|
h3.appendChild(a);
|
||||||
} else {
|
} else {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
@@ -326,19 +411,23 @@
|
|||||||
|
|
||||||
categoryDiv.appendChild(h3);
|
categoryDiv.appendChild(h3);
|
||||||
|
|
||||||
|
if (category.description) {
|
||||||
|
const description = document.createElement('p');
|
||||||
|
description.className = 'mega-menu-description';
|
||||||
|
description.textContent = category.description;
|
||||||
|
categoryDiv.appendChild(description);
|
||||||
|
}
|
||||||
|
|
||||||
if (category.items.length > 0) {
|
if (category.items.length > 0) {
|
||||||
const ul = document.createElement('ul');
|
const ul = document.createElement('ul');
|
||||||
ul.setAttribute('role', 'group');
|
|
||||||
|
|
||||||
category.items.forEach(link => {
|
category.items.forEach(link => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.setAttribute('role', 'none');
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = link.url;
|
a.href = link.url || '#';
|
||||||
a.textContent = link.title;
|
a.textContent = link.title;
|
||||||
a.setAttribute('role', 'menuitem');
|
this.applyLinkBehavior(a, link);
|
||||||
a.setAttribute('tabindex', '0');
|
|
||||||
|
|
||||||
li.appendChild(a);
|
li.appendChild(a);
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
@@ -350,37 +439,129 @@
|
|||||||
return categoryDiv;
|
return categoryDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSettingsItem() {
|
createFlyoutMenu(items) {
|
||||||
|
const flyout = document.createElement('div');
|
||||||
|
flyout.className = 'mega-menu mega-menu-flyout';
|
||||||
|
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'flyout-list flyout-level-2';
|
||||||
|
items.forEach(item => list.appendChild(this.createFlyoutItem(item)));
|
||||||
|
|
||||||
|
flyout.appendChild(list);
|
||||||
|
return flyout;
|
||||||
|
}
|
||||||
|
|
||||||
|
createFlyoutItem(item) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.setAttribute('role', 'none');
|
const hasChildren = item.items && item.items.length > 0;
|
||||||
|
li.className = hasChildren ? 'flyout-item has-children' : 'flyout-item';
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const element = document.createElement(item.url ? 'a' : 'span');
|
||||||
button.className = 'menu-item-settings';
|
if (item.url) {
|
||||||
button.setAttribute('type', 'button');
|
element.href = item.url;
|
||||||
button.setAttribute('role', 'menuitem');
|
this.applyLinkBehavior(element, item);
|
||||||
button.setAttribute('tabindex', '0');
|
} else {
|
||||||
button.setAttribute('aria-label', 'Einstellungen');
|
element.setAttribute('tabindex', '0');
|
||||||
button.onclick = () => this._openSettings();
|
}
|
||||||
|
element.textContent = item.title;
|
||||||
|
element.className = hasChildren ? 'flyout-link flyout-link-has-children' : 'flyout-link';
|
||||||
|
li.appendChild(element);
|
||||||
|
|
||||||
const icon = document.createElement('i');
|
if (hasChildren) {
|
||||||
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
|
const toggle = document.createElement('button');
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
const childId = 'Mega-Menu-Flyout-' + String(item.id).replace(/[^a-z0-9_-]/gi, '');
|
||||||
button.appendChild(icon);
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'flyout-toggle';
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
toggle.setAttribute('aria-controls', childId);
|
||||||
|
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.title);
|
||||||
|
toggle.innerHTML = '<span aria-hidden="true"></span>';
|
||||||
|
li.appendChild(toggle);
|
||||||
|
const childList = document.createElement('ul');
|
||||||
|
childList.className = 'flyout-list flyout-level-3';
|
||||||
|
childList.id = childId;
|
||||||
|
childList.setAttribute('aria-label', item.title + ' ' + this.strings.submenuLabel);
|
||||||
|
item.items.forEach(child => childList.appendChild(this.createFlyoutItem(child)));
|
||||||
|
li.appendChild(childList);
|
||||||
|
}
|
||||||
|
|
||||||
li.appendChild(button);
|
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachEventListeners() {
|
attachEventListeners() {
|
||||||
// Keyboard and mouse event handling (simplified)
|
const renderer = this;
|
||||||
const menuItems = document.querySelectorAll('#Mega-Menu [role="menuitem"]');
|
const toggles = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle');
|
||||||
menuItems.forEach(item => {
|
Array.prototype.forEach.call(toggles, toggle => {
|
||||||
item.addEventListener('keydown', this.handleKeyDown.bind(this));
|
const parent = toggle.parentElement;
|
||||||
|
const panel = parent.querySelector('.mega-menu');
|
||||||
|
const setOpen = open => {
|
||||||
|
renderer.closeAll(panel);
|
||||||
|
toggle.setAttribute('aria-expanded', String(open));
|
||||||
|
toggle.setAttribute('aria-label', (open ? renderer.strings.closeSubmenu : renderer.strings.openSubmenu) + ' ' + renderer.getToggleLabel(toggle));
|
||||||
|
parent.classList.toggle('is-open', open);
|
||||||
|
panel.setAttribute('aria-expanded', String(open));
|
||||||
|
panel.classList.toggle('js-open', open);
|
||||||
|
};
|
||||||
|
toggle.addEventListener('click', event => { event.preventDefault(); setOpen(toggle.getAttribute('aria-expanded') !== 'true'); });
|
||||||
|
toggle.addEventListener('keydown', event => {
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault(); setOpen(true);
|
||||||
|
const firstLink = panel.querySelector('a');
|
||||||
|
if (firstLink) firstLink.focus();
|
||||||
|
} else if (event.key === 'Escape' || event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault(); setOpen(false); toggle.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let closeTimer;
|
||||||
|
parent.addEventListener('mouseenter', () => { window.clearTimeout(closeTimer); setOpen(true); });
|
||||||
|
parent.addEventListener('mouseleave', () => { closeTimer = window.setTimeout(() => setOpen(false), 0); });
|
||||||
|
});
|
||||||
|
|
||||||
const parent = item.parentElement;
|
const flyoutToggles = document.querySelectorAll('#Mega-Menu .flyout-toggle');
|
||||||
if (parent && parent.querySelector('.mega-menu')) {
|
Array.prototype.forEach.call(flyoutToggles, toggle => {
|
||||||
parent.addEventListener('mouseenter', this.showMegaMenu);
|
const parent = toggle.parentElement;
|
||||||
parent.addEventListener('mouseleave', this.hideMegaMenu);
|
const setOpen = open => {
|
||||||
|
toggle.setAttribute('aria-expanded', String(open));
|
||||||
|
const label = parent.querySelector('.flyout-link');
|
||||||
|
toggle.setAttribute('aria-label', (open ? renderer.strings.closeSubmenu : renderer.strings.openSubmenu) + ' ' + (label ? label.textContent : ''));
|
||||||
|
parent.classList.toggle('is-open', open);
|
||||||
|
};
|
||||||
|
toggle.addEventListener('click', event => { event.preventDefault(); setOpen(toggle.getAttribute('aria-expanded') !== 'true'); });
|
||||||
|
toggle.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); setOpen(false); toggle.focus(); } });
|
||||||
|
parent.addEventListener('mouseenter', () => setOpen(true));
|
||||||
|
parent.addEventListener('mouseleave', () => setOpen(false));
|
||||||
|
parent.addEventListener('focusin', () => setOpen(true));
|
||||||
|
parent.addEventListener('focusout', () => window.setTimeout(() => { if (!parent.contains(document.activeElement)) setOpen(false); }, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const mobileToggle = document.querySelector('#Mega-Menu > .mega-menu-mobile-toggle');
|
||||||
|
if (mobileToggle) {
|
||||||
|
mobileToggle.addEventListener('click', () => {
|
||||||
|
const nav = mobileToggle.parentElement;
|
||||||
|
const open = mobileToggle.getAttribute('aria-expanded') !== 'true';
|
||||||
|
mobileToggle.setAttribute('aria-expanded', String(open));
|
||||||
|
mobileToggle.setAttribute('aria-label', open ? renderer.strings.closeNavigation : renderer.strings.openNavigation);
|
||||||
|
nav.classList.toggle('is-mobile-open', open);
|
||||||
|
if (!open) renderer.closeAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
const openPanel = document.querySelector('#Mega-Menu .mega-menu.js-open');
|
||||||
|
if (openPanel) {
|
||||||
|
const toggle = openPanel.previousElementSibling;
|
||||||
|
renderer.closePanel(toggle, openPanel);
|
||||||
|
toggle.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && document.activeElement.closest('.mega-menu-top-item')) {
|
||||||
|
const items = Array.prototype.slice.call(document.querySelectorAll('#Mega-Menu > ul > .mega-menu-top-item'));
|
||||||
|
const current = document.activeElement.closest('.mega-menu-top-item');
|
||||||
|
const index = items.indexOf(current);
|
||||||
|
const target = items[(index + (event.key === 'ArrowRight' ? 1 : -1) + items.length) % items.length];
|
||||||
|
const focusTarget = target.querySelector('a, .menu-item-toggle');
|
||||||
|
if (focusTarget) { event.preventDefault(); focusTarget.focus(); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -395,61 +576,35 @@
|
|||||||
document.body.appendChild(announcer);
|
document.body.appendChild(announcer);
|
||||||
}
|
}
|
||||||
|
|
||||||
showMegaMenu() {
|
closePanel(toggle, panel) {
|
||||||
const megaMenu = this.querySelector('.mega-menu');
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
if (megaMenu) {
|
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + this.getToggleLabel(toggle));
|
||||||
megaMenu.classList.add('js-open');
|
panel.setAttribute('aria-expanded', 'false');
|
||||||
const menuItem = this.querySelector('[role="menuitem"]');
|
panel.classList.remove('js-open');
|
||||||
if (menuItem) {
|
}
|
||||||
menuItem.setAttribute('aria-expanded', 'true');
|
|
||||||
}
|
closeAll(exceptPanel) {
|
||||||
|
const panels = document.querySelectorAll('#Mega-Menu .mega-menu.js-open');
|
||||||
|
Array.prototype.forEach.call(panels, panel => {
|
||||||
|
if (panel !== exceptPanel) this.closePanel(panel.previousElementSibling, panel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getToggleLabel(toggle) {
|
||||||
|
const label = toggle.parentElement.querySelector('.menu-item-link, .menu-item-text');
|
||||||
|
return label ? label.textContent : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLinkBehavior(link, item) {
|
||||||
|
if (item.openInNewWindow) {
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
}
|
||||||
|
if (item.external) {
|
||||||
|
link.setAttribute('aria-label', item.title + ' (' + this.strings.externalLink + ')');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideMegaMenu() {
|
|
||||||
const megaMenu = this.querySelector('.mega-menu');
|
|
||||||
if (megaMenu) {
|
|
||||||
megaMenu.classList.remove('js-open');
|
|
||||||
const menuItem = this.querySelector('[role="menuitem"]');
|
|
||||||
if (menuItem) {
|
|
||||||
menuItem.setAttribute('aria-expanded', 'false');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown(e) {
|
|
||||||
// Keyboard navigation logic (simplified)
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
const parent = e.target.parentElement;
|
|
||||||
const megaMenu = parent.querySelector('.mega-menu');
|
|
||||||
if (megaMenu) {
|
|
||||||
megaMenu.classList.toggle('js-open');
|
|
||||||
e.target.setAttribute('aria-expanded',
|
|
||||||
megaMenu.classList.contains('js-open') ? 'true' : 'false');
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
const megaMenu = document.querySelector('.mega-menu.js-open');
|
|
||||||
if (megaMenu) {
|
|
||||||
megaMenu.classList.remove('js-open');
|
|
||||||
const menuItem = megaMenu.parentElement.querySelector('[role="menuitem"]');
|
|
||||||
if (menuItem) {
|
|
||||||
menuItem.setAttribute('aria-expanded', 'false');
|
|
||||||
menuItem.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasManagePermissions() {
|
|
||||||
// Simplified permission check for classic SharePoint
|
|
||||||
return _spPageContextInfo.isSiteAdmin || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_openSettings() {
|
|
||||||
console.log('[MegaMenu] Settings not implemented in classic mode');
|
|
||||||
alert('Einstellungen sind nur in der modernen SPFx-Version verfügbar.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================
|
// ===========================================
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
|
||||||
"workingDir": "./temp/deploy/",
|
|
||||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
|
||||||
"container": "mega-menu",
|
|
||||||
"accessKey": "<!-- ACCESS KEY -->"
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"solution": {
|
"solution": {
|
||||||
"name": "mega-menu-client-side-solution",
|
"name": "mega-menu-client-side-solution",
|
||||||
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
|
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
|
||||||
"version": "1.0.0",
|
"version": "2.2.4.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": true
|
"skipFeatureDeployment": true
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-4
@@ -6,11 +6,14 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"pageUrl": "https://sharepoint.contoso.local/SitePages/Neues-Zuhause.aspx",
|
"pageUrl": "https://sharepoint.contoso.local/SitePages/Neues-Zuhause.aspx",
|
||||||
"customActions": {
|
"customActions": {
|
||||||
"abc3361f-bb2d-491f-aba3-cd51c19a299b": {
|
"c0abbffb-355d-4d4e-bd38-9e15fb811506": {
|
||||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||||
"properties": {
|
"properties": {
|
||||||
"termSetName": "Horizontale Navigation",
|
"termSetName": "Horizontale Navigation",
|
||||||
"additionalCssUrl": "/siteAssets/Navigation/Navigation.css"
|
"cacheMinutes": 15,
|
||||||
|
"cacheVersion": "1",
|
||||||
|
"menuMode": "megaMenu",
|
||||||
|
"debug": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,10 +21,14 @@
|
|||||||
"megaMenu": {
|
"megaMenu": {
|
||||||
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
|
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
|
||||||
"customActions": {
|
"customActions": {
|
||||||
"abc3361f-bb2d-491f-aba3-cd51c19a299b": {
|
"c0abbffb-355d-4d4e-bd38-9e15fb811506": {
|
||||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||||
"properties": {
|
"properties": {
|
||||||
"testMessage": "Test message"
|
"termSetName": "Global Navigation",
|
||||||
|
"cacheMinutes": 15,
|
||||||
|
"cacheVersion": "1",
|
||||||
|
"menuMode": "flyout",
|
||||||
|
"debug": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-69
@@ -1,104 +1,118 @@
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[Alias('WebUrl')]
|
|
||||||
[string]$SiteUrl,
|
[string]$SiteUrl,
|
||||||
|
|
||||||
[string]$TermSetName = '',
|
[string]$TermSetName = '',
|
||||||
|
|
||||||
[string]$CssUrl = '',
|
[string]$TermSetId = '',
|
||||||
|
|
||||||
|
[ValidateRange(1, 1440)]
|
||||||
|
[int]$CacheMinutes = 15,
|
||||||
|
|
||||||
|
[string]$CacheVersion = '1',
|
||||||
|
|
||||||
|
[ValidateSet('megaMenu', 'flyout')]
|
||||||
|
[string]$MenuMode = 'megaMenu',
|
||||||
|
|
||||||
[switch]$EnableDebug,
|
[switch]$EnableDebug,
|
||||||
|
|
||||||
[string]$Description = 'MSFT-Custom-Solution:MegaMenu'
|
[string]$Description = 'MSFT-Custom-Solution:MegaMenu'
|
||||||
)
|
)
|
||||||
|
|
||||||
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue | Out-Null
|
if (-not (Get-Command -Name Get-SPSite -ErrorAction SilentlyContinue)) {
|
||||||
|
if (Get-Module -ListAvailable -Name Microsoft.SharePoint.PowerShell) {
|
||||||
$componentId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
Import-Module Microsoft.SharePoint.PowerShell -DisableNameChecking
|
||||||
$location = 'ClientSideExtension.ApplicationCustomizer'
|
}
|
||||||
$name = 'MegaMenu'
|
elseif (Get-PSSnapin -Registered -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) {
|
||||||
$title = 'Mega Menu'
|
Add-PSSnapin Microsoft.SharePoint.PowerShell
|
||||||
|
}
|
||||||
function Get-MegaMenuPropertiesJson {
|
else {
|
||||||
param(
|
throw 'Die SharePoint PowerShell-Komponenten wurden nicht gefunden. Bitte in der SharePoint Management Shell ausfuehren.'
|
||||||
[string]$CurrentTermSetName,
|
}
|
||||||
[string]$CurrentCssUrl,
|
|
||||||
[bool]$CurrentDebug
|
|
||||||
)
|
|
||||||
|
|
||||||
return (@{
|
|
||||||
termSetName = $CurrentTermSetName
|
|
||||||
cssUrl = $CurrentCssUrl
|
|
||||||
debug = $CurrentDebug
|
|
||||||
} | ConvertTo-Json -Depth 10 -Compress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Remove-WebScopedComponentActions {
|
$componentId = [Guid]'c0abbffb-355d-4d4e-bd38-9e15fb811506'
|
||||||
param(
|
$legacyComponentId = [Guid]'abc3361f-bb2d-491f-aba3-cd51c19a299b'
|
||||||
[Microsoft.SharePoint.SPSite]$CurrentSite,
|
$legacyPropertyName = 'PortalSettings.MegaMenu.Configuration'
|
||||||
[string]$CurrentComponentId,
|
|
||||||
[string]$CurrentLocation
|
|
||||||
)
|
|
||||||
|
|
||||||
$removedCount = 0
|
if ([string]::IsNullOrWhiteSpace($TermSetId) -and [string]::IsNullOrWhiteSpace($TermSetName)) {
|
||||||
|
throw 'Bitte TermSetId oder TermSetName angeben.'
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($web in $CurrentSite.AllWebs) {
|
$properties = [string](@{
|
||||||
|
termSetName = $TermSetName.Trim()
|
||||||
|
termSetId = $TermSetId.Trim()
|
||||||
|
cacheMinutes = $CacheMinutes
|
||||||
|
cacheVersion = $CacheVersion
|
||||||
|
menuMode = $MenuMode
|
||||||
|
debug = [bool]$EnableDebug.IsPresent
|
||||||
|
} | ConvertTo-Json -Depth 10 -Compress)
|
||||||
|
|
||||||
|
$site = Get-SPSite -Identity $SiteUrl
|
||||||
|
|
||||||
|
try {
|
||||||
|
@($site.UserCustomActions | Where-Object {
|
||||||
|
$_.ClientSideComponentId -eq $componentId -or
|
||||||
|
$_.ClientSideComponentId -eq $legacyComponentId
|
||||||
|
}) | ForEach-Object {
|
||||||
|
Write-Host ('Entferne alte Site-Action: ' + $_.Id)
|
||||||
|
$_.Delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($site.RootWeb.AllProperties.ContainsKey($legacyPropertyName)) {
|
||||||
|
$site.RootWeb.AllProperties.Remove($legacyPropertyName)
|
||||||
|
$site.RootWeb.Update()
|
||||||
|
Write-Host ('Alte Property-Bag-Konfiguration entfernt: ' + $legacyPropertyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning ('Die alte Property-Bag-Konfiguration konnte nicht entfernt werden: ' + $_.Exception.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($web in $site.AllWebs) {
|
||||||
try {
|
try {
|
||||||
$webActions = @($web.UserCustomActions | Where-Object {
|
$webUrl = $web.Url
|
||||||
$_.Location -eq $CurrentLocation -and
|
$actions = @($web.UserCustomActions | Where-Object {
|
||||||
$_.ClientSideComponentId -and
|
$_.ClientSideComponentId -eq $componentId -or
|
||||||
$_.ClientSideComponentId.ToString().ToLower() -eq $CurrentComponentId
|
$_.ClientSideComponentId -eq $legacyComponentId
|
||||||
})
|
})
|
||||||
|
|
||||||
foreach ($webAction in $webActions) {
|
foreach ($action in $actions) {
|
||||||
$web.UserCustomActions.Delete($webAction.Id)
|
Write-Host ('Entferne alte Web-Action: ' + $webUrl + ' / ' + $action.Id)
|
||||||
$removedCount++
|
$action.Delete()
|
||||||
}
|
|
||||||
|
|
||||||
if ($webActions.Count -gt 0) {
|
|
||||||
$web.Update()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
$web.Dispose()
|
$web.Dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return $removedCount
|
finally {
|
||||||
|
$site.Dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
$site = Get-SPSite -Identity $SiteUrl
|
$site = Get-SPSite -Identity $SiteUrl
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$removedWebScopedActions = Remove-WebScopedComponentActions -CurrentSite $site -CurrentComponentId $componentId -CurrentLocation $location
|
$action = $site.UserCustomActions.Add()
|
||||||
|
$action.Name = [Guid]::NewGuid().ToString('B')
|
||||||
$existingAction = $site.UserCustomActions | Where-Object {
|
$action.Title = 'MegaMenu Application Customizer'
|
||||||
$_.Location -eq $location -and $_.ClientSideComponentId -and $_.ClientSideComponentId.ToString().ToLower() -eq $componentId
|
|
||||||
} | Select-Object -First 1
|
|
||||||
|
|
||||||
if ($existingAction) {
|
|
||||||
$action = $existingAction
|
|
||||||
Write-Host 'Aktualisiere vorhandene site-scoped Mega Menu UserCustomAction...' -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$action = $site.UserCustomActions.Add()
|
|
||||||
Write-Host 'Erstelle neue site-scoped Mega Menu UserCustomAction...' -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
$action.Name = $name
|
|
||||||
$action.Title = $title
|
|
||||||
$action.Description = $Description
|
$action.Description = $Description
|
||||||
$action.Location = $location
|
$action.Location = 'ClientSideExtension.ApplicationCustomizer'
|
||||||
$action.ClientSideComponentId = [Guid]$componentId
|
$action.ClientSideComponentId = $componentId
|
||||||
$action.ClientSideComponentProperties = Get-MegaMenuPropertiesJson -CurrentTermSetName $TermSetName -CurrentCssUrl $CssUrl -CurrentDebug ([bool]$EnableDebug.IsPresent)
|
$action.ClientSideComponentProperties = $properties
|
||||||
|
$action.Sequence = 100
|
||||||
$action.Update()
|
$action.Update()
|
||||||
|
|
||||||
Write-Host 'Mega Menu wurde site-scoped registriert.' -ForegroundColor Green
|
Write-Host ''
|
||||||
Write-Host ('Entfernte web-scoped Eintraege: ' + $removedWebScopedActions)
|
Write-Host 'MegaMenu wurde zentral fuer die Site Collection registriert.' -ForegroundColor Green
|
||||||
Write-Host ('Beschreibung: ' + $Description)
|
Write-Host ('Site Collection: ' + $site.Url)
|
||||||
Write-Host ('Properties: ' + $action.ClientSideComponentProperties)
|
Write-Host ('Action-ID: ' + $action.Id)
|
||||||
|
Write-Host ('Menu mode: ' + $MenuMode)
|
||||||
|
Write-Host ('Properties: ' + $properties)
|
||||||
|
Write-Host 'Das MegaMenu-SPFx-Paket muss im App Catalog zentral bereitgestellt sein.' -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if ($site) {
|
$site.Dispose()
|
||||||
$site.Dispose()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"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": [
|
|
||||||
"https://localhost:4321/"
|
|
||||||
],
|
|
||||||
"scriptResources": {
|
|
||||||
"mega-menu-application-customizer": {
|
|
||||||
"type": "path",
|
|
||||||
"path": "dist/mega-menu-application-customizer.js"
|
|
||||||
},
|
|
||||||
"MegaMenuApplicationCustomizerStrings": {
|
|
||||||
"defaultPath": "lib/extensions/megaMenu/loc/en-us.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,5 +3,6 @@
|
|||||||
const build = require('@microsoft/sp-build-web');
|
const build = require('@microsoft/sp-build-web');
|
||||||
|
|
||||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||||
|
build.addSuppression(`Warning - MegaMenuApplicationCustomizer: Admins can make this solution available to all sites in the organization, but extensions won’t automatically appear. SharePoint Framework extensions must be specifically associated to sites, lists, and fields programmatically to be visible to site users.`);
|
||||||
|
|
||||||
build.initialize(require('gulp'));
|
build.initialize(require('gulp'));
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
|
|||||||
declare const styles: {
|
|
||||||
mmSlideIn: string;
|
|
||||||
mmFadeIn: string;
|
|
||||||
};
|
|
||||||
export default styles;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
require('./MegaMenu.module.css');
|
|
||||||
var styles = {
|
|
||||||
mmSlideIn: 'mmSlideIn_09a8e1a7',
|
|
||||||
mmFadeIn: 'mmFadeIn_09a8e1a7',
|
|
||||||
};
|
|
||||||
export default styles;
|
|
||||||
/* tslint:enable */
|
|
||||||
|
|
||||||
//# sourceMappingURL=MegaMenu.module.scss.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"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"}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
|
|
||||||
import './MegaMenu.module.scss';
|
|
||||||
export declare const UserCustomActionMegaMenuId: string;
|
|
||||||
/**
|
|
||||||
* Properties for the MegaMenu Application Customizer
|
|
||||||
*/
|
|
||||||
export interface IMegaMenuApplicationCustomizerProperties {
|
|
||||||
/**
|
|
||||||
* The name of the termset to load menu items from
|
|
||||||
*/
|
|
||||||
termSetName: string;
|
|
||||||
/**
|
|
||||||
* Optional URL to an external CSS file
|
|
||||||
*/
|
|
||||||
cssUrl?: string;
|
|
||||||
}
|
|
||||||
/** A Custom Action which can be run during execution of a Client Side Application */
|
|
||||||
export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
|
|
||||||
private _topPlaceholder;
|
|
||||||
onInit(): Promise<void>;
|
|
||||||
private _renderPlaceHolders();
|
|
||||||
private _updateCallback;
|
|
||||||
private _renderMegaMenu(termSetName);
|
|
||||||
private _getOrCreateContainer(id, placeholder);
|
|
||||||
private _loadExternalCss(cssUrl);
|
|
||||||
private _onDispose();
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// tslint:disable:max-line-length
|
|
||||||
// tslint:disable:match-default-export-name
|
|
||||||
// tslint:disable:typedef
|
|
||||||
// tslint:disable:variable-name
|
|
||||||
var __extends = (this && this.__extends) || (function () {
|
|
||||||
var extendStatics = Object.setPrototypeOf ||
|
|
||||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
||||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
|
||||||
return function (d, b) {
|
|
||||||
extendStatics(d, b);
|
|
||||||
function __() { this.constructor = d; }
|
|
||||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
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 { override } from '@microsoft/decorators';
|
|
||||||
import { Log } from '@microsoft/sp-core-library';
|
|
||||||
import { BaseApplicationCustomizer, PlaceholderName } from '@microsoft/sp-application-base';
|
|
||||||
import * as strings from 'MegaMenuApplicationCustomizerStrings';
|
|
||||||
import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService';
|
|
||||||
import { MegaMenuRenderer } from './MegaMenuRenderer';
|
|
||||||
// Import SCSS module
|
|
||||||
import './MegaMenu.module.scss';
|
|
||||||
var LOG_SOURCE = 'MegaMenuApplicationCustomizer';
|
|
||||||
export var UserCustomActionMegaMenuId = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
|
|
||||||
/** A Custom Action which can be run during execution of a Client Side Application */
|
|
||||||
var MegaMenuApplicationCustomizer = (function (_super) {
|
|
||||||
__extends(MegaMenuApplicationCustomizer, _super);
|
|
||||||
function MegaMenuApplicationCustomizer() {
|
|
||||||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
|
||||||
_this._updateCallback = function (data) {
|
|
||||||
_this._loadExternalCss(data.cssUrl);
|
|
||||||
_this._renderMegaMenu(data.termSetName);
|
|
||||||
};
|
|
||||||
return _this;
|
|
||||||
}
|
|
||||||
MegaMenuApplicationCustomizer.prototype.onInit = function () {
|
|
||||||
Log.info(LOG_SOURCE, "Initialized " + strings.Title);
|
|
||||||
// Load external CSS if provided
|
|
||||||
if (this.properties.cssUrl) {
|
|
||||||
this._loadExternalCss(this.properties.cssUrl);
|
|
||||||
}
|
|
||||||
// Wait for the page to be ready
|
|
||||||
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
|
|
||||||
// Initial render
|
|
||||||
this._renderPlaceHolders();
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
MegaMenuApplicationCustomizer.prototype._renderPlaceHolders = function () {
|
|
||||||
console.log('Available placeholders: ', this.context.placeholderProvider.placeholderNames.map(function (name) { return PlaceholderName[name]; }).join(', '));
|
|
||||||
// Handling the top placeholder
|
|
||||||
if (!this._topPlaceholder) {
|
|
||||||
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose });
|
|
||||||
// The extension should not assume that the expected placeholder is available.
|
|
||||||
if (!this._topPlaceholder) {
|
|
||||||
console.error('The expected placeholder (Top) was not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.properties.termSetName) {
|
|
||||||
console.error('TermSetName property is required but not provided.');
|
|
||||||
1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._renderMegaMenu(this.properties.termSetName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MegaMenuApplicationCustomizer.prototype._renderMegaMenu = function (termSetName) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var taxonomyService, menuItems, renderer, container, error_1;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
if (!this._topPlaceholder) {
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
_a.label = 1;
|
|
||||||
case 1:
|
|
||||||
_a.trys.push([1, 3, , 4]);
|
|
||||||
taxonomyService = new TaxonomyNavigationService(this.context, termSetName);
|
|
||||||
return [4 /*yield*/, taxonomyService.getMenuItems()];
|
|
||||||
case 2:
|
|
||||||
menuItems = _a.sent();
|
|
||||||
renderer = new MegaMenuRenderer(this.context, menuItems, this._updateCallback);
|
|
||||||
container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
|
||||||
if (container) {
|
|
||||||
renderer.render(container);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
renderer.render(this._topPlaceholder.domElement);
|
|
||||||
}
|
|
||||||
Log.info(LOG_SOURCE, "MegaMenu rendered successfully with " + menuItems.length + " top-level items");
|
|
||||||
return [3 /*break*/, 4];
|
|
||||||
case 3:
|
|
||||||
error_1 = _a.sent();
|
|
||||||
console.error('Error rendering MegaMenu:', error_1);
|
|
||||||
return [3 /*break*/, 4];
|
|
||||||
case 4: return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuApplicationCustomizer.prototype._getOrCreateContainer = function (id, placeholder) {
|
|
||||||
var container = document.getElementById(id);
|
|
||||||
if (container) {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
container.appendChild(div);
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return placeholder.domElement;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MegaMenuApplicationCustomizer.prototype._loadExternalCss = function (cssUrl) {
|
|
||||||
var externalCssLinkId = 'mega-menu-additional-css-34FAB720';
|
|
||||||
var link = document.getElementById(externalCssLinkId);
|
|
||||||
if (cssUrl && cssUrl.trim() !== '') {
|
|
||||||
// update previous link, if present
|
|
||||||
if (!link) {
|
|
||||||
var head = document.getElementsByTagName('head')[0];
|
|
||||||
link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.type = 'text/css';
|
|
||||||
link.id = externalCssLinkId;
|
|
||||||
link.onload = function () {
|
|
||||||
Log.info(LOG_SOURCE, "External CSS loaded successfully from: " + cssUrl);
|
|
||||||
};
|
|
||||||
link.onerror = function () {
|
|
||||||
console.warn("Failed to load external CSS from: " + cssUrl);
|
|
||||||
};
|
|
||||||
head.appendChild(link);
|
|
||||||
}
|
|
||||||
link.href = cssUrl;
|
|
||||||
}
|
|
||||||
else if (link) {
|
|
||||||
link.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MegaMenuApplicationCustomizer.prototype._onDispose = function () {
|
|
||||||
console.log('[MegaMenuApplicationCustomizer._onDispose] Disposed custom top placeholder.');
|
|
||||||
};
|
|
||||||
__decorate([
|
|
||||||
override
|
|
||||||
], MegaMenuApplicationCustomizer.prototype, "onInit", null);
|
|
||||||
return MegaMenuApplicationCustomizer;
|
|
||||||
}(BaseApplicationCustomizer));
|
|
||||||
export default MegaMenuApplicationCustomizer;
|
|
||||||
|
|
||||||
//# sourceMappingURL=MegaMenuApplicationCustomizer.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json",
|
|
||||||
|
|
||||||
"id": "abc3361f-bb2d-491f-aba3-cd51c19a299b",
|
|
||||||
"alias": "MegaMenuApplicationCustomizer",
|
|
||||||
"componentType": "Extension",
|
|
||||||
"extensionType": "ApplicationCustomizer",
|
|
||||||
|
|
||||||
// The "*" signifies that the version should be taken from the package.json
|
|
||||||
"version": "*",
|
|
||||||
"manifestVersion": 2,
|
|
||||||
|
|
||||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
|
||||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
|
||||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
|
||||||
"requiresCustomScript": false
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
|
||||||
import { IMenuItem } from '../../services/IMenuItem';
|
|
||||||
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
|
|
||||||
export declare class MegaMenuRenderer {
|
|
||||||
private context;
|
|
||||||
private menuItems;
|
|
||||||
private updateCallback;
|
|
||||||
private _settingsPanel?;
|
|
||||||
constructor(context: ApplicationCustomizerContext, menuItems: IMenuItem[], updateCallback: (data: IMegaMenuApplicationCustomizerProperties) => void);
|
|
||||||
render(container: HTMLElement): void;
|
|
||||||
private createSettingsItem();
|
|
||||||
private openSettings();
|
|
||||||
private createTopLevelItem(item);
|
|
||||||
private createTopLevelElement(item);
|
|
||||||
private createMegaMenu(parentItem);
|
|
||||||
private createCategorySection(item);
|
|
||||||
private attachEventListeners();
|
|
||||||
private attachKeyboardNavigation(heading, megaMenu);
|
|
||||||
private attachMouseEvents(heading, megaMenu);
|
|
||||||
private attachFocusManagement(heading, megaMenu);
|
|
||||||
private attachGlobalKeyboardNavigation();
|
|
||||||
private openMegaMenu(trigger, menu);
|
|
||||||
private closeMegaMenu(trigger, menu);
|
|
||||||
private toggleMegaMenu(trigger, menu);
|
|
||||||
private closeAllMegaMenus();
|
|
||||||
private focusFirstLink(megaMenu);
|
|
||||||
private createScreenReaderAnnouncer();
|
|
||||||
}
|
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
// tslint:disable:max-line-length
|
|
||||||
// tslint:disable:match-default-export-name
|
|
||||||
// tslint:disable:typedef
|
|
||||||
// tslint:disable:variable-name
|
|
||||||
import { SPPermission } from '@microsoft/sp-page-context';
|
|
||||||
import { MegaMenuSettingsPanel } from './MegaMenuSettings';
|
|
||||||
var MegaMenuRenderer = (function () {
|
|
||||||
function MegaMenuRenderer(context, menuItems, updateCallback) {
|
|
||||||
this.context = context;
|
|
||||||
this.menuItems = menuItems;
|
|
||||||
this.updateCallback = updateCallback;
|
|
||||||
}
|
|
||||||
MegaMenuRenderer.prototype.render = function (container) {
|
|
||||||
var _this = this;
|
|
||||||
// Clear any existing content
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.id = 'CustomNavigation';
|
|
||||||
// Create the main nav element
|
|
||||||
var nav = document.createElement('nav');
|
|
||||||
nav.id = 'Mega-Menu';
|
|
||||||
nav.className = 'mega-menu-main';
|
|
||||||
nav.setAttribute('role', 'navigation');
|
|
||||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
|
||||||
// Create the top-level menubar
|
|
||||||
var topLevelUl = document.createElement('ul');
|
|
||||||
topLevelUl.setAttribute('role', 'menubar');
|
|
||||||
// Process each top-level menu item
|
|
||||||
this.menuItems.forEach(function (topLevelItem) {
|
|
||||||
var topLevelLi = _this.createTopLevelItem(topLevelItem);
|
|
||||||
topLevelUl.appendChild(topLevelLi);
|
|
||||||
});
|
|
||||||
// Only if current user has ManageWeb permissions in this website can they access the settings
|
|
||||||
if (this.context.pageContext.web.permissions.hasPermission(SPPermission.manageWeb)) {
|
|
||||||
topLevelUl.appendChild(this.createSettingsItem());
|
|
||||||
}
|
|
||||||
nav.appendChild(topLevelUl);
|
|
||||||
container.appendChild(nav);
|
|
||||||
// Attach accessibility event listeners after rendering
|
|
||||||
this.attachEventListeners();
|
|
||||||
this.createScreenReaderAnnouncer();
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.createSettingsItem = function () {
|
|
||||||
var _this = this;
|
|
||||||
var li = document.createElement('li');
|
|
||||||
li.setAttribute('role', 'none');
|
|
||||||
// Verwenden eines Links-ähnlichen Buttons damit Styling identisch zu Top-Level Items ist
|
|
||||||
var btn = document.createElement('button');
|
|
||||||
btn.type = 'button';
|
|
||||||
btn.className = 'menu-item-link menu-item-settings'; // reuse same base styles
|
|
||||||
btn.setAttribute('role', 'menuitem');
|
|
||||||
btn.setAttribute('tabindex', '0');
|
|
||||||
btn.setAttribute('aria-haspopup', 'false');
|
|
||||||
btn.setAttribute('aria-label', 'Einstellungen');
|
|
||||||
btn.title = 'Einstellungen';
|
|
||||||
// Nur EIN Icon: Wenn Fabric Icons geladen sind, zeigt ms-Icon das Symbol. Fallback via aria-label für Screenreader
|
|
||||||
var icon = document.createElement('span');
|
|
||||||
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
|
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
|
||||||
btn.appendChild(icon);
|
|
||||||
btn.addEventListener('click', function () { return _this.openSettings(); });
|
|
||||||
btn.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
_this.openSettings();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
li.appendChild(btn);
|
|
||||||
return li;
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.openSettings = function () {
|
|
||||||
if (!this._settingsPanel) {
|
|
||||||
this._settingsPanel = new MegaMenuSettingsPanel(this.context, this.updateCallback);
|
|
||||||
}
|
|
||||||
this._settingsPanel.open();
|
|
||||||
};
|
|
||||||
// Panel logic moved to MegaMenuSettingsPanel
|
|
||||||
MegaMenuRenderer.prototype.createTopLevelItem = function (item) {
|
|
||||||
var li = document.createElement('li');
|
|
||||||
li.setAttribute('role', 'none');
|
|
||||||
// Create the top-level element (link or span)
|
|
||||||
var topElement = this.createTopLevelElement(item);
|
|
||||||
li.appendChild(topElement);
|
|
||||||
// If the item has children, create the mega menu
|
|
||||||
if (item.hasChildren() && item.items && item.items.length > 0) {
|
|
||||||
var megaMenu = this.createMegaMenu(item);
|
|
||||||
li.appendChild(megaMenu);
|
|
||||||
}
|
|
||||||
return li;
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.createTopLevelElement = function (item) {
|
|
||||||
var element;
|
|
||||||
if (item.url) {
|
|
||||||
// Create as link
|
|
||||||
element = document.createElement('a');
|
|
||||||
element.href = item.url;
|
|
||||||
element.className = 'menu-item-link';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Create as span (no link)
|
|
||||||
element = document.createElement('span');
|
|
||||||
element.className = 'menu-item-text';
|
|
||||||
element.setAttribute('tabindex', '0');
|
|
||||||
}
|
|
||||||
element.setAttribute('role', 'menuitem');
|
|
||||||
element.setAttribute('aria-haspopup', 'true');
|
|
||||||
element.setAttribute('aria-expanded', 'false');
|
|
||||||
element.textContent = item.label;
|
|
||||||
if (item.hoverText) {
|
|
||||||
element.title = item.hoverText;
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.createMegaMenu = function (parentItem) {
|
|
||||||
var _this = this;
|
|
||||||
var megaMenuDiv = document.createElement('div');
|
|
||||||
megaMenuDiv.className = 'mega-menu';
|
|
||||||
megaMenuDiv.setAttribute('role', 'menu');
|
|
||||||
megaMenuDiv.setAttribute('aria-label', parentItem.label + " Unterkategorien");
|
|
||||||
var gridDiv = document.createElement('div');
|
|
||||||
gridDiv.className = 'mega-menu-grid';
|
|
||||||
// Process second-level items (categories)
|
|
||||||
if (parentItem.items) {
|
|
||||||
parentItem.items.forEach(function (secondLevelItem) {
|
|
||||||
var categoryDiv = _this.createCategorySection(secondLevelItem);
|
|
||||||
gridDiv.appendChild(categoryDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
megaMenuDiv.appendChild(gridDiv);
|
|
||||||
return megaMenuDiv;
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.createCategorySection = function (item) {
|
|
||||||
var categoryDiv = document.createElement('div');
|
|
||||||
categoryDiv.className = 'mega-menu-category';
|
|
||||||
// Create the category header (h3)
|
|
||||||
var h3 = document.createElement('h3');
|
|
||||||
if (item.url) {
|
|
||||||
// Category header as link
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.href = item.url;
|
|
||||||
link.textContent = item.label;
|
|
||||||
if (item.hoverText) {
|
|
||||||
link.title = item.hoverText;
|
|
||||||
}
|
|
||||||
h3.appendChild(link);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Category header as span (no link)
|
|
||||||
var span = document.createElement('span');
|
|
||||||
span.textContent = item.label;
|
|
||||||
if (item.hoverText) {
|
|
||||||
span.title = item.hoverText;
|
|
||||||
}
|
|
||||||
h3.appendChild(span);
|
|
||||||
}
|
|
||||||
categoryDiv.appendChild(h3);
|
|
||||||
// Create the third-level items list if they exist
|
|
||||||
if (item.hasChildren() && item.items && item.items.length > 0) {
|
|
||||||
var ul_1 = document.createElement('ul');
|
|
||||||
item.items.forEach(function (thirdLevelItem) {
|
|
||||||
var li = document.createElement('li');
|
|
||||||
var link = document.createElement('a');
|
|
||||||
link.href = thirdLevelItem.url || '#';
|
|
||||||
link.textContent = thirdLevelItem.label;
|
|
||||||
if (thirdLevelItem.hoverText) {
|
|
||||||
link.title = thirdLevelItem.hoverText;
|
|
||||||
}
|
|
||||||
li.appendChild(link);
|
|
||||||
ul_1.appendChild(li);
|
|
||||||
});
|
|
||||||
categoryDiv.appendChild(ul_1);
|
|
||||||
}
|
|
||||||
return categoryDiv;
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.attachEventListeners = function () {
|
|
||||||
var headings = document.querySelectorAll('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span[role="menuitem"]');
|
|
||||||
for (var i = 0; i < headings.length; i++) {
|
|
||||||
var heading = headings[i];
|
|
||||||
var megaMenu = heading.nextElementSibling;
|
|
||||||
if (megaMenu && megaMenu.classList.contains('mega-menu')) {
|
|
||||||
this.attachKeyboardNavigation(heading, megaMenu);
|
|
||||||
this.attachMouseEvents(heading, megaMenu);
|
|
||||||
this.attachFocusManagement(heading, megaMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Global keyboard navigation
|
|
||||||
this.attachGlobalKeyboardNavigation();
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.attachKeyboardNavigation = function (heading, megaMenu) {
|
|
||||||
var _this = this;
|
|
||||||
heading.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
// Enter: Navigation (nur bei Links ohne Mega-Menu-Override)
|
|
||||||
if (heading.tagName === 'A') {
|
|
||||||
// Lasse normale Link-Navigation zu (KEIN preventDefault!)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Bei span: Toggle Menu
|
|
||||||
e.preventDefault();
|
|
||||||
_this.toggleMegaMenu(heading, megaMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e.key === ' ') {
|
|
||||||
// Space: Toggle Dropdown (immer)
|
|
||||||
e.preventDefault();
|
|
||||||
_this.toggleMegaMenu(heading, megaMenu);
|
|
||||||
}
|
|
||||||
else if (e.key === 'ArrowDown') {
|
|
||||||
// Pfeil runter: Menü öffnen + erster Link
|
|
||||||
e.preventDefault();
|
|
||||||
_this.openMegaMenu(heading, megaMenu);
|
|
||||||
_this.focusFirstLink(megaMenu);
|
|
||||||
}
|
|
||||||
else if (e.key === 'ArrowUp') {
|
|
||||||
// Pfeil hoch: Menü schließen
|
|
||||||
e.preventDefault();
|
|
||||||
_this.closeMegaMenu(heading, megaMenu);
|
|
||||||
}
|
|
||||||
else if (e.key === 'Escape') {
|
|
||||||
// Escape: Menü schließen
|
|
||||||
e.preventDefault();
|
|
||||||
_this.closeMegaMenu(heading, megaMenu);
|
|
||||||
heading.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click Event für Top-Level Links - NORMALE Navigation erlauben
|
|
||||||
if (heading.tagName === 'A') {
|
|
||||||
heading.addEventListener('click', function (e) {
|
|
||||||
// Links sollen normal navigieren, nicht das Mega-Menu toglen
|
|
||||||
// Wenn der User das Menu öffnen will, soll er Space oder Pfeil↓ nutzen
|
|
||||||
console.log('Link geklickt:', heading.href);
|
|
||||||
// KEIN preventDefault() - normale Link-Navigation
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Focus Management - KEIN automatisches Öffnen oder Schließen
|
|
||||||
heading.addEventListener('focus', function () {
|
|
||||||
console.log('Focus auf:', heading.textContent);
|
|
||||||
// Einfach nur fokussiert - keine automatischen Aktionen!
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.attachMouseEvents = function (heading, megaMenu) {
|
|
||||||
var _this = this;
|
|
||||||
var parentLi = heading.parentElement;
|
|
||||||
parentLi.addEventListener('mouseenter', function () {
|
|
||||||
_this.openMegaMenu(heading, megaMenu);
|
|
||||||
});
|
|
||||||
parentLi.addEventListener('mouseleave', function () {
|
|
||||||
_this.closeMegaMenu(heading, megaMenu);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.attachFocusManagement = function (heading, megaMenu) {
|
|
||||||
var _this = this;
|
|
||||||
// Focus-Verlust-Behandlung - vereinfacht
|
|
||||||
megaMenu.addEventListener('focusout', function (e) {
|
|
||||||
// Kurz warten, um zu prüfen ob Focus innerhalb des Mega-Menus bleibt
|
|
||||||
setTimeout(function () {
|
|
||||||
var focusedElement = document.activeElement;
|
|
||||||
var isInsideThisMenu = megaMenu.contains(focusedElement);
|
|
||||||
var isOnThisTrigger = focusedElement === heading;
|
|
||||||
var isInAnyMegaMenu = focusedElement.closest('.mega-menu');
|
|
||||||
var isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
|
|
||||||
// Nur schließen wenn Focus komplett außerhalb der Navigation ist
|
|
||||||
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
|
|
||||||
console.log('Schließe Menu wegen Focus-Verlust');
|
|
||||||
_this.closeMegaMenu(heading, megaMenu);
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.attachGlobalKeyboardNavigation = function () {
|
|
||||||
var _this = this;
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
var activeElement = document.activeElement;
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
var openMenu = document.querySelector('.mega-menu[aria-expanded="true"]');
|
|
||||||
if (openMenu) {
|
|
||||||
var triggerLink = openMenu.previousElementSibling;
|
|
||||||
_this.closeMegaMenu(triggerLink, openMenu);
|
|
||||||
triggerLink.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Tab-Navigation: Nur bei spezifischen Übergängen eingreifen
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
// Von Top-Level zum ersten Link im offenen Menu
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
var currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
|
|
||||||
if (currentTopLevel) {
|
|
||||||
var parentLi = currentTopLevel.closest('li');
|
|
||||||
var megaMenu = parentLi.querySelector('.mega-menu.js-open');
|
|
||||||
if (megaMenu) {
|
|
||||||
// Nur eingreifen wenn wir vom Trigger weg-tabben
|
|
||||||
e.preventDefault();
|
|
||||||
var firstLink = megaMenu.querySelector('a');
|
|
||||||
if (firstLink) {
|
|
||||||
firstLink.focus();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Shift+Tab: Vom ersten Link im Menu zurück zum Trigger
|
|
||||||
if (e.shiftKey) {
|
|
||||||
var megaMenu = activeElement.closest('.mega-menu');
|
|
||||||
if (megaMenu && megaMenu.classList.contains('js-open')) {
|
|
||||||
var allLinksInMenu = megaMenu.querySelectorAll('a');
|
|
||||||
var firstLinkInMenu = allLinksInMenu[0];
|
|
||||||
// Nur eingreifen wenn wir beim ersten Link sind
|
|
||||||
if (activeElement === firstLinkInMenu) {
|
|
||||||
e.preventDefault();
|
|
||||||
var triggerElement = megaMenu.previousElementSibling;
|
|
||||||
triggerElement.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ansonsten: Normale Tab-Navigation nicht unterbrechen!
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.openMegaMenu = function (trigger, menu) {
|
|
||||||
trigger.setAttribute('aria-expanded', 'true');
|
|
||||||
menu.setAttribute('aria-expanded', 'true');
|
|
||||||
menu.classList.add('js-open');
|
|
||||||
console.log('Menu geöffnet:', trigger.textContent);
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.closeMegaMenu = function (trigger, menu) {
|
|
||||||
trigger.setAttribute('aria-expanded', 'false');
|
|
||||||
menu.setAttribute('aria-expanded', 'false');
|
|
||||||
menu.classList.remove('js-open');
|
|
||||||
console.log('Menu geschlossen:', trigger.textContent);
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.toggleMegaMenu = function (trigger, menu) {
|
|
||||||
var isOpen = trigger.getAttribute('aria-expanded') === 'true';
|
|
||||||
if (isOpen) {
|
|
||||||
this.closeMegaMenu(trigger, menu);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.closeAllMegaMenus();
|
|
||||||
this.openMegaMenu(trigger, menu);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.closeAllMegaMenus = function () {
|
|
||||||
var allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]');
|
|
||||||
for (var i = 0; i < allTriggers.length; i++) {
|
|
||||||
var trigger = allTriggers[i];
|
|
||||||
var menu = trigger.nextElementSibling;
|
|
||||||
if (menu) {
|
|
||||||
this.closeMegaMenu(trigger, menu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// removed unused closeOtherMegaMenus (was previously declared but not used)
|
|
||||||
MegaMenuRenderer.prototype.focusFirstLink = function (megaMenu) {
|
|
||||||
var firstLink = megaMenu.querySelector('a');
|
|
||||||
if (firstLink) {
|
|
||||||
firstLink.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MegaMenuRenderer.prototype.createScreenReaderAnnouncer = function () {
|
|
||||||
// Screenreader-Ankündigungen
|
|
||||||
var srAnnouncer = document.createElement('div');
|
|
||||||
srAnnouncer.setAttribute('aria-live', 'polite');
|
|
||||||
srAnnouncer.setAttribute('aria-atomic', 'true');
|
|
||||||
srAnnouncer.className = 'sr-only';
|
|
||||||
document.body.appendChild(srAnnouncer);
|
|
||||||
console.log('Screenreader-Ankündigungen sind jetzt bereit');
|
|
||||||
};
|
|
||||||
return MegaMenuRenderer;
|
|
||||||
}());
|
|
||||||
export { MegaMenuRenderer };
|
|
||||||
|
|
||||||
//# sourceMappingURL=MegaMenuRenderer.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
-25
@@ -1,25 +0,0 @@
|
|||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
|
||||||
import { IMegaMenuApplicationCustomizerProperties } from './MegaMenuApplicationCustomizer';
|
|
||||||
export declare class MegaMenuSettingsPanel {
|
|
||||||
private context;
|
|
||||||
private dataUpdated;
|
|
||||||
private _service;
|
|
||||||
private _ucaId;
|
|
||||||
private _panelElement;
|
|
||||||
private _overlayElement;
|
|
||||||
constructor(context: ApplicationCustomizerContext, dataUpdated: (data: IMegaMenuApplicationCustomizerProperties) => void);
|
|
||||||
open(): Promise<void>;
|
|
||||||
close(): void;
|
|
||||||
private readApplicationCustomizerProps();
|
|
||||||
private saveApplicationCustomizerProps(componentProps);
|
|
||||||
private _createPanel(props);
|
|
||||||
private _getMarkup(termSet, cssUrl);
|
|
||||||
private _save();
|
|
||||||
/**
|
|
||||||
* Hält den Fokus innerhalb des Settings-Panels gefangen (Focus Trapping).
|
|
||||||
* Wichtig für Accessibility: Verhindert, dass Tab-Navigation aus dem modalen Dialog herausführt.
|
|
||||||
* Bei Tab am letzten Element → springt zum ersten, bei Shift+Tab am ersten → springt zum letzten.
|
|
||||||
*/
|
|
||||||
private _trapFocus(e);
|
|
||||||
private _escape(value);
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
// tslint:disable:max-line-length export-name
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
||||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (_) try {
|
|
||||||
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [0, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
import { UserCustomActionMegaMenuId } from './MegaMenuApplicationCustomizer';
|
|
||||||
import { UserCustomActionService } from '../../services/UserCustomActionService/UserCustomActionService';
|
|
||||||
import { UserCustomActionScope } from '../../services/UserCustomActionService/UserCustomActionScope';
|
|
||||||
var MegaMenuSettingsPanel = (function () {
|
|
||||||
function MegaMenuSettingsPanel(context, dataUpdated) {
|
|
||||||
this.context = context;
|
|
||||||
this.dataUpdated = dataUpdated;
|
|
||||||
this._panelElement = undefined;
|
|
||||||
this._overlayElement = undefined;
|
|
||||||
this._service = new UserCustomActionService(this.context);
|
|
||||||
}
|
|
||||||
MegaMenuSettingsPanel.prototype.open = function () {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var currentProps;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
if (this._panelElement) {
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
return [4 /*yield*/, this.readApplicationCustomizerProps()];
|
|
||||||
case 1:
|
|
||||||
currentProps = _a.sent();
|
|
||||||
this._createPanel(currentProps);
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype.close = function () {
|
|
||||||
if (this._panelElement) {
|
|
||||||
this._panelElement.remove();
|
|
||||||
this._panelElement = undefined;
|
|
||||||
}
|
|
||||||
if (this._overlayElement) {
|
|
||||||
this._overlayElement.remove();
|
|
||||||
this._overlayElement = undefined;
|
|
||||||
}
|
|
||||||
// Fokus entfernen (kein sichtbarer Fokus-Rahmen nach Dialog-Schließung)
|
|
||||||
document.body.focus();
|
|
||||||
document.body.blur();
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype.readApplicationCustomizerProps = function () {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var ucas, candidates, uca;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0: return [4 /*yield*/, this._service.getUserCustomActions(UserCustomActionScope.Site)];
|
|
||||||
case 1:
|
|
||||||
ucas = _a.sent();
|
|
||||||
candidates = ucas.filter(function (uca) { return uca.ClientSideComponentId === UserCustomActionMegaMenuId; });
|
|
||||||
if (candidates.length) {
|
|
||||||
uca = candidates[0];
|
|
||||||
this._ucaId = uca.Id;
|
|
||||||
return [2 /*return*/, JSON.parse(uca.ClientSideComponentProperties)];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.error('UserCustomAction für das Megamenü nicht gefunden. Alles ist sinnlos.');
|
|
||||||
return [2 /*return*/, {
|
|
||||||
termSetName: '',
|
|
||||||
cssUrl: ''
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype.saveApplicationCustomizerProps = function (componentProps) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var newUserCustomActionsProperty, e_1;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
_a.trys.push([0, 2, , 3]);
|
|
||||||
newUserCustomActionsProperty = {
|
|
||||||
ClientSideComponentProperties: JSON.stringify(componentProps)
|
|
||||||
};
|
|
||||||
return [4 /*yield*/, this._service.updateUserCustomAction(UserCustomActionScope.Site, this._ucaId, newUserCustomActionsProperty)];
|
|
||||||
case 1:
|
|
||||||
_a.sent();
|
|
||||||
this.dataUpdated(componentProps);
|
|
||||||
return [3 /*break*/, 3];
|
|
||||||
case 2:
|
|
||||||
e_1 = _a.sent();
|
|
||||||
console.error(e_1);
|
|
||||||
return [3 /*break*/, 3];
|
|
||||||
case 3: return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype._createPanel = function (props) {
|
|
||||||
var _this = this;
|
|
||||||
var overlay = document.createElement('div');
|
|
||||||
overlay.className = 'mm-settings-overlay';
|
|
||||||
overlay.tabIndex = -1;
|
|
||||||
overlay.onclick = function () { return _this.close(); };
|
|
||||||
this._overlayElement = overlay;
|
|
||||||
var panel = document.createElement('div');
|
|
||||||
panel.className = 'mm-settings-panel';
|
|
||||||
panel.setAttribute('role', 'dialog');
|
|
||||||
panel.setAttribute('aria-modal', 'true');
|
|
||||||
panel.setAttribute('aria-label', 'MegaMenu Einstellungen');
|
|
||||||
panel.innerHTML = this._getMarkup(props.termSetName, props.cssUrl);
|
|
||||||
this._panelElement = panel;
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
document.body.appendChild(panel);
|
|
||||||
var closeBtn = panel.querySelector('.mm-settings-close');
|
|
||||||
var cancelBtn = panel.querySelector('.mm-settings-cancel');
|
|
||||||
var saveBtn = panel.querySelector('.mm-settings-save');
|
|
||||||
var firstInput = panel.querySelector('#mm-setting-termset');
|
|
||||||
if (closeBtn) {
|
|
||||||
closeBtn.onclick = function () { return _this.close(); };
|
|
||||||
}
|
|
||||||
if (cancelBtn) {
|
|
||||||
cancelBtn.onclick = function () { return _this.close(); };
|
|
||||||
}
|
|
||||||
if (saveBtn) {
|
|
||||||
saveBtn.onclick = function () { return _this._save(); };
|
|
||||||
}
|
|
||||||
panel.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
_this.close();
|
|
||||||
}
|
|
||||||
else if (e.key === 'Tab') {
|
|
||||||
_this._trapFocus(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setTimeout(function () { if (firstInput) {
|
|
||||||
firstInput.focus();
|
|
||||||
} }, 0);
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype._getMarkup = function (termSet, cssUrl) {
|
|
||||||
return "<div class='mm-settings-header'>\n <h2 class='mm-settings-title'>Einstellungen</h2>\n <button type='button' class='mm-settings-close' aria-label='Schlie\u00DFen'>\u00D7</button>\n </div>\n <div class='mm-settings-body'>\n <div class='mm-settings-field'>\n <label for='mm-setting-termset'>Name des Navigations-Termsets</label>\n <input id='mm-setting-termset' type='text' value='" + this._escape(termSet) + "' />\n </div>\n <div class='mm-settings-field'>\n <label for='mm-setting-css'>Pfad zu zus\u00E4tzlicher CSS-Datei</label>\n <input id='mm-setting-css' type='text' value='" + this._escape(cssUrl) + "' />\n </div>\n </div>\n <div class='mm-settings-footer'>\n <button type='button' class='mm-settings-save ms-Button ms-Button--primary'><span>Speichern</span></button>\n <button type='button' class='mm-settings-cancel ms-Button'><span>Abbrechen</span></button>\n </div>";
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype._save = function () {
|
|
||||||
if (!this._panelElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var termSetInput = this._panelElement.querySelector('#mm-setting-termset');
|
|
||||||
var cssInput = this._panelElement.querySelector('#mm-setting-css');
|
|
||||||
this.saveApplicationCustomizerProps({
|
|
||||||
termSetName: termSetInput && termSetInput.value ? termSetInput.value : '',
|
|
||||||
cssUrl: cssInput && cssInput.value ? cssInput.value : ''
|
|
||||||
});
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Hält den Fokus innerhalb des Settings-Panels gefangen (Focus Trapping).
|
|
||||||
* Wichtig für Accessibility: Verhindert, dass Tab-Navigation aus dem modalen Dialog herausführt.
|
|
||||||
* Bei Tab am letzten Element → springt zum ersten, bei Shift+Tab am ersten → springt zum letzten.
|
|
||||||
*/
|
|
||||||
MegaMenuSettingsPanel.prototype._trapFocus = function (e) {
|
|
||||||
if (!this._panelElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var focusable = this._panelElement.querySelectorAll('button, input');
|
|
||||||
if (!focusable || focusable.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var first = focusable[0];
|
|
||||||
var last = focusable[focusable.length - 1];
|
|
||||||
var active = document.activeElement;
|
|
||||||
if (e.shiftKey && active === first) {
|
|
||||||
e.preventDefault();
|
|
||||||
last.focus();
|
|
||||||
}
|
|
||||||
else if (!e.shiftKey && active === last) {
|
|
||||||
e.preventDefault();
|
|
||||||
first.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MegaMenuSettingsPanel.prototype._escape = function (value) {
|
|
||||||
if (value) {
|
|
||||||
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return MegaMenuSettingsPanel;
|
|
||||||
}());
|
|
||||||
export { MegaMenuSettingsPanel };
|
|
||||||
|
|
||||||
//# sourceMappingURL=MegaMenuSettings.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
|
|||||||
define([], function() {
|
|
||||||
return {
|
|
||||||
"Title": "MegaMenuApplicationCustomizer"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
export { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
|
|
||||||
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
|
|
||||||
export { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';
|
|
||||||
export { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// tslint:disable:max-line-length no-any
|
|
||||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
||||||
// Export services for classic SharePoint usage
|
|
||||||
export { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
|
|
||||||
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
|
|
||||||
export { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';
|
|
||||||
export { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.__megaMenuServices = {
|
|
||||||
TaxonomyNavigationService: require('./services/TaxonomyNavigationService').TaxonomyNavigationService,
|
|
||||||
MegaMenuRenderer: require('./extensions/megaMenu/MegaMenuRenderer').MegaMenuRenderer,
|
|
||||||
UserCustomActionService: require('./services/UserCustomActionService/UserCustomActionService').UserCustomActionService,
|
|
||||||
UserCustomActionScope: require('./services/UserCustomActionService/UserCustomActionScope').UserCustomActionScope
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//# sourceMappingURL=index.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,wFAAwF;AAExF,+CAA+C;AAC/C,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,4DAA4D,CAAC;AACrG,OAAO,EAAE,qBAAqB,EAAE,MAAM,0DAA0D,CAAC;AAKjG,EAAE,CAAC,CAAC,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,kBAAkB,GAAG;QACxB,yBAAyB,EAAE,OAAO,CAAC,sCAAsC,CAAC,CAAC,yBAAyB;QACpG,gBAAgB,EAAE,OAAO,CAAC,wCAAwC,CAAC,CAAC,gBAAgB;QACpF,uBAAuB,EAAE,OAAO,CAAC,4DAA4D,CAAC,CAAC,uBAAuB;QACtH,qBAAqB,EAAE,OAAO,CAAC,0DAA0D,CAAC,CAAC,qBAAqB;KACnH,CAAC;AACN,CAAC","file":"index.js","sourcesContent":["// tslint:disable:max-line-length no-any\r\n// A file is required to be in the root of the /src directory by the TypeScript compiler\r\n\r\n// Export services for classic SharePoint usage\r\nexport { TaxonomyNavigationService } from './services/TaxonomyNavigationService';\r\nexport { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';\r\nexport { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';\r\nexport { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';\r\n\r\n// Expose services globally for classic JavaScript consumption\r\ndeclare var window: any;\r\n\r\nif (typeof window !== 'undefined') {\r\n window.__megaMenuServices = {\r\n TaxonomyNavigationService: require('./services/TaxonomyNavigationService').TaxonomyNavigationService,\r\n MegaMenuRenderer: require('./extensions/megaMenu/MegaMenuRenderer').MegaMenuRenderer,\r\n UserCustomActionService: require('./services/UserCustomActionService/UserCustomActionService').UserCustomActionService,\r\n UserCustomActionScope: require('./services/UserCustomActionService/UserCustomActionScope').UserCustomActionScope\r\n };\r\n}\r\n"],"sourceRoot":"..\\src"}
|
|
||||||
Vendored
-10
@@ -1,10 +0,0 @@
|
|||||||
export interface IMenuItem {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
icon?: string;
|
|
||||||
hoverText: string;
|
|
||||||
url?: string;
|
|
||||||
pathDepth: number;
|
|
||||||
items?: IMenuItem[];
|
|
||||||
hasChildren: () => boolean;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=IMenuItem.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/IMenuItem.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
|
|
||||||
Vendored
-9
@@ -1,9 +0,0 @@
|
|||||||
export interface IPickerTerm {
|
|
||||||
name: string;
|
|
||||||
key: string;
|
|
||||||
path: string;
|
|
||||||
termSet: string;
|
|
||||||
termSetName?: string;
|
|
||||||
}
|
|
||||||
export interface IPickerTerms extends Array<IPickerTerm> {
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=IPickerTerm.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/IPickerTerm.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
|
|
||||||
-84
@@ -1,84 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interfaces for Term store, groups and term sets
|
|
||||||
* This code is a copy from the library @pnp/sp-dev-fx-controls-react
|
|
||||||
*/
|
|
||||||
export interface ITermStore {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_ObjectIdentity_: string;
|
|
||||||
Id: string;
|
|
||||||
Name: string;
|
|
||||||
Groups: IGroups;
|
|
||||||
}
|
|
||||||
export interface IGroups {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_Child_Items_: IGroup[];
|
|
||||||
}
|
|
||||||
export interface IGroup {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_ObjectIdentity_: string;
|
|
||||||
TermSets: ITermSets;
|
|
||||||
Id: string;
|
|
||||||
Name: string;
|
|
||||||
IsSystemGroup: boolean;
|
|
||||||
}
|
|
||||||
export interface ITermSets {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_Child_Items_: ITermSet[];
|
|
||||||
}
|
|
||||||
export interface ITermSet {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_ObjectIdentity_: string;
|
|
||||||
Id: string;
|
|
||||||
CustomSortOrder?: string;
|
|
||||||
Name: string;
|
|
||||||
Description: string;
|
|
||||||
Names: ITermSetNames;
|
|
||||||
Terms?: ITerm[];
|
|
||||||
}
|
|
||||||
export interface ITermSetMinimal {
|
|
||||||
_ObjectType_?: string;
|
|
||||||
_ObjectIdentity_?: string;
|
|
||||||
Id: string;
|
|
||||||
Name: string;
|
|
||||||
}
|
|
||||||
export interface ITermSetNames {
|
|
||||||
[locale: string]: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Interfaces for the terms
|
|
||||||
*/
|
|
||||||
export interface ITerms {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_Child_Items_: ITerm[];
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Term
|
|
||||||
*/
|
|
||||||
export interface ITerm {
|
|
||||||
_ObjectType_: string;
|
|
||||||
_ObjectIdentity_: string;
|
|
||||||
Id: string;
|
|
||||||
Name: string;
|
|
||||||
Description: string;
|
|
||||||
IsDeprecated: boolean;
|
|
||||||
IsAvailableForTagging: boolean;
|
|
||||||
IsRoot: boolean;
|
|
||||||
PathOfTerm: string;
|
|
||||||
TermSet: ITermSetMinimal;
|
|
||||||
CustomSortOrderIndex?: number;
|
|
||||||
PathDepth?: number;
|
|
||||||
ParentId?: string;
|
|
||||||
TermsCount?: number;
|
|
||||||
LocalCustomProperties?: {
|
|
||||||
[property: string]: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export interface ISuggestTerm {
|
|
||||||
Id: string;
|
|
||||||
DefaultLabel: string;
|
|
||||||
Description: string;
|
|
||||||
IsKeyword: boolean;
|
|
||||||
IsSynonym: boolean;
|
|
||||||
Paths: Array<string>;
|
|
||||||
Synonyms: string;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=ISPTermStorePickerService.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/ISPTermStorePickerService.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface ISPTermStorePickerServiceProps {
|
|
||||||
termsetNameOrID: string;
|
|
||||||
useSessionStorage: boolean;
|
|
||||||
hideDeprecatedTags: boolean;
|
|
||||||
hideTagsNotAvailableForTagging: boolean;
|
|
||||||
anchorId: string;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=ISPTermStorePickerServiceProps.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/ISPTermStorePickerServiceProps.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { IMenuItem } from './IMenuItem';
|
|
||||||
export interface ITaxonomyNavigationService {
|
|
||||||
getMenuItems(): Promise<IMenuItem[]>;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=ITaxonomyNavigationService.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/ITaxonomyNavigationService.js","sourcesContent":[],"sourceRoot":"..\\..\\src"}
|
|
||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
export declare class ItemDictionary<T> {
|
|
||||||
Get(key: string): T;
|
|
||||||
Add(key: string, value: T): void;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
var ItemDictionary = (function () {
|
|
||||||
function ItemDictionary() {
|
|
||||||
}
|
|
||||||
ItemDictionary.prototype.Get = function (key) {
|
|
||||||
return this[key];
|
|
||||||
};
|
|
||||||
ItemDictionary.prototype.Add = function (key, value) {
|
|
||||||
this[key] = value;
|
|
||||||
};
|
|
||||||
return ItemDictionary;
|
|
||||||
}());
|
|
||||||
export { ItemDictionary };
|
|
||||||
|
|
||||||
//# sourceMappingURL=ItemDictionary.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["services/ItemDictionary.ts"],"names":[],"mappings":"AAAA;IAAA;IAQA,CAAC;IAPU,4BAAG,GAAV,UAAW,GAAW;QAClB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAEM,4BAAG,GAAV,UAAW,GAAW,EAAE,KAAQ;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IACL,qBAAC;AAAD,CARA,AAQC,IAAA","file":"services/ItemDictionary.js","sourcesContent":["export class ItemDictionary<T> {\r\n public Get(key: string): T {\r\n return this[key];\r\n }\r\n\r\n public Add(key: string, value: T): void {\r\n this[key] = value;\r\n }\r\n}\r\n"],"sourceRoot":"..\\..\\src"}
|
|
||||||
Vendored
-14
@@ -1,14 +0,0 @@
|
|||||||
import { IMenuItem } from './IMenuItem';
|
|
||||||
import { ITerm } from './ISPTermStorePickerService';
|
|
||||||
export declare class MenuItem implements IMenuItem {
|
|
||||||
level: number;
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
hoverText: string;
|
|
||||||
pathDepth: number;
|
|
||||||
url?: string;
|
|
||||||
items?: IMenuItem[];
|
|
||||||
constructor(term: ITerm, level: number, siteCollectionUrl?: string);
|
|
||||||
hasChildren(): boolean;
|
|
||||||
command(): void;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// tslint:disable:no-any no-string-literal max-line-length
|
|
||||||
var MenuItem = (function () {
|
|
||||||
function MenuItem(term, level, siteCollectionUrl) {
|
|
||||||
this.level = level;
|
|
||||||
this.id = term.Id;
|
|
||||||
this.label = term.Name;
|
|
||||||
this.hoverText = term.LocalCustomProperties['_Sys_Nav_HoverText'];
|
|
||||||
this.pathDepth = term.PathDepth;
|
|
||||||
var rawUrl = term.LocalCustomProperties['_Sys_Nav_SimpleLinkUrl'] || term.LocalCustomProperties['_Sys_Nav_TargetUrl'];
|
|
||||||
if (rawUrl) {
|
|
||||||
this.url = siteCollectionUrl && rawUrl.indexOf('~sitecollection') === 0
|
|
||||||
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
|
|
||||||
: rawUrl;
|
|
||||||
}
|
|
||||||
this.items = [];
|
|
||||||
}
|
|
||||||
MenuItem.prototype.hasChildren = function () {
|
|
||||||
return this.items && this.items.length > 0;
|
|
||||||
};
|
|
||||||
MenuItem.prototype.command = function () {
|
|
||||||
if (this.url) {
|
|
||||||
window.location.href = this.url;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return MenuItem;
|
|
||||||
}());
|
|
||||||
export { MenuItem };
|
|
||||||
|
|
||||||
//# sourceMappingURL=MenuItem.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["services/MenuItem.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAK1D;IAQI,kBAAY,IAAW,EAAS,KAAa,EAAE,iBAA0B;QAAzC,UAAK,GAAL,KAAK,CAAQ;QACzC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;QAClE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAM,MAAM,GAAW,IAAI,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,IAAI,IAAI,CAAC,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;QAChI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACT,IAAI,CAAC,GAAG,GAAG,iBAAiB,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC;kBACjE,iBAAiB,GAAG,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,MAAM,CAAC;kBAC9D,MAAM,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IACpB,CAAC;IAEM,8BAAW,GAAlB;QACI,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/C,CAAC;IAEM,0BAAO,GAAd;QACI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACV,MAAc,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;QAC7C,CAAC;IACL,CAAC;IACL,eAAC;AAAD,CA/BA,AA+BC,IAAA","file":"services/MenuItem.js","sourcesContent":["// tslint:disable:no-any no-string-literal max-line-length\r\n\r\nimport { IMenuItem } from './IMenuItem';\r\nimport { ITerm } from './ISPTermStorePickerService';\r\n\r\nexport class MenuItem implements IMenuItem {\r\n public id: string;\r\n public label: string;\r\n public hoverText: string;\r\n public pathDepth: number;\r\n public url?: string;\r\n public items?: IMenuItem[];\r\n\r\n constructor(term: ITerm, public level: number, siteCollectionUrl?: string) {\r\n this.id = term.Id;\r\n this.label = term.Name;\r\n this.hoverText = term.LocalCustomProperties['_Sys_Nav_HoverText'];\r\n this.pathDepth = term.PathDepth;\r\n const rawUrl: string = term.LocalCustomProperties['_Sys_Nav_SimpleLinkUrl'] || term.LocalCustomProperties['_Sys_Nav_TargetUrl'];\r\n if (rawUrl) {\r\n this.url = siteCollectionUrl && rawUrl.indexOf('~sitecollection') === 0\r\n ? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)\r\n : rawUrl;\r\n }\r\n this.items = [];\r\n }\r\n\r\n public hasChildren(): boolean {\r\n return this.items && this.items.length > 0;\r\n }\r\n\r\n public command(): void {\r\n if (this.url) {\r\n (window as any).location.href = this.url;\r\n }\r\n }\r\n}\r\n"],"sourceRoot":"..\\..\\src"}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { IMenuItem } from './IMenuItem';
|
|
||||||
import { ITaxonomyNavigationService } from './ITaxonomyNavigationService';
|
|
||||||
export default class MockTaxonomyNavigationService implements ITaxonomyNavigationService {
|
|
||||||
getMenuItems(): Promise<IMenuItem[]>;
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import * as uuid from 'uuid';
|
|
||||||
var MockTaxonomyNavigationService = (function () {
|
|
||||||
function MockTaxonomyNavigationService() {
|
|
||||||
}
|
|
||||||
MockTaxonomyNavigationService.prototype.getMenuItems = function () {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
resolve([
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Menu Item 1',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
hoverText: 'Hover me!',
|
|
||||||
pathDepth: 1,
|
|
||||||
hasChildren: function () { return true; },
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 1',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Menu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 1,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Menu Item 3',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 1,
|
|
||||||
hasChildren: function () { return true; },
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 1',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: function () { return true; },
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 1',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 3,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 3,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 3',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 4',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: function () { return false; }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return MockTaxonomyNavigationService;
|
|
||||||
}());
|
|
||||||
export default MockTaxonomyNavigationService;
|
|
||||||
|
|
||||||
//# sourceMappingURL=MockTaxonomyNavigationService.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
-76
@@ -1,76 +0,0 @@
|
|||||||
import { ITermStore, ITermSet } from './ISPTermStorePickerService';
|
|
||||||
import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps';
|
|
||||||
import { IPickerTerm } from './IPickerTerm';
|
|
||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
|
||||||
/**
|
|
||||||
* Service implementation to manage term stores in SharePoint
|
|
||||||
*/
|
|
||||||
export default class SPTermStorePickerService {
|
|
||||||
private props;
|
|
||||||
private context;
|
|
||||||
private clientServiceUrl;
|
|
||||||
private suggestionServiceUrl;
|
|
||||||
/**
|
|
||||||
* Service constructor
|
|
||||||
*/
|
|
||||||
constructor(props: ISPTermStorePickerServiceProps, context: ApplicationCustomizerContext);
|
|
||||||
getTermLabels(termId: string): Promise<string[]>;
|
|
||||||
/**
|
|
||||||
* Gets the collection of term stores in the current SharePoint env
|
|
||||||
*/
|
|
||||||
getTermStores(): Promise<ITermStore[]>;
|
|
||||||
/**
|
|
||||||
* Gets the current term set
|
|
||||||
*/
|
|
||||||
getTermSet(): Promise<ITermSet>;
|
|
||||||
/**
|
|
||||||
* Retrieve all terms for the given term set
|
|
||||||
* @param termset
|
|
||||||
*/
|
|
||||||
getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean): Promise<ITermSet>;
|
|
||||||
/**
|
|
||||||
* Retrieve all terms that starts with the searchText
|
|
||||||
* @param searchText
|
|
||||||
*/
|
|
||||||
searchTermsByName(searchText: string): Promise<IPickerTerm[]>;
|
|
||||||
searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]>;
|
|
||||||
/**
|
|
||||||
* Retrieve all terms for the given term set and anchorId
|
|
||||||
*/
|
|
||||||
getAllTermsByAnchorId(termsetNameOrID: string, anchorId: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage?: boolean): Promise<IPickerTerm[]>;
|
|
||||||
/**
|
|
||||||
* Clean the Guid from the Web Service response
|
|
||||||
* @param guid
|
|
||||||
*/
|
|
||||||
cleanGuid(guid: string): string;
|
|
||||||
/**
|
|
||||||
* Get the term set ID by its name
|
|
||||||
* @param termstore
|
|
||||||
* @param termset
|
|
||||||
*/
|
|
||||||
private getTermSetId(termstore, termsetName);
|
|
||||||
private getTermsById(termId, useSessionStorage?);
|
|
||||||
private searchTermsBySearchText(terms, searchText);
|
|
||||||
/**
|
|
||||||
* Searches terms for the given term set
|
|
||||||
* @param searchText
|
|
||||||
* @param termsetId
|
|
||||||
*/
|
|
||||||
private searchTermsByTermSet(searchText);
|
|
||||||
private isGuid(strGuid);
|
|
||||||
/**
|
|
||||||
* Sorting terms based on their path and depth
|
|
||||||
*
|
|
||||||
* @param terms
|
|
||||||
*/
|
|
||||||
private sortTerms(terms);
|
|
||||||
/**
|
|
||||||
* Sort the terms by their path
|
|
||||||
*
|
|
||||||
* @param a term 2
|
|
||||||
* @param b term 2
|
|
||||||
*/
|
|
||||||
private sortTermByPath(a, b);
|
|
||||||
private convertTermToPickerTerm(term);
|
|
||||||
private convertSuggestTermToPickerTerm(term);
|
|
||||||
}
|
|
||||||
@@ -1,574 +0,0 @@
|
|||||||
/* tslint:disable:no-null-keyword max-line-length typedef no-any no-string-literal variable-name */
|
|
||||||
/**
|
|
||||||
* This code is a copy from the library @pnp/sp-dev-fx-controls-react
|
|
||||||
*/
|
|
||||||
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 { SPHttpClient } from '@microsoft/sp-http';
|
|
||||||
import { findIndex } from '@microsoft/sp-lodash-subset';
|
|
||||||
var EmptyGuid = '00000000-0000-0000-0000-000000000000';
|
|
||||||
/**
|
|
||||||
* Service implementation to manage term stores in SharePoint
|
|
||||||
*/
|
|
||||||
var SPTermStorePickerService = (function () {
|
|
||||||
/**
|
|
||||||
* Service constructor
|
|
||||||
*/
|
|
||||||
function SPTermStorePickerService(props, context) {
|
|
||||||
this.props = props;
|
|
||||||
this.context = context;
|
|
||||||
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
|
|
||||||
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
|
|
||||||
}
|
|
||||||
SPTermStorePickerService.prototype.getTermLabels = function (termId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var result, data, reqHeaders, httpPostOptions, callResult, jsonResult, node, error_1;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
result = null;
|
|
||||||
_a.label = 1;
|
|
||||||
case 1:
|
|
||||||
_a.trys.push([1, 4, , 5]);
|
|
||||||
data = "<Request AddExpandoFieldTypeSuffix=\"true\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\".NET Library\" xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\"><Actions><ObjectPath Id=\"8\" ObjectPathId=\"7\" /><ObjectIdentityQuery Id=\"9\" ObjectPathId=\"7\" /><ObjectPath Id=\"11\" ObjectPathId=\"10\" /><ObjectIdentityQuery Id=\"12\" ObjectPathId=\"10\" /><ObjectPath Id=\"14\" ObjectPathId=\"13\" /><ObjectIdentityQuery Id=\"15\" ObjectPathId=\"13\" /><Query Id=\"16\" ObjectPathId=\"13\"><Query SelectAllProperties=\"false\"><Properties><Property Name=\"Labels\" SelectAll=\"true\"><Query SelectAllProperties=\"false\"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id=\"7\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"10\" ParentId=\"7\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"13\" ParentId=\"10\" Name=\"GetTerm\"><Parameters><Parameter Type=\"Guid\">" + termId + "</Parameter></Parameters></Method></ObjectPaths></Request>";
|
|
||||||
reqHeaders = new Headers();
|
|
||||||
reqHeaders.append('accept', 'application/json');
|
|
||||||
reqHeaders.append('content-type', 'application/xml');
|
|
||||||
httpPostOptions = {
|
|
||||||
headers: reqHeaders,
|
|
||||||
body: data
|
|
||||||
};
|
|
||||||
return [4 /*yield*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions)];
|
|
||||||
case 2:
|
|
||||||
callResult = _a.sent();
|
|
||||||
return [4 /*yield*/, callResult.json()];
|
|
||||||
case 3:
|
|
||||||
jsonResult = _a.sent();
|
|
||||||
node = jsonResult.find(function (x) { return x._ObjectType_ === 'SP.Taxonomy.Term'; });
|
|
||||||
if (node && node.Labels && node.Labels._Child_Items_) {
|
|
||||||
result = node.Labels._Child_Items_.map(function (termLabel) { return termLabel.Value; });
|
|
||||||
}
|
|
||||||
return [3 /*break*/, 5];
|
|
||||||
case 4:
|
|
||||||
error_1 = _a.sent();
|
|
||||||
result = null;
|
|
||||||
console.log(error_1.message);
|
|
||||||
return [3 /*break*/, 5];
|
|
||||||
case 5: return [2 /*return*/, result];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Gets the collection of term stores in the current SharePoint env
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.getTermStores = function () {
|
|
||||||
var _this = this;
|
|
||||||
// Retrieve the term store name, groups, and term sets
|
|
||||||
var 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="2" ObjectPathId="1" /><ObjectIdentityQuery Id="3" ObjectPathId="1" /><ObjectPath Id="5" ObjectPathId="4" /><ObjectIdentityQuery Id="6" ObjectPathId="4" /><Query Id="7" ObjectPathId="4"><Query SelectAllProperties="false"><Properties><Property Name="Id" ScalarProperty="true" /><Property Name="Name" ScalarProperty="true" /><Property Name="Groups"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="IsSystemGroup" ScalarProperty="true" /><Property Name="TermSets"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="Name" ScalarProperty="true" /><Property Name="Id" ScalarProperty="true" /><Property Name="Description" ScalarProperty="true" /><Property Name="Names" ScalarProperty="true" /></Properties></ChildItemQuery></Property></Properties></ChildItemQuery></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="1" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="4" ParentId="1" Name="GetDefaultSiteCollectionTermStore" /></ObjectPaths></Request>';
|
|
||||||
var reqHeaders = new Headers();
|
|
||||||
reqHeaders.append('accept', 'application/json');
|
|
||||||
reqHeaders.append('content-type', 'application/xml');
|
|
||||||
var httpPostOptions = {
|
|
||||||
headers: reqHeaders,
|
|
||||||
body: data
|
|
||||||
};
|
|
||||||
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
|
|
||||||
return serviceResponse.json().then(function (serviceJSONResponse) {
|
|
||||||
// Construct results
|
|
||||||
var termStoreResult = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermStore'; });
|
|
||||||
// Check if term store was retrieved
|
|
||||||
if (termStoreResult.length > 0) {
|
|
||||||
// Check if the termstore needs to be filtered or limited
|
|
||||||
if (_this.props.termsetNameOrID) {
|
|
||||||
return termStoreResult.map(function (termstore) {
|
|
||||||
var termGroups = termstore.Groups._Child_Items_;
|
|
||||||
// Check if the groups have to be limited to a specific term set
|
|
||||||
if (_this.props.termsetNameOrID) {
|
|
||||||
var termsetNameOrId_1 = _this.props.termsetNameOrID;
|
|
||||||
termGroups = termGroups.map(function (group) {
|
|
||||||
group.TermSets._Child_Items_ = group.TermSets._Child_Items_.filter(function (termSet) { return termSet.Name === termsetNameOrId_1 || _this.cleanGuid(termSet.Id).toLowerCase() === _this.cleanGuid(termsetNameOrId_1).toLowerCase(); });
|
|
||||||
return group;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Filter out all systen groups
|
|
||||||
termGroups = termGroups.filter(function (group) { return !group.IsSystemGroup; });
|
|
||||||
// Filter out empty groups
|
|
||||||
termGroups = termGroups.filter(function (group) { return group.TermSets._Child_Items_.length > 0; });
|
|
||||||
// Map the new groups
|
|
||||||
termstore.Groups._Child_Items_ = termGroups;
|
|
||||||
return termstore;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Return the term store results
|
|
||||||
return termStoreResult;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Gets the current term set
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.getTermSet = function () {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var termStore;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0: return [4 /*yield*/, this.getTermStores()];
|
|
||||||
case 1:
|
|
||||||
termStore = _a.sent();
|
|
||||||
return [2 /*return*/, this.getTermSetId(termStore, this.props.termsetNameOrID)];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Retrieve all terms for the given term set
|
|
||||||
* @param termset
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.getAllTerms = function (termset, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage) {
|
|
||||||
if (useSessionStorage === void 0) { useSessionStorage = true; }
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var _this = this;
|
|
||||||
var termsetId, termStore, crntTermSet, childTerms, data, reqHeaders, httpPostOptions;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
termsetId = termset;
|
|
||||||
if (!!this.isGuid(termset)) return [3 /*break*/, 2];
|
|
||||||
return [4 /*yield*/, this.getTermStores()];
|
|
||||||
case 1:
|
|
||||||
termStore = _a.sent();
|
|
||||||
crntTermSet = this.getTermSetId(termStore, termset);
|
|
||||||
if (crntTermSet) {
|
|
||||||
termsetId = this.cleanGuid(crntTermSet.Id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return [2 /*return*/, null];
|
|
||||||
}
|
|
||||||
_a.label = 2;
|
|
||||||
case 2:
|
|
||||||
childTerms = this.getTermsById(termsetId, useSessionStorage);
|
|
||||||
if (childTerms) {
|
|
||||||
return [2 /*return*/, childTerms];
|
|
||||||
}
|
|
||||||
data = "<Request xmlns=\"http://schemas.microsoft.com/sharepoint/clientquery/2009\" SchemaVersion=\"15.0.0.0\" LibraryVersion=\"16.0.0.0\" ApplicationName=\"Javascript Library\"><Actions><ObjectPath Id=\"1\" ObjectPathId=\"0\" /><ObjectIdentityQuery Id=\"2\" ObjectPathId=\"0\" /><ObjectPath Id=\"4\" ObjectPathId=\"3\" /><ObjectIdentityQuery Id=\"5\" ObjectPathId=\"3\" /><ObjectPath Id=\"7\" ObjectPathId=\"6\" /><ObjectIdentityQuery Id=\"8\" ObjectPathId=\"6\" /><ObjectPath Id=\"10\" ObjectPathId=\"9\" /><Query Id=\"11\" ObjectPathId=\"6\"><Query SelectAllProperties=\"true\"><Properties /></Query></Query><Query Id=\"12\" ObjectPathId=\"9\"><Query SelectAllProperties=\"false\"><Properties /></Query><ChildItemQuery SelectAllProperties=\"false\"><Properties><Property Name=\"IsRoot\" SelectAll=\"true\" /><Property Name=\"Labels\" SelectAll=\"true\" /><Property Name=\"TermsCount\" SelectAll=\"true\" /><Property Name=\"CustomSortOrder\" SelectAll=\"true\" /><Property Name=\"Id\" SelectAll=\"true\" /><Property Name=\"Name\" SelectAll=\"true\" /><Property Name=\"PathOfTerm\" SelectAll=\"true\" /><Property Name=\"Parent\" SelectAll=\"true\" /><Property Name=\"LocalCustomProperties\" SelectAll=\"true\" /><Property Name=\"IsDeprecated\" ScalarProperty=\"true\" /><Property Name=\"IsAvailableForTagging\" ScalarProperty=\"true\" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id=\"0\" Name=\"GetTaxonomySession\" TypeId=\"{981cbc68-9edc-4f8d-872f-71146fcbb84f}\" /><Method Id=\"3\" ParentId=\"0\" Name=\"GetDefaultKeywordsTermStore\" /><Method Id=\"6\" ParentId=\"3\" Name=\"GetTermSet\"><Parameters><Parameter Type=\"Guid\">" + termsetId + "</Parameter></Parameters></Method><Method Id=\"9\" ParentId=\"6\" Name=\"GetAllTerms\" /></ObjectPaths></Request>";
|
|
||||||
reqHeaders = new Headers();
|
|
||||||
reqHeaders.append('accept', 'application/json');
|
|
||||||
reqHeaders.append('content-type', 'application/xml');
|
|
||||||
httpPostOptions = {
|
|
||||||
headers: reqHeaders,
|
|
||||||
body: data
|
|
||||||
};
|
|
||||||
return [2 /*return*/, this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
|
|
||||||
return serviceResponse.json().then(function (serviceJSONResponse) {
|
|
||||||
var termStoreResultTermSets = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermSet'; });
|
|
||||||
if (termStoreResultTermSets.length > 0) {
|
|
||||||
var termStoreResultTermSet_1 = termStoreResultTermSets[0];
|
|
||||||
termStoreResultTermSet_1.Terms = [];
|
|
||||||
// Retrieve the term collection results
|
|
||||||
var termStoreResultTerms = serviceJSONResponse.filter(function (r) { return r['_ObjectType_'] === 'SP.Taxonomy.TermCollection'; });
|
|
||||||
if (termStoreResultTerms.length > 0) {
|
|
||||||
// Retrieve all terms
|
|
||||||
var terms = termStoreResultTerms[0]._Child_Items_;
|
|
||||||
if (hideDeprecatedTags === true) {
|
|
||||||
terms = terms.filter(function (d) { return d.IsDeprecated === false; });
|
|
||||||
}
|
|
||||||
if (hideTagsNotAvailableForTagging === true) {
|
|
||||||
terms = terms.filter(function (d) { return d.IsAvailableForTagging === true; });
|
|
||||||
}
|
|
||||||
// Clean the term ID and specify the path depth
|
|
||||||
terms = terms.map(function (term) {
|
|
||||||
if (term.IsRoot) {
|
|
||||||
term.CustomSortOrderIndex = (termStoreResultTermSet_1.CustomSortOrder) ? termStoreResultTermSet_1.CustomSortOrder.split(':').indexOf(_this.cleanGuid(term.Id)) : -1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
term.CustomSortOrderIndex = (term['Parent'].CustomSortOrder) ? term['Parent'].CustomSortOrder.split(':').indexOf(_this.cleanGuid(term.Id)) : -1;
|
|
||||||
}
|
|
||||||
term.Id = _this.cleanGuid(term.Id);
|
|
||||||
term['PathDepth'] = term.PathOfTerm.split(';').length;
|
|
||||||
term.TermSet = { Id: _this.cleanGuid(termStoreResultTermSet_1.Id), Name: termStoreResultTermSet_1.Name };
|
|
||||||
if (term['Parent']) {
|
|
||||||
term.ParentId = _this.cleanGuid(term['Parent'].Id);
|
|
||||||
}
|
|
||||||
return term;
|
|
||||||
});
|
|
||||||
// Check if the term set was not empty
|
|
||||||
if (terms.length > 0) {
|
|
||||||
// Sort the terms by PathOfTerm and their depth
|
|
||||||
terms = _this.sortTerms(terms);
|
|
||||||
termStoreResultTermSet_1.Terms = terms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (useSessionStorage && window.sessionStorage) {
|
|
||||||
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet_1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Do nothing, sometimes "storage quota exceeded" error if too many items
|
|
||||||
}
|
|
||||||
return termStoreResultTermSet_1;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
})];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Retrieve all terms that starts with the searchText
|
|
||||||
* @param searchText
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.searchTermsByName = function (searchText) {
|
|
||||||
return this.searchTermsByTermSet(searchText);
|
|
||||||
};
|
|
||||||
SPTermStorePickerService.prototype.searchTermsByTermId = function (searchText, termId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var useSessionStorage, childTerms, _a, termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging, terms;
|
|
||||||
return __generator(this, function (_b) {
|
|
||||||
switch (_b.label) {
|
|
||||||
case 0:
|
|
||||||
useSessionStorage = this.props.useSessionStorage;
|
|
||||||
childTerms = this.getTermsById(termId, useSessionStorage);
|
|
||||||
if (!childTerms) return [3 /*break*/, 1];
|
|
||||||
return [2 /*return*/, this.searchTermsBySearchText(childTerms, searchText)];
|
|
||||||
case 1:
|
|
||||||
_a = this.props, termsetNameOrID = _a.termsetNameOrID, hideDeprecatedTags = _a.hideDeprecatedTags, hideTagsNotAvailableForTagging = _a.hideTagsNotAvailableForTagging;
|
|
||||||
return [4 /*yield*/, this.getAllTermsByAnchorId(termsetNameOrID, termId, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage)];
|
|
||||||
case 2:
|
|
||||||
terms = _b.sent();
|
|
||||||
if (terms) {
|
|
||||||
return [2 /*return*/, this.searchTermsBySearchText(terms, searchText)];
|
|
||||||
}
|
|
||||||
_b.label = 3;
|
|
||||||
case 3: return [2 /*return*/, null];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Retrieve all terms for the given term set and anchorId
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.getAllTermsByAnchorId = function (termsetNameOrID, anchorId, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage) {
|
|
||||||
if (useSessionStorage === void 0) { useSessionStorage = true; }
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var _this = this;
|
|
||||||
var returnTerms, childTerms, termSet, terms, anchorTerm_1, anchorTermPath_1, anchorTerms;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
returnTerms = [];
|
|
||||||
childTerms = this.getTermsById(anchorId, useSessionStorage);
|
|
||||||
if (childTerms) {
|
|
||||||
return [2 /*return*/, childTerms];
|
|
||||||
}
|
|
||||||
return [4 /*yield*/, this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging)];
|
|
||||||
case 1:
|
|
||||||
termSet = _a.sent();
|
|
||||||
terms = termSet.Terms;
|
|
||||||
if (anchorId) {
|
|
||||||
anchorTerm_1 = terms.filter(function (t) { return t.Id.toLowerCase() === anchorId.toLowerCase(); }).shift();
|
|
||||||
if (anchorTerm_1) {
|
|
||||||
anchorTermPath_1 = anchorTerm_1.PathOfTerm + ";";
|
|
||||||
anchorTerms = terms.filter(function (t) { return t.PathOfTerm.substring(0, anchorTermPath_1.length) === anchorTermPath_1 && t.Id !== anchorTerm_1.Id; });
|
|
||||||
anchorTerms.forEach(function (term) {
|
|
||||||
returnTerms.push(_this.convertTermToPickerTerm(term));
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (useSessionStorage && window.sessionStorage) {
|
|
||||||
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
terms.forEach(function (term) {
|
|
||||||
returnTerms.push(_this.convertTermToPickerTerm(term));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [2 /*return*/, returnTerms];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Clean the Guid from the Web Service response
|
|
||||||
* @param guid
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.cleanGuid = function (guid) {
|
|
||||||
if (guid !== undefined) {
|
|
||||||
return guid.replace('/Guid(', '').replace('/', '').replace(')', '');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Get the term set ID by its name
|
|
||||||
* @param termstore
|
|
||||||
* @param termset
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.getTermSetId = function (termstore, termsetName) {
|
|
||||||
if (termstore && termstore.length > 0 && termsetName) {
|
|
||||||
// Get the first term store
|
|
||||||
var ts = termstore[0];
|
|
||||||
// Check if the term store contains groups
|
|
||||||
if (ts.Groups && ts.Groups._Child_Items_) {
|
|
||||||
for (var _i = 0, _a = ts.Groups._Child_Items_; _i < _a.length; _i++) {
|
|
||||||
var group = _a[_i];
|
|
||||||
// Check if the group contains term sets
|
|
||||||
if (group.TermSets && group.TermSets._Child_Items_) {
|
|
||||||
for (var _b = 0, _c = group.TermSets._Child_Items_; _b < _c.length; _b++) {
|
|
||||||
var termSet = _c[_b];
|
|
||||||
// Check if the term set is found
|
|
||||||
if (termSet.Name === termsetName) {
|
|
||||||
return termSet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
SPTermStorePickerService.prototype.getTermsById = function (termId, useSessionStorage) {
|
|
||||||
if (useSessionStorage === void 0) { useSessionStorage = true; }
|
|
||||||
try {
|
|
||||||
if (useSessionStorage && window.sessionStorage) {
|
|
||||||
var terms = window.sessionStorage.getItem(termId);
|
|
||||||
if (terms) {
|
|
||||||
return JSON.parse(terms);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
SPTermStorePickerService.prototype.searchTermsBySearchText = function (terms, searchText) {
|
|
||||||
if (terms) {
|
|
||||||
return terms.filter(function (t) { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Searches terms for the given term set
|
|
||||||
* @param searchText
|
|
||||||
* @param termsetId
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.searchTermsByTermSet = function (searchText) {
|
|
||||||
var _this = this;
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
_this.getTermStores().then(function (termStore) {
|
|
||||||
var termSetId = _this.props.termsetNameOrID;
|
|
||||||
if (!_this.isGuid(termSetId)) {
|
|
||||||
// Get the ID of the provided term set name
|
|
||||||
var crntTermSet = _this.getTermSetId(termStore, termSetId);
|
|
||||||
if (crntTermSet) {
|
|
||||||
termSetId = _this.cleanGuid(crntTermSet.Id);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (termStore === undefined || termStore.length === 0) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var loc = _this.context.pageContext.cultureInfo.currentUICultureName === 'de-de' ? 1031 : 1033;
|
|
||||||
var data = {
|
|
||||||
start: searchText,
|
|
||||||
lcid: loc !== 0 ? loc : _this.context.pageContext.web.language,
|
|
||||||
sspList: _this.cleanGuid(termStore[0].Id),
|
|
||||||
termSetList: termSetId,
|
|
||||||
anchorId: _this.props.anchorId ? _this.props.anchorId : EmptyGuid,
|
|
||||||
isSpanTermStores: false,
|
|
||||||
isSpanTermSets: false,
|
|
||||||
isIncludeUnavailable: _this.props.hideTagsNotAvailableForTagging === true,
|
|
||||||
isIncludeDeprecated: _this.props.hideDeprecatedTags === true,
|
|
||||||
isAddTerms: false,
|
|
||||||
isIncludePathData: false,
|
|
||||||
excludeKeyword: false,
|
|
||||||
excludedTermset: EmptyGuid
|
|
||||||
};
|
|
||||||
var reqHeaders = new Headers();
|
|
||||||
reqHeaders.append('accept', 'application/json');
|
|
||||||
reqHeaders.append('content-type', 'application/json');
|
|
||||||
var httpPostOptions = {
|
|
||||||
headers: reqHeaders,
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
};
|
|
||||||
return _this.context.spHttpClient.post(_this.suggestionServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then(function (serviceResponse) {
|
|
||||||
return serviceResponse.json().then(function (serviceJSONResponse) {
|
|
||||||
var groups = serviceJSONResponse.d.Groups;
|
|
||||||
if (groups && groups.length > 0) {
|
|
||||||
// Retrieve the term collection results
|
|
||||||
var terms = groups[0].Suggestions;
|
|
||||||
if (terms.length > 0) {
|
|
||||||
// Retrieve all terms
|
|
||||||
var returnTerms = terms.map(function (term) { return _this.convertSuggestTermToPickerTerm(term); });
|
|
||||||
resolve(returnTerms);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
SPTermStorePickerService.prototype.isGuid = function (strGuid) {
|
|
||||||
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(strGuid);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Sorting terms based on their path and depth
|
|
||||||
*
|
|
||||||
* @param terms
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.sortTerms = function (terms) {
|
|
||||||
// Start sorting by depth
|
|
||||||
var newTermsOrder = [];
|
|
||||||
var itemsToSort = true;
|
|
||||||
var pathLevel = 1;
|
|
||||||
while (itemsToSort) {
|
|
||||||
// Get terms for the current level
|
|
||||||
var crntTerms = terms.filter(function (term) { return term.PathDepth === pathLevel; });
|
|
||||||
if (crntTerms && crntTerms.length > 0) {
|
|
||||||
crntTerms = crntTerms.sort(this.sortTermByPath);
|
|
||||||
if (pathLevel !== 1) {
|
|
||||||
crntTerms = crntTerms.reverse();
|
|
||||||
var _loop_1 = function (crntTerm) {
|
|
||||||
var pathElms = crntTerm.PathOfTerm.split(';');
|
|
||||||
// Last item is not needed for parent path
|
|
||||||
pathElms.pop();
|
|
||||||
// Find the parent item and add the new item
|
|
||||||
var idx = findIndex(newTermsOrder, function (term) { return term.PathOfTerm === pathElms.join(';'); });
|
|
||||||
if (idx !== -1) {
|
|
||||||
newTermsOrder.splice(idx + 1, 0, crntTerm);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Push the item at the end if the parent couldn't be found
|
|
||||||
newTermsOrder.push(crntTerm);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (var _i = 0, crntTerms_1 = crntTerms; _i < crntTerms_1.length; _i++) {
|
|
||||||
var crntTerm = crntTerms_1[_i];
|
|
||||||
_loop_1(crntTerm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newTermsOrder = crntTerms;
|
|
||||||
}
|
|
||||||
++pathLevel;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
itemsToSort = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newTermsOrder;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Sort the terms by their path
|
|
||||||
*
|
|
||||||
* @param a term 2
|
|
||||||
* @param b term 2
|
|
||||||
*/
|
|
||||||
SPTermStorePickerService.prototype.sortTermByPath = function (a, b) {
|
|
||||||
if (a.CustomSortOrderIndex === -1) {
|
|
||||||
if (a.PathOfTerm.toLowerCase() < b.PathOfTerm.toLowerCase()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.PathOfTerm.toLowerCase() > b.PathOfTerm.toLowerCase()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (a.CustomSortOrderIndex < b.CustomSortOrderIndex) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.CustomSortOrderIndex > b.CustomSortOrderIndex) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
SPTermStorePickerService.prototype.convertTermToPickerTerm = function (term) {
|
|
||||||
return {
|
|
||||||
key: this.cleanGuid(term.Id),
|
|
||||||
name: term.Name,
|
|
||||||
path: term.PathOfTerm,
|
|
||||||
termSet: this.cleanGuid(term.TermSet.Id),
|
|
||||||
termSetName: term.TermSet.Name
|
|
||||||
};
|
|
||||||
};
|
|
||||||
SPTermStorePickerService.prototype.convertSuggestTermToPickerTerm = function (term) {
|
|
||||||
var path = '';
|
|
||||||
var termSetName = '';
|
|
||||||
if (term.Paths && term.Paths.length > 0) {
|
|
||||||
var fullPath = term.Paths[0].replace(/^\[/, '').replace(/\]$/, '');
|
|
||||||
var fullPathParts = fullPath.split(':');
|
|
||||||
path = fullPathParts.join(';') + ';' + term.DefaultLabel;
|
|
||||||
termSetName = fullPathParts[0];
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
key: this.cleanGuid(term.Id),
|
|
||||||
name: term.DefaultLabel,
|
|
||||||
path: path,
|
|
||||||
termSet: EmptyGuid,
|
|
||||||
termSetName: termSetName
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return SPTermStorePickerService;
|
|
||||||
}());
|
|
||||||
export default SPTermStorePickerService;
|
|
||||||
|
|
||||||
//# sourceMappingURL=SPTermStorePickerService.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
-11
@@ -1,11 +0,0 @@
|
|||||||
import { ITaxonomyNavigationService } from './ITaxonomyNavigationService';
|
|
||||||
import { IMenuItem } from './IMenuItem';
|
|
||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
|
||||||
export declare class TaxonomyNavigationService implements ITaxonomyNavigationService {
|
|
||||||
private context;
|
|
||||||
private termSetName;
|
|
||||||
private _taxonomyPickerService;
|
|
||||||
private _noTerm;
|
|
||||||
constructor(context: ApplicationCustomizerContext, termSetName: string);
|
|
||||||
getMenuItems(): Promise<IMenuItem[]>;
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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 { sp } from '@pnp/sp';
|
|
||||||
import { MenuItem } from './MenuItem';
|
|
||||||
import SPTermStorePickerService from './SPTermStorePickerService';
|
|
||||||
import { ItemDictionary } from './ItemDictionary';
|
|
||||||
var TaxonomyNavigationService = (function () {
|
|
||||||
function TaxonomyNavigationService(context /*, private lcid: number*/, termSetName) {
|
|
||||||
this.context = context; /*, private lcid: number*/
|
|
||||||
this.termSetName = termSetName;
|
|
||||||
this._noTerm = {
|
|
||||||
_ObjectType_: '',
|
|
||||||
_ObjectIdentity_: '',
|
|
||||||
CustomSortOrderIndex: 0,
|
|
||||||
Description: '',
|
|
||||||
Id: '',
|
|
||||||
IsAvailableForTagging: false,
|
|
||||||
IsDeprecated: false,
|
|
||||||
IsRoot: true,
|
|
||||||
LocalCustomProperties: {
|
|
||||||
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
|
|
||||||
_Sys_Nav_ExcludedProviders: undefined,
|
|
||||||
_Sys_Nav_SimpleLinkUrl: undefined
|
|
||||||
},
|
|
||||||
Name: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
|
|
||||||
PathOfTerm: '',
|
|
||||||
TermSet: undefined
|
|
||||||
};
|
|
||||||
sp.setup({
|
|
||||||
spfxContext: context
|
|
||||||
});
|
|
||||||
this._taxonomyPickerService = new SPTermStorePickerService({
|
|
||||||
anchorId: '',
|
|
||||||
termsetNameOrID: termSetName,
|
|
||||||
useSessionStorage: true,
|
|
||||||
hideDeprecatedTags: true,
|
|
||||||
hideTagsNotAvailableForTagging: false
|
|
||||||
}, this.context);
|
|
||||||
}
|
|
||||||
// Implement the methods from ITaxonomyNavigationService interface here
|
|
||||||
TaxonomyNavigationService.prototype.getMenuItems = function () {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var _this = this;
|
|
||||||
var siteCollectionUrl, termset, itemsDict, menuItems;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
siteCollectionUrl = this.context.pageContext.site.absoluteUrl;
|
|
||||||
return [4 /*yield*/, this._taxonomyPickerService.getAllTerms(this.termSetName)];
|
|
||||||
case 1:
|
|
||||||
termset = _a.sent();
|
|
||||||
itemsDict = new ItemDictionary();
|
|
||||||
menuItems = [];
|
|
||||||
if (!termset || !termset.Terms) {
|
|
||||||
console.warn('No terms found in the term set');
|
|
||||||
return [2 /*return*/, [new MenuItem(this._noTerm, 0, siteCollectionUrl)]];
|
|
||||||
}
|
|
||||||
termset.Terms.forEach(function (term) { return __awaiter(_this, void 0, void 0, function () {
|
|
||||||
var menuItem, parentItem;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
menuItem = new MenuItem(term, 0, siteCollectionUrl);
|
|
||||||
itemsDict.Add(term.Id, menuItem);
|
|
||||||
if (menuItem.pathDepth === 1) {
|
|
||||||
menuItems.push(menuItem);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
parentItem = itemsDict.Get(term.ParentId);
|
|
||||||
if (parentItem) {
|
|
||||||
parentItem.items.push(menuItem);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn("Item without parent: " + term.PathOfTerm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [2 /*return*/];
|
|
||||||
});
|
|
||||||
}); });
|
|
||||||
return [2 /*return*/, menuItems];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return TaxonomyNavigationService;
|
|
||||||
}());
|
|
||||||
export { TaxonomyNavigationService };
|
|
||||||
|
|
||||||
//# sourceMappingURL=TaxonomyNavigationService.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,20 +0,0 @@
|
|||||||
export interface IUserCustomActionProps {
|
|
||||||
Id?: string;
|
|
||||||
Title: string;
|
|
||||||
Name?: string;
|
|
||||||
Description?: string;
|
|
||||||
Location: string;
|
|
||||||
ScriptSrc?: string;
|
|
||||||
ScriptBlock?: string;
|
|
||||||
Url?: string;
|
|
||||||
Sequence?: number;
|
|
||||||
Group?: string;
|
|
||||||
ImageUrl?: string;
|
|
||||||
CommandUIExtension?: string;
|
|
||||||
RegistrationType?: number;
|
|
||||||
RegistrationId?: string;
|
|
||||||
Rights?: {};
|
|
||||||
Scope?: number;
|
|
||||||
ClientSideComponentId?: string;
|
|
||||||
ClientSideComponentProperties?: string;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=IUserCustomActionProps.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/UserCustomActionService/IUserCustomActionProps.js","sourcesContent":[],"sourceRoot":"..\\..\\..\\src"}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { UserCustomActionScope } from './UserCustomActionScope';
|
|
||||||
import { IUserCustomActionProps } from './IUserCustomActionProps';
|
|
||||||
import { UserCustomActionAddResult, UserCustomActionUpdateResult } from '@pnp/sp';
|
|
||||||
export interface IUserCustomActionService {
|
|
||||||
getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>;
|
|
||||||
getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>;
|
|
||||||
addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>;
|
|
||||||
updateUserCustomAction(scope: UserCustomActionScope, id: string, props: {}, listId?: string): Promise<UserCustomActionUpdateResult>;
|
|
||||||
deleteUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=IUserCustomActionService.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"services/UserCustomActionService/IUserCustomActionService.js","sourcesContent":[],"sourceRoot":"..\\..\\..\\src"}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export declare enum UserCustomActionScope {
|
|
||||||
Web = "web",
|
|
||||||
Site = "site",
|
|
||||||
List = "list",
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export var UserCustomActionScope;
|
|
||||||
(function (UserCustomActionScope) {
|
|
||||||
UserCustomActionScope["Web"] = "web";
|
|
||||||
UserCustomActionScope["Site"] = "site";
|
|
||||||
UserCustomActionScope["List"] = "list";
|
|
||||||
})(UserCustomActionScope = UserCustomActionScope || (UserCustomActionScope = {}));
|
|
||||||
|
|
||||||
//# sourceMappingURL=UserCustomActionScope.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["services/UserCustomActionService/UserCustomActionScope.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,qBAIX;AAJD,WAAY,qBAAqB;IAC7B,oCAAW,CAAA;IACX,sCAAa,CAAA;IACb,sCAAa,CAAA;AACjB,CAAC,EAJW,qBAAqB,GAArB,qBAAqB,KAArB,qBAAqB,QAIhC","file":"services/UserCustomActionService/UserCustomActionScope.js","sourcesContent":["export enum UserCustomActionScope {\r\n Web = 'web',\r\n Site = 'site',\r\n List = 'list'\r\n}\r\n"],"sourceRoot":"..\\..\\..\\src"}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { UserCustomActionAddResult, UserCustomActionUpdateResult } from '@pnp/sp/src/usercustomactions';
|
|
||||||
import { IUserCustomActionService } from './IUserCustomActionService';
|
|
||||||
import { UserCustomActionScope } from './UserCustomActionScope';
|
|
||||||
import { IUserCustomActionProps } from './IUserCustomActionProps';
|
|
||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
|
||||||
export declare class UserCustomActionService implements IUserCustomActionService {
|
|
||||||
constructor(context: ApplicationCustomizerContext);
|
|
||||||
getUserCustomActions(scope: UserCustomActionScope, listId?: string): Promise<IUserCustomActionProps[]>;
|
|
||||||
getUserCustomActionById(scope: UserCustomActionScope, id: string, listId?: string): Promise<IUserCustomActionProps>;
|
|
||||||
addUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<UserCustomActionAddResult>;
|
|
||||||
updateUserCustomAction(scope: UserCustomActionScope, id: string, props: {}, listId?: string): Promise<UserCustomActionUpdateResult>;
|
|
||||||
deleteUserCustomAction(scope: UserCustomActionScope, customAction: IUserCustomActionProps, listId?: string): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
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 { sp } from '@pnp/sp';
|
|
||||||
import { UserCustomActionScope } from './UserCustomActionScope';
|
|
||||||
var UserCustomActionService = (function () {
|
|
||||||
function UserCustomActionService(context) {
|
|
||||||
sp.setup({
|
|
||||||
spfxContext: context
|
|
||||||
});
|
|
||||||
}
|
|
||||||
UserCustomActionService.prototype.getUserCustomActions = function (scope, listId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var actions, _a, error_1;
|
|
||||||
return __generator(this, function (_b) {
|
|
||||||
switch (_b.label) {
|
|
||||||
case 0:
|
|
||||||
_b.trys.push([0, 9, , 10]);
|
|
||||||
actions = void 0;
|
|
||||||
_a = scope;
|
|
||||||
switch (_a) {
|
|
||||||
case UserCustomActionScope.Web: return [3 /*break*/, 1];
|
|
||||||
case UserCustomActionScope.Site: return [3 /*break*/, 3];
|
|
||||||
case UserCustomActionScope.List: return [3 /*break*/, 5];
|
|
||||||
}
|
|
||||||
return [3 /*break*/, 7];
|
|
||||||
case 1: return [4 /*yield*/, sp.web.userCustomActions.get()];
|
|
||||||
case 2:
|
|
||||||
actions = _b.sent();
|
|
||||||
return [3 /*break*/, 8];
|
|
||||||
case 3: return [4 /*yield*/, sp.site.userCustomActions.get()];
|
|
||||||
case 4:
|
|
||||||
actions = _b.sent();
|
|
||||||
return [3 /*break*/, 8];
|
|
||||||
case 5:
|
|
||||||
if (!listId) {
|
|
||||||
throw new Error('List ID is required for List scope');
|
|
||||||
}
|
|
||||||
return [4 /*yield*/, sp.web.lists.getById(listId).userCustomActions.get()];
|
|
||||||
case 6:
|
|
||||||
actions = _b.sent();
|
|
||||||
return [3 /*break*/, 8];
|
|
||||||
case 7: throw new Error('Invalid scope');
|
|
||||||
case 8: return [2 /*return*/, actions];
|
|
||||||
case 9:
|
|
||||||
error_1 = _b.sent();
|
|
||||||
console.error('Error getting user custom actions: ', error_1);
|
|
||||||
throw error_1;
|
|
||||||
case 10: return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
UserCustomActionService.prototype.getUserCustomActionById = function (scope, id, listId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
try {
|
|
||||||
switch (scope) {
|
|
||||||
case UserCustomActionScope.Web:
|
|
||||||
return [2 /*return*/, sp.web.userCustomActions.getById(id)];
|
|
||||||
case UserCustomActionScope.Site:
|
|
||||||
return [2 /*return*/, sp.site.userCustomActions.getById(id)];
|
|
||||||
case UserCustomActionScope.List:
|
|
||||||
if (!listId) {
|
|
||||||
throw new Error('List ID is required for List scope');
|
|
||||||
}
|
|
||||||
return [2 /*return*/, sp.web.lists.getById(listId).userCustomActions.getById(id)];
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid scope');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error getting user custom action by ID: ', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return [2 /*return*/];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
UserCustomActionService.prototype.addUserCustomAction = function (scope, customAction, listId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
try {
|
|
||||||
switch (scope) {
|
|
||||||
case UserCustomActionScope.Web:
|
|
||||||
return [2 /*return*/, sp.web.userCustomActions.add(customAction)];
|
|
||||||
case UserCustomActionScope.Site:
|
|
||||||
return [2 /*return*/, sp.site.userCustomActions.add(customAction)];
|
|
||||||
case UserCustomActionScope.List:
|
|
||||||
if (!listId) {
|
|
||||||
throw new Error('List ID is required for List scope');
|
|
||||||
}
|
|
||||||
return [2 /*return*/, sp.web.lists.getById(listId).userCustomActions.add(customAction)];
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid scope');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error adding user custom action: ', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return [2 /*return*/];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
UserCustomActionService.prototype.updateUserCustomAction = function (scope, id, props, listId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var result, _a, error_2;
|
|
||||||
return __generator(this, function (_b) {
|
|
||||||
switch (_b.label) {
|
|
||||||
case 0:
|
|
||||||
_b.trys.push([0, 9, , 10]);
|
|
||||||
result = void 0;
|
|
||||||
_a = scope;
|
|
||||||
switch (_a) {
|
|
||||||
case UserCustomActionScope.Web: return [3 /*break*/, 1];
|
|
||||||
case UserCustomActionScope.Site: return [3 /*break*/, 3];
|
|
||||||
case UserCustomActionScope.List: return [3 /*break*/, 5];
|
|
||||||
}
|
|
||||||
return [3 /*break*/, 7];
|
|
||||||
case 1: return [4 /*yield*/, sp.web.userCustomActions.getById(id).update(props)];
|
|
||||||
case 2:
|
|
||||||
result = _b.sent();
|
|
||||||
return [3 /*break*/, 8];
|
|
||||||
case 3: return [4 /*yield*/, sp.site.userCustomActions.getById(id).update(props)];
|
|
||||||
case 4:
|
|
||||||
result = _b.sent();
|
|
||||||
return [3 /*break*/, 8];
|
|
||||||
case 5:
|
|
||||||
if (!listId) {
|
|
||||||
throw new Error('List ID is required for List scope');
|
|
||||||
}
|
|
||||||
return [4 /*yield*/, sp.web.lists.getById(listId).userCustomActions.getById(id).update(props)];
|
|
||||||
case 6:
|
|
||||||
result = _b.sent();
|
|
||||||
return [3 /*break*/, 8];
|
|
||||||
case 7: throw new Error('Invalid scope');
|
|
||||||
case 8: return [2 /*return*/, result];
|
|
||||||
case 9:
|
|
||||||
error_2 = _b.sent();
|
|
||||||
console.error('Error updating user custom action: ', error_2);
|
|
||||||
throw error_2;
|
|
||||||
case 10: return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
UserCustomActionService.prototype.deleteUserCustomAction = function (scope, customAction, listId) {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
try {
|
|
||||||
switch (scope) {
|
|
||||||
case UserCustomActionScope.Web:
|
|
||||||
return [2 /*return*/, sp.web.userCustomActions.getById(customAction.Id).delete()];
|
|
||||||
case UserCustomActionScope.Site:
|
|
||||||
return [2 /*return*/, sp.site.userCustomActions.getById(customAction.Id).delete()];
|
|
||||||
case UserCustomActionScope.List:
|
|
||||||
if (!listId) {
|
|
||||||
throw new Error('List ID is required for List scope');
|
|
||||||
}
|
|
||||||
return [2 /*return*/, sp.web.lists.getById(listId).userCustomActions.getById(customAction.Id).delete()];
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid scope');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error deleting user custom action: ', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return [2 /*return*/];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return UserCustomActionService;
|
|
||||||
}());
|
|
||||||
export { UserCustomActionService };
|
|
||||||
|
|
||||||
//# sourceMappingURL=UserCustomActionService.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mega-menu",
|
"name": "mega-menu",
|
||||||
"version": "1.0.4",
|
"version": "2.2.4",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+6
-4
@@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "mega-menu",
|
"name": "mega-menu",
|
||||||
"version": "1.0.4",
|
"version": "2.2.4",
|
||||||
"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:classic": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\build-classic.ps1",
|
||||||
|
"build": "npm run build:classic && gulp bundle",
|
||||||
"clean": "gulp clean",
|
"clean": "gulp clean",
|
||||||
"test": "gulp test"
|
"test": "tsc -p tests/tsconfig.json && node temp/tests/tests/MegaMenuCore.test.js && node --check classic/megamenu-services-standalone.js && node --check classic/megamenu-classic.js && node tests/validate-static-assets.js",
|
||||||
|
"package": "npm test && npm run build:classic && gulp clean && gulp bundle --ship && gulp package-solution --ship"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/decorators": "~1.4.1",
|
"@microsoft/decorators": "~1.4.1",
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>MegaMenu – Flyout-Vorschau</title>
|
||||||
|
<link rel="stylesheet" href="../classic/megamenu-classic.css" />
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: #faf9f8; color: #323130; font-family: "Segoe UI", Arial, sans-serif; }
|
||||||
|
.suite-bar { height: 48px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: #0078d4; color: #fff; }
|
||||||
|
.suite-brand { display: flex; align-items: center; gap: 14px; font-size: 16px; font-weight: 600; }
|
||||||
|
.waffle { display: grid; grid-template-columns: repeat(3, 4px); gap: 3px; }
|
||||||
|
.waffle span { width: 4px; height: 4px; background: #fff; }
|
||||||
|
.suite-user { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||||
|
.avatar { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: #fff; color: #0078d4; font-weight: 700; }
|
||||||
|
.site-header { display: flex; align-items: center; justify-content: space-between; min-height: 72px; padding: 0 32px; background: #fff; }
|
||||||
|
.site-title { font-size: 24px; font-weight: 300; }
|
||||||
|
.site-search { width: 280px; padding: 9px 12px; border: 1px solid #8a8886; background: #fff; }
|
||||||
|
.preview-label { padding: 7px 24px; background: #deecf9; color: #605e5c; font-size: 12px; text-align: center; }
|
||||||
|
.page { max-width: 1200px; margin: 28px auto; padding: 0 24px; }
|
||||||
|
.page h1 { margin: 0 0 8px; font-size: 32px; font-weight: 300; }
|
||||||
|
.page p { color: #605e5c; }
|
||||||
|
.news { max-width: 760px; margin-top: 28px; border-top: 1px solid #edebe9; }
|
||||||
|
.news article { padding: 18px 0; border-bottom: 1px solid #edebe9; }
|
||||||
|
.news time { color: #605e5c; font-size: 12px; }
|
||||||
|
.news h2 { margin: 5px 0; font-size: 18px; font-weight: 600; }
|
||||||
|
@media (max-width: 760px) { .site-search { display: none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="suite-bar">
|
||||||
|
<div class="suite-brand"><span class="waffle" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>SharePoint</div>
|
||||||
|
<div class="suite-user"><span>Max Mustermann</span><span class="avatar">MM</span></div>
|
||||||
|
</header>
|
||||||
|
<div class="site-header"><div class="site-title">Intranet Portal</div><input class="site-search" type="search" placeholder="Auf dieser Website suchen" /></div>
|
||||||
|
|
||||||
|
<div class="megamenu-classic-container">
|
||||||
|
<nav id="Mega-Menu" class="mega-menu-main mega-menu-mode-flyout" data-menu-mode="flyout" aria-label="Hauptnavigation">
|
||||||
|
<button type="button" class="mega-menu-mobile-toggle" aria-expanded="false" aria-controls="Mega-Menu-Items" aria-label="Hauptnavigation öffnen"><span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label">Hauptnavigation</span></button>
|
||||||
|
<ul id="Mega-Menu-Items" class="mega-menu-top-level">
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link" href="#">Startseite</a></li>
|
||||||
|
<li class="mega-menu-top-item is-active">
|
||||||
|
<a class="menu-item-link menu-item-has-children" href="#unternehmen">Unternehmen</a>
|
||||||
|
<button type="button" class="menu-item-toggle" aria-haspopup="true" aria-expanded="true" aria-controls="Mega-Menu-Panel-Unternehmen" aria-label="Untermenü schließen für Unternehmen"><span aria-hidden="true"></span></button>
|
||||||
|
<div id="Mega-Menu-Panel-Unternehmen" class="mega-menu mega-menu-flyout js-open" aria-expanded="true">
|
||||||
|
<ul class="flyout-list flyout-level-2">
|
||||||
|
<li class="flyout-item has-children">
|
||||||
|
<a class="flyout-link flyout-link-has-children" href="#ueber-uns">Über uns</a>
|
||||||
|
<button type="button" class="flyout-toggle" aria-expanded="false" aria-controls="Flyout-UeberUns" aria-label="Untermenü öffnen für Über uns"><span aria-hidden="true"></span></button>
|
||||||
|
<ul id="Flyout-UeberUns" class="flyout-list flyout-level-3"><li class="flyout-item"><a class="flyout-link" href="#profil">Unternehmensprofil</a></li><li class="flyout-item"><a class="flyout-link" href="#leitung">Geschäftsleitung</a></li><li class="flyout-item"><a class="flyout-link" href="#standorte">Standorte</a></li></ul>
|
||||||
|
</li>
|
||||||
|
<li class="flyout-item has-children">
|
||||||
|
<a class="flyout-link flyout-link-has-children" href="#organisation">Organisation</a>
|
||||||
|
<button type="button" class="flyout-toggle" aria-expanded="false" aria-controls="Flyout-Organisation" aria-label="Untermenü öffnen für Organisation"><span aria-hidden="true"></span></button>
|
||||||
|
<ul id="Flyout-Organisation" class="flyout-list flyout-level-3"><li class="flyout-item"><a class="flyout-link" href="#bereiche">Fachbereiche</a></li><li class="flyout-item"><a class="flyout-link" href="#organigramm">Organigramm</a></li><li class="flyout-item"><a class="flyout-link" href="#ansprechpartner">Ansprechpartner</a></li></ul>
|
||||||
|
</li>
|
||||||
|
<li class="flyout-item is-current"><a class="flyout-link" href="#news">Aktuelle Meldungen</a></li>
|
||||||
|
<li class="flyout-item"><a class="flyout-link" href="#termine">Veranstaltungen</a></li>
|
||||||
|
<li class="flyout-item"><a class="flyout-link" href="#telefonbuch">Telefonbuch</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#personal" aria-haspopup="true" aria-expanded="false">Personal</a></li>
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#services" aria-haspopup="true" aria-expanded="false">Services</a></li>
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link" href="#projekte">Projekte</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="preview-label">Vorschau: menuMode = "flyout" · Für Ebene 3 mit der Maus über „Über uns“ oder „Organisation“ fahren</div>
|
||||||
|
|
||||||
|
<main class="page">
|
||||||
|
<h1>Aktuelle Meldungen</h1>
|
||||||
|
<p>Die Flyout-Navigation bleibt kompakt und folgt der hierarchischen Struktur des Termsets.</p>
|
||||||
|
<section class="news"><article><time>20. Juli 2026</time><h2>Neue Services im Intranet verfügbar</h2><p>Alle wichtigen Anwendungen sind jetzt zentral über das Serviceportal erreichbar.</p></article><article><time>18. Juli 2026</time><h2>Informationen aus der Geschäftsleitung</h2><p>Die aktuelle Mitarbeitendeninformation steht zum Abruf bereit.</p></article></section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var triggers = document.querySelectorAll('.mega-menu-top-level > .mega-menu-top-item > .menu-item-toggle');
|
||||||
|
Array.prototype.forEach.call(triggers, function (trigger) {
|
||||||
|
trigger.addEventListener('click', function (event) {
|
||||||
|
var panel = trigger.nextElementSibling;
|
||||||
|
if (!panel || !panel.classList.contains('mega-menu')) { return; }
|
||||||
|
event.preventDefault();
|
||||||
|
var open = panel.classList.contains('js-open');
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.mega-menu.js-open'), function (item) { item.classList.remove('js-open'); if (item.previousElementSibling) { item.previousElementSibling.setAttribute('aria-expanded', 'false'); } });
|
||||||
|
panel.classList.toggle('js-open', !open); trigger.setAttribute('aria-expanded', String(!open));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var mobile = document.querySelector('.mega-menu-mobile-toggle');
|
||||||
|
mobile.addEventListener('click', function () { var open = mobile.getAttribute('aria-expanded') !== 'true'; mobile.setAttribute('aria-expanded', String(open)); document.getElementById('Mega-Menu').classList.toggle('is-mobile-open', open); });
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.flyout-toggle'), function (toggle) { toggle.addEventListener('click', function () { var open = toggle.getAttribute('aria-expanded') !== 'true'; toggle.setAttribute('aria-expanded', String(open)); toggle.parentElement.classList.toggle('is-open', open); }); });
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>MegaMenu – Mega-Menu-Vorschau</title>
|
||||||
|
<link rel="stylesheet" href="../classic/megamenu-classic.css" />
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: #faf9f8; color: #323130; font-family: "Segoe UI", Arial, sans-serif; }
|
||||||
|
.suite-bar { height: 48px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: #0078d4; color: #fff; }
|
||||||
|
.suite-brand { display: flex; align-items: center; gap: 14px; font-size: 16px; font-weight: 600; }
|
||||||
|
.waffle { display: grid; grid-template-columns: repeat(3, 4px); gap: 3px; }
|
||||||
|
.waffle span { width: 4px; height: 4px; background: #fff; }
|
||||||
|
.suite-user { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||||
|
.avatar { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: #fff; color: #0078d4; font-weight: 700; }
|
||||||
|
.site-header { display: flex; align-items: center; justify-content: space-between; min-height: 72px; padding: 0 32px; background: #fff; }
|
||||||
|
.site-title { font-size: 24px; font-weight: 300; }
|
||||||
|
.site-search { width: 280px; padding: 9px 12px; border: 1px solid #8a8886; background: #fff; }
|
||||||
|
.preview-label { padding: 7px 24px; background: #fff4ce; color: #605e5c; font-size: 12px; text-align: center; }
|
||||||
|
.page { max-width: 1200px; margin: 28px auto; padding: 0 24px; }
|
||||||
|
.page h1 { margin: 0 0 8px; font-size: 32px; font-weight: 300; }
|
||||||
|
.page p { color: #605e5c; }
|
||||||
|
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 28px; }
|
||||||
|
.card { min-height: 150px; padding: 20px; border: 1px solid #edebe9; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.06); }
|
||||||
|
.card strong { color: #0078d4; }
|
||||||
|
@media (max-width: 760px) { .site-search { display: none; } .cards { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="suite-bar">
|
||||||
|
<div class="suite-brand"><span class="waffle" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>SharePoint</div>
|
||||||
|
<div class="suite-user"><span>Max Mustermann</span><span class="avatar">MM</span></div>
|
||||||
|
</header>
|
||||||
|
<div class="site-header"><div class="site-title">Intranet Portal</div><input class="site-search" type="search" placeholder="Auf dieser Website suchen" /></div>
|
||||||
|
|
||||||
|
<div class="megamenu-classic-container">
|
||||||
|
<nav id="Mega-Menu" class="mega-menu-main mega-menu-mode-megaMenu" data-menu-mode="megaMenu" aria-label="Hauptnavigation">
|
||||||
|
<button type="button" class="mega-menu-mobile-toggle" aria-expanded="false" aria-controls="Mega-Menu-Items" aria-label="Hauptnavigation öffnen"><span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label">Hauptnavigation</span></button>
|
||||||
|
<ul id="Mega-Menu-Items" class="mega-menu-top-level">
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link" href="#">Startseite</a></li>
|
||||||
|
<li class="mega-menu-top-item is-active">
|
||||||
|
<a class="menu-item-link menu-item-has-children" href="#unternehmen">Unternehmen</a>
|
||||||
|
<button type="button" class="menu-item-toggle" aria-haspopup="true" aria-expanded="true" aria-controls="Mega-Menu-Panel-Unternehmen" aria-label="Untermenü schließen für Unternehmen"><span aria-hidden="true"></span></button>
|
||||||
|
<div id="Mega-Menu-Panel-Unternehmen" class="mega-menu js-open" aria-expanded="true">
|
||||||
|
<div class="mega-menu-grid" style="--megaMenuColumnCount:4">
|
||||||
|
<section class="mega-menu-category">
|
||||||
|
<h3 class="mega-menu-category-title"><a href="#ueber-uns">Über uns</a></h3>
|
||||||
|
<ul class="mega-menu-links"><li><a href="#profil">Unternehmensprofil</a></li><li><a href="#leitung">Geschäftsleitung</a></li><li><a href="#standorte">Standorte</a></li></ul>
|
||||||
|
</section>
|
||||||
|
<section class="mega-menu-category">
|
||||||
|
<h3 class="mega-menu-category-title"><a href="#organisation">Organisation</a></h3>
|
||||||
|
<ul class="mega-menu-links"><li><a href="#bereiche">Fachbereiche</a></li><li><a href="#organigramm">Organigramm</a></li><li><a href="#ansprechpartner">Ansprechpartner</a></li></ul>
|
||||||
|
</section>
|
||||||
|
<section class="mega-menu-category">
|
||||||
|
<h3 class="mega-menu-category-title"><a href="#kommunikation">Kommunikation</a></h3>
|
||||||
|
<ul class="mega-menu-links"><li class="is-current"><a class="is-current" href="#news">Aktuelle Meldungen</a></li><li><a href="#termine">Veranstaltungen</a></li><li><a href="#medien">Mediencenter</a></li></ul>
|
||||||
|
</section>
|
||||||
|
<section class="mega-menu-category">
|
||||||
|
<h3 class="mega-menu-category-title"><span>Direktzugriff</span></h3>
|
||||||
|
<ul class="mega-menu-links"><li><a href="#telefonbuch">Telefonbuch</a></li><li><a href="#formulare">Formulare</a></li><li><a href="#vorlagen">Vorlagen</a></li></ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#personal" aria-haspopup="true" aria-expanded="false">Personal</a></li>
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link menu-item-has-children" href="#services" aria-haspopup="true" aria-expanded="false">Services</a></li>
|
||||||
|
<li class="mega-menu-top-item"><a class="menu-item-link" href="#projekte">Projekte</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="preview-label">Vorschau: menuMode = "megaMenu" · „Unternehmen“ ist zur Ansicht geöffnet</div>
|
||||||
|
|
||||||
|
<main class="page">
|
||||||
|
<h1>Willkommen im Intranet</h1>
|
||||||
|
<p>Diese statische Vorschau verwendet dieselben CSS-Klassen wie die MegaMenu-Solution.</p>
|
||||||
|
<div class="cards"><div class="card"><strong>Aktuelle Meldungen</strong><p>Neuigkeiten und wichtige Informationen aus dem Unternehmen.</p></div><div class="card"><strong>Schnellzugriff</strong><p>Formulare, Vorlagen und häufig verwendete Anwendungen.</p></div><div class="card"><strong>Meine Aufgaben</strong><p>Persönliche Aufgaben und zuletzt verwendete Dokumente.</p></div></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var triggers = document.querySelectorAll('.mega-menu-top-level > .mega-menu-top-item > .menu-item-toggle');
|
||||||
|
function closeAll(except) {
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.mega-menu.js-open'), function (panel) {
|
||||||
|
if (panel !== except) { panel.classList.remove('js-open'); panel.setAttribute('aria-expanded', 'false'); if (panel.previousElementSibling) { panel.previousElementSibling.setAttribute('aria-expanded', 'false'); } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Array.prototype.forEach.call(triggers, function (trigger) {
|
||||||
|
trigger.addEventListener('click', function (event) {
|
||||||
|
var panel = trigger.nextElementSibling;
|
||||||
|
if (!panel || !panel.classList.contains('mega-menu')) { return; }
|
||||||
|
event.preventDefault();
|
||||||
|
var open = panel.classList.contains('js-open'); closeAll(panel);
|
||||||
|
panel.classList.toggle('js-open', !open); panel.setAttribute('aria-expanded', String(!open)); trigger.setAttribute('aria-expanded', String(!open));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var mobile = document.querySelector('.mega-menu-mobile-toggle');
|
||||||
|
mobile.addEventListener('click', function () { var open = mobile.getAttribute('aria-expanded') !== 'true'; mobile.setAttribute('aria-expanded', String(open)); document.getElementById('Mega-Menu').classList.toggle('is-mobile-open', open); });
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,474 +1,532 @@
|
|||||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||||
|
|
||||||
/*
|
$white: "[theme: white, default: #ffffff]";
|
||||||
Global override example:
|
$neutralLighter: "[theme: neutralLighter, default: #f4f4f4]";
|
||||||
:root {
|
$neutralLight: "[theme: neutralLight, default: #eaeaea]";
|
||||||
--themePrimary: #00407f;
|
$neutralTertiary: "[theme: neutralTertiary, default: #a6a6a6]";
|
||||||
--themeLighterAlt: #f0f5fa;
|
$neutralSecondary: "[theme: neutralSecondary, default: #666666]";
|
||||||
--themeLighter: #c5d8eb;
|
$neutralPrimary: "[theme: neutralPrimary, default: #333333]";
|
||||||
--themeLight: #98b8d9;
|
$themePrimary: "[theme: themePrimary, default: #0078d7]";
|
||||||
--themeTertiary: #477db3;
|
$themeDark: "[theme: themeDark, default: #005a9e]";
|
||||||
--themeSecondary: #11508f;
|
$themeLighterAlt: "[theme: themeLighterAlt, default: #eff6fc]";
|
||||||
--themeDarkAlt: #003973;
|
$menuShadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||||
--themeDark: #003061;
|
$menuLayer: var(--megaMenuZIndex, 6000);
|
||||||
--themeDarker: #002447;
|
|
||||||
--neutralLighterAlt: #faf9f8;
|
|
||||||
--neutralLighter: #f3f2f1;
|
|
||||||
--neutralLight: #edebe9;
|
|
||||||
--neutralQuaternaryAlt: #e1dfdd;
|
|
||||||
--neutralQuaternary: #d0d0d0;
|
|
||||||
--neutralTertiaryAlt: #c8c6c4;
|
|
||||||
--neutralTertiary: #a19f9d;
|
|
||||||
--neutralSecondary: #605e5c;
|
|
||||||
--neutralSecondaryAlt: #8a8886;
|
|
||||||
--neutralPrimaryAlt: #3b3a39;
|
|
||||||
--neutralPrimary: #323130;
|
|
||||||
--neutralDark: #201f1e;
|
|
||||||
--black: #000000;
|
|
||||||
--white: #ffffff;
|
|
||||||
|
|
||||||
--megaMenuNavBackground: #5f6f74;
|
|
||||||
--megaMenuNavHoverBackground: #6f8085;
|
|
||||||
--megaMenuNavOpenBackground: #728388;
|
|
||||||
--megaMenuNavTextColor: #ffffff;
|
|
||||||
--megaMenuNavDividerColor: rgba(255, 255, 255, 0.22);
|
|
||||||
--megaMenuPanelWidth: 1280px;
|
|
||||||
--megaMenuZIndex: 6000;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
$mm-white: var(--white, #{$ms-color-white});
|
|
||||||
$mm-panel-surface: var(--white, #{$ms-color-white});
|
|
||||||
$mm-panel-border: var(--neutralLight, #{$ms-color-neutralLight});
|
|
||||||
$mm-panel-divider: var(--neutralQuaternaryAlt, #{$ms-color-neutralQuaternaryAlt});
|
|
||||||
$mm-text: var(--neutralPrimary, #{$ms-color-neutralPrimary});
|
|
||||||
$mm-text-muted: var(--neutralSecondary, #{$ms-color-neutralSecondary});
|
|
||||||
$mm-nav-background: var(--megaMenuNavBackground, #5f6f74);
|
|
||||||
$mm-nav-hover-background: var(--megaMenuNavHoverBackground, #6f8085);
|
|
||||||
$mm-nav-open-background: var(--megaMenuNavOpenBackground, #728388);
|
|
||||||
$mm-nav-divider: var(--megaMenuNavDividerColor, rgba(255, 255, 255, 0.22));
|
|
||||||
$mm-nav-text: var(--megaMenuNavTextColor, #ffffff);
|
|
||||||
$mm-focus-accent: var(--themePrimary, #{$ms-color-themePrimary});
|
|
||||||
$mm-focus-soft: var(--themeLighterAlt, #{$ms-color-themeLighterAlt});
|
|
||||||
$mm-panel-shadow: var(--megaMenuPanelShadow, 0 14px 28px rgba(0, 0, 0, 0.18));
|
|
||||||
$mm-panel-width: var(--megaMenuPanelWidth, 1280px);
|
|
||||||
$mm-nav-height: var(--megaMenuTopNavHeight, 34px);
|
|
||||||
$mm-nav-inline-padding: var(--megaMenuNavInlinePadding, 8px);
|
|
||||||
$mm-item-inline-padding: var(--megaMenuItemInlinePadding, 12px);
|
|
||||||
$mm-column-padding-x: var(--megaMenuColumnPaddingX, 18px);
|
|
||||||
$mm-column-padding-y: var(--megaMenuColumnPaddingY, 16px);
|
|
||||||
$mm-link-radius: var(--megaMenuLinkRadius, 2px);
|
|
||||||
$mm-layer-index: var(--megaMenuZIndex, 6000);
|
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
:root {
|
:root {
|
||||||
--megaMenuNavBackground: #5f6f74;
|
--megaMenuBarHeight: 44px;
|
||||||
--megaMenuNavHoverBackground: #6f8085;
|
--megaMenuContentWidth: 1200px;
|
||||||
--megaMenuNavOpenBackground: #728388;
|
--megaMenuFlyoutWidth: 288px;
|
||||||
--megaMenuNavTextColor: #ffffff;
|
--megaMenuZIndex: 9.5;
|
||||||
--megaMenuNavDividerColor: rgba(255, 255, 255, 0.22);
|
|
||||||
--megaMenuTopNavHeight: 34px;
|
|
||||||
--megaMenuPanelWidth: 1280px;
|
|
||||||
--megaMenuPanelShadow: 0 14px 28px rgba(0, 0, 0, 0.18);
|
|
||||||
--megaMenuZIndex: 6000;
|
|
||||||
--megaMenuNavInlinePadding: 8px;
|
|
||||||
--megaMenuItemInlinePadding: 12px;
|
|
||||||
--megaMenuColumnPaddingX: 18px;
|
|
||||||
--megaMenuColumnPaddingY: 16px;
|
|
||||||
--megaMenuLinkRadius: 2px;
|
|
||||||
--megaMenuColumnCount: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-link {
|
|
||||||
position: absolute;
|
|
||||||
top: -40px;
|
|
||||||
left: 6px;
|
|
||||||
background: $mm-focus-accent;
|
|
||||||
color: $mm-white;
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 4px;
|
|
||||||
z-index: 10000;
|
|
||||||
transition: top 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-link:focus {
|
|
||||||
top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#CustomNavigation {
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
z-index: $mm-layer-index;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #5f6f74;
|
|
||||||
background-color: $mm-nav-background;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.18);
|
|
||||||
box-shadow: none;
|
|
||||||
overflow: visible;
|
|
||||||
z-index: $mm-layer-index;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul,
|
|
||||||
.mega-menu-top-level {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: $mm-panel-width;
|
|
||||||
background-color: inherit;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0 $mm-nav-inline-padding;
|
|
||||||
padding-inline-start: $mm-nav-inline-padding;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li,
|
|
||||||
.mega-menu-top-item {
|
|
||||||
position: static;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
border-right: 1px solid $mm-nav-divider;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li:last-child,
|
|
||||||
.mega-menu-top-item:last-child {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li > a,
|
|
||||||
.mega-menu-main > ul > li > span[role="menuitem"],
|
|
||||||
.menu-item-link,
|
|
||||||
.menu-item-text {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: $mm-nav-height;
|
|
||||||
padding: 0 $mm-item-inline-padding;
|
|
||||||
color: $mm-nav-text;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1;
|
|
||||||
text-decoration: none;
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
transition: background-color 0.14s ease, color 0.14s ease;
|
|
||||||
outline: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item-text {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item-has-children::after {
|
|
||||||
content: '';
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
margin-left: 8px;
|
|
||||||
border-right: 1px solid currentColor;
|
|
||||||
border-bottom: 1px solid currentColor;
|
|
||||||
transform: translateY(-1px) rotate(45deg);
|
|
||||||
transition: transform 0.14s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item-has-children[aria-expanded="true"]::after {
|
|
||||||
transform: translateY(1px) rotate(-135deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-top-item.is-active > a,
|
|
||||||
.mega-menu-top-item.is-active > span,
|
|
||||||
.menu-item-link.is-active,
|
|
||||||
.menu-item-text.is-active {
|
|
||||||
background: $mm-nav-open-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-top-item.is-current > a,
|
|
||||||
.mega-menu-top-item.is-current > span,
|
|
||||||
.menu-item-link.is-current,
|
|
||||||
.menu-item-text.is-current {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li:hover > a,
|
|
||||||
.mega-menu-main > ul > li:hover > span[role="menuitem"],
|
|
||||||
.mega-menu-main > ul > li > a[aria-expanded="true"],
|
|
||||||
.mega-menu-main > ul > li > span[aria-expanded="true"],
|
|
||||||
.menu-item-link:hover,
|
|
||||||
.menu-item-text:hover {
|
|
||||||
color: $mm-nav-text;
|
|
||||||
background: $mm-nav-hover-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li > a[aria-expanded="true"],
|
|
||||||
.mega-menu-main > ul > li > span[aria-expanded="true"] {
|
|
||||||
background: $mm-nav-open-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li > a:focus,
|
|
||||||
.mega-menu-main > ul > li > span[role="menuitem"]:focus,
|
|
||||||
.menu-item-link:focus,
|
|
||||||
.menu-item-text:focus {
|
|
||||||
color: $mm-nav-text;
|
|
||||||
background: $mm-nav-open-background;
|
|
||||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
display: none;
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
background-color: #ffffff;
|
|
||||||
background-color: $mm-panel-surface;
|
|
||||||
background-image: none;
|
|
||||||
mix-blend-mode: normal;
|
|
||||||
border: 1px solid $mm-panel-border;
|
|
||||||
border-top: 0;
|
|
||||||
box-shadow: $mm-panel-shadow;
|
|
||||||
max-height: calc(100vh - #{$mm-nav-height});
|
|
||||||
isolation: isolate;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
z-index: calc(#{$mm-layer-index} + 1);
|
|
||||||
transition: opacity 0.16s ease, visibility 0.16s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-main > ul > li:hover .mega-menu,
|
|
||||||
.mega-menu.js-open {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
display: block;
|
|
||||||
background-color: #ffffff;
|
|
||||||
background-color: $mm-panel-surface;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-grid {
|
|
||||||
max-width: $mm-panel-width;
|
|
||||||
background-color: inherit;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(var(--megaMenuColumnCount, 4), minmax(0, 1fr));
|
|
||||||
gap: 0;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category {
|
|
||||||
min-width: 0;
|
|
||||||
padding: $mm-column-padding-y $mm-column-padding-x 14px;
|
|
||||||
border-right: 1px solid $mm-panel-divider;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-grid > .mega-menu-category:last-child {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category > h3,
|
|
||||||
.mega-menu-category-title {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category > h3 > span,
|
|
||||||
.mega-menu-category > h3 > a {
|
|
||||||
display: block;
|
|
||||||
color: $mm-text;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 18px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category.is-active > h3 > span,
|
|
||||||
.mega-menu-category.is-active > h3 > a,
|
|
||||||
.mega-menu-category > h3 > span.is-active,
|
|
||||||
.mega-menu-category > h3 > a.is-active {
|
|
||||||
color: $mm-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category.is-current > h3 > a,
|
|
||||||
.mega-menu-category > h3 > a.is-current {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category > h3 > a:hover {
|
|
||||||
color: $mm-text;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category > h3 > a:focus {
|
|
||||||
background: $mm-focus-soft;
|
|
||||||
color: $mm-text;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 0 0 2px $mm-focus-accent;
|
|
||||||
padding: 1px 4px;
|
|
||||||
margin: -1px -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul,
|
|
||||||
.mega-menu-links {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li,
|
|
||||||
.mega-menu-links li {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li:last-child,
|
|
||||||
.mega-menu-links li:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li a,
|
|
||||||
.mega-menu-links li a {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1px 2px;
|
|
||||||
border-radius: $mm-link-radius;
|
|
||||||
color: $mm-text-muted;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li.is-active > a,
|
|
||||||
.mega-menu-links li.is-active > a,
|
|
||||||
.mega-menu-category ul li a.is-active,
|
|
||||||
.mega-menu-links li a.is-active {
|
|
||||||
color: $mm-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li.is-current > a,
|
|
||||||
.mega-menu-links li.is-current > a,
|
|
||||||
.mega-menu-category ul li a.is-current,
|
|
||||||
.mega-menu-links li a.is-current {
|
|
||||||
color: $mm-text;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li a:hover,
|
|
||||||
.mega-menu-links li a:hover {
|
|
||||||
background: transparent;
|
|
||||||
color: $mm-text;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mega-menu-category ul li a:focus,
|
|
||||||
.mega-menu-links li a:focus {
|
|
||||||
background: $mm-focus-soft;
|
|
||||||
color: $mm-text;
|
|
||||||
box-shadow: 0 0 0 2px $mm-focus-accent;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.mega-menu-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu-category {
|
#CustomNavigation {
|
||||||
padding: 14px 16px 12px;
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
border-top: 1px solid #edebe9;
|
||||||
|
border-bottom: 1px solid #edebe9;
|
||||||
|
z-index: $menuLayer;
|
||||||
|
font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-status {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-bottom: 1px solid $neutralLight;
|
||||||
|
background: $themeLighterAlt;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-main {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--megaMenuBarHeight, 44px);
|
||||||
|
overflow: visible;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
background: $white;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
z-index: $menuLayer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.mega-menu-main > ul,
|
|
||||||
.mega-menu-top-level {
|
.mega-menu-top-level {
|
||||||
padding: 0 6px;
|
display: flex;
|
||||||
padding-inline-start: 6px;
|
align-items: stretch;
|
||||||
flex-wrap: wrap;
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--megaMenuContentWidth, 1200px);
|
||||||
|
min-height: var(--megaMenuBarHeight, 44px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu-main > ul > li,
|
|
||||||
.mega-menu-top-item {
|
.mega-menu-top-item {
|
||||||
border-right: 0;
|
position: static;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-top-item:hover,
|
||||||
|
.mega-menu-top-item:focus-within,
|
||||||
|
.mega-menu-top-item:active,
|
||||||
|
.mega-menu-top-item.is-open,
|
||||||
|
.mega-menu-top-item.is-active {
|
||||||
|
background: $neutralLighter;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mode-flyout .mega-menu-top-item {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu-main > ul > li > a,
|
|
||||||
.mega-menu-main > ul > li > span[role="menuitem"],
|
|
||||||
.menu-item-link,
|
.menu-item-link,
|
||||||
.menu-item-text {
|
.menu-item-text {
|
||||||
min-height: 36px;
|
position: relative;
|
||||||
padding: 0 10px;
|
display: flex;
|
||||||
font-size: 12px;
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: var(--megaMenuBarHeight, 44px);
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 32px;
|
||||||
|
min-height: var(--megaMenuBarHeight, 44px);
|
||||||
|
margin: 0 8px 0 -10px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle > span::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-right: 1px solid currentColor;
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle[aria-expanded='true'] > span::after {
|
||||||
|
transform: rotate(225deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-link:hover,
|
||||||
|
.menu-item-text:hover,
|
||||||
|
.menu-item-toggle:hover,
|
||||||
|
.menu-item-toggle[aria-expanded='true'] {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-top-item.is-active > .menu-item-link::before,
|
||||||
|
.mega-menu-top-item.is-active > .menu-item-text::before,
|
||||||
|
.mega-menu-top-item.is-current > .menu-item-link::before,
|
||||||
|
.mega-menu-top-item.is-current > .menu-item-text::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 12px;
|
||||||
|
height: 2px;
|
||||||
|
background: $themePrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-top-item.is-current > .menu-item-link,
|
||||||
|
.mega-menu-top-item.is-current > .menu-item-text {
|
||||||
|
color: $themeDark;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-link:focus,
|
||||||
|
.menu-item-text:focus,
|
||||||
|
.menu-item-toggle:focus,
|
||||||
|
.mega-menu-mobile-toggle:focus,
|
||||||
|
.mega-menu-category a:focus,
|
||||||
|
.flyout-link:focus {
|
||||||
|
outline: 2px solid $themePrimary;
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu {
|
.mega-menu {
|
||||||
border-left: 0;
|
position: absolute;
|
||||||
border-right: 0;
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
border: 1px solid #edebe9;
|
||||||
|
border-top: 0;
|
||||||
|
background: $white;
|
||||||
|
box-shadow: $menuShadow;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
z-index: calc(#{$menuLayer} + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu.js-open {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu-grid {
|
.mega-menu-grid {
|
||||||
grid-template-columns: 1fr;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--megaMenuColumnCount, 6), minmax(0, 1fr));
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--megaMenuContentWidth, 1200px);
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px;
|
||||||
|
gap: 24px 32px;
|
||||||
|
background: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu-category {
|
.mega-menu-category {
|
||||||
padding: 14px 12px;
|
min-width: 0;
|
||||||
border-right: 0;
|
padding: 0 0 8px;
|
||||||
border-bottom: 1px solid $mm-panel-divider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mega-menu-grid > .mega-menu-category:last-child {
|
.mega-menu-description {
|
||||||
border-bottom: 0;
|
margin: -4px 0 12px;
|
||||||
|
color: $neutralSecondary;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
.mega-menu-category-title {
|
||||||
.skip-link,
|
margin: 0 0 10px;
|
||||||
.menu-item-has-children::after,
|
padding: 0 0 8px;
|
||||||
.mega-menu-main > ul > li > a,
|
border-bottom: 1px solid $neutralLight;
|
||||||
.mega-menu-main > ul > li > span[role="menuitem"],
|
|
||||||
.menu-item-link,
|
|
||||||
.menu-item-text,
|
|
||||||
.mega-menu,
|
|
||||||
.mega-menu-category ul li a,
|
|
||||||
.mega-menu-links li a {
|
|
||||||
transition: none;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-contrast: high) {
|
.mega-menu-category-title > a,
|
||||||
.mega-menu-main > ul > li > a:focus,
|
.mega-menu-category-title > span {
|
||||||
.mega-menu-main > ul > li > span[role="menuitem"]:focus,
|
display: block;
|
||||||
.menu-item-link:focus,
|
color: $neutralPrimary;
|
||||||
.menu-item-text:focus,
|
font-size: 14px;
|
||||||
.mega-menu-category > h3 > a:focus,
|
font-weight: 600;
|
||||||
.mega-menu-category ul li a:focus,
|
line-height: 20px;
|
||||||
.mega-menu-links li a:focus {
|
text-decoration: none;
|
||||||
outline: 2px solid;
|
}
|
||||||
outline-offset: 2px;
|
|
||||||
box-shadow: none;
|
.mega-menu-category-title > a:hover {
|
||||||
|
color: $themePrimary;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-links {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-links li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-links a {
|
||||||
|
display: block;
|
||||||
|
margin: 0 -8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: $neutralSecondary;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-links a:hover {
|
||||||
|
background: $neutralLighter;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-links a.is-current,
|
||||||
|
.mega-menu-links li.is-current > a {
|
||||||
|
color: $themeDark;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-flyout {
|
||||||
|
right: auto;
|
||||||
|
width: var(--megaMenuFlyoutWidth, 288px);
|
||||||
|
min-width: 240px;
|
||||||
|
border-top: 1px solid $neutralLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
background: $white;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 36px 8px 16px;
|
||||||
|
border: 0;
|
||||||
|
background: $white;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item:hover > .flyout-link,
|
||||||
|
.flyout-item:focus-within > .flyout-link,
|
||||||
|
.flyout-item:active > .flyout-link,
|
||||||
|
.flyout-item.is-active > .flyout-link {
|
||||||
|
background: $neutralLighter;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item.is-current > .flyout-link {
|
||||||
|
color: $themeDark;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-toggle > span::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-top: 1px solid currentColor;
|
||||||
|
border-right: 1px solid currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-toggle:focus {
|
||||||
|
outline: 2px solid $themePrimary;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-level-3 {
|
||||||
|
position: absolute;
|
||||||
|
top: -9px;
|
||||||
|
left: 100%;
|
||||||
|
display: none;
|
||||||
|
width: var(--megaMenuFlyoutWidth, 288px);
|
||||||
|
border: 1px solid $neutralLight;
|
||||||
|
box-shadow: $menuShadow;
|
||||||
|
z-index: calc(#{$menuLayer} + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item:hover > .flyout-level-3,
|
||||||
|
.flyout-item:focus-within > .flyout-level-3,
|
||||||
|
.flyout-item.is-open > .flyout-level-3 {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.mega-menu-top-level {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mega-menu-mobile-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--megaMenuBarHeight, 44px);
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
background: $white;
|
||||||
|
color: $neutralPrimary;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-icon,
|
||||||
|
.mega-menu-mobile-icon::before,
|
||||||
|
.mega-menu-mobile-icon::after {
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-icon {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-icon::before,
|
||||||
|
.mega-menu-mobile-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mobile-icon::before { top: -6px; }
|
||||||
|
.mega-menu-mobile-icon::after { top: 6px; }
|
||||||
|
|
||||||
|
.mega-menu-top-level {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid $neutralLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-main.is-mobile-open .mega-menu-top-level {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-top-item,
|
||||||
|
.mega-menu-mode-flyout .mega-menu-top-item {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 44px;
|
||||||
|
border-bottom: 1px solid $neutralLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-link,
|
||||||
|
.menu-item-text {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-toggle {
|
||||||
|
width: 44px;
|
||||||
|
margin: 0;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu {
|
||||||
|
position: static;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
border-right: 0;
|
||||||
|
border-left: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-menu-mode-flyout .mega-menu-flyout {
|
||||||
|
position: fixed;
|
||||||
|
right: 8px;
|
||||||
|
left: 8px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-level-3 {
|
||||||
|
position: static;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0 0 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item.is-open > .flyout-level-3 {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mega-menu,
|
||||||
|
.menu-item-link,
|
||||||
|
.menu-item-text,
|
||||||
|
.flyout-link {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
.menu-item-link:focus,
|
||||||
|
.menu-item-text:focus,
|
||||||
|
.mega-menu-category a:focus,
|
||||||
|
.flyout-link:focus {
|
||||||
|
outline: 2px solid CanvasText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} /* End :global */
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
require('./MegaMenu.module.css');
|
require('./MegaMenu.module.css');
|
||||||
const styles = {
|
const styles = {
|
||||||
mmSlideIn: 'mmSlideIn_09a8e1a7',
|
|
||||||
mmFadeIn: 'mmFadeIn_09a8e1a7',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default styles;
|
export default styles;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json",
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json",
|
||||||
|
|
||||||
"id": "abc3361f-bb2d-491f-aba3-cd51c19a299b",
|
"id": "c0abbffb-355d-4d4e-bd38-9e15fb811506",
|
||||||
"alias": "MegaMenuApplicationCustomizer",
|
"alias": "MegaMenuApplicationCustomizer",
|
||||||
"componentType": "Extension",
|
"componentType": "Extension",
|
||||||
"extensionType": "ApplicationCustomizer",
|
"extensionType": "ApplicationCustomizer",
|
||||||
|
|||||||
@@ -15,94 +15,165 @@ import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationServ
|
|||||||
import { MegaMenuRenderer } from './MegaMenuRenderer';
|
import { MegaMenuRenderer } from './MegaMenuRenderer';
|
||||||
import { IMenuItem } from '../../services/IMenuItem';
|
import { IMenuItem } from '../../services/IMenuItem';
|
||||||
import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
|
import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
|
||||||
|
import { MegaMenuMode, normalizeMegaMenuMode } from './MegaMenuMode';
|
||||||
|
|
||||||
import './MegaMenu.module.scss';
|
import './MegaMenu.module.scss';
|
||||||
|
|
||||||
const LOG_SOURCE: string = 'MegaMenuApplicationCustomizer';
|
const LOG_SOURCE: string = 'MegaMenuApplicationCustomizer';
|
||||||
export const UserCustomActionMegaMenuId: string = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
|
export const UserCustomActionMegaMenuId: string = 'c0abbffb-355d-4d4e-bd38-9e15fb811506';
|
||||||
|
|
||||||
export interface IMegaMenuApplicationCustomizerProperties {
|
export interface IMegaMenuApplicationCustomizerProperties {
|
||||||
termSetName: string;
|
termSetName?: string;
|
||||||
cssUrl?: string;
|
termSetId?: string;
|
||||||
|
cacheMinutes?: number;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
|
menuMode?: MegaMenuMode;
|
||||||
|
cacheVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MegaMenuApplicationCustomizer
|
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;
|
||||||
|
private _statusElement: HTMLElement | undefined;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
public onInit(): Promise<void> {
|
public onInit(): Promise<void> {
|
||||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Initialized ' + strings.Title);
|
const debug: boolean = this._isDebugEnabled();
|
||||||
|
debugLog(debug, LOG_SOURCE, 'onInit started.', {
|
||||||
if (this.properties.cssUrl) {
|
componentId: UserCustomActionMegaMenuId,
|
||||||
this._loadExternalCss(this.properties.cssUrl);
|
version: '2.2.4',
|
||||||
}
|
properties: this.properties || {},
|
||||||
|
pageUrl: window.location.href,
|
||||||
|
webUrl: this.context.pageContext.web.absoluteUrl,
|
||||||
|
siteUrl: this.context.pageContext.site.absoluteUrl
|
||||||
|
});
|
||||||
|
|
||||||
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
|
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Placeholder changedEvent handler registered.');
|
||||||
this._renderPlaceHolders();
|
this._renderPlaceHolders();
|
||||||
|
|
||||||
|
debugLog(debug, LOG_SOURCE, 'onInit completed. Asynchronous menu loading may still be running.');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPlaceHolders(): void {
|
private _renderPlaceHolders(): void {
|
||||||
|
const debug: boolean = this._isDebugEnabled();
|
||||||
const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames
|
const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames
|
||||||
.map(name => PlaceholderName[name])
|
.map(name => PlaceholderName[name])
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders);
|
debugLog(debug, LOG_SOURCE, 'Rendering placeholders.', {
|
||||||
|
availablePlaceholders: availablePlaceholders,
|
||||||
|
topPlaceholderAlreadyCreated: !!this._topPlaceholder
|
||||||
|
});
|
||||||
|
|
||||||
if (!this._topPlaceholder) {
|
if (!this._topPlaceholder) {
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Requesting the Top placeholder.');
|
||||||
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
|
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
|
||||||
PlaceholderName.Top,
|
PlaceholderName.Top,
|
||||||
{ onDispose: this._onDispose }
|
{ onDispose: this._onDispose }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this._topPlaceholder) {
|
if (!this._topPlaceholder) {
|
||||||
debugError(this._isDebugEnabled(), LOG_SOURCE, 'The expected placeholder (Top) was not found.');
|
debugError(debug, LOG_SOURCE, 'The expected placeholder (Top) was not found.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.properties.termSetName) {
|
debugLog(debug, LOG_SOURCE, 'Top placeholder acquired.', {
|
||||||
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetName configured. Mega Menu rendering is skipped.');
|
domElementId: this._topPlaceholder.domElement.id || '',
|
||||||
|
domElementClassName: this._topPlaceholder.domElement.className || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const termSetIdentifier: string = this._getTermSetIdentifier();
|
||||||
|
if (!termSetIdentifier) {
|
||||||
|
debugWarn(debug, LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.', this.properties || {});
|
||||||
|
this._renderStatus(strings.ConfigurationMissing);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._renderMegaMenu(this.properties.termSetName);
|
debugLog(debug, LOG_SOURCE, 'Starting MegaMenu rendering.', {
|
||||||
|
termSetIdentifier: termSetIdentifier,
|
||||||
|
cacheMinutes: this._getCacheMinutes(),
|
||||||
|
menuMode: this._getMenuMode()
|
||||||
|
});
|
||||||
|
this._renderMegaMenu(termSetIdentifier, debug);
|
||||||
|
} else {
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Top placeholder is already initialized; no duplicate render is started.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _renderMegaMenu(termSetName: string, debug: boolean = this._isDebugEnabled()): Promise<void> {
|
private async _renderMegaMenu(termSetIdentifier: string, debug: boolean = this._isDebugEnabled()): Promise<void> {
|
||||||
if (!this._topPlaceholder) {
|
if (!this._topPlaceholder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._renderStatus(strings.Loading);
|
||||||
try {
|
try {
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Creating TaxonomyNavigationService.');
|
||||||
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
|
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
|
||||||
this.context,
|
this.context,
|
||||||
termSetName,
|
termSetIdentifier,
|
||||||
debug
|
debug,
|
||||||
|
this._getCacheMinutes(),
|
||||||
|
this._getCacheVersion(),
|
||||||
|
this._getLanguageLcid()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Requesting navigation items from taxonomy.');
|
||||||
const menuItems: IMenuItem[] = await taxonomyService.getMenuItems();
|
const menuItems: IMenuItem[] = await taxonomyService.getMenuItems();
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Navigation items received.', {
|
||||||
|
topLevelItemCount: menuItems.length
|
||||||
|
});
|
||||||
|
|
||||||
const renderer: MegaMenuRenderer = new MegaMenuRenderer(
|
if (!menuItems || menuItems.length === 0) {
|
||||||
|
this._renderStatus(strings.EmptyNavigation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._clearStatus();
|
||||||
|
|
||||||
|
if (this._renderer) {
|
||||||
|
debugLog(debug, LOG_SOURCE, 'Disposing the previous renderer instance.');
|
||||||
|
this._renderer.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._renderer = new MegaMenuRenderer(
|
||||||
menuItems,
|
menuItems,
|
||||||
debug
|
debug,
|
||||||
|
this._getMenuMode(),
|
||||||
|
{
|
||||||
|
navigationLabel: strings.NavigationLabel,
|
||||||
|
openNavigation: strings.OpenNavigation,
|
||||||
|
closeNavigation: strings.CloseNavigation,
|
||||||
|
openSubmenu: strings.OpenSubmenu,
|
||||||
|
closeSubmenu: strings.CloseSubmenu,
|
||||||
|
submenuLabel: strings.SubmenuLabel,
|
||||||
|
externalLink: strings.ExternalLink,
|
||||||
|
opened: strings.Opened,
|
||||||
|
closed: strings.Closed
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const container = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
|
||||||
|
|
||||||
if (container) {
|
if (this._menuContainer) {
|
||||||
renderer.render(container);
|
debugLog(debug, LOG_SOURCE, 'Rendering into the selected container.', {
|
||||||
|
id: this._menuContainer.id || '',
|
||||||
|
className: this._menuContainer.className || ''
|
||||||
|
});
|
||||||
|
this._renderer.render(this._menuContainer);
|
||||||
} else {
|
} else {
|
||||||
renderer.render(this._topPlaceholder.domElement);
|
debugWarn(debug, LOG_SOURCE, 'No dedicated container was returned; rendering into the Top placeholder.');
|
||||||
|
this._renderer.render(this._topPlaceholder.domElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
|
debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error);
|
debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', this._describeError(error), error);
|
||||||
|
this._renderStatus(strings.LoadError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,42 +181,108 @@ export default class MegaMenuApplicationCustomizer
|
|||||||
const container = document.getElementById(id);
|
const container = document.getElementById(id);
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
const div = document.createElement('div');
|
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing host container found.', {
|
||||||
container.appendChild(div);
|
requestedId: id,
|
||||||
return div;
|
containerId: container.id,
|
||||||
|
containerClassName: container.className || ''
|
||||||
|
});
|
||||||
|
let host: HTMLElement = document.getElementById('MegaMenuHost');
|
||||||
|
if (!host) {
|
||||||
|
host = document.createElement('div');
|
||||||
|
host.id = 'MegaMenuHost';
|
||||||
|
container.appendChild(host);
|
||||||
|
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'MegaMenuHost was created inside the existing host container.');
|
||||||
|
} else {
|
||||||
|
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing MegaMenuHost will be reused.');
|
||||||
|
}
|
||||||
|
return host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'No external host container found; the Top placeholder will be used.', {
|
||||||
|
requestedId: id
|
||||||
|
});
|
||||||
return placeholder.domElement;
|
return placeholder.domElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _loadExternalCss(cssUrl?: string, debug: boolean = this._isDebugEnabled()): void {
|
private _onDispose = (): void => {
|
||||||
const externalCssLinkId: string = 'mega-menu-additional-css-34FAB720';
|
this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders);
|
||||||
let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement;
|
this._clearStatus();
|
||||||
|
if (this._renderer) {
|
||||||
if (cssUrl && cssUrl.trim() !== '') {
|
this._renderer.dispose();
|
||||||
if (!link) {
|
this._renderer = undefined;
|
||||||
const head: HTMLHeadElement = document.getElementsByTagName('head')[0];
|
|
||||||
link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.type = 'text/css';
|
|
||||||
link.id = externalCssLinkId;
|
|
||||||
link.onload = () => {
|
|
||||||
debugLog(debug, LOG_SOURCE, 'External CSS loaded successfully from:', cssUrl);
|
|
||||||
};
|
|
||||||
link.onerror = () => {
|
|
||||||
debugWarn(debug, LOG_SOURCE, 'Failed to load external CSS from:', cssUrl);
|
|
||||||
};
|
|
||||||
head.appendChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
link.href = cssUrl;
|
|
||||||
} else if (link) {
|
|
||||||
link.remove();
|
|
||||||
}
|
}
|
||||||
|
this._menuContainer = undefined;
|
||||||
|
this._topPlaceholder = undefined;
|
||||||
|
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onDispose = (): void => {
|
private _getTermSetIdentifier(): string {
|
||||||
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
|
const termSetId: string = this.properties && this.properties.termSetId
|
||||||
|
? this.properties.termSetId.trim()
|
||||||
|
: '';
|
||||||
|
if (termSetId && this._isGuid(termSetId)) {
|
||||||
|
return termSetId;
|
||||||
|
}
|
||||||
|
return this.properties && this.properties.termSetName
|
||||||
|
? this.properties.termSetName.trim()
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCacheMinutes(): number {
|
||||||
|
const value: number = this.properties ? Number(this.properties.cacheMinutes) : 15;
|
||||||
|
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getMenuMode(): MegaMenuMode {
|
||||||
|
return normalizeMegaMenuMode(this.properties && this.properties.menuMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCacheVersion(): string {
|
||||||
|
const value: string = this.properties && this.properties.cacheVersion
|
||||||
|
? String(this.properties.cacheVersion).trim()
|
||||||
|
: '1';
|
||||||
|
return value || '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLanguageLcid(): number {
|
||||||
|
const legacyContext: any = (this.context.pageContext as any).legacyPageContext; // tslint:disable-line:no-any
|
||||||
|
const value: number = legacyContext ? Number(legacyContext.currentLanguage) : 0;
|
||||||
|
return isFinite(value) && value > 0 ? value : 1033;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderStatus(message: string): void {
|
||||||
|
if (!this._topPlaceholder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._clearStatus();
|
||||||
|
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);
|
||||||
|
this._statusElement = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearStatus(): void {
|
||||||
|
if (this._statusElement && this._statusElement.parentNode) {
|
||||||
|
this._statusElement.parentNode.removeChild(this._statusElement);
|
||||||
|
}
|
||||||
|
this._statusElement = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isGuid(value: string): boolean {
|
||||||
|
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _describeError(error: any): { name: string; message: string; stack: string } { // tslint:disable-line:no-any
|
||||||
|
return {
|
||||||
|
name: error && error.name ? String(error.name) : '',
|
||||||
|
message: error && error.message ? String(error.message) : String(error || 'Unknown error'),
|
||||||
|
stack: error && error.stack ? String(error.stack) : ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isDebugEnabled(debugOverride?: boolean): boolean {
|
private _isDebugEnabled(debugOverride?: boolean): boolean {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type MegaMenuMode = 'megaMenu' | 'flyout';
|
||||||
|
|
||||||
|
export function normalizeMegaMenuMode(value?: string): MegaMenuMode {
|
||||||
|
return value && value.toLowerCase() === 'flyout' ? 'flyout' : 'megaMenu';
|
||||||
|
}
|
||||||
@@ -5,33 +5,70 @@
|
|||||||
|
|
||||||
import { IMenuItem } from '../../services/IMenuItem';
|
import { IMenuItem } from '../../services/IMenuItem';
|
||||||
import { debugLog } from '../../services/MegaMenuDebug';
|
import { debugLog } from '../../services/MegaMenuDebug';
|
||||||
|
import { MegaMenuMode } from './MegaMenuMode';
|
||||||
import styles from './MegaMenu.module.scss'; // tslint:disable-line:no-unused-variable
|
import { isNavigationUrlActive } from '../../services/MegaMenuCore';
|
||||||
|
|
||||||
const LOG_SOURCE: string = 'MegaMenuRenderer';
|
const LOG_SOURCE: string = 'MegaMenuRenderer';
|
||||||
|
|
||||||
|
export interface IMegaMenuRendererStrings {
|
||||||
|
navigationLabel: string;
|
||||||
|
openNavigation: string;
|
||||||
|
closeNavigation: string;
|
||||||
|
openSubmenu: string;
|
||||||
|
closeSubmenu: string;
|
||||||
|
submenuLabel: string;
|
||||||
|
externalLink: string;
|
||||||
|
opened: string;
|
||||||
|
closed: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class MegaMenuRenderer {
|
export class MegaMenuRenderer {
|
||||||
private static readonly hoverOpenDelayMs: number = 140;
|
private static readonly hoverOpenDelayMs: number = 0;
|
||||||
private static readonly hoverCloseDelayMs: number = 180;
|
private static readonly hoverCloseDelayMs: number = 0;
|
||||||
|
private _container: HTMLElement | undefined;
|
||||||
|
private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined;
|
||||||
|
private _documentClickHandler: ((event: MouseEvent) => void) | undefined;
|
||||||
|
private _announcer: HTMLElement | undefined;
|
||||||
|
private _mobileToggle: HTMLButtonElement | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private menuItems: IMenuItem[],
|
private menuItems: IMenuItem[],
|
||||||
private debug: boolean = false
|
private debug: boolean = false,
|
||||||
|
private menuMode: MegaMenuMode = 'megaMenu',
|
||||||
|
private strings: IMegaMenuRendererStrings
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public render(container: HTMLElement) {
|
public render(container: HTMLElement): void {
|
||||||
|
debugLog(this.debug, LOG_SOURCE, 'Render started.', {
|
||||||
|
menuItemCount: this.menuItems.length,
|
||||||
|
containerId: container.id || '',
|
||||||
|
containerClassName: container.className || ''
|
||||||
|
});
|
||||||
|
this.dispose();
|
||||||
|
this._container = container;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
container.id = 'CustomNavigation';
|
container.id = 'CustomNavigation';
|
||||||
|
|
||||||
const nav = document.createElement('nav');
|
const nav = document.createElement('nav');
|
||||||
nav.id = 'Mega-Menu';
|
nav.id = 'Mega-Menu';
|
||||||
nav.className = 'mega-menu-main';
|
nav.className = this.joinClasses('mega-menu-main', 'mega-menu-mode-' + this.menuMode);
|
||||||
nav.setAttribute('role', 'navigation');
|
nav.setAttribute('data-menu-mode', this.menuMode);
|
||||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
nav.setAttribute('aria-label', this.strings.navigationLabel);
|
||||||
|
|
||||||
|
this._mobileToggle = document.createElement('button');
|
||||||
|
this._mobileToggle.type = 'button';
|
||||||
|
this._mobileToggle.className = 'mega-menu-mobile-toggle';
|
||||||
|
this._mobileToggle.setAttribute('aria-expanded', 'false');
|
||||||
|
this._mobileToggle.setAttribute('aria-controls', 'Mega-Menu-Items');
|
||||||
|
this._mobileToggle.setAttribute('aria-label', this.strings.openNavigation);
|
||||||
|
this._mobileToggle.innerHTML = '<span class="mega-menu-mobile-icon" aria-hidden="true"></span><span class="mega-menu-mobile-label"></span>';
|
||||||
|
const mobileLabel = this._mobileToggle.querySelector('.mega-menu-mobile-label') as HTMLElement;
|
||||||
|
mobileLabel.textContent = this.strings.navigationLabel;
|
||||||
|
nav.appendChild(this._mobileToggle);
|
||||||
|
|
||||||
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');
|
topLevelUl.id = 'Mega-Menu-Items';
|
||||||
|
|
||||||
this.menuItems.forEach(topLevelItem => {
|
this.menuItems.forEach(topLevelItem => {
|
||||||
const topLevelLi = this.createTopLevelItem(topLevelItem);
|
const topLevelLi = this.createTopLevelItem(topLevelItem);
|
||||||
@@ -43,6 +80,35 @@ export class MegaMenuRenderer {
|
|||||||
|
|
||||||
this.attachEventListeners();
|
this.attachEventListeners();
|
||||||
this.createScreenReaderAnnouncer();
|
this.createScreenReaderAnnouncer();
|
||||||
|
debugLog(this.debug, LOG_SOURCE, 'Render completed.', {
|
||||||
|
navigationId: nav.id,
|
||||||
|
topLevelElements: topLevelUl.children.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
debugLog(this.debug, LOG_SOURCE, 'Dispose started.', {
|
||||||
|
hasContainer: !!this._container,
|
||||||
|
hasAnnouncer: !!this._announcer
|
||||||
|
});
|
||||||
|
if (this._documentKeydownHandler) {
|
||||||
|
document.removeEventListener('keydown', this._documentKeydownHandler);
|
||||||
|
this._documentKeydownHandler = undefined;
|
||||||
|
}
|
||||||
|
if (this._documentClickHandler) {
|
||||||
|
document.removeEventListener('click', this._documentClickHandler);
|
||||||
|
this._documentClickHandler = undefined;
|
||||||
|
}
|
||||||
|
if (this._announcer && this._announcer.parentElement) {
|
||||||
|
this._announcer.parentElement.removeChild(this._announcer);
|
||||||
|
}
|
||||||
|
this._announcer = undefined;
|
||||||
|
this._mobileToggle = undefined;
|
||||||
|
if (this._container) {
|
||||||
|
this._container.innerHTML = '';
|
||||||
|
}
|
||||||
|
this._container = undefined;
|
||||||
|
debugLog(this.debug, LOG_SOURCE, 'Dispose completed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTopLevelItem(item: IMenuItem): HTMLLIElement {
|
private createTopLevelItem(item: IMenuItem): HTMLLIElement {
|
||||||
@@ -57,14 +123,26 @@ 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);
|
||||||
|
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
const megaMenu = this.createMegaMenu(item);
|
const toggle: HTMLButtonElement = document.createElement('button');
|
||||||
li.appendChild(megaMenu);
|
const menuId: string = 'Mega-Menu-Panel-' + item.id.replace(/[^a-z0-9_-]/gi, '');
|
||||||
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'menu-item-toggle';
|
||||||
|
toggle.setAttribute('aria-haspopup', 'true');
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
toggle.setAttribute('aria-controls', menuId);
|
||||||
|
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.label);
|
||||||
|
toggle.innerHTML = '<span aria-hidden="true"></span>';
|
||||||
|
li.appendChild(toggle);
|
||||||
|
const childMenu = this.menuMode === 'flyout'
|
||||||
|
? this.createFlyoutMenu(item)
|
||||||
|
: this.createMegaMenu(item);
|
||||||
|
childMenu.id = menuId;
|
||||||
|
li.appendChild(childMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
return li;
|
return li;
|
||||||
@@ -80,10 +158,11 @@ export class MegaMenuRenderer {
|
|||||||
} else {
|
} else {
|
||||||
element = document.createElement('span');
|
element = document.createElement('span');
|
||||||
element.className = 'menu-item-text';
|
element.className = 'menu-item-text';
|
||||||
element.setAttribute('tabindex', '0');
|
if (!hasChildren) {
|
||||||
|
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,
|
||||||
@@ -92,11 +171,6 @@ export class MegaMenuRenderer {
|
|||||||
isCurrent ? 'is-current' : undefined
|
isCurrent ? 'is-current' : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
element.setAttribute('aria-haspopup', 'true');
|
|
||||||
element.setAttribute('aria-expanded', 'false');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCurrent && item.url) {
|
if (isCurrent && item.url) {
|
||||||
element.setAttribute('aria-current', 'page');
|
element.setAttribute('aria-current', 'page');
|
||||||
}
|
}
|
||||||
@@ -105,15 +179,18 @@ export class MegaMenuRenderer {
|
|||||||
element.title = item.hoverText;
|
element.title = item.hoverText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.url) {
|
||||||
|
this.applyLinkBehavior(element as HTMLAnchorElement, item);
|
||||||
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 + ' ' + this.strings.submenuLabel);
|
||||||
|
|
||||||
const gridDiv = document.createElement('div');
|
const gridDiv = document.createElement('div');
|
||||||
gridDiv.className = 'mega-menu-grid';
|
gridDiv.className = 'mega-menu-grid';
|
||||||
@@ -147,6 +224,7 @@ export class MegaMenuRenderer {
|
|||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = item.url;
|
link.href = item.url;
|
||||||
link.textContent = item.label;
|
link.textContent = item.label;
|
||||||
|
this.applyLinkBehavior(link, item);
|
||||||
link.className = this.joinClasses(
|
link.className = this.joinClasses(
|
||||||
isActive ? 'is-active' : undefined,
|
isActive ? 'is-active' : undefined,
|
||||||
isCurrent ? 'is-current' : undefined
|
isCurrent ? 'is-current' : undefined
|
||||||
@@ -170,6 +248,13 @@ export class MegaMenuRenderer {
|
|||||||
h3.appendChild(span);
|
h3.appendChild(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.description) {
|
||||||
|
const description: HTMLParagraphElement = document.createElement('p');
|
||||||
|
description.className = 'mega-menu-description';
|
||||||
|
description.textContent = item.description;
|
||||||
|
categoryDiv.appendChild(description);
|
||||||
|
}
|
||||||
|
|
||||||
categoryDiv.appendChild(h3);
|
categoryDiv.appendChild(h3);
|
||||||
|
|
||||||
if (item.hasChildren() && item.items && item.items.length > 0) {
|
if (item.hasChildren() && item.items && item.items.length > 0) {
|
||||||
@@ -189,6 +274,7 @@ export class MegaMenuRenderer {
|
|||||||
|
|
||||||
link.href = thirdLevelItem.url || '#';
|
link.href = thirdLevelItem.url || '#';
|
||||||
link.textContent = thirdLevelItem.label;
|
link.textContent = thirdLevelItem.label;
|
||||||
|
this.applyLinkBehavior(link, thirdLevelItem);
|
||||||
link.className = this.joinClasses(
|
link.className = this.joinClasses(
|
||||||
isLinkActive ? 'is-active' : undefined,
|
isLinkActive ? 'is-active' : undefined,
|
||||||
isLinkCurrent ? 'is-current' : undefined
|
isLinkCurrent ? 'is-current' : undefined
|
||||||
@@ -212,8 +298,98 @@ export class MegaMenuRenderer {
|
|||||||
return categoryDiv;
|
return categoryDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createFlyoutMenu(parentItem: IMenuItem): HTMLDivElement {
|
||||||
|
const flyout = document.createElement('div');
|
||||||
|
flyout.className = 'mega-menu mega-menu-flyout';
|
||||||
|
flyout.setAttribute('aria-expanded', 'false');
|
||||||
|
flyout.setAttribute('aria-label', parentItem.label + ' ' + this.strings.submenuLabel);
|
||||||
|
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'flyout-list flyout-level-2';
|
||||||
|
|
||||||
|
if (parentItem.items) {
|
||||||
|
parentItem.items.forEach(secondLevelItem => {
|
||||||
|
list.appendChild(this.createFlyoutItem(secondLevelItem));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
flyout.appendChild(list);
|
||||||
|
return flyout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFlyoutItem(item: IMenuItem): HTMLLIElement {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const hasChildren = item.hasChildren() && item.items && item.items.length > 0;
|
||||||
|
const isActive = this.isItemActive(item);
|
||||||
|
const isCurrent = this.isCurrentUrl(item.url);
|
||||||
|
|
||||||
|
li.className = this.joinClasses(
|
||||||
|
'flyout-item',
|
||||||
|
hasChildren ? 'has-children' : undefined,
|
||||||
|
isActive ? 'is-active' : undefined,
|
||||||
|
isCurrent ? 'is-current' : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = this.createFlyoutElement(item, hasChildren, isActive, isCurrent);
|
||||||
|
li.appendChild(element);
|
||||||
|
|
||||||
|
if (hasChildren && item.items) {
|
||||||
|
const toggle: HTMLButtonElement = document.createElement('button');
|
||||||
|
const childId: string = 'Mega-Menu-Flyout-' + item.id.replace(/[^a-z0-9_-]/gi, '');
|
||||||
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'flyout-toggle';
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
toggle.setAttribute('aria-controls', childId);
|
||||||
|
toggle.setAttribute('aria-label', this.strings.openSubmenu + ' ' + item.label);
|
||||||
|
toggle.innerHTML = '<span aria-hidden="true"></span>';
|
||||||
|
li.appendChild(toggle);
|
||||||
|
const childList = document.createElement('ul');
|
||||||
|
childList.className = 'flyout-list flyout-level-3';
|
||||||
|
childList.id = childId;
|
||||||
|
childList.setAttribute('aria-label', item.label + ' ' + this.strings.submenuLabel);
|
||||||
|
|
||||||
|
item.items.forEach(thirdLevelItem => {
|
||||||
|
childList.appendChild(this.createFlyoutItem(thirdLevelItem));
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(childList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFlyoutElement(item: IMenuItem, hasChildren: boolean, isActive: boolean, isCurrent: boolean): HTMLElement {
|
||||||
|
const element: HTMLElement = item.url
|
||||||
|
? document.createElement('a')
|
||||||
|
: document.createElement('span');
|
||||||
|
|
||||||
|
if (item.url) {
|
||||||
|
(element as HTMLAnchorElement).href = item.url;
|
||||||
|
this.applyLinkBehavior(element as HTMLAnchorElement, item);
|
||||||
|
} else {
|
||||||
|
element.setAttribute('tabindex', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
element.textContent = item.label;
|
||||||
|
element.className = this.joinClasses(
|
||||||
|
'flyout-link',
|
||||||
|
hasChildren ? 'flyout-link-has-children' : undefined,
|
||||||
|
isActive ? 'is-active' : undefined,
|
||||||
|
isCurrent ? 'is-current' : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCurrent && item.url) {
|
||||||
|
element.setAttribute('aria-current', 'page');
|
||||||
|
}
|
||||||
|
if (item.hoverText) {
|
||||||
|
element.title = item.hoverText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
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 > .menu-item-toggle');
|
||||||
|
|
||||||
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;
|
||||||
@@ -228,21 +404,17 @@ export class MegaMenuRenderer {
|
|||||||
|
|
||||||
this.attachGlobalKeyboardNavigation();
|
this.attachGlobalKeyboardNavigation();
|
||||||
this.attachGlobalClickOutsideHandler();
|
this.attachGlobalClickOutsideHandler();
|
||||||
|
this.attachMobileToggle();
|
||||||
|
this.attachFlyoutToggles();
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachKeyboardNavigation(heading: HTMLElement, megaMenu: HTMLElement): void {
|
private attachKeyboardNavigation(heading: HTMLElement, megaMenu: HTMLElement): void {
|
||||||
|
heading.addEventListener('click', (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.toggleMegaMenu(heading, megaMenu);
|
||||||
|
});
|
||||||
heading.addEventListener('keydown', (e: KeyboardEvent) => {
|
heading.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'ArrowDown') {
|
||||||
if (heading.tagName === 'A') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
this.toggleMegaMenu(heading, megaMenu);
|
|
||||||
} else if (e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.toggleMegaMenu(heading, megaMenu);
|
|
||||||
} else if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.openMegaMenu(heading, megaMenu);
|
this.openMegaMenu(heading, megaMenu);
|
||||||
this.focusFirstLink(megaMenu);
|
this.focusFirstLink(megaMenu);
|
||||||
@@ -256,12 +428,6 @@ export class MegaMenuRenderer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (heading.tagName === 'A') {
|
|
||||||
heading.addEventListener('click', () => {
|
|
||||||
debugLog(this.debug, LOG_SOURCE, 'Link clicked:', (heading as HTMLAnchorElement).href);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
heading.addEventListener('focus', () => {
|
heading.addEventListener('focus', () => {
|
||||||
debugLog(this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
|
debugLog(this.debug, LOG_SOURCE, 'Focus moved to:', heading.textContent);
|
||||||
});
|
});
|
||||||
@@ -325,7 +491,7 @@ export class MegaMenuRenderer {
|
|||||||
const isInsideThisMenu = megaMenu.contains(focusedElement);
|
const isInsideThisMenu = megaMenu.contains(focusedElement);
|
||||||
const isOnThisTrigger = focusedElement === heading;
|
const isOnThisTrigger = focusedElement === heading;
|
||||||
const isInAnyMegaMenu = focusedElement.closest('.mega-menu');
|
const isInAnyMegaMenu = focusedElement.closest('.mega-menu');
|
||||||
const isOnAnyTopLevel = focusedElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span');
|
const isOnAnyTopLevel = focusedElement.closest('.mega-menu-top-item');
|
||||||
|
|
||||||
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
|
if (!isInsideThisMenu && !isOnThisTrigger && !isInAnyMegaMenu && !isOnAnyTopLevel) {
|
||||||
debugLog(this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.');
|
debugLog(this.debug, LOG_SOURCE, 'Closing menu because focus left the navigation.');
|
||||||
@@ -336,7 +502,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') {
|
||||||
@@ -348,9 +514,30 @@ export class MegaMenuRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && activeElement.closest('.mega-menu-top-item')) {
|
||||||
|
const currentItem = activeElement.closest('.mega-menu-top-item') as HTMLElement;
|
||||||
|
const items = document.querySelectorAll('#Mega-Menu > ul > .mega-menu-top-item');
|
||||||
|
let currentIndex: number = -1;
|
||||||
|
for (let index: number = 0; index < items.length; index++) {
|
||||||
|
if (items[index] === currentItem) {
|
||||||
|
currentIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentIndex >= 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
const offset: number = e.key === 'ArrowRight' ? 1 : -1;
|
||||||
|
const targetIndex: number = (currentIndex + offset + items.length) % items.length;
|
||||||
|
const target = (items[targetIndex] as HTMLElement).querySelector('a, .menu-item-toggle') as HTMLElement;
|
||||||
|
if (target) {
|
||||||
|
target.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
const currentTopLevel = activeElement.closest('#Mega-Menu > ul > li > a, #Mega-Menu > ul > li > span') as HTMLElement;
|
const currentTopLevel = activeElement.closest('.menu-item-toggle') as HTMLElement;
|
||||||
if (currentTopLevel) {
|
if (currentTopLevel) {
|
||||||
const parentLi = currentTopLevel.closest('li') as HTMLElement;
|
const parentLi = currentTopLevel.closest('li') as HTMLElement;
|
||||||
const megaMenu = parentLi.querySelector('.mega-menu.js-open') as HTMLElement;
|
const megaMenu = parentLi.querySelector('.mega-menu.js-open') as HTMLElement;
|
||||||
@@ -380,11 +567,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,22 +587,92 @@ 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 attachMobileToggle(): void {
|
||||||
|
if (!this._mobileToggle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._mobileToggle.addEventListener('click', (): void => {
|
||||||
|
const nav: HTMLElement = this._mobileToggle.closest('#Mega-Menu') as HTMLElement;
|
||||||
|
const isOpen: boolean = this._mobileToggle.getAttribute('aria-expanded') === 'true';
|
||||||
|
this._mobileToggle.setAttribute('aria-expanded', String(!isOpen));
|
||||||
|
this._mobileToggle.setAttribute('aria-label', isOpen ? this.strings.openNavigation : this.strings.closeNavigation);
|
||||||
|
nav.classList.toggle('is-mobile-open', !isOpen);
|
||||||
|
if (isOpen) {
|
||||||
|
this.closeAllMegaMenus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nav: HTMLElement = this._mobileToggle.closest('#Mega-Menu') as HTMLElement;
|
||||||
|
nav.addEventListener('click', (event: MouseEvent): void => {
|
||||||
|
const target: Element = event.target as Element;
|
||||||
|
if (target && target.closest('a') && window.innerWidth <= 767) {
|
||||||
|
this._mobileToggle.setAttribute('aria-expanded', 'false');
|
||||||
|
this._mobileToggle.setAttribute('aria-label', this.strings.openNavigation);
|
||||||
|
nav.classList.remove('is-mobile-open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachFlyoutToggles(): void {
|
||||||
|
const toggles = document.querySelectorAll('#Mega-Menu .flyout-toggle');
|
||||||
|
for (let i: number = 0; i < toggles.length; i++) {
|
||||||
|
const toggle: HTMLButtonElement = toggles[i] as HTMLButtonElement;
|
||||||
|
const parent: HTMLElement = toggle.parentElement as HTMLElement;
|
||||||
|
const setOpen = (open: boolean): void => {
|
||||||
|
toggle.setAttribute('aria-expanded', String(open));
|
||||||
|
toggle.setAttribute('aria-label', (open ? this.strings.closeSubmenu : this.strings.openSubmenu)
|
||||||
|
+ ' ' + this.getFlyoutToggleLabel(toggle));
|
||||||
|
parent.classList.toggle('is-open', open);
|
||||||
|
};
|
||||||
|
toggle.addEventListener('click', (event: MouseEvent): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
|
||||||
|
});
|
||||||
|
toggle.addEventListener('keydown', (event: KeyboardEvent): void => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
toggle.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parent.addEventListener('mouseenter', (): void => setOpen(true));
|
||||||
|
parent.addEventListener('mouseleave', (): void => setOpen(false));
|
||||||
|
parent.addEventListener('focusin', (): void => setOpen(true));
|
||||||
|
parent.addEventListener('focusout', (): void => {
|
||||||
|
window.setTimeout((): void => {
|
||||||
|
if (!parent.contains(document.activeElement)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
private openMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
||||||
this.closeAllMegaMenus(menu);
|
this.closeAllMegaMenus(menu);
|
||||||
|
const label: string = this.getTriggerLabel(trigger);
|
||||||
trigger.setAttribute('aria-expanded', 'true');
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
trigger.setAttribute('aria-label', this.strings.closeSubmenu + ' ' + label);
|
||||||
|
(trigger.parentElement as HTMLElement).classList.add('is-open');
|
||||||
menu.setAttribute('aria-expanded', 'true');
|
menu.setAttribute('aria-expanded', 'true');
|
||||||
menu.classList.add('js-open');
|
menu.classList.add('js-open');
|
||||||
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', trigger.textContent);
|
this.announce(label + ' ' + this.strings.opened);
|
||||||
|
debugLog(this.debug, LOG_SOURCE, 'Menu opened:', label);
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
private closeMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
||||||
|
const label: string = this.getTriggerLabel(trigger);
|
||||||
trigger.setAttribute('aria-expanded', 'false');
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
trigger.setAttribute('aria-label', this.strings.openSubmenu + ' ' + label);
|
||||||
|
(trigger.parentElement as HTMLElement).classList.remove('is-open');
|
||||||
menu.setAttribute('aria-expanded', 'false');
|
menu.setAttribute('aria-expanded', 'false');
|
||||||
menu.classList.remove('js-open');
|
menu.classList.remove('js-open');
|
||||||
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', trigger.textContent);
|
this.announce(label + ' ' + this.strings.closed);
|
||||||
|
debugLog(this.debug, LOG_SOURCE, 'Menu closed:', label);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
private toggleMegaMenu(trigger: HTMLElement, menu: HTMLElement): void {
|
||||||
@@ -427,7 +685,7 @@ export class MegaMenuRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private closeAllMegaMenus(exceptMenu?: HTMLElement): void {
|
private closeAllMegaMenus(exceptMenu?: HTMLElement): void {
|
||||||
const allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > a[aria-expanded="true"], #Mega-Menu > ul > li > span[aria-expanded="true"]');
|
const allTriggers = document.querySelectorAll('#Mega-Menu > ul > li > .menu-item-toggle[aria-expanded="true"]');
|
||||||
for (let i = 0; i < allTriggers.length; i++) {
|
for (let i = 0; i < allTriggers.length; i++) {
|
||||||
const trigger = allTriggers[i] as HTMLElement;
|
const trigger = allTriggers[i] as HTMLElement;
|
||||||
const menu = trigger.nextElementSibling as HTMLElement;
|
const menu = trigger.nextElementSibling as HTMLElement;
|
||||||
@@ -463,44 +721,40 @@ export class MegaMenuRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isCurrentUrl(url?: string): boolean {
|
private isCurrentUrl(url?: string): boolean {
|
||||||
const currentPath = this.normalizePath(window.location.href);
|
return isNavigationUrlActive(window.location.href, url || '', window.location.origin);
|
||||||
const itemPath = this.normalizePath(url);
|
|
||||||
|
|
||||||
if (!currentPath || !itemPath) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemPath === '/') {
|
|
||||||
return currentPath === '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPath === itemPath) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentPath.indexOf(itemPath + '/') === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizePath(url?: string): string {
|
|
||||||
if (!url) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url, window.location.origin);
|
|
||||||
const pathname = (parsedUrl.pathname || '/').replace(/\/+$/, '');
|
|
||||||
return (pathname || '/').toLowerCase();
|
|
||||||
} catch (error) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getColumnCount(items?: IMenuItem[]): string {
|
private getColumnCount(items?: IMenuItem[]): string {
|
||||||
const itemCount = items ? items.length : 0;
|
const itemCount = items ? items.length : 0;
|
||||||
const columnCount = Math.min(Math.max(itemCount, 1), 4);
|
const columnCount = Math.min(Math.max(itemCount, 1), 6);
|
||||||
return columnCount.toString();
|
return columnCount.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyLinkBehavior(link: HTMLAnchorElement, item: IMenuItem): void {
|
||||||
|
if (item.openInNewWindow) {
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
}
|
||||||
|
if (item.external) {
|
||||||
|
const currentLabel: string = link.getAttribute('aria-label') || item.label;
|
||||||
|
link.setAttribute('aria-label', currentLabel + ' (' + this.strings.externalLink + ')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTriggerLabel(trigger: HTMLElement): string {
|
||||||
|
const labelElement: HTMLElement = trigger.parentElement
|
||||||
|
? trigger.parentElement.querySelector('.menu-item-link, .menu-item-text') as HTMLElement
|
||||||
|
: undefined;
|
||||||
|
return labelElement ? labelElement.textContent || '' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFlyoutToggleLabel(toggle: HTMLElement): string {
|
||||||
|
const labelElement: HTMLElement = toggle.parentElement
|
||||||
|
? toggle.parentElement.querySelector('.flyout-link') as HTMLElement
|
||||||
|
: undefined;
|
||||||
|
return labelElement ? labelElement.textContent || '' : '';
|
||||||
|
}
|
||||||
|
|
||||||
private joinClasses(...classNames: Array<string | undefined>): string {
|
private joinClasses(...classNames: Array<string | undefined>): string {
|
||||||
const filteredClassNames: string[] = [];
|
const filteredClassNames: string[] = [];
|
||||||
|
|
||||||
@@ -514,7 +768,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 +780,19 @@ export class MegaMenuRenderer {
|
|||||||
srAnnouncer.setAttribute('aria-atomic', 'true');
|
srAnnouncer.setAttribute('aria-atomic', 'true');
|
||||||
srAnnouncer.className = 'sr-only';
|
srAnnouncer.className = 'sr-only';
|
||||||
document.body.appendChild(srAnnouncer);
|
document.body.appendChild(srAnnouncer);
|
||||||
|
this._announcer = srAnnouncer;
|
||||||
|
|
||||||
debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.');
|
debugLog(this.debug, LOG_SOURCE, 'Screen reader announcer is ready.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private announce(message: string): void {
|
||||||
|
if (this._announcer) {
|
||||||
|
this._announcer.textContent = '';
|
||||||
|
window.setTimeout((): void => {
|
||||||
|
if (this._announcer) {
|
||||||
|
this._announcer.textContent = message;
|
||||||
|
}
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
// tslint:disable:max-line-length export-name
|
|
||||||
|
|
||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
|
||||||
import { IMegaMenuApplicationCustomizerProperties, UserCustomActionMegaMenuId } from './MegaMenuApplicationCustomizer';
|
|
||||||
import { UserCustomActionService } from '../../services/UserCustomActionService/UserCustomActionService';
|
|
||||||
import { UserCustomActionScope } from '../../services/UserCustomActionService/UserCustomActionScope';
|
|
||||||
import { IUserCustomActionProps } from '../../services/UserCustomActionService/IUserCustomActionProps';
|
|
||||||
import { debugError } from '../../services/MegaMenuDebug';
|
|
||||||
|
|
||||||
const LOG_SOURCE: string = 'MegaMenuSettingsPanel';
|
|
||||||
|
|
||||||
export class MegaMenuSettingsPanel {
|
|
||||||
private _service: UserCustomActionService;
|
|
||||||
private _ucaId: string;
|
|
||||||
private _panelElement: HTMLElement | undefined = undefined;
|
|
||||||
private _overlayElement: HTMLElement | undefined = undefined;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private context: ApplicationCustomizerContext,
|
|
||||||
private dataUpdated: (data: IMegaMenuApplicationCustomizerProperties) => void,
|
|
||||||
private debug: boolean = false
|
|
||||||
) {
|
|
||||||
this._service = new UserCustomActionService(this.context, this.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open(): Promise<void> {
|
|
||||||
if (this._panelElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentProps: IMegaMenuApplicationCustomizerProperties = await this.readApplicationCustomizerProps();
|
|
||||||
this._createPanel(currentProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
public close(): void {
|
|
||||||
if (this._panelElement) {
|
|
||||||
this._panelElement.remove();
|
|
||||||
this._panelElement = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._overlayElement) {
|
|
||||||
this._overlayElement.remove();
|
|
||||||
this._overlayElement = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.focus();
|
|
||||||
document.body.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async readApplicationCustomizerProps(): Promise<IMegaMenuApplicationCustomizerProperties> {
|
|
||||||
const ucas: IUserCustomActionProps[] = await this._service.getUserCustomActions(UserCustomActionScope.Site);
|
|
||||||
const candidates: IUserCustomActionProps[] = ucas.filter(uca => uca.ClientSideComponentId === UserCustomActionMegaMenuId);
|
|
||||||
|
|
||||||
if (candidates.length) {
|
|
||||||
const uca: IUserCustomActionProps = candidates[0];
|
|
||||||
this._ucaId = uca.Id;
|
|
||||||
|
|
||||||
if (uca.ClientSideComponentProperties) {
|
|
||||||
return JSON.parse(uca.ClientSideComponentProperties) as IMegaMenuApplicationCustomizerProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
termSetName: '',
|
|
||||||
cssUrl: '',
|
|
||||||
debug: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
debugError(this.debug, LOG_SOURCE, 'UserCustomAction for the MegaMenu was not found.');
|
|
||||||
return {
|
|
||||||
termSetName: '',
|
|
||||||
cssUrl: '',
|
|
||||||
debug: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveApplicationCustomizerProps(componentProps: IMegaMenuApplicationCustomizerProperties): Promise<void> {
|
|
||||||
try {
|
|
||||||
const newUserCustomActionsProperty: {} = {
|
|
||||||
ClientSideComponentProperties: JSON.stringify(componentProps)
|
|
||||||
};
|
|
||||||
|
|
||||||
await this._service.updateUserCustomAction(UserCustomActionScope.Site, this._ucaId, newUserCustomActionsProperty);
|
|
||||||
this.debug = componentProps.debug === true;
|
|
||||||
this._service = new UserCustomActionService(this.context, this.debug);
|
|
||||||
this.dataUpdated(componentProps);
|
|
||||||
} catch (e) {
|
|
||||||
debugError(this.debug, LOG_SOURCE, 'Error saving MegaMenu settings.', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createPanel(props: IMegaMenuApplicationCustomizerProperties): void {
|
|
||||||
const overlay: HTMLElement = document.createElement('div');
|
|
||||||
overlay.className = 'mm-settings-overlay';
|
|
||||||
overlay.tabIndex = -1;
|
|
||||||
overlay.onclick = () => this.close();
|
|
||||||
this._overlayElement = overlay;
|
|
||||||
|
|
||||||
const panel: HTMLElement = document.createElement('div');
|
|
||||||
panel.className = 'mm-settings-panel';
|
|
||||||
panel.setAttribute('role', 'dialog');
|
|
||||||
panel.setAttribute('aria-modal', 'true');
|
|
||||||
panel.setAttribute('aria-label', 'MegaMenu Einstellungen');
|
|
||||||
panel.innerHTML = this._getMarkup(props.termSetName, props.cssUrl || '', props.debug === true);
|
|
||||||
this._panelElement = panel;
|
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
document.body.appendChild(panel);
|
|
||||||
|
|
||||||
const closeBtn: HTMLButtonElement = panel.querySelector('.mm-settings-close') as HTMLButtonElement;
|
|
||||||
const cancelBtn: HTMLButtonElement = panel.querySelector('.mm-settings-cancel') as HTMLButtonElement;
|
|
||||||
const saveBtn: HTMLButtonElement = panel.querySelector('.mm-settings-save') as HTMLButtonElement;
|
|
||||||
const firstInput: HTMLInputElement = panel.querySelector('#mm-setting-termset') as HTMLInputElement;
|
|
||||||
|
|
||||||
if (closeBtn) { closeBtn.onclick = () => this.close(); }
|
|
||||||
if (cancelBtn) { cancelBtn.onclick = () => this.close(); }
|
|
||||||
if (saveBtn) { saveBtn.onclick = () => this._save(); }
|
|
||||||
|
|
||||||
panel.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.close();
|
|
||||||
} else if (e.key === 'Tab') {
|
|
||||||
this._trapFocus(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (firstInput) {
|
|
||||||
firstInput.focus();
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getMarkup(termSet: string, cssUrl: string, debug: boolean): string {
|
|
||||||
return `<div class='mm-settings-header'>
|
|
||||||
<h2 class='mm-settings-title'>Einstellungen</h2>
|
|
||||||
<button type='button' class='mm-settings-close' aria-label='Schliessen'>×</button>
|
|
||||||
</div>
|
|
||||||
<div class='mm-settings-body'>
|
|
||||||
<div class='mm-settings-field'>
|
|
||||||
<label for='mm-setting-termset'>Name des Navigations-Termsets</label>
|
|
||||||
<input id='mm-setting-termset' type='text' value='${this._escape(termSet)}' />
|
|
||||||
</div>
|
|
||||||
<div class='mm-settings-field'>
|
|
||||||
<label for='mm-setting-css'>Pfad zu zusaetzlicher CSS-Datei</label>
|
|
||||||
<input id='mm-setting-css' type='text' value='${this._escape(cssUrl)}' />
|
|
||||||
</div>
|
|
||||||
<div class='mm-settings-field'>
|
|
||||||
<label for='mm-setting-debug'>
|
|
||||||
<input id='mm-setting-debug' type='checkbox' ${debug ? 'checked' : ''} />
|
|
||||||
Debug-Ausgaben in der Browser-Konsole aktivieren
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class='mm-settings-footer'>
|
|
||||||
<button type='button' class='mm-settings-save ms-Button ms-Button--primary'><span>Speichern</span></button>
|
|
||||||
<button type='button' class='mm-settings-cancel ms-Button'><span>Abbrechen</span></button>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _save(): void {
|
|
||||||
if (!this._panelElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const termSetInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-termset') as HTMLInputElement;
|
|
||||||
const cssInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-css') as HTMLInputElement;
|
|
||||||
const debugInput: HTMLInputElement = this._panelElement.querySelector('#mm-setting-debug') as HTMLInputElement;
|
|
||||||
|
|
||||||
this.saveApplicationCustomizerProps({
|
|
||||||
termSetName: termSetInput && termSetInput.value ? termSetInput.value : '',
|
|
||||||
cssUrl: cssInput && cssInput.value ? cssInput.value : '',
|
|
||||||
debug: !!(debugInput && debugInput.checked)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _trapFocus(e: KeyboardEvent): void {
|
|
||||||
if (!this._panelElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusable: NodeListOf<Element> = this._panelElement.querySelectorAll('button, input');
|
|
||||||
if (!focusable || focusable.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first: HTMLElement = focusable[0] as HTMLElement;
|
|
||||||
const last: HTMLElement = focusable[focusable.length - 1] as HTMLElement;
|
|
||||||
const active: HTMLElement = document.activeElement as HTMLElement;
|
|
||||||
|
|
||||||
if (e.shiftKey && active === first) {
|
|
||||||
e.preventDefault();
|
|
||||||
last.focus();
|
|
||||||
} else if (!e.shiftKey && active === last) {
|
|
||||||
e.preventDefault();
|
|
||||||
first.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _escape(value: string): string {
|
|
||||||
if (value) {
|
|
||||||
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"Title": "MegaMenuApplicationCustomizer",
|
||||||
|
"ConfigurationMissing": "Die Navigation ist nicht konfiguriert.",
|
||||||
|
"LoadError": "Die Navigation konnte nicht geladen werden.",
|
||||||
|
"Loading": "Navigation wird geladen…",
|
||||||
|
"EmptyNavigation": "Die Navigation enthält keine sichtbaren Einträge.",
|
||||||
|
"NavigationLabel": "Hauptnavigation",
|
||||||
|
"OpenNavigation": "Hauptnavigation öffnen",
|
||||||
|
"CloseNavigation": "Hauptnavigation schließen",
|
||||||
|
"OpenSubmenu": "Untermenü öffnen für",
|
||||||
|
"CloseSubmenu": "Untermenü schließen für",
|
||||||
|
"SubmenuLabel": "Untermenü",
|
||||||
|
"ExternalLink": "externer Link",
|
||||||
|
"Opened": "geöffnet",
|
||||||
|
"Closed": "geschlossen"
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
define([], function() {
|
define([], function() {
|
||||||
return {
|
return {
|
||||||
"Title": "MegaMenuApplicationCustomizer"
|
"Title": "MegaMenuApplicationCustomizer",
|
||||||
|
"ConfigurationMissing": "Navigation is not configured.",
|
||||||
|
"LoadError": "Navigation could not be loaded.",
|
||||||
|
"Loading": "Loading navigation…",
|
||||||
|
"EmptyNavigation": "The navigation does not contain any visible entries.",
|
||||||
|
"NavigationLabel": "Main navigation",
|
||||||
|
"OpenNavigation": "Open main navigation",
|
||||||
|
"CloseNavigation": "Close main navigation",
|
||||||
|
"OpenSubmenu": "Open submenu for",
|
||||||
|
"CloseSubmenu": "Close submenu for",
|
||||||
|
"SubmenuLabel": "submenu",
|
||||||
|
"ExternalLink": "external link",
|
||||||
|
"Opened": "opened",
|
||||||
|
"Closed": "closed"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
+13
@@ -1,5 +1,18 @@
|
|||||||
declare interface IMegaMenuApplicationCustomizerStrings {
|
declare interface IMegaMenuApplicationCustomizerStrings {
|
||||||
Title: string;
|
Title: string;
|
||||||
|
ConfigurationMissing: string;
|
||||||
|
LoadError: string;
|
||||||
|
Loading: string;
|
||||||
|
EmptyNavigation: string;
|
||||||
|
NavigationLabel: string;
|
||||||
|
OpenNavigation: string;
|
||||||
|
CloseNavigation: string;
|
||||||
|
OpenSubmenu: string;
|
||||||
|
CloseSubmenu: string;
|
||||||
|
SubmenuLabel: string;
|
||||||
|
ExternalLink: string;
|
||||||
|
Opened: string;
|
||||||
|
Closed: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'MegaMenuApplicationCustomizerStrings' {
|
declare module 'MegaMenuApplicationCustomizerStrings' {
|
||||||
|
|||||||
+1
-15
@@ -1,20 +1,6 @@
|
|||||||
// tslint:disable:max-line-length no-any
|
// tslint:disable:max-line-length no-any
|
||||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
||||||
|
|
||||||
// Export services for classic SharePoint usage
|
|
||||||
export { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
|
export { TaxonomyNavigationService } from './services/TaxonomyNavigationService';
|
||||||
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
|
export { MegaMenuRenderer } from './extensions/megaMenu/MegaMenuRenderer';
|
||||||
export { UserCustomActionService } from './services/UserCustomActionService/UserCustomActionService';
|
export { MegaMenuMode, normalizeMegaMenuMode } from './extensions/megaMenu/MegaMenuMode';
|
||||||
export { UserCustomActionScope } from './services/UserCustomActionService/UserCustomActionScope';
|
|
||||||
|
|
||||||
// Expose services globally for classic JavaScript consumption
|
|
||||||
declare var window: any;
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.__megaMenuServices = {
|
|
||||||
TaxonomyNavigationService: require('./services/TaxonomyNavigationService').TaxonomyNavigationService,
|
|
||||||
MegaMenuRenderer: require('./extensions/megaMenu/MegaMenuRenderer').MegaMenuRenderer,
|
|
||||||
UserCustomActionService: require('./services/UserCustomActionService/UserCustomActionService').UserCustomActionService,
|
|
||||||
UserCustomActionScope: require('./services/UserCustomActionService/UserCustomActionScope').UserCustomActionScope
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export interface IMenuItem {
|
|||||||
label: string;
|
label: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
hoverText: string;
|
hoverText: string;
|
||||||
|
description?: string;
|
||||||
|
openInNewWindow?: boolean;
|
||||||
|
external?: boolean;
|
||||||
url?: string;
|
url?: string;
|
||||||
pathDepth: number;
|
pathDepth: number;
|
||||||
items?: IMenuItem[];
|
items?: IMenuItem[];
|
||||||
|
|||||||
@@ -78,11 +78,22 @@ export interface ITerm {
|
|||||||
PathDepth?: number;
|
PathDepth?: number;
|
||||||
ParentId?: string;
|
ParentId?: string;
|
||||||
TermsCount?: number;
|
TermsCount?: number;
|
||||||
|
Labels?: ITermLabelCollection;
|
||||||
LocalCustomProperties?: {
|
LocalCustomProperties?: {
|
||||||
[property: string]: any
|
[property: string]: any
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITermLabelCollection {
|
||||||
|
_Child_Items_: ITermLabel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITermLabel {
|
||||||
|
Language: number;
|
||||||
|
Value: string;
|
||||||
|
IsDefaultForLanguage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISuggestTerm {
|
export interface ISuggestTerm {
|
||||||
Id: string;
|
Id: string;
|
||||||
DefaultLabel: string;
|
DefaultLabel: string;
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ export interface ISPTermStorePickerServiceProps {
|
|||||||
hideDeprecatedTags: boolean;
|
hideDeprecatedTags: boolean;
|
||||||
hideTagsNotAvailableForTagging: boolean;
|
hideTagsNotAvailableForTagging: boolean;
|
||||||
anchorId: string;
|
anchorId: string;
|
||||||
|
cacheMinutes: number;
|
||||||
|
siteCollectionUrl: string;
|
||||||
|
languageLcid: number;
|
||||||
|
cacheVersion: string;
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export class ItemDictionary<T> {
|
|
||||||
public Get(key: string): T {
|
|
||||||
return this[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
public Add(key: string, value: T): void {
|
|
||||||
this[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { ITerm, ITermLabelCollection } from './ISPTermStorePickerService';
|
||||||
|
|
||||||
|
export const MegaMenuCacheSchemaVersion: string = '4';
|
||||||
|
|
||||||
|
export const MegaMenuTermProperties = {
|
||||||
|
hidden: 'MegaMenu.Hidden',
|
||||||
|
openInNewWindow: 'MegaMenu.OpenInNewWindow',
|
||||||
|
description: 'MegaMenu.Description',
|
||||||
|
excludedProviders: '_Sys_Nav_ExcludedProviders'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalNavigationTaxonomyProvider: string = 'GlobalNavigationTaxonomyProvider';
|
||||||
|
|
||||||
|
export interface IMegaMenuCacheEnvelope<T> {
|
||||||
|
expiresAt: number;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readCachedValue<T>(serialized: string, now: number, allowExpired: boolean): T | undefined {
|
||||||
|
if (!serialized) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const entry: IMegaMenuCacheEnvelope<T> = JSON.parse(serialized) as IMegaMenuCacheEnvelope<T>;
|
||||||
|
if (!entry || !entry.value || !entry.expiresAt || (!allowExpired && entry.expiresAt <= now)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return entry.value;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBooleanProperty(value: any): boolean { // tslint:disable-line:no-any
|
||||||
|
if (value === true || value === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const normalized: string = String(value || '').trim().toLowerCase();
|
||||||
|
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'ja';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTermProperty(term: ITerm, propertyName: string): string {
|
||||||
|
const properties: { [property: string]: any } = term && term.LocalCustomProperties // tslint:disable-line:no-any
|
||||||
|
? term.LocalCustomProperties
|
||||||
|
: {};
|
||||||
|
const value: any = properties[propertyName]; // tslint:disable-line:no-any
|
||||||
|
return value === undefined || value === null ? '' : String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTermHidden(term: ITerm): boolean {
|
||||||
|
return parseBooleanProperty(getTermProperty(term, MegaMenuTermProperties.hidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTermExcludedFromGlobalNavigation(term: ITerm): boolean {
|
||||||
|
const value: string = getTermProperty(term, MegaMenuTermProperties.excludedProviders);
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const providers: string[] = value.replace(/["']/g, '').split(/[;,|]/);
|
||||||
|
const expected: string = GlobalNavigationTaxonomyProvider.toLowerCase();
|
||||||
|
return providers.some((provider: string): boolean => provider.trim().toLowerCase() === expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalizedTermLabel(term: ITerm, languageLcid: number): string {
|
||||||
|
const labels: ITermLabelCollection | undefined = term && term.Labels;
|
||||||
|
const items = labels && labels._Child_Items_ ? labels._Child_Items_ : [];
|
||||||
|
let fallback: string = term && term.Name ? term.Name : '';
|
||||||
|
|
||||||
|
for (let i: number = 0; i < items.length; i++) {
|
||||||
|
const labelLanguage: number = Number(items[i].Language);
|
||||||
|
if (labelLanguage === languageLcid && items[i].Value) {
|
||||||
|
return items[i].Value;
|
||||||
|
}
|
||||||
|
if (items[i].IsDefaultForLanguage && items[i].Value) {
|
||||||
|
fallback = items[i].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveNavigationUrl(rawUrl: string, siteCollectionUrl: string): string | undefined {
|
||||||
|
const value: string = String(rawUrl || '').trim();
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved: string = value.toLowerCase().indexOf('~sitecollection') === 0
|
||||||
|
? String(siteCollectionUrl || '').replace(/\/+$/, '') + value.substring('~sitecollection'.length)
|
||||||
|
: value;
|
||||||
|
|
||||||
|
const protocolMatch: RegExpMatchArray | null = resolved.match(/^([a-z][a-z0-9+.-]*):/i);
|
||||||
|
if (protocolMatch) {
|
||||||
|
const protocol: string = protocolMatch[1].toLowerCase();
|
||||||
|
if (protocol !== 'http' && protocol !== 'https' && protocol !== 'mailto') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExternalNavigationUrl(url: string, currentOrigin: string): boolean {
|
||||||
|
const absoluteMatch: RegExpMatchArray | null = String(url || '').match(/^(?:(https?):)?\/\/([^/]+)/i);
|
||||||
|
if (!absoluteMatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const originMatch: RegExpMatchArray | null = String(currentOrigin || '').match(/^(https?):\/\/([^/]+)/i);
|
||||||
|
if (!originMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const targetProtocol: string = absoluteMatch[1] || originMatch[1];
|
||||||
|
return targetProtocol.toLowerCase() !== originMatch[1].toLowerCase()
|
||||||
|
|| absoluteMatch[2].toLowerCase() !== originMatch[2].toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeNavigationPath(url: string, currentOrigin: string): string {
|
||||||
|
let value: string = String(url || '').trim();
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const origin: string = String(currentOrigin || '').replace(/\/+$/, '');
|
||||||
|
if (origin && value.toLowerCase().indexOf(origin.toLowerCase()) === 0) {
|
||||||
|
value = value.substring(origin.length) || '/';
|
||||||
|
} else {
|
||||||
|
value = value.replace(/^https?:\/\/[^/]+/i, '');
|
||||||
|
}
|
||||||
|
value = value.split('#')[0].split('?')[0].replace(/\/+$/, '');
|
||||||
|
return (value || '/').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNavigationUrlActive(currentUrl: string, itemUrl: string, currentOrigin: string): boolean {
|
||||||
|
const currentPath: string = normalizeNavigationPath(currentUrl, currentOrigin);
|
||||||
|
const itemPath: string = normalizeNavigationPath(itemUrl, currentOrigin);
|
||||||
|
if (!currentPath || !itemPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return itemPath === '/' ? currentPath === '/' : currentPath === itemPath || currentPath.indexOf(itemPath + '/') === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTermCacheKey(
|
||||||
|
siteCollectionUrl: string,
|
||||||
|
termSetId: string,
|
||||||
|
languageLcid: number,
|
||||||
|
cacheVersion?: string
|
||||||
|
): string {
|
||||||
|
const normalize = (value: string): string => encodeURIComponent(String(value || '').trim().toLowerCase());
|
||||||
|
return 'MegaMenu:TermSet:v' + MegaMenuCacheSchemaVersion + ':'
|
||||||
|
+ normalize(siteCollectionUrl) + ':'
|
||||||
|
+ normalize(termSetId) + ':'
|
||||||
|
+ String(languageLcid || 0) + ':'
|
||||||
|
+ normalize(cacheVersion || '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMegaMenuHierarchyEntry<T> {
|
||||||
|
id: string;
|
||||||
|
parentId?: string;
|
||||||
|
depth: number;
|
||||||
|
item: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMenuHierarchy<T extends { items?: T[] }>(entries: Array<IMegaMenuHierarchyEntry<T>>): T[] {
|
||||||
|
const byId: { [id: string]: T } = {};
|
||||||
|
const roots: T[] = [];
|
||||||
|
|
||||||
|
for (let i: number = 0; i < entries.length; i++) {
|
||||||
|
byId[String(entries[i].id || '').toLowerCase()] = entries[i].item;
|
||||||
|
entries[i].item.items = [];
|
||||||
|
}
|
||||||
|
for (let i: number = 0; i < entries.length; i++) {
|
||||||
|
const entry: IMegaMenuHierarchyEntry<T> = entries[i];
|
||||||
|
const parent: T = entry.parentId ? byId[String(entry.parentId).toLowerCase()] : undefined;
|
||||||
|
if (entry.depth <= 1) {
|
||||||
|
roots.push(entry.item);
|
||||||
|
} else if (parent) {
|
||||||
|
parent.items.push(entry.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareTermsByConfiguredOrder(a: ITerm, b: ITerm): number {
|
||||||
|
const aIndex: number = typeof a.CustomSortOrderIndex === 'number' ? a.CustomSortOrderIndex : -1;
|
||||||
|
const bIndex: number = typeof b.CustomSortOrderIndex === 'number' ? b.CustomSortOrderIndex : -1;
|
||||||
|
if (aIndex >= 0 || bIndex >= 0) {
|
||||||
|
if (aIndex < 0) { return 1; }
|
||||||
|
if (bIndex < 0) { return -1; }
|
||||||
|
return aIndex - bIndex;
|
||||||
|
}
|
||||||
|
const aPath: string = String(a.PathOfTerm || '').toLowerCase();
|
||||||
|
const bPath: string = String(b.PathOfTerm || '').toLowerCase();
|
||||||
|
return aPath < bPath ? -1 : (aPath > bPath ? 1 : 0);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,37 @@
|
|||||||
|
|
||||||
import { IMenuItem } from './IMenuItem';
|
import { IMenuItem } from './IMenuItem';
|
||||||
import { ITerm } from './ISPTermStorePickerService';
|
import { ITerm } from './ISPTermStorePickerService';
|
||||||
|
import {
|
||||||
|
getLocalizedTermLabel,
|
||||||
|
getTermProperty,
|
||||||
|
isExternalNavigationUrl,
|
||||||
|
MegaMenuTermProperties,
|
||||||
|
parseBooleanProperty,
|
||||||
|
resolveNavigationUrl
|
||||||
|
} from './MegaMenuCore';
|
||||||
|
|
||||||
export class MenuItem implements IMenuItem {
|
export class MenuItem implements IMenuItem {
|
||||||
public id: string;
|
public id: string;
|
||||||
public label: string;
|
public label: string;
|
||||||
public hoverText: string;
|
public hoverText: string;
|
||||||
|
public description: string;
|
||||||
|
public openInNewWindow: boolean;
|
||||||
|
public external: boolean;
|
||||||
public pathDepth: number;
|
public pathDepth: number;
|
||||||
public url?: string;
|
public url?: string;
|
||||||
public items?: IMenuItem[];
|
public items?: IMenuItem[];
|
||||||
|
|
||||||
constructor(term: ITerm, public level: number, siteCollectionUrl?: string) {
|
constructor(term: ITerm, public level: number, siteCollectionUrl?: string, languageLcid: number = 1033) {
|
||||||
this.id = term.Id;
|
this.id = term.Id;
|
||||||
this.label = term.Name;
|
this.label = getLocalizedTermLabel(term, languageLcid);
|
||||||
this.hoverText = term.LocalCustomProperties['_Sys_Nav_HoverText'];
|
const properties = term.LocalCustomProperties || {};
|
||||||
|
this.hoverText = properties['_Sys_Nav_HoverText'];
|
||||||
|
this.description = getTermProperty(term, MegaMenuTermProperties.description);
|
||||||
|
this.openInNewWindow = parseBooleanProperty(getTermProperty(term, MegaMenuTermProperties.openInNewWindow));
|
||||||
this.pathDepth = term.PathDepth;
|
this.pathDepth = term.PathDepth;
|
||||||
const rawUrl: string = term.LocalCustomProperties['_Sys_Nav_SimpleLinkUrl'] || term.LocalCustomProperties['_Sys_Nav_TargetUrl'];
|
const rawUrl: string = properties['_Sys_Nav_SimpleLinkUrl'] || properties['_Sys_Nav_TargetUrl'];
|
||||||
if (rawUrl) {
|
this.url = resolveNavigationUrl(rawUrl, siteCollectionUrl || '');
|
||||||
this.url = siteCollectionUrl && rawUrl.indexOf('~sitecollection') === 0
|
this.external = !!this.url && isExternalNavigationUrl(this.url, this.getOrigin(siteCollectionUrl || ''));
|
||||||
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
|
|
||||||
: rawUrl;
|
|
||||||
}
|
|
||||||
this.items = [];
|
this.items = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,4 +45,9 @@ export class MenuItem implements IMenuItem {
|
|||||||
(window as any).location.href = this.url;
|
(window as any).location.href = this.url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getOrigin(url: string): string {
|
||||||
|
const match: RegExpMatchArray | null = String(url || '').match(/^(https?:\/\/[^/]+)/i);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import { IMenuItem } from './IMenuItem';
|
|
||||||
import { ITaxonomyNavigationService } from './ITaxonomyNavigationService';
|
|
||||||
import * as uuid from 'uuid';
|
|
||||||
|
|
||||||
export default class MockTaxonomyNavigationService implements ITaxonomyNavigationService {
|
|
||||||
public getMenuItems(): Promise<IMenuItem[]> {
|
|
||||||
return new Promise<IMenuItem[]>((resolve) => {
|
|
||||||
resolve([
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Menu Item 1',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
hoverText: 'Hover me!',
|
|
||||||
pathDepth: 1,
|
|
||||||
hasChildren: () => true,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 1',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: () => false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: () => false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Menu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 1,
|
|
||||||
hasChildren: () => false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Menu Item 3',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 1,
|
|
||||||
hasChildren: () => true,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 1',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: () => false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: () => true,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 1',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 3,
|
|
||||||
hasChildren: () => false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 2',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 3,
|
|
||||||
hasChildren: () => false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 3',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: () => false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuid.v4(),
|
|
||||||
label: 'Submenu Item 4',
|
|
||||||
hoverText: 'Huch!',
|
|
||||||
url: 'https://www.bing.com',
|
|
||||||
pathDepth: 2,
|
|
||||||
hasChildren: () => false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,24 @@
|
|||||||
|
|
||||||
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
|
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
|
||||||
import { ITermStore, ITerms, ITerm, IGroup, ITermSet, ISuggestTerm } from './ISPTermStorePickerService';
|
import { ITermStore, ITerms, ITerm, IGroup, ITermSet, ISuggestTerm } from './ISPTermStorePickerService';
|
||||||
import { findIndex } from '@microsoft/sp-lodash-subset';
|
|
||||||
import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps';
|
import { ISPTermStorePickerServiceProps } from './ISPTermStorePickerServiceProps';
|
||||||
import { IPickerTerm } from './IPickerTerm';
|
import { IPickerTerm } from './IPickerTerm';
|
||||||
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
|
||||||
import { debugError } from './MegaMenuDebug';
|
import { debugError, debugLog, debugWarn } from './MegaMenuDebug';
|
||||||
|
import { buildTermCacheKey, compareTermsByConfiguredOrder, readCachedValue } from './MegaMenuCore';
|
||||||
|
|
||||||
const EmptyGuid: string = '00000000-0000-0000-0000-000000000000';
|
const EmptyGuid: string = '00000000-0000-0000-0000-000000000000';
|
||||||
|
const PickerCachePrefix: string = 'MegaMenu:PickerTerms:v2:';
|
||||||
|
|
||||||
|
interface ITermCacheEntry {
|
||||||
|
expiresAt: number;
|
||||||
|
value: ITermSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPickerTermCacheEntry {
|
||||||
|
expiresAt: number;
|
||||||
|
value: IPickerTerm[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service implementation to manage term stores in SharePoint
|
* Service implementation to manage term stores in SharePoint
|
||||||
@@ -26,12 +37,20 @@ export default class SPTermStorePickerService {
|
|||||||
constructor(private props: ISPTermStorePickerServiceProps, private context: ApplicationCustomizerContext, private debug: boolean = false) {
|
constructor(private props: ISPTermStorePickerServiceProps, private context: ApplicationCustomizerContext, private debug: boolean = false) {
|
||||||
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
|
this.clientServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/client.svc/ProcessQuery';
|
||||||
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
|
this.suggestionServiceUrl = this.context.pageContext.web.absoluteUrl + '/_vti_bin/TaxonomyInternalService.json/GetSuggestions';
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'Service initialized.', {
|
||||||
|
clientServiceUrl: this.clientServiceUrl,
|
||||||
|
termSetNameOrId: this.props.termsetNameOrID,
|
||||||
|
useSessionStorage: this.props.useSessionStorage,
|
||||||
|
cacheMinutes: this.props.cacheMinutes,
|
||||||
|
cacheVersion: this.props.cacheVersion,
|
||||||
|
languageLcid: this.props.languageLcid
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +62,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 +93,7 @@ export default class SPTermStorePickerService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
|
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
|
||||||
|
this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
|
||||||
return serviceResponse.json().then((serviceJSONResponse: any) => {
|
return serviceResponse.json().then((serviceJSONResponse: any) => {
|
||||||
// Construct results
|
// Construct results
|
||||||
const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore');
|
const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore');
|
||||||
@@ -124,8 +145,21 @@ export default class SPTermStorePickerService {
|
|||||||
* Retrieve all terms for the given term set
|
* Retrieve all terms for the given term set
|
||||||
* @param termset
|
* @param termset
|
||||||
*/
|
*/
|
||||||
public async getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage: boolean = true): Promise<ITermSet> {
|
public async getAllTerms(
|
||||||
|
termset: string,
|
||||||
|
hideDeprecatedTags?: boolean,
|
||||||
|
hideTagsNotAvailableForTagging?: boolean,
|
||||||
|
useSessionStorage: boolean = true,
|
||||||
|
cacheMinutes: number = 15
|
||||||
|
): Promise<ITermSet> {
|
||||||
let termsetId: string = termset;
|
let termsetId: string = termset;
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'getAllTerms started.', {
|
||||||
|
configuredTermSet: termset,
|
||||||
|
hideDeprecatedTags: hideDeprecatedTags === true,
|
||||||
|
hideTagsNotAvailableForTagging: hideTagsNotAvailableForTagging === true,
|
||||||
|
useSessionStorage: useSessionStorage,
|
||||||
|
cacheMinutes: cacheMinutes
|
||||||
|
});
|
||||||
// Check if the provided term set property is a GUID or string
|
// Check if the provided term set property is a GUID or string
|
||||||
if (!this.isGuid(termset)) {
|
if (!this.isGuid(termset)) {
|
||||||
// Fetch the term store information
|
// Fetch the term store information
|
||||||
@@ -134,19 +168,33 @@ export default class SPTermStorePickerService {
|
|||||||
const crntTermSet = this.getTermSetId(termStore, termset);
|
const crntTermSet = this.getTermSetId(termStore, termset);
|
||||||
if (crntTermSet) {
|
if (crntTermSet) {
|
||||||
termsetId = this.cleanGuid(crntTermSet.Id);
|
termsetId = this.cleanGuid(crntTermSet.Id);
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'Term set name resolved to an ID.', {
|
||||||
|
termsetId: termsetId
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
debugWarn(this.debug, 'SPTermStorePickerService', 'Configured term set could not be resolved.', termset);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const childTerms = this.getTermsById(termsetId, useSessionStorage);
|
const childTerms = this.getCachedTermSet(termsetId, useSessionStorage, false);
|
||||||
|
const staleTerms = this.getCachedTermSet(termsetId, useSessionStorage, true);
|
||||||
|
|
||||||
if (childTerms) {
|
if (childTerms) {
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'Returning terms from session cache.', {
|
||||||
|
termsetId: termsetId,
|
||||||
|
termCount: childTerms.Terms ? childTerms.Terms.length : 0
|
||||||
|
});
|
||||||
return childTerms;
|
return childTerms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'No valid cache entry found; sending ProcessQuery request.', {
|
||||||
|
termsetId: termsetId,
|
||||||
|
url: this.clientServiceUrl
|
||||||
|
});
|
||||||
|
|
||||||
// Request body to retrieve all terms for the given term set
|
// Request body to retrieve all terms for the given term set
|
||||||
const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultKeywordsTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`;
|
const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultSiteCollectionTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`;
|
||||||
|
|
||||||
const reqHeaders = new Headers();
|
const reqHeaders = new Headers();
|
||||||
reqHeaders.append('accept', 'application/json');
|
reqHeaders.append('accept', 'application/json');
|
||||||
@@ -158,7 +206,16 @@ export default class SPTermStorePickerService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
|
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'ProcessQuery response received.', {
|
||||||
|
status: serviceResponse.status,
|
||||||
|
statusText: serviceResponse.statusText,
|
||||||
|
ok: serviceResponse.ok
|
||||||
|
});
|
||||||
|
this.ensureSuccessfulResponse(serviceResponse, 'Terms');
|
||||||
return serviceResponse.json().then((serviceJSONResponse: any) => {
|
return serviceResponse.json().then((serviceJSONResponse: any) => {
|
||||||
|
if (!Array.isArray(serviceJSONResponse)) {
|
||||||
|
throw new Error('The taxonomy service returned an unexpected response.');
|
||||||
|
}
|
||||||
const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet');
|
const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet');
|
||||||
|
|
||||||
if (termStoreResultTermSets.length > 0) {
|
if (termStoreResultTermSets.length > 0) {
|
||||||
@@ -171,11 +228,21 @@ export default class SPTermStorePickerService {
|
|||||||
let terms = termStoreResultTerms[0]._Child_Items_;
|
let terms = termStoreResultTerms[0]._Child_Items_;
|
||||||
|
|
||||||
if (hideDeprecatedTags === true) {
|
if (hideDeprecatedTags === true) {
|
||||||
|
const countBeforeDeprecatedFilter: number = terms.length;
|
||||||
terms = terms.filter(d => d.IsDeprecated === false);
|
terms = terms.filter(d => d.IsDeprecated === false);
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'Deprecated terms filtered.', {
|
||||||
|
before: countBeforeDeprecatedFilter,
|
||||||
|
after: terms.length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hideTagsNotAvailableForTagging === true) {
|
if (hideTagsNotAvailableForTagging === true) {
|
||||||
|
const countBeforeAvailableFilter: number = terms.length;
|
||||||
terms = terms.filter(d => d.IsAvailableForTagging === true);
|
terms = terms.filter(d => d.IsAvailableForTagging === true);
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'Terms unavailable for tagging filtered.', {
|
||||||
|
before: countBeforeAvailableFilter,
|
||||||
|
after: terms.length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean the term ID and specify the path depth
|
// Clean the term ID and specify the path depth
|
||||||
@@ -203,15 +270,36 @@ 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
|
||||||
|
};
|
||||||
|
const cacheKey: string = this.getTermCacheKey(termsetId);
|
||||||
|
window.sessionStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'Term set stored in session cache.', {
|
||||||
|
key: cacheKey,
|
||||||
|
expiresAt: cacheEntry.expiresAt
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Do nothing, sometimes "storage quota exceeded" error if too many items
|
debugWarn(this.debug, 'SPTermStorePickerService', 'Term cache could not be written.', error);
|
||||||
}
|
}
|
||||||
|
debugLog(this.debug, 'SPTermStorePickerService', 'getAllTerms completed.', {
|
||||||
|
termsetId: termsetId,
|
||||||
|
termCount: termStoreResultTermSet.Terms ? termStoreResultTermSet.Terms.length : 0
|
||||||
|
});
|
||||||
return termStoreResultTermSet;
|
return termStoreResultTermSet;
|
||||||
}
|
}
|
||||||
|
debugWarn(this.debug, 'SPTermStorePickerService', 'ProcessQuery response contained no term set.');
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
}).catch((error: any) => {
|
||||||
|
if (staleTerms) {
|
||||||
|
debugWarn(this.debug, 'SPTermStorePickerService', 'Taxonomy request failed; returning stale cache.', error);
|
||||||
|
return staleTerms;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +313,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 +345,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 +371,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 +430,20 @@ export default class SPTermStorePickerService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTermsById(termId, useSessionStorage: boolean = true) {
|
private getCachedTermSet(termId: string, useSessionStorage: boolean = true, allowExpired: boolean = false): ITermSet {
|
||||||
try {
|
try {
|
||||||
if (useSessionStorage && window.sessionStorage) {
|
if (useSessionStorage && window.sessionStorage) {
|
||||||
const terms = window.sessionStorage.getItem(termId);
|
const key: string = this.getTermCacheKey(termId);
|
||||||
if (terms) {
|
const terms: string = window.sessionStorage.getItem(key);
|
||||||
return JSON.parse(terms);
|
if (!terms) {
|
||||||
} else {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedValue: ITermSet = readCachedValue<ITermSet>(terms, Date.now(), allowExpired);
|
||||||
|
if (!cachedValue && allowExpired) {
|
||||||
|
window.sessionStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
return cachedValue || null;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -349,6 +452,51 @@ 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 getTermCacheKey(termId: string): string {
|
||||||
|
return buildTermCacheKey(
|
||||||
|
this.props.siteCollectionUrl,
|
||||||
|
termId,
|
||||||
|
this.props.languageLcid,
|
||||||
|
this.props.cacheVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureSuccessfulResponse(response: SPHttpClientResponse, operation: string): void {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(operation + ' request failed (' + response.status + ' ' + response.statusText + ').');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private searchTermsBySearchText(terms, searchText) {
|
private searchTermsBySearchText(terms, searchText) {
|
||||||
if (terms) {
|
if (terms) {
|
||||||
return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
|
return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });
|
||||||
@@ -456,7 +604,7 @@ export default class SPTermStorePickerService {
|
|||||||
// Last item is not needed for parent path
|
// Last item is not needed for parent path
|
||||||
pathElms.pop();
|
pathElms.pop();
|
||||||
// Find the parent item and add the new item
|
// Find the parent item and add the new item
|
||||||
const idx: number = findIndex(newTermsOrder, term => term.PathOfTerm === pathElms.join(';'));
|
const idx: number = this.findTermIndexByPath(newTermsOrder, pathElms.join(';'));
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
newTermsOrder.splice(idx + 1, 0, crntTerm);
|
newTermsOrder.splice(idx + 1, 0, crntTerm);
|
||||||
} else {
|
} else {
|
||||||
@@ -476,6 +624,16 @@ export default class SPTermStorePickerService {
|
|||||||
return newTermsOrder;
|
return newTermsOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private findTermIndexByPath(terms: ITerm[], path: string): number {
|
||||||
|
for (let index: number = 0; index < terms.length; index++) {
|
||||||
|
if (terms[index].PathOfTerm === path) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort the terms by their path
|
* Sort the terms by their path
|
||||||
*
|
*
|
||||||
@@ -483,23 +641,7 @@ export default class SPTermStorePickerService {
|
|||||||
* @param b term 2
|
* @param b term 2
|
||||||
*/
|
*/
|
||||||
private sortTermByPath(a: ITerm, b: ITerm) {
|
private sortTermByPath(a: ITerm, b: ITerm) {
|
||||||
if (a.CustomSortOrderIndex === -1) {
|
return compareTermsByConfiguredOrder(a, b);
|
||||||
if (a.PathOfTerm.toLowerCase() < b.PathOfTerm.toLowerCase()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.PathOfTerm.toLowerCase() > b.PathOfTerm.toLowerCase()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
if (a.CustomSortOrderIndex < b.CustomSortOrderIndex) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.CustomSortOrderIndex > b.CustomSortOrderIndex) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertTermToPickerTerm(term: ITerm): IPickerTerm {
|
private convertTermToPickerTerm(term: ITerm): IPickerTerm {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user