Initial commit
This commit is contained in:
216
classic/classic-deployment.md
Normal file
216
classic/classic-deployment.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# MegaMenu für klassische SharePoint-Seiten
|
||||
|
||||
Diese Dateien ermöglichen die Verwendung des MegaMenus auf klassischen SharePoint-Seiten (2019/SE) ohne SPFx Framework.
|
||||
|
||||
## 📁 Dateien
|
||||
|
||||
- `megamenu-classic.js` - Hauptlogik für klassische Seiten
|
||||
- `megamenu-classic.css` - Styling für klassische Seiten
|
||||
- `classic-deployment.md` - Diese Dokumentation
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Option 1: Master Page (empfohlen)
|
||||
|
||||
1. **Dateien hochladen:**
|
||||
```
|
||||
/SiteAssets/megamenu/megamenu-classic.js
|
||||
/SiteAssets/megamenu/megamenu-classic.css
|
||||
```
|
||||
|
||||
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()
|
||||
```
|
||||
|
||||
### Option 3: Content Editor Web Part
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" type="text/css" href="/SiteAssets/megamenu/megamenu-classic.css" />
|
||||
<script type="text/javascript" src="/SiteAssets/megamenu/megamenu-classic.js"></script>
|
||||
```
|
||||
|
||||
## ⚙️ Konfiguration
|
||||
|
||||
### Basis-Konfiguration
|
||||
|
||||
```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>
|
||||
<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
|
||||
|
||||
```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);
|
||||
});
|
||||
```
|
||||
|
||||
## 🔧 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
|
||||
74
classic/megamenu-classic.css
Normal file
74
classic/megamenu-classic.css
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 */
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure proper z-index in classic SharePoint context */
|
||||
#Mega-Menu {
|
||||
position: relative;
|
||||
z-index: 999; /* Below SharePoint dialogs but above content */
|
||||
}
|
||||
|
||||
.mega-menu {
|
||||
z-index: 1000; /* Above the main menu */
|
||||
}
|
||||
|
||||
/* Classic SharePoint ribbon compatibility */
|
||||
body.ms-backgroundImage #Mega-Menu {
|
||||
/* Adjust if SharePoint has background images */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ensure settings panel works in classic mode too */
|
||||
.mm-settings-panel {
|
||||
z-index: 4001; /* Above everything else */
|
||||
}
|
||||
|
||||
/* Classic SharePoint v4.master specific adjustments */
|
||||
.v4master #Mega-Menu {
|
||||
/* Any v4.master specific styles if needed */
|
||||
}
|
||||
|
||||
/* Classic SharePoint seattle.master specific adjustments */
|
||||
.seattle #Mega-Menu {
|
||||
/* Any seattle.master specific styles if needed */
|
||||
}
|
||||
|
||||
/* Responsive adjustments for classic SharePoint layouts */
|
||||
@media (max-width: 1024px) {
|
||||
.megamenu-classic-container {
|
||||
/* Classic SharePoint is often used on older devices */
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles for classic SharePoint */
|
||||
@media print {
|
||||
#Mega-Menu,
|
||||
.megamenu-classic-container {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
265
classic/megamenu-classic.js
Normal file
265
classic/megamenu-classic.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* SharePoint MegaMenu for Classic Pages
|
||||
* Version: 1.0.2
|
||||
*
|
||||
* This version uses standalone services that replicate the SPFx logic
|
||||
* without requiring the SharePoint Framework.
|
||||
*
|
||||
* Prerequisites:
|
||||
* 1. megamenu-services-standalone.js must be loaded first
|
||||
* 2. MegaMenu.css must be included
|
||||
* 3. SP.js and SP.Taxonomy.js must be available
|
||||
*
|
||||
* Usage:
|
||||
* <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>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var MEGAMENU_UCA_ID = 'abc3361f-bb2d-491f-aba3-cd51c19a299b'; // Same as in MegaMenuApplicationCustomizer.ts
|
||||
|
||||
var config = window.MegaMenuConfig || {
|
||||
containerId: 's4-titlerow',
|
||||
debug: false
|
||||
};
|
||||
|
||||
function log(message, data) {
|
||||
if (config.debug && console && console.log) {
|
||||
console.log('[MegaMenu Classic] ' + message, data || '');
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for dependencies: SharePoint JSOM, Taxonomy, and our standalone services
|
||||
function waitForDependencies(callback) {
|
||||
if (typeof SP !== 'undefined' &&
|
||||
SP.SOD &&
|
||||
typeof SP.Taxonomy !== 'undefined' &&
|
||||
typeof window.MegaMenuServices !== 'undefined' &&
|
||||
document.readyState === 'complete') {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(function() { waitForDependencies(callback); }, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Classic SharePoint context wrapper
|
||||
function createClassicContext() {
|
||||
return {
|
||||
pageContext: {
|
||||
site: {
|
||||
absoluteUrl: _spPageContextInfo.siteAbsoluteUrl
|
||||
},
|
||||
web: {
|
||||
absoluteUrl: _spPageContextInfo.webAbsoluteUrl,
|
||||
permissions: {
|
||||
hasPermission: function(permission) {
|
||||
return _spPageContextInfo.isSiteAdmin === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Configuration reader using REST API (same logic as MegaMenuSettings.ts)
|
||||
function readMegaMenuConfiguration(callback) {
|
||||
log('Reading MegaMenu configuration from UserCustomAction...');
|
||||
|
||||
var restUrl = _spPageContextInfo.siteAbsoluteUrl +
|
||||
"/_api/site/userCustomActions?$filter=ClientSideComponentId eq guid'" + MEGAMENU_UCA_ID + "'";
|
||||
|
||||
fetch(restUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json;odata=nometadata'
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status + ' - ' + response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && data.value && data.value.length > 0) {
|
||||
var uca = data.value[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: '' });
|
||||
}
|
||||
} else {
|
||||
log('No UserCustomAction found - using defaults');
|
||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
log('Error reading configuration: ' + error.message);
|
||||
callback({ termSetName: 'Navigation', cssUrl: '' });
|
||||
});
|
||||
}
|
||||
|
||||
// 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...');
|
||||
|
||||
// Check if services are available
|
||||
if (!window.MegaMenuServices) {
|
||||
console.error('[MegaMenu] Standalone services not found! Please include megamenu-services-standalone.js first.');
|
||||
return;
|
||||
}
|
||||
|
||||
readMegaMenuConfiguration(function(props) {
|
||||
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
|
||||
);
|
||||
|
||||
// Load menu items
|
||||
taxonomyService.getMenuItems()
|
||||
.then(function(menuItems) {
|
||||
log('Menu items loaded:', menuItems);
|
||||
|
||||
// Find target container
|
||||
var container = document.getElementById(config.containerId);
|
||||
if (!container) {
|
||||
log('Container not found: ' + config.containerId + '. Creating fallback container.');
|
||||
// Create fallback container at top of page
|
||||
container = document.createElement('div');
|
||||
container.id = 'megamenu-fallback-container';
|
||||
document.body.insertBefore(container, document.body.firstChild);
|
||||
}
|
||||
|
||||
// Create menu wrapper
|
||||
var menuWrapper = document.createElement('div');
|
||||
menuWrapper.className = 'megamenu-classic-container';
|
||||
|
||||
// Insert menu wrapper
|
||||
if (container.nextSibling) {
|
||||
container.parentNode.insertBefore(menuWrapper, container.nextSibling);
|
||||
} else {
|
||||
container.parentNode.appendChild(menuWrapper);
|
||||
}
|
||||
|
||||
// Create renderer using standalone implementation
|
||||
var renderer = new window.MegaMenuServices.MegaMenuRenderer(
|
||||
context,
|
||||
menuItems,
|
||||
function(updatedProps) {
|
||||
log('Properties updated (not persisted in classic mode):', updatedProps);
|
||||
}
|
||||
);
|
||||
|
||||
// Render the menu
|
||||
renderer.render(menuWrapper);
|
||||
|
||||
log('✅ MegaMenu rendered successfully!');
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('[MegaMenu] Error loading menu items:', 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Bootstrap function
|
||||
function bootstrap() {
|
||||
log('Bootstrapping MegaMenu...');
|
||||
|
||||
// Wait for SharePoint JSOM to be ready
|
||||
if (typeof SP === 'undefined' || !SP.SOD) {
|
||||
setTimeout(bootstrap, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load required SharePoint libraries
|
||||
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function() {
|
||||
SP.SOD.executeFunc('sp.taxonomy.js', 'SP.Taxonomy.TaxonomySession', function() {
|
||||
// Wait for all dependencies and initialize
|
||||
waitForDependencies(function() {
|
||||
initMegaMenu();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-start based on DOM state
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||
} else {
|
||||
// DOM already loaded
|
||||
setTimeout(bootstrap, 100);
|
||||
}
|
||||
|
||||
// Expose public API
|
||||
window.MegaMenuClassic = {
|
||||
init: initMegaMenu,
|
||||
bootstrap: bootstrap,
|
||||
config: config,
|
||||
|
||||
// Utility methods
|
||||
setConfig: function(newConfig) {
|
||||
Object.assign(config, newConfig);
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
// Remove existing menu and reload
|
||||
var existing = document.querySelector('.megamenu-classic-container');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
initMegaMenu();
|
||||
}
|
||||
};
|
||||
|
||||
log('MegaMenu Classic wrapper loaded. Waiting for dependencies...');
|
||||
|
||||
})();
|
||||
468
classic/megamenu-services-standalone.js
Normal file
468
classic/megamenu-services-standalone.js
Normal file
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* Standalone MegaMenu Services for Classic SharePoint
|
||||
*
|
||||
* This file contains extracted and adapted versions of the SPFx services
|
||||
* that work in classic SharePoint without SPFx dependencies.
|
||||
*/
|
||||
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
// ===========================================
|
||||
// 1. UTILITY CLASSES (from SPFx services)
|
||||
// ===========================================
|
||||
|
||||
class ItemDictionary {
|
||||
constructor() {
|
||||
this._items = {};
|
||||
}
|
||||
|
||||
Add(key, value) {
|
||||
this._items[key] = value;
|
||||
}
|
||||
|
||||
Get(key) {
|
||||
return this._items[key];
|
||||
}
|
||||
}
|
||||
|
||||
class MenuItem {
|
||||
constructor(term, depth, siteCollectionUrl) {
|
||||
this.title = term.Name;
|
||||
this.url = this._getNavigationUrl(term, siteCollectionUrl);
|
||||
this.items = [];
|
||||
this.pathDepth = this._calculateDepth(term.PathOfTerm);
|
||||
this.parentId = this._getParentId(term.PathOfTerm);
|
||||
this.id = term.Id;
|
||||
this._term = term;
|
||||
}
|
||||
|
||||
_calculateDepth(pathOfTerm) {
|
||||
if (!pathOfTerm) return 1;
|
||||
return pathOfTerm.split(';').length;
|
||||
}
|
||||
|
||||
_getParentId(pathOfTerm) {
|
||||
if (!pathOfTerm) return null;
|
||||
const parts = pathOfTerm.split(';');
|
||||
if (parts.length <= 1) return null;
|
||||
// Return parent term ID (simplified - may need adjustment)
|
||||
return parts[parts.length - 2];
|
||||
}
|
||||
|
||||
_getNavigationUrl(term, siteCollectionUrl) {
|
||||
// Extract URL from term properties
|
||||
if (term.LocalCustomProperties && term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl) {
|
||||
return term.LocalCustomProperties._Sys_Nav_SimpleLinkUrl;
|
||||
}
|
||||
|
||||
// Fallback: generate URL based on term name
|
||||
const termName = term.Name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
return `${siteCollectionUrl}/pages/${termName}.aspx`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// 2. TAXONOMY NAVIGATION SERVICE (adapted)
|
||||
// ===========================================
|
||||
|
||||
class TaxonomyNavigationService {
|
||||
constructor(context, termSetName) {
|
||||
this.context = context;
|
||||
this.termSetName = termSetName;
|
||||
this._siteCollectionUrl = context.pageContext.site.absoluteUrl;
|
||||
}
|
||||
|
||||
async getMenuItems() {
|
||||
console.log('[TaxonomyService] Loading terms for:', this.termSetName);
|
||||
|
||||
try {
|
||||
const termSet = await this._loadTermSet(this.termSetName);
|
||||
return this._processTerms(termSet);
|
||||
} catch (error) {
|
||||
console.error('[TaxonomyService] Error loading terms:', error);
|
||||
return [this._createNoTermsItem()];
|
||||
}
|
||||
}
|
||||
|
||||
_loadTermSet(termSetName) {
|
||||
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);
|
||||
|
||||
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()))
|
||||
);
|
||||
} else {
|
||||
reject(new Error(`Term set '${termSetName}' not found`));
|
||||
}
|
||||
},
|
||||
(sender, args) => reject(new Error(args.get_message()))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_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
|
||||
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
|
||||
}));
|
||||
|
||||
resolve(processedTerms);
|
||||
},
|
||||
(sender, args) => reject(new Error(args.get_message()))
|
||||
);
|
||||
}
|
||||
|
||||
_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_HoverText: props._Sys_Nav_HoverText || null
|
||||
};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
_processTerms(termsData) {
|
||||
const itemsDict = new ItemDictionary();
|
||||
const menuItems = [];
|
||||
|
||||
// Create MenuItem objects
|
||||
termsData.forEach(termData => {
|
||||
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
|
||||
itemsDict.Add(termData.Id, menuItem);
|
||||
|
||||
if (menuItem.pathDepth === 1) {
|
||||
menuItems.push(menuItem);
|
||||
}
|
||||
});
|
||||
|
||||
// Build hierarchy
|
||||
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);
|
||||
|
||||
if (parentItem) {
|
||||
parentItem.items.push(menuItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return menuItems.length > 0 ? menuItems : [this._createNoTermsItem()];
|
||||
}
|
||||
|
||||
_createNoTermsItem() {
|
||||
return new MenuItem({
|
||||
Id: 'no-terms',
|
||||
Name: 'Es wurden keine Terms gefunden. Bitte überprüfen Sie Ihre Einstellungen.',
|
||||
PathOfTerm: '',
|
||||
LocalCustomProperties: {}
|
||||
}, 0, this._siteCollectionUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// 3. MEGA MENU RENDERER (adapted from SPFx)
|
||||
// ===========================================
|
||||
|
||||
class MegaMenuRenderer {
|
||||
constructor(context, menuItems, updateCallback) {
|
||||
this.context = context;
|
||||
this.menuItems = menuItems;
|
||||
this.updateCallback = updateCallback;
|
||||
}
|
||||
|
||||
render(container) {
|
||||
container.innerHTML = '';
|
||||
|
||||
const nav = document.createElement('nav');
|
||||
nav.id = 'Mega-Menu';
|
||||
nav.className = 'mega-menu-main';
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', 'Hauptnavigation');
|
||||
|
||||
const topLevelUl = document.createElement('ul');
|
||||
topLevelUl.setAttribute('role', 'menubar');
|
||||
|
||||
this.menuItems.forEach(topLevelItem => {
|
||||
const topLevelLi = this.createTopLevelItem(topLevelItem);
|
||||
topLevelUl.appendChild(topLevelLi);
|
||||
});
|
||||
|
||||
// Add settings if user has permissions (simplified check)
|
||||
if (this._hasManagePermissions()) {
|
||||
topLevelUl.appendChild(this.createSettingsItem());
|
||||
}
|
||||
|
||||
nav.appendChild(topLevelUl);
|
||||
container.appendChild(nav);
|
||||
|
||||
this.attachEventListeners();
|
||||
this.createScreenReaderAnnouncer();
|
||||
}
|
||||
|
||||
createTopLevelItem(item) {
|
||||
const li = document.createElement('li');
|
||||
li.setAttribute('role', 'none');
|
||||
|
||||
if (item.url && item.items.length === 0) {
|
||||
// Simple link
|
||||
const a = document.createElement('a');
|
||||
a.href = item.url;
|
||||
a.textContent = item.title;
|
||||
a.className = 'menu-item-link';
|
||||
a.setAttribute('role', 'menuitem');
|
||||
a.setAttribute('tabindex', '0');
|
||||
li.appendChild(a);
|
||||
} else {
|
||||
// Menu with submenu
|
||||
const span = document.createElement('span');
|
||||
span.textContent = item.title;
|
||||
span.className = 'menu-item-text';
|
||||
span.setAttribute('role', 'menuitem');
|
||||
span.setAttribute('tabindex', '0');
|
||||
span.setAttribute('aria-haspopup', 'true');
|
||||
span.setAttribute('aria-expanded', 'false');
|
||||
li.appendChild(span);
|
||||
|
||||
if (item.items.length > 0) {
|
||||
const megaMenu = this.createMegaMenu(item.items);
|
||||
li.appendChild(megaMenu);
|
||||
}
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
createMegaMenu(categories) {
|
||||
const megaMenuDiv = document.createElement('div');
|
||||
megaMenuDiv.className = 'mega-menu';
|
||||
megaMenuDiv.setAttribute('role', 'menu');
|
||||
|
||||
const gridDiv = document.createElement('div');
|
||||
gridDiv.className = 'mega-menu-grid';
|
||||
|
||||
categories.forEach(category => {
|
||||
const categoryDiv = this.createCategory(category);
|
||||
gridDiv.appendChild(categoryDiv);
|
||||
});
|
||||
|
||||
megaMenuDiv.appendChild(gridDiv);
|
||||
return megaMenuDiv;
|
||||
}
|
||||
|
||||
createCategory(category) {
|
||||
const categoryDiv = document.createElement('div');
|
||||
categoryDiv.className = 'mega-menu-category';
|
||||
|
||||
const h3 = document.createElement('h3');
|
||||
|
||||
if (category.url) {
|
||||
const a = document.createElement('a');
|
||||
a.href = category.url;
|
||||
a.textContent = category.title;
|
||||
a.setAttribute('tabindex', '0');
|
||||
h3.appendChild(a);
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = category.title;
|
||||
h3.appendChild(span);
|
||||
}
|
||||
|
||||
categoryDiv.appendChild(h3);
|
||||
|
||||
if (category.items.length > 0) {
|
||||
const ul = document.createElement('ul');
|
||||
ul.setAttribute('role', 'group');
|
||||
|
||||
category.items.forEach(link => {
|
||||
const li = document.createElement('li');
|
||||
li.setAttribute('role', 'none');
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = link.url;
|
||||
a.textContent = link.title;
|
||||
a.setAttribute('role', 'menuitem');
|
||||
a.setAttribute('tabindex', '0');
|
||||
|
||||
li.appendChild(a);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
categoryDiv.appendChild(ul);
|
||||
}
|
||||
|
||||
return categoryDiv;
|
||||
}
|
||||
|
||||
createSettingsItem() {
|
||||
const li = document.createElement('li');
|
||||
li.setAttribute('role', 'none');
|
||||
|
||||
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 icon = document.createElement('i');
|
||||
icon.className = 'ms-Icon ms-Icon--Settings menu-item-settings__icon';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
button.appendChild(icon);
|
||||
|
||||
li.appendChild(button);
|
||||
return li;
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Keyboard and mouse event handling (simplified)
|
||||
const menuItems = document.querySelectorAll('#Mega-Menu [role="menuitem"]');
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
|
||||
const parent = item.parentElement;
|
||||
if (parent && parent.querySelector('.mega-menu')) {
|
||||
parent.addEventListener('mouseenter', this.showMegaMenu);
|
||||
parent.addEventListener('mouseleave', this.hideMegaMenu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createScreenReaderAnnouncer() {
|
||||
// Accessibility support
|
||||
const announcer = document.createElement('div');
|
||||
announcer.id = 'mega-menu-announcer';
|
||||
announcer.className = 'sr-only';
|
||||
announcer.setAttribute('aria-live', 'polite');
|
||||
announcer.setAttribute('aria-atomic', 'true');
|
||||
document.body.appendChild(announcer);
|
||||
}
|
||||
|
||||
showMegaMenu() {
|
||||
const megaMenu = this.querySelector('.mega-menu');
|
||||
if (megaMenu) {
|
||||
megaMenu.classList.add('js-open');
|
||||
const menuItem = this.querySelector('[role="menuitem"]');
|
||||
if (menuItem) {
|
||||
menuItem.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// 4. EXPOSE SERVICES GLOBALLY
|
||||
// ===========================================
|
||||
|
||||
global.MegaMenuServices = {
|
||||
TaxonomyNavigationService: TaxonomyNavigationService,
|
||||
MegaMenuRenderer: MegaMenuRenderer,
|
||||
MenuItem: MenuItem,
|
||||
ItemDictionary: ItemDictionary
|
||||
};
|
||||
|
||||
console.log('[MegaMenu] Standalone services loaded successfully');
|
||||
|
||||
})(window);
|
||||
Reference in New Issue
Block a user