242 lines
8.7 KiB
TypeScript
242 lines
8.7 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;
|
|
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> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|