Initial commit
This commit is contained in:
372
lib/extensions/megaMenu/MegaMenuRenderer.js
Normal file
372
lib/extensions/megaMenu/MegaMenuRenderer.js
Normal file
@@ -0,0 +1,372 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user