feat: Add support for flyout menu mode in MegaMenu

- Introduced `MegaMenuMode` type and `normalizeMegaMenuMode` function to handle menu modes.
- Updated `MegaMenuApplicationCustomizer` to accept `menuMode` property.
- Enhanced `MegaMenuRenderer` to render flyout menus based on the selected mode.
- Created new HTML previews for both flyout and mega menu modes.
- Removed unused mock services and user custom action service interfaces.
This commit is contained in:
Torsten Brendgen
2026-07-20 20:37:32 +02:00
parent e75b11117f
commit bc61166267
27 changed files with 1320 additions and 1833 deletions

View File

@@ -1,216 +1,62 @@
# 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` - Styling für klassische Seiten
- `classic-deployment.md` - Diese Dokumentation
- `megamenu-classic.css`
- `megamenu-services-standalone.js`
- `megamenu-classic.js`
## 🚀 Installation
Mit `build-classic.ps1` werden die Dateien nach `classic/dist` kopiert.
### Option 1: Master Page (empfohlen)
## Konfiguration
1. **Dateien hochladen:**
```
/SiteAssets/megamenu/megamenu-classic.js
/SiteAssets/megamenu/megamenu-classic.css
```
Die Classic-Variante liest die `ClientSideComponentProperties` der zentralen MegaMenu-Site-Action:
2. **Master Page bearbeiten:**
```html
<!-- In den <head> Bereich -->
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
<!-- Vor dem schließenden </body> Tag -->
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
```
### 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()
```json
{
"termSetId": "7948e6f9-7af4-431e-b6fe-328122f2746c",
"termSetName": "Global Navigation",
"cacheMinutes": 15,
"menuMode": "megaMenu",
"debug": false
}
```
### Option 3: Content Editor Web Part
`menuMode` erlaubt `megaMenu` oder `flyout`. `termSetId` hat Vorrang vor `termSetName`.
## Einbindung
Die drei Dateien beispielsweise nach `/SiteAssets/megamenu/` hochladen und in dieser Reihenfolge laden:
```html
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
<link rel="stylesheet" href="/SiteAssets/megamenu/megamenu-classic.css" />
<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
<script type="text/javascript">
// Konfiguration vor dem Laden des Scripts setzen
window.MegaMenuConfig = {
termSetName: 'Navigation', // Name des Taxonomy Term Sets
cssUrl: '', // Optional: Externe CSS-Datei
containerId: 's4-titlerow', // SharePoint Container ID
debug: true // Debug-Modus aktivieren
};
<script>
window.MegaMenuConfig = {
containerId: 's4-titlerow',
debug: true
};
</script>
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
```
### Container IDs für verschiedene SharePoint-Versionen
| 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
## Diagnose
```javascript
// Nach der Initialisierung eigene Logik hinzufügen
document.addEventListener('DOMContentLoaded', function() {
// Warten bis MegaMenu geladen ist
setTimeout(function() {
if (window.MegaMenuClassic) {
console.log('MegaMenu ist bereit!');
// Eigene Anpassungen hier...
}
}, 1000);
});
typeof window.MegaMenuServices
typeof window.MegaMenuClassic
window.MegaMenuClassic.reload()
```
## 🔧 Erweiterte Konfiguration
### 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
Benutzer benötigen Leserechte auf den verwendeten Managed-Metadata-Termstore.

View File

@@ -1,74 +1,295 @@
/**
* MegaMenu CSS for Classic SharePoint Pages
* Version: 1.0.2
*
* This file imports the exact same CSS as the modern SPFx version
* to ensure 100% visual consistency.
*
* 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.
*/
/* SharePoint MegaMenu 2.1.0 - Classic pages */
:root {
--megaMenuBarHeight: 44px;
--megaMenuContentWidth: 1200px;
--megaMenuFlyoutWidth: 288px;
--megaMenuZIndex: 6000;
}
/*
* The main MegaMenu.css should be loaded first!
* This file only contains classic SharePoint specific overrides.
*/
/* Classic SharePoint specific container adjustments */
.megamenu-classic-container {
/* Insert after s4-titlerow or other SharePoint containers */
position: relative;
width: 100%;
margin: 0;
padding: 0;
overflow: visible;
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: 1px solid #edebe9;
border-bottom: 1px solid #edebe9;
background: #fff;
color: #323130;
}
.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 {
.mega-menu-mode-flyout .mega-menu-top-item {
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-text[aria-haspopup="true"]::after {
content: "";
width: 6px;
height: 6px;
margin: -3px 0 0 10px;
border-right: 1px solid currentColor;
border-bottom: 1px solid currentColor;
transform: rotate(45deg);
}
.menu-item-link:hover,
.menu-item-text:hover,
.menu-item-text[aria-expanded="true"] {
background: #f3f2f1;
color: #323130;
text-decoration: none;
}
.menu-item-link:focus,
.menu-item-text:focus,
.mega-menu-category a:focus,
.flyout-link:focus {
outline: 2px solid #0078d4;
outline-offset: -2px;
}
.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 */
body.ms-backgroundImage #Mega-Menu {
/* Adjust if SharePoint has background images */
.mega-menu.js-open {
display: block;
}
.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-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;
margin: 0;
padding: 0;
}
/* Ensure settings panel works in classic mode too */
.mm-settings-panel {
z-index: 4001; /* Above everything else */
.flyout-link {
position: relative;
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 */
.v4master #Mega-Menu {
/* Any v4.master specific styles if needed */
.flyout-link:hover,
.flyout-item:focus-within > .flyout-link {
background: #f3f2f1;
color: #323130;
}
/* Classic SharePoint seattle.master specific adjustments */
.seattle #Mega-Menu {
/* Any seattle.master specific styles if needed */
.flyout-link-has-children::after {
content: "";
position: absolute;
top: 50%;
right: 18px;
width: 6px;
height: 6px;
margin-top: -4px;
border-top: 1px solid currentColor;
border-right: 1px solid currentColor;
transform: rotate(45deg);
}
/* Responsive adjustments for classic SharePoint layouts */
@media (max-width: 1024px) {
.megamenu-classic-container {
/* Classic SharePoint is often used on older devices */
overflow-x: auto;
.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 {
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-top-level {
flex-wrap: wrap;
padding: 0 8px;
}
.mega-menu-grid {
grid-template-columns: 1fr;
padding: 16px;
}
.flyout-level-3 {
position: static;
display: block;
width: 100%;
border: 0;
box-shadow: none;
padding-left: 16px;
}
}
/* Print styles for classic SharePoint */
@media print {
#Mega-Menu,
.megamenu-classic-container {
display: none !important;
}
}
}

View File

@@ -1,6 +1,6 @@
/**
* SharePoint MegaMenu for Classic Pages
* Version: 1.0.2
* Version: 2.1.0
*
* This version uses standalone services that replicate the SPFx logic
* without requiring the SharePoint Framework.
@@ -21,10 +21,9 @@
var MEGAMENU_UCA_ID = 'c0abbffb-355d-4d4e-bd38-9e15fb811506'; // Same as in MegaMenuApplicationCustomizer.ts
var config = window.MegaMenuConfig || {
containerId: 's4-titlerow',
debug: false
};
var config = window.MegaMenuConfig || {};
config.containerId = config.containerId || 's4-titlerow';
config.debug = config.debug === true;
function log(message, data) {
if (config.debug && console && console.log) {
@@ -84,44 +83,30 @@
return response.json();
})
.then(function(data) {
if (data && data.value && data.value.length > 0) {
var uca = data.value[0];
var actions = data && data.value
? data.value
: (data && data.d && data.d.results ? data.d.results : []);
if (actions.length > 0) {
var uca = actions[0];
try {
var props = JSON.parse(uca.ClientSideComponentProperties);
log('Configuration found', props);
callback(props);
} catch (e) {
log('Error parsing UserCustomAction properties: ' + e.message);
callback({ termSetName: 'Navigation', cssUrl: '' });
callback({ termSetName: 'Navigation', menuMode: 'megaMenu', debug: false });
}
} else {
log('No UserCustomAction found - using defaults');
callback({ termSetName: 'Navigation', cssUrl: '' });
callback({ termSetName: 'Navigation', menuMode: 'megaMenu', debug: false });
}
})
.catch(function(error) {
log('Error reading configuration: ' + error.message);
callback({ termSetName: 'Navigation', cssUrl: '' });
callback({ termSetName: 'Navigation', menuMode: 'megaMenu', debug: false });
});
}
// 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
function initMegaMenu() {
log('Initializing MegaMenu for classic SharePoint...');
@@ -133,20 +118,16 @@
}
readMegaMenuConfiguration(function(props) {
config.debug = props.debug === true || config.debug === true;
log('Using configuration:', props);
// Load external CSS if specified
if (props.cssUrl) {
loadExternalCSS(props.cssUrl);
}
// Create context
var context = createClassicContext();
// Create taxonomy service using standalone implementation
var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService(
context,
props.termSetName
props.termSetId || props.termSetName
);
// Load menu items
@@ -179,9 +160,7 @@
var renderer = new window.MegaMenuServices.MegaMenuRenderer(
context,
menuItems,
function(updatedProps) {
log('Properties updated (not persisted in classic mode):', updatedProps);
}
props.menuMode === 'flyout' ? 'flyout' : 'megaMenu'
);
// Render the menu

View File

@@ -51,14 +51,14 @@
}
_getNavigationUrl(term, siteCollectionUrl) {
// Extract URL from term properties
if (term.LocalCustomProperties && term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl) {
return term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl;
const properties = term.LocalCustomProperties || {};
const rawUrl = properties._Sys_Nav_SimpleLinkUrl || properties._Sys_Nav_TargetUrl;
if (!rawUrl) {
return undefined;
}
// Fallback: generate URL based on term name
const termName = term.Name.toLowerCase().replace(/[^a-z0-9]/g, '-');
return `${siteCollectionUrl}/pages/${termName}.aspx`;
return rawUrl.indexOf('~sitecollection') === 0
? siteCollectionUrl + rawUrl.substring('~sitecollection'.length)
: rawUrl;
}
}
@@ -67,17 +67,17 @@
// ===========================================
class TaxonomyNavigationService {
constructor(context, termSetName) {
constructor(context, termSetIdentifier) {
this.context = context;
this.termSetName = termSetName;
this.termSetIdentifier = termSetIdentifier;
this._siteCollectionUrl = context.pageContext.site.absoluteUrl;
}
async getMenuItems() {
console.log('[TaxonomyService] Loading terms for:', this.termSetName);
console.log('[TaxonomyService] Loading terms for:', this.termSetIdentifier);
try {
const termSet = await this._loadTermSet(this.termSetName);
const termSet = await this._loadTermSet(this.termSetIdentifier);
return this._processTerms(termSet);
} catch (error) {
console.error('[TaxonomyService] Error loading terms:', error);
@@ -85,31 +85,28 @@
}
}
_loadTermSet(termSetName) {
_loadTermSet(termSetIdentifier) {
return new Promise((resolve, reject) => {
const context = SP.ClientContext.get_current();
const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
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, 1033);
context.load(termSets);
context.executeQueryAsync(
() => {
if (termSets.get_count() > 0) {
const termSet = termSets.get_item(0);
const terms = termSet.get_terms();
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()))
);
this._loadAllTerms(context, termSet, resolve, reject);
} 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()))
@@ -117,32 +114,26 @@
});
}
_loadAllTermsRecursively(context, terms, resolve, reject) {
const allTerms = [];
const termsEnum = terms.getEnumerator();
// 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
_loadAllTerms(context, termSet, resolve, reject) {
const terms = termSet.getAllTerms();
context.load(terms, 'Include(Id,Name,PathOfTerm,LocalCustomProperties,IsAvailableForTagging,IsDeprecated)');
context.executeQueryAsync(
() => {
const processedTerms = allTerms.map(term => ({
Id: term.get_id().toString(),
Name: term.get_name(),
PathOfTerm: term.get_pathOfTerm(),
LocalCustomProperties: this._getCustomProperties(term),
IsRoot: term.get_pathOfTerm().split(';').length === 1
}));
const processedTerms = [];
const enumerator = terms.getEnumerator();
while (enumerator.moveNext()) {
const term = enumerator.get_current();
if (!term.get_isAvailableForTagging() || term.get_isDeprecated()) {
continue;
}
processedTerms.push({
Id: term.get_id().toString(),
Name: term.get_name(),
PathOfTerm: term.get_pathOfTerm(),
LocalCustomProperties: this._getCustomProperties(term),
IsRoot: term.get_pathOfTerm().split(';').length === 1
});
}
resolve(processedTerms);
},
@@ -150,25 +141,12 @@
);
}
_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) {
try {
const props = term.get_localCustomProperties();
return {
_Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null,
_Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null,
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null
};
} catch (e) {
@@ -178,12 +156,14 @@
_processTerms(termsData) {
const itemsDict = new ItemDictionary();
const itemsByPath = {};
const menuItems = [];
// Create MenuItem objects
termsData.forEach(termData => {
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
itemsDict.Add(termData.Id, menuItem);
itemsByPath[termData.PathOfTerm] = menuItem;
if (menuItem.pathDepth === 1) {
menuItems.push(menuItem);
@@ -194,8 +174,9 @@
termsData.forEach(termData => {
if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) {
const menuItem = itemsDict.Get(termData.Id);
const parentId = menuItem.parentId;
const parentItem = itemsDict.Get(parentId);
const pathParts = termData.PathOfTerm.split(';');
pathParts.pop();
const parentItem = itemsByPath[pathParts.join(';')];
if (parentItem) {
parentItem.items.push(menuItem);
@@ -221,10 +202,10 @@
// ===========================================
class MegaMenuRenderer {
constructor(context, menuItems, updateCallback) {
constructor(context, menuItems, menuMode) {
this.context = context;
this.menuItems = menuItems;
this.updateCallback = updateCallback;
this.menuMode = menuMode === 'flyout' ? 'flyout' : 'megaMenu';
}
render(container) {
@@ -232,11 +213,13 @@
const nav = document.createElement('nav');
nav.id = 'Mega-Menu';
nav.className = 'mega-menu-main';
nav.className = 'mega-menu-main mega-menu-mode-' + this.menuMode;
nav.setAttribute('data-menu-mode', this.menuMode);
nav.setAttribute('role', 'navigation');
nav.setAttribute('aria-label', 'Hauptnavigation');
const topLevelUl = document.createElement('ul');
topLevelUl.className = 'mega-menu-top-level';
topLevelUl.setAttribute('role', 'menubar');
this.menuItems.forEach(topLevelItem => {
@@ -244,11 +227,6 @@
topLevelUl.appendChild(topLevelLi);
});
// Add settings if user has permissions (simplified check)
if (this._hasManagePermissions()) {
topLevelUl.appendChild(this.createSettingsItem());
}
nav.appendChild(topLevelUl);
container.appendChild(nav);
@@ -258,6 +236,7 @@
createTopLevelItem(item) {
const li = document.createElement('li');
li.className = 'mega-menu-top-item';
li.setAttribute('role', 'none');
if (item.url && item.items.length === 0) {
@@ -281,7 +260,9 @@
li.appendChild(span);
if (item.items.length > 0) {
const megaMenu = this.createMegaMenu(item.items);
const megaMenu = this.menuMode === 'flyout'
? this.createFlyoutMenu(item.items)
: this.createMegaMenu(item.items);
li.appendChild(megaMenu);
}
}
@@ -350,24 +331,46 @@
return categoryDiv;
}
createSettingsItem() {
createFlyoutMenu(items) {
const flyout = document.createElement('div');
flyout.className = 'mega-menu mega-menu-flyout';
flyout.setAttribute('role', 'menu');
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');
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');
button.className = 'menu-item-settings';
button.setAttribute('type', 'button');
button.setAttribute('role', 'menuitem');
button.setAttribute('tabindex', '0');
button.setAttribute('aria-label', 'Einstellungen');
button.onclick = () => this._openSettings();
const element = document.createElement(item.url ? 'a' : 'span');
if (item.url) {
element.href = item.url;
} else {
element.setAttribute('tabindex', '0');
}
element.textContent = item.title;
element.className = hasChildren ? 'flyout-link flyout-link-has-children' : 'flyout-link';
element.setAttribute('role', 'menuitem');
if (hasChildren) {
element.setAttribute('aria-haspopup', 'true');
}
li.appendChild(element);
const icon = document.createElement('i');
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
icon.setAttribute('aria-hidden', 'true');
button.appendChild(icon);
if (hasChildren) {
const childList = document.createElement('ul');
childList.className = 'flyout-list flyout-level-3';
childList.setAttribute('role', 'menu');
item.items.forEach(child => childList.appendChild(this.createFlyoutItem(child)));
li.appendChild(childList);
}
li.appendChild(button);
return li;
}
@@ -441,15 +444,6 @@
}
}
_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.');
}
}
// ===========================================
@@ -465,4 +459,4 @@
console.log('[MegaMenu] Standalone services loaded successfully');
})(window);
})(window);