/** * 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) { const properties = term.LocalCustomProperties || {}; const rawUrl = properties._Sys_Nav_SimpleLinkUrl || properties._Sys_Nav_TargetUrl; if (!rawUrl) { return undefined; } return rawUrl.indexOf('~sitecollection') === 0 ? siteCollectionUrl + rawUrl.substring('~sitecollection'.length) : rawUrl; } } // =========================================== // 2. TAXONOMY NAVIGATION SERVICE (adapted) // =========================================== class TaxonomyNavigationService { constructor(context, termSetIdentifier) { this.context = context; this.termSetIdentifier = termSetIdentifier; this._siteCollectionUrl = context.pageContext.site.absoluteUrl; } async getMenuItems() { console.log('[TaxonomyService] Loading terms for:', this.termSetIdentifier); try { const termSet = await this._loadTermSet(this.termSetIdentifier); return this._processTerms(termSet); } catch (error) { console.error('[TaxonomyService] Error loading terms:', error); return [this._createNoTermsItem()]; } } _loadTermSet(termSetIdentifier) { return new Promise((resolve, reject) => { const context = SP.ClientContext.get_current(); const session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context); const termStore = session.getDefaultSiteCollectionTermStore(); 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); this._loadAllTerms(context, termSet, resolve, reject); } else { reject(new Error(`Term set '${termSetIdentifier}' not found`)); } }, (sender, args) => reject(new Error(args.get_message())) ); }); } _loadAllTerms(context, termSet, resolve, reject) { const terms = termSet.getAllTerms(); context.load(terms, 'Include(Id,Name,PathOfTerm,LocalCustomProperties,IsAvailableForTagging,IsDeprecated)'); context.executeQueryAsync( () => { 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); }, (sender, args) => reject(new Error(args.get_message())) ); } _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) { return {}; } } _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); } }); // Build hierarchy termsData.forEach(termData => { if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) { const menuItem = itemsDict.Get(termData.Id); const pathParts = termData.PathOfTerm.split(';'); pathParts.pop(); const parentItem = itemsByPath[pathParts.join(';')]; 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, menuMode) { this.context = context; this.menuItems = menuItems; this.menuMode = menuMode === 'flyout' ? 'flyout' : 'megaMenu'; } render(container) { container.innerHTML = ''; const nav = document.createElement('nav'); nav.id = 'Mega-Menu'; 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 => { const topLevelLi = this.createTopLevelItem(topLevelItem); topLevelUl.appendChild(topLevelLi); }); nav.appendChild(topLevelUl); container.appendChild(nav); this.attachEventListeners(); this.createScreenReaderAnnouncer(); } createTopLevelItem(item) { const li = document.createElement('li'); li.className = 'mega-menu-top-item'; 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.menuMode === 'flyout' ? this.createFlyoutMenu(item.items) : 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; } 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'); const hasChildren = item.items && item.items.length > 0; li.className = hasChildren ? 'flyout-item has-children' : 'flyout-item'; 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); 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); } 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(); } } } } } // =========================================== // 4. EXPOSE SERVICES GLOBALLY // =========================================== global.MegaMenuServices = { TaxonomyNavigationService: TaxonomyNavigationService, MegaMenuRenderer: MegaMenuRenderer, MenuItem: MenuItem, ItemDictionary: ItemDictionary }; console.log('[MegaMenu] Standalone services loaded successfully'); })(window);