// 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; debug?: boolean; } export default class MegaMenuApplicationCustomizer extends BaseApplicationCustomizer { private _topPlaceholder: PlaceholderContent | undefined; private _menuContainer: HTMLElement | undefined; private _renderer: MegaMenuRenderer | undefined; @override public onInit(): Promise { const debug: boolean = this._isDebugEnabled(); debugLog(debug, LOG_SOURCE, 'onInit started.', { componentId: UserCustomActionMegaMenuId, version: '1.1.3', properties: this.properties || {}, pageUrl: window.location.href, webUrl: this.context.pageContext.web.absoluteUrl, siteUrl: this.context.pageContext.site.absoluteUrl }); this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders); debugLog(debug, LOG_SOURCE, 'Placeholder changedEvent handler registered.'); this._renderPlaceHolders(); debugLog(debug, LOG_SOURCE, 'onInit completed. Asynchronous menu loading may still be running.'); return Promise.resolve(); } private _renderPlaceHolders(): void { const debug: boolean = this._isDebugEnabled(); const availablePlaceholders: string = this.context.placeholderProvider.placeholderNames .map(name => PlaceholderName[name]) .join(', '); debugLog(debug, LOG_SOURCE, 'Rendering placeholders.', { availablePlaceholders: availablePlaceholders, topPlaceholderAlreadyCreated: !!this._topPlaceholder }); if (!this._topPlaceholder) { debugLog(debug, LOG_SOURCE, 'Requesting the Top placeholder.'); this._topPlaceholder = this.context.placeholderProvider.tryCreateContent( PlaceholderName.Top, { onDispose: this._onDispose } ); if (!this._topPlaceholder) { debugError(debug, LOG_SOURCE, 'The expected placeholder (Top) was not found.'); return; } debugLog(debug, LOG_SOURCE, 'Top placeholder acquired.', { domElementId: this._topPlaceholder.domElement.id || '', domElementClassName: this._topPlaceholder.domElement.className || '' }); const termSetIdentifier: string = this._getTermSetIdentifier(); if (!termSetIdentifier) { debugWarn(debug, LOG_SOURCE, 'No termSetId or termSetName configured. Mega Menu rendering is skipped.', this.properties || {}); this._renderStatus(strings.ConfigurationMissing); return; } debugLog(debug, LOG_SOURCE, 'Starting MegaMenu rendering.', { termSetIdentifier: termSetIdentifier, cacheMinutes: this._getCacheMinutes() }); this._renderMegaMenu(termSetIdentifier, debug); } else { debugLog(debug, LOG_SOURCE, 'Top placeholder is already initialized; no duplicate render is started.'); } } private async _renderMegaMenu(termSetIdentifier: string, debug: boolean = this._isDebugEnabled()): Promise { if (!this._topPlaceholder) { return; } try { debugLog(debug, LOG_SOURCE, 'Creating TaxonomyNavigationService.'); const taxonomyService: TaxonomyNavigationService = new TaxonomyNavigationService( this.context, termSetIdentifier, debug, this._getCacheMinutes() ); debugLog(debug, LOG_SOURCE, 'Requesting navigation items from taxonomy.'); const menuItems: IMenuItem[] = await taxonomyService.getMenuItems(); debugLog(debug, LOG_SOURCE, 'Navigation items received.', { topLevelItemCount: menuItems.length }); if (this._renderer) { debugLog(debug, LOG_SOURCE, 'Disposing the previous renderer instance.'); this._renderer.dispose(); } this._renderer = new MegaMenuRenderer( menuItems, debug ); this._menuContainer = this._getOrCreateContainer('CustomHeader', this._topPlaceholder); if (this._menuContainer) { debugLog(debug, LOG_SOURCE, 'Rendering into the selected container.', { id: this._menuContainer.id || '', className: this._menuContainer.className || '' }); this._renderer.render(this._menuContainer); } else { debugWarn(debug, LOG_SOURCE, 'No dedicated container was returned; rendering into the Top placeholder.'); 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.', this._describeError(error), error); this._renderStatus(strings.LoadError); } } private _getOrCreateContainer(id: string, placeholder: PlaceholderContent): HTMLElement { const container = document.getElementById(id); if (container) { debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing host container found.', { requestedId: id, containerId: container.id, containerClassName: container.className || '' }); let host: HTMLElement = document.getElementById('MegaMenuHost'); if (!host) { host = document.createElement('div'); host.id = 'MegaMenuHost'; container.appendChild(host); debugLog(this._isDebugEnabled(), LOG_SOURCE, 'MegaMenuHost was created inside the existing host container.'); } else { debugLog(this._isDebugEnabled(), LOG_SOURCE, 'Existing MegaMenuHost will be reused.'); } return host; } debugLog(this._isDebugEnabled(), LOG_SOURCE, 'No external host container found; the Top placeholder will be used.', { requestedId: id }); return placeholder.domElement; } private _onDispose = (): void => { this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceHolders); if (this._renderer) { this._renderer.dispose(); this._renderer = undefined; } 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 _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 _describeError(error: any): { name: string; message: string; stack: string } { // tslint:disable-line:no-any return { name: error && error.name ? String(error.name) : '', message: error && error.message ? String(error.message) : String(error || 'Unknown error'), stack: error && error.stack ? String(error.stack) : '' }; } private _isDebugEnabled(debugOverride?: boolean): boolean { if (typeof debugOverride === 'boolean') { return debugOverride; } return !!(this.properties && this.properties.debug === true); } }