Files
Megamenu/src/extensions/megaMenu/MegaMenuApplicationCustomizer.ts
Torsten Brendgen 97d2b6b5d2 Update MegaMenu Application Customizer: Version bump to 1.1.0, localization support added, and unnecessary files removed
- Deleted obsolete localization file for English (en-us).
- Updated version number from 1.0.4 to 1.1.0 in manifests.js and manifests.json.
- Added localization support for German (de-de) in a new localization file.
- Removed unnecessary dependencies from manifests.
- Added .gitignore to exclude build artifacts and temporary files.
2026-07-16 22:43:31 +02:00

244 lines
7.9 KiB
TypeScript

// tslint:disable:max-line-length
// tslint:disable:match-default-export-name
// tslint:disable:typedef
// tslint:disable:variable-name
import { override } from '@microsoft/decorators';
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName
} from '@microsoft/sp-application-base';
import * as strings from 'MegaMenuApplicationCustomizerStrings';
import { TaxonomyNavigationService } from '../../services/TaxonomyNavigationService';
import { MegaMenuRenderer } from './MegaMenuRenderer';
import { IMenuItem } from '../../services/IMenuItem';
import { debugError, debugLog, debugWarn } from '../../services/MegaMenuDebug';
import './MegaMenu.module.scss';
const LOG_SOURCE: string = 'MegaMenuApplicationCustomizer';
export const UserCustomActionMegaMenuId: string = 'abc3361f-bb2d-491f-aba3-cd51c19a299b';
export interface IMegaMenuApplicationCustomizerProperties {
termSetName?: string;
termSetId?: string;
cacheMinutes?: number;
cssUrl?: string;
debug?: boolean;
}
export default class MegaMenuApplicationCustomizer
extends BaseApplicationCustomizer<IMegaMenuApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
private _menuContainer: HTMLElement | undefined;
private _renderer: MegaMenuRenderer | undefined;
@override
public onInit(): Promise<void> {
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Initialized ' + strings.Title);
if (this.properties.cssUrl) {
this._loadExternalCss(this.properties.cssUrl);
}
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
this._renderPlaceHolders();
return Promise.resolve();
}
private _renderPlaceHolders(): void {
const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames
.map(name => PlaceholderName[name])
.join(', ');
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Available placeholders:', availablePlaceholders);
if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{ onDispose: this._onDispose }
);
if (!this._topPlaceholder) {
debugError(this._isDebugEnabled(), LOG_SOURCE, 'The expected placeholder (Top) was not found.');
return;
}
const termSetIdentifier: string = this._getTermSetIdentifier();
if (!termSetIdentifier) {
debugWarn(this._isDebugEnabled(), LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.');
this._renderStatus(strings.ConfigurationMissing);
return;
}
this._renderMegaMenu(termSetIdentifier);
}
}
private async _renderMegaMenu(termSetIdentifier: string, debug: boolean = this._isDebugEnabled()): Promise<void> {
if (!this._topPlaceholder) {
return;
}
try {
const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService(
this.context,
termSetIdentifier,
debug,
this._getCacheMinutes()
);
const menuItems: IMenuItem[] = await taxonomyService.getMenuItems();
if (this._renderer) {
this._renderer.dispose();
}
this._renderer = new MegaMenuRenderer(
menuItems,
debug
);
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
if (this._menuContainer) {
this._renderer.render(this._menuContainer);
} else {
this._renderer.render(this._topPlaceholder.domElement);
}
debugLog(debug, LOG_SOURCE, 'MegaMenu rendered successfully with ' + menuItems.length + ' top-level items.');
} catch (error) {
debugError(debug, LOG_SOURCE, 'Error rendering MegaMenu.', error);
this._renderStatus(strings.LoadError);
}
}
private _getOrCreateContainer(id: string, placeholder: PlaceholderContent): HTMLElement {
const container = document.getElementById(id);
if (container) {
let host: HTMLElement = document.getElementById('MegaMenuHost');
if (!host) {
host = document.createElement('div');
host.id = 'MegaMenuHost';
container.appendChild(host);
}
return host;
}
return placeholder.domElement;
}
private _loadExternalCss(cssUrl?: string, debug: boolean = this._isDebugEnabled()): void {
const externalCssLinkId: string = 'mega-menu-additional-css-34FAB720';
let link: HTMLLinkElement = document.getElementById(externalCssLinkId) as HTMLLinkElement;
if (cssUrl && cssUrl.trim() !== '') {
const validatedUrl: string | undefined = this._validateCssUrl(cssUrl);
if (!validatedUrl) {
debugWarn(debug, LOG_SOURCE, 'Rejected unsafe external CSS URL:', cssUrl);
if (link) {
link.remove();
}
return;
}
if (!link) {
const head: HTMLHeadElement = document.getElementsByTagName('head')[0];
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.id = externalCssLinkId;
link.onload = () => {
debugLog(debug, LOG_SOURCE, 'External CSS loaded successfully from:', cssUrl);
};
link.onerror = () => {
debugWarn(debug, LOG_SOURCE, 'Failed to load external CSS from:', cssUrl);
};
head.appendChild(link);
}
link.href = validatedUrl;
} else if (link) {
link.remove();
}
}
private _onDispose = (): void => {
this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders);
if (this._renderer) {
this._renderer.dispose();
this._renderer = undefined;
}
const externalCssLink: HTMLElement = document.getElementById('mega-menu-additional-css-34FAB720');
if (externalCssLink) {
externalCssLink.remove();
}
this._menuContainer = undefined;
this._topPlaceholder = undefined;
debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Disposed custom top placeholder.');
}
private _getTermSetIdentifier(): string {
const termSetId: string = this.properties && this.properties.termSetId
? this.properties.termSetId.trim()
: '';
if (termSetId && this._isGuid(termSetId)) {
return termSetId;
}
return this.properties && this.properties.termSetName
? this.properties.termSetName.trim()
: '';
}
private _getCacheMinutes(): number {
const value: number = this.properties ? Number(this.properties.cacheMinutes) : 15;
return isFinite(value) ? Math.min(Math.max(Math.floor(value), 1), 1440) : 15;
}
private _validateCssUrl(cssUrl: string): string | undefined {
try {
const parsed: URL = new URL(cssUrl.trim(), window.location.origin);
const isHttp: boolean = parsed.protocol === 'http:' || parsed.protocol === 'https:';
const isSameOrigin: boolean = parsed.origin === window.location.origin;
if (!isHttp || (!isSameOrigin && parsed.protocol !== 'https:')) {
return undefined;
}
return parsed.href;
} catch (error) {
return undefined;
}
}
private _renderStatus(message: string): void {
if (!this._topPlaceholder) {
return;
}
this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder);
const container: HTMLElement = this._menuContainer || this._topPlaceholder.domElement;
container.innerHTML = '';
const status: HTMLElement = document.createElement('div');
status.className = 'mega-menu-status';
status.setAttribute('role', 'status');
status.textContent = message;
container.appendChild(status);
}
private _isGuid(value: string): boolean {
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
}
private _isDebugEnabled(debugOverride?: boolean): boolean {
if (typeof debugOverride === 'boolean') {
return debugOverride;
}
return !!(this.properties && this.properties.debug === true);
}
}