import { ICurrentNavigationItem, ICurrentNavigationTerm } from './CurrentNavigationTypes'; export const CurrentNavigationProperties = { root: 'CurrentNavigation.Root', hidden: 'CurrentNavigation.Hidden', simpleLinkUrl: '_Sys_Nav_SimpleLinkUrl', targetUrl: '_Sys_Nav_TargetUrl' }; export function getLocalProperty(term: ICurrentNavigationTerm, name: string): string { const properties: { [key: string]: any } = term && term.LocalCustomProperties || {}; // tslint:disable-line:no-any const value: any = properties[name]; // tslint:disable-line:no-any return value === undefined || value === null ? '' : String(value).trim(); } export function parseBoolean(value: any): boolean { // tslint:disable-line:no-any if (value === true || value === 1) { return true; } const normalized: string = String(value || '').trim().toLowerCase(); return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'ja'; } export function resolveTermUrl(term: ICurrentNavigationTerm, siteCollectionUrl: string): string | undefined { const raw: string = getLocalProperty(term, CurrentNavigationProperties.simpleLinkUrl) || getLocalProperty(term, CurrentNavigationProperties.targetUrl); if (!raw || /[\u0000-\u001f\u007f]/.test(raw)) { return undefined; } const resolved: string = raw.toLowerCase().indexOf('~sitecollection') === 0 ? String(siteCollectionUrl || '').replace(/\/+$/, '') + raw.substring('~sitecollection'.length) : raw; const protocol: RegExpMatchArray = resolved.match(/^([a-z][a-z0-9+.-]*):/i); if (protocol && protocol[1].toLowerCase() !== 'http' && protocol[1].toLowerCase() !== 'https') { return undefined; } return resolved; } export function normalizeNavigationPath(value: string): string { let path: string = String(value || '').trim().toLowerCase(); path = path.replace(/^https?:\/\/[^/]+/i, '').split('#')[0].split('?')[0].replace(/\/+$/, ''); try { path = decodeURIComponent(path); } catch (error) { /* Keep malformed URLs comparable as supplied. */ } return path || '/'; } export function normalizeRootWebPath(value: string): string { let path: string = normalizeNavigationPath(value); path = path.replace(/\/(?:sitepages|pages)\/.*\.aspx$/i, ''); path = path.replace(/\/(?:default|home)\.aspx$/i, ''); return path || '/'; } export function selectCurrentNavigationRoot( terms: ICurrentNavigationTerm[], currentWebUrl: string, siteCollectionUrl: string ): ICurrentNavigationTerm | undefined { const webPath: string = normalizeNavigationPath(currentWebUrl); const hierarchyRoot: ICurrentNavigationTerm = selectRootByTermHierarchy( terms, webPath, siteCollectionUrl ); if (hierarchyRoot) { return hierarchyRoot; } let selected: ICurrentNavigationTerm | undefined; let selectedScore: number = -1; for (let i: number = 0; i < terms.length; i++) { if (!parseBoolean(getLocalProperty(terms[i], CurrentNavigationProperties.root))) { continue; } const url: string = resolveTermUrl(terms[i], siteCollectionUrl); if (!url) { continue; } const candidatePath: string = normalizeRootWebPath(url); let score: number = -1; if (candidatePath === webPath) { score = 30000 + candidatePath.length; } else if (isDescendantPath(webPath, candidatePath)) { score = 20000 + candidatePath.length; } if (score > selectedScore) { selected = terms[i]; selectedScore = score; } } return selected; } function selectRootByTermHierarchy( terms: ICurrentNavigationTerm[], webPath: string, siteCollectionUrl: string ): ICurrentNavigationTerm | undefined { let selected: ICurrentNavigationTerm | undefined; let selectedDepth: number = -1; for (let i: number = 0; i < terms.length; i++) { const termUrl: string = resolveTermUrl(terms[i], siteCollectionUrl); if (!termUrl || normalizeRootWebPath(termUrl) !== webPath) { continue; } const currentPath: string = String(terms[i].PathOfTerm || '').toLowerCase(); for (let j: number = 0; j < terms.length; j++) { if (!parseBoolean(getLocalProperty(terms[j], CurrentNavigationProperties.root))) { continue; } const rootPath: string = String(terms[j].PathOfTerm || '').toLowerCase(); const isAncestor: boolean = currentPath === rootPath || currentPath.indexOf(rootPath + ';') === 0; const rootDepth: number = pathDepth(rootPath); if (isAncestor && rootDepth > selectedDepth) { selected = terms[j]; selectedDepth = rootDepth; } } } return selected; } function isDescendantPath(path: string, parentPath: string): boolean { return parentPath === '/' ? path !== '/' : path.indexOf(parentPath + '/') === 0; } export function buildCurrentNavigationItems( terms: ICurrentNavigationTerm[], root: ICurrentNavigationTerm, siteCollectionUrl: string, languageLcid: number, maxDepth: number, showRoot: boolean ): ICurrentNavigationItem[] { const rootPath: string = String(root.PathOfTerm || ''); const rootDepth: number = pathDepth(rootPath); const hiddenPaths: string[] = terms.filter((term: ICurrentNavigationTerm): boolean => parseBoolean(getLocalProperty(term, CurrentNavigationProperties.hidden)) ).map((term: ICurrentNavigationTerm): string => String(term.PathOfTerm || '').toLowerCase()); const candidates: ICurrentNavigationTerm[] = terms.filter((term: ICurrentNavigationTerm): boolean => { const path: string = String(term.PathOfTerm || ''); const isRoot: boolean = term.Id.toLowerCase() === root.Id.toLowerCase(); const isDescendant: boolean = path.toLowerCase().indexOf(rootPath.toLowerCase() + ';') === 0; if ((!showRoot && isRoot) || (!isRoot && !isDescendant)) { return false; } const relativeDepth: number = pathDepth(path) - rootDepth + (showRoot ? 1 : 0); if (relativeDepth < 1 || relativeDepth > maxDepth) { return false; } const lowerPath: string = path.toLowerCase(); for (let i: number = 0; i < hiddenPaths.length; i++) { if (lowerPath === hiddenPaths[i] || lowerPath.indexOf(hiddenPaths[i] + ';') === 0) { return false; } } return true; }); candidates.sort((a: ICurrentNavigationTerm, b: ICurrentNavigationTerm): number => { const depthDifference: number = pathDepth(a.PathOfTerm) - pathDepth(b.PathOfTerm); return depthDifference || compareTerms(a, b); }); const byPath: { [path: string]: ICurrentNavigationItem } = {}; const result: ICurrentNavigationItem[] = []; for (let i: number = 0; i < candidates.length; i++) { const term: ICurrentNavigationTerm = candidates[i]; const item: ICurrentNavigationItem = { id: cleanGuid(term.Id), label: localizedLabel(term, languageLcid), url: resolveTermUrl(term, siteCollectionUrl), items: [] }; byPath[String(term.PathOfTerm || '').toLowerCase()] = item; const parent: ICurrentNavigationItem = byPath[getParentPath(term.PathOfTerm).toLowerCase()]; if (parent) { parent.items.push(item); } else { result.push(item); } } return result; } export function cleanGuid(value: string): string { const match: RegExpMatchArray = String(value || '').match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); return match ? match[0] : String(value || ''); } function localizedLabel(term: ICurrentNavigationTerm, languageLcid: number): string { const labels = term.Labels && term.Labels._Child_Items_ ? term.Labels._Child_Items_ : []; let fallback: string = term.Name || ''; for (let i: number = 0; i < labels.length; i++) { if (Number(labels[i].Language) === languageLcid && labels[i].Value) { return labels[i].Value; } if (labels[i].IsDefaultForLanguage && labels[i].Value) { fallback = labels[i].Value; } } return fallback; } function pathDepth(path: string): number { return path ? path.split(';').length : 1; } function getParentPath(path: string): string { const parts: string[] = String(path || '').split(';'); parts.pop(); return parts.join(';'); } function compareTerms(a: ICurrentNavigationTerm, b: ICurrentNavigationTerm): number { const ai: number = typeof a.CustomSortOrderIndex === 'number' ? a.CustomSortOrderIndex : -1; const bi: number = typeof b.CustomSortOrderIndex === 'number' ? b.CustomSortOrderIndex : -1; if (ai >= 0 || bi >= 0) { if (ai < 0) { return 1; } if (bi < 0) { return -1; } return ai - bi; } const ap: string = String(a.PathOfTerm || '').toLowerCase(); const bp: string = String(b.PathOfTerm || '').toLowerCase(); return ap < bp ? -1 : (ap > bp ? 1 : 0); }