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

@@ -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);