/** * 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);