Add Current Navigation Application Customizer
- Created a new SPFx Application Customizer for current navigation. - Implemented core functionality to fetch and render navigation items from a SharePoint term set. - Added styles for the navigation component using SCSS. - Included localization support for English and German languages. - Developed unit tests for core navigation functionalities. - Configured TypeScript and TSLint settings for the project. - Added validation scripts for static assets and package configuration.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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(/\/+$/, '');
|
||||
return path || '/';
|
||||
}
|
||||
|
||||
export function selectCurrentNavigationRoot(
|
||||
terms: ICurrentNavigationTerm[],
|
||||
currentWebUrl: string,
|
||||
siteCollectionUrl: string
|
||||
): ICurrentNavigationTerm | undefined {
|
||||
const webPath: string = normalizeNavigationPath(currentWebUrl);
|
||||
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 = normalizeNavigationPath(url);
|
||||
let score: number = -1;
|
||||
if (candidatePath === webPath) {
|
||||
score = 30000 + candidatePath.length;
|
||||
} else if (candidatePath.indexOf(webPath + '/') === 0) {
|
||||
score = 20000 + webPath.length;
|
||||
} else if (webPath.indexOf(candidatePath + '/') === 0) {
|
||||
score = 10000 + candidatePath.length;
|
||||
}
|
||||
if (score > selectedScore) { selected = terms[i]; selectedScore = score; }
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user