- Added localization support for loading states, empty navigation, and submenu actions in both German and English. - Introduced new properties in IMenuItem for description, openInNewWindow, and external links. - Updated SPTermStorePickerService to handle cache versioning and language-specific caching. - Implemented a new MegaMenuCore service for handling term properties, cache management, and navigation URL resolution. - Refactored TaxonomyNavigationService to build menu hierarchy and filter hidden terms. - Added automated tests for MegaMenu core functionalities and static asset validation. - Removed unused ItemDictionary service. - Created a comprehensive ToDo document outlining the implementation status and future tasks.
286 lines
11 KiB
JavaScript
286 lines
11 KiB
JavaScript
/**
|
|
* SharePoint MegaMenu for Classic Pages
|
|
* Version: 2.2.0
|
|
*
|
|
* 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 = 'c0abbffb-355d-4d4e-bd38-9e15fb811506'; // Same as in MegaMenuApplicationCustomizer.ts
|
|
|
|
var config = window.MegaMenuConfig || {};
|
|
config.containerId = config.containerId || 's4-titlerow';
|
|
config.debug = config.debug === true;
|
|
|
|
function getLanguageLcid() {
|
|
return Number(window._spPageContextInfo && _spPageContextInfo.currentLanguage) || 1033;
|
|
}
|
|
|
|
function getStrings() {
|
|
return getLanguageLcid() === 1031 ? {
|
|
navigationLabel: 'Hauptnavigation', openNavigation: 'Hauptnavigation öffnen', closeNavigation: 'Hauptnavigation schließen',
|
|
openSubmenu: 'Untermenü öffnen für', closeSubmenu: 'Untermenü schließen für', submenuLabel: 'Untermenü',
|
|
externalLink: 'externer Link', loading: 'Navigation wird geladen…', missing: 'Die Navigation ist nicht konfiguriert.',
|
|
empty: 'Die Navigation enthält keine sichtbaren Einträge.', error: 'Die Navigation konnte nicht geladen werden.'
|
|
} : {
|
|
navigationLabel: 'Main navigation', openNavigation: 'Open main navigation', closeNavigation: 'Close main navigation',
|
|
openSubmenu: 'Open submenu for', closeSubmenu: 'Close submenu for', submenuLabel: 'submenu',
|
|
externalLink: 'external link', loading: 'Loading navigation…', missing: 'Navigation is not configured.',
|
|
empty: 'The navigation does not contain any visible entries.', error: 'Navigation could not be loaded.'
|
|
};
|
|
}
|
|
|
|
function showStatus(message) {
|
|
var existing = document.querySelector('.megamenu-classic-container');
|
|
var container = existing || document.createElement('div');
|
|
container.className = 'megamenu-classic-container';
|
|
container.innerHTML = '';
|
|
var status = document.createElement('div');
|
|
status.className = 'mega-menu-status';
|
|
status.setAttribute('role', 'status');
|
|
status.textContent = message;
|
|
container.appendChild(status);
|
|
if (!existing) {
|
|
var anchor = document.getElementById(config.containerId) || document.body.firstChild;
|
|
anchor.parentNode.insertBefore(container, anchor.nextSibling);
|
|
}
|
|
return container;
|
|
}
|
|
|
|
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) {
|
|
var actions = data && data.value
|
|
? data.value
|
|
: (data && data.d && data.d.results ? data.d.results : []);
|
|
if (actions.length > 0) {
|
|
var uca = actions[0];
|
|
try {
|
|
var props = JSON.parse(uca.ClientSideComponentProperties);
|
|
log('Configuration found', props);
|
|
callback(props);
|
|
} catch (e) {
|
|
log('Error parsing UserCustomAction properties: ' + e.message);
|
|
callback({});
|
|
}
|
|
} else {
|
|
log('No UserCustomAction found - using defaults');
|
|
callback({});
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
log('Error reading configuration: ' + error.message);
|
|
callback({});
|
|
});
|
|
}
|
|
|
|
// 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) {
|
|
config.debug = props.debug === true || config.debug === true;
|
|
log('Using configuration:', props);
|
|
var strings = getStrings();
|
|
var termSetIdentifier = props.termSetId || props.termSetName;
|
|
if (!termSetIdentifier) {
|
|
showStatus(strings.missing);
|
|
return;
|
|
}
|
|
var loadingContainer = showStatus(strings.loading);
|
|
|
|
// Create context
|
|
var context = createClassicContext();
|
|
|
|
// Create taxonomy service using standalone implementation
|
|
var taxonomyService = new window.MegaMenuServices.TaxonomyNavigationService(
|
|
context,
|
|
termSetIdentifier,
|
|
{
|
|
cacheMinutes: props.cacheMinutes,
|
|
cacheVersion: props.cacheVersion,
|
|
languageLcid: getLanguageLcid()
|
|
}
|
|
);
|
|
|
|
// Load menu items
|
|
taxonomyService.getMenuItems()
|
|
.then(function(menuItems) {
|
|
log('Menu items loaded:', menuItems);
|
|
if (!menuItems || menuItems.length === 0) {
|
|
showStatus(strings.empty);
|
|
return;
|
|
}
|
|
if (loadingContainer && loadingContainer.parentNode) {
|
|
loadingContainer.parentNode.removeChild(loadingContainer);
|
|
}
|
|
|
|
// 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,
|
|
props.menuMode === 'flyout' ? 'flyout' : 'megaMenu',
|
|
strings
|
|
);
|
|
|
|
// Render the menu
|
|
renderer.render(menuWrapper);
|
|
|
|
log('✅ MegaMenu rendered successfully!');
|
|
})
|
|
.catch(function(error) {
|
|
console.error('[MegaMenu] Error loading menu items:', error);
|
|
showStatus(strings.error);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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...');
|
|
|
|
})();
|