// 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 { private _topPlaceholder: PlaceholderContent | undefined; private _menuContainer: HTMLElement | undefined; private _renderer: MegaMenuRenderer | undefined; @override public onInit(): Promise { 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 { 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); } }