Update Current Navigation to version 1.1.4 with new features and improvements

This commit is contained in:
Torsten Brendgen
2026-08-18 21:18:11 +02:00
parent 9b73bb4614
commit 61a968562d
9 changed files with 100 additions and 18 deletions
+6 -3
View File
@@ -1,6 +1,6 @@
# CurrentNavigation
CurrentNavigation 1.1.3 ist ein SPFx-1.4.1-Application-Customizer für moderne Seiten in SharePoint Server
CurrentNavigation 1.1.4 ist ein SPFx-1.4.1-Application-Customizer für moderne Seiten in SharePoint Server
Subscription Edition. Die Solution ergänzt unterhalb der vorhandenen Einträge im modernen SiteNavigation-Bereich
einen Treeview für das aktuelle Web. Native Einträge wie Documents bleiben unverändert sichtbar.
@@ -12,8 +12,11 @@ Die Steuerung erfolgt über Local Custom Properties:
- CurrentNavigation.Hidden = true: Dieser Term und sein kompletter Unterbaum werden ausgeblendet.
Die URL des Root-Terms wird mit pageContext.web.absoluteUrl verglichen. Sowohl die Web-URL selbst als auch eine
Homepage unterhalb der Web-URL werden erkannt. Der Root-Term wird standardmäßig nicht dargestellt; seine direkten
Kinder bilden die erste Navigationsebene.
Homepage unterhalb der Web-URL werden erkannt. Der nächstgelegene Root-Term gilt außerdem für alle darunterliegenden
Webs. Der Root-Term wird standardmäßig nicht dargestellt; seine direkten Kinder bilden die erste Navigationsebene.
`TopNavigation.Hidden` beeinflusst die Current Navigation nicht. Ausschließlich `CurrentNavigation.Hidden` blendet
einen Term und seinen Unterbaum aus.
Der Treeview unterstützt auf- und zuklappbare Zweige, aktive Seitenpfade sowie die Navigation mit den Pfeiltasten,
Pos1 und Ende.
+2 -1
View File
@@ -1,6 +1,6 @@
# CurrentNavigation – Umsetzungsstand
Zielversion: 1.1.3
Zielversion: 1.1.4
Status: Implementierung und lokale Tests abgeschlossen; Ship-Build auf Node 8 ausstehend
- [x] SPFx Application Customizer für moderne Seiten
@@ -11,6 +11,7 @@ Status: Implementierung und lokale Tests abgeschlossen; Ship-Build auf Node 8 au
- [x] Cache-Dauer, Cache-Version und Debug-Modus
- [x] CurrentNavigation.Root und CurrentNavigation.Hidden
- [x] URL-basierte Zuordnung zum aktuellen Web
- [x] Vererbung des nächstgelegenen CurrentNavigation.Root auf darunterliegende Webs
- [x] Begrenzbare Navigationstiefe
- [x] Sicherer DOM-Renderer ohne innerHTML
- [x] Session-Cache mit Stale-Fallback
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": {
"name": "current-navigation-client-side-solution",
"id": "bb8f9af5-b318-4111-8401-ce189cfe1b1a",
"version": "1.1.3.0",
"version": "1.1.4.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "current-navigation",
"version": "1.1.3",
"version": "1.1.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "current-navigation",
"version": "1.1.3",
"version": "1.1.4",
"private": true,
"main": "lib/index.js",
"engines": {
@@ -3,7 +3,6 @@
:global {
#CurrentNavigationHost {
width: 100%;
border-top: 1px solid "[theme: neutralLight, default: #edebe9]";
}
.current-navigation {
+45 -5
View File
@@ -36,6 +36,14 @@ export function resolveTermUrl(term: ICurrentNavigationTerm, siteCollectionUrl:
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 || '/';
}
@@ -45,26 +53,58 @@ export function selectCurrentNavigationRoot(
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 = normalizeNavigationPath(url);
const candidatePath: string = normalizeRootWebPath(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;
} 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,
+40 -1
View File
@@ -1,5 +1,6 @@
import {
buildCurrentNavigationItems, getLocalProperty, parseBoolean, resolveTermUrl, selectCurrentNavigationRoot
buildCurrentNavigationItems, getLocalProperty, normalizeRootWebPath, parseBoolean, resolveTermUrl,
selectCurrentNavigationRoot
} from '../src/services/CurrentNavigationCore';
import { ICurrentNavigationTerm } from '../src/services/CurrentNavigationTypes';
@@ -33,6 +34,8 @@ const root: ICurrentNavigationTerm = selectCurrentNavigationRoot(
terms, 'https://portal/sites/portal/hr', 'https://portal/sites/portal'
);
assert('root is selected through its site home URL', !!root && root.Id === 'root');
assert('site home URL is normalized to its web',
normalizeRootWebPath('/sites/portal/hr/SitePages/Home.aspx') === '/sites/portal/hr');
const items = buildCurrentNavigationItems(terms, root, 'https://portal/sites/portal', 1031, 3, false);
assert('root is not rendered by default', items.length === 1 && items[0].id === 'docs');
assert('descendants are nested', items[0].items.length === 1 && items[0].items[0].id === 'forms');
@@ -44,4 +47,40 @@ const unordered = buildCurrentNavigationItems(
);
assert('hierarchy is independent of taxonomy response order',
unordered.length === 1 && unordered[0].id === 'docs' && unordered[0].items[0].id === 'forms');
const inheritedTerms: ICurrentNavigationTerm[] = [
term({ Id: 'kits', Name: 'KITS', PathOfTerm: 'Organisation;CIR;KITS', LocalCustomProperties: {
'CurrentNavigation.Root': 'true',
'_Sys_Nav_SimpleLinkUrl': 'http://dhs.contoso.local/organisation/cir/CIR_3/'
}}),
term({ Id: 'team', Name: 'Team SharePoint', PathOfTerm: 'Organisation;CIR;KITS;Team SharePoint',
LocalCustomProperties: {
'TopNavigation.Hidden': 'true',
'_Sys_Nav_SimpleLinkUrl': 'http://dhs.contoso.local/organisation/cir/CIR_13/'
}}),
term({ Id: 'games', Name: 'Spielewiese', PathOfTerm: 'Organisation;CIR;KITS;Team SharePoint;Spielewiese',
LocalCustomProperties: {
'_Sys_Nav_SimpleLinkUrl': 'http://dhs.contoso.local/organisation/cir/CIR_21/'
}})
];
const inheritedRoot: ICurrentNavigationTerm = selectCurrentNavigationRoot(
inheritedTerms,
'http://dhs.contoso.local/organisation/cir/CIR_21/',
'http://dhs.contoso.local/organisation/cir'
);
assert('nearest taxonomy ancestor root applies to a web with a generic URL',
!!inheritedRoot && inheritedRoot.Id === 'kits');
const teamRoot: ICurrentNavigationTerm = selectCurrentNavigationRoot(
inheritedTerms,
'http://dhs.contoso.local/organisation/cir/CIR_13/',
'http://dhs.contoso.local/organisation/cir'
);
assert('taxonomy ancestor root also applies to the direct child web',
!!teamRoot && teamRoot.Id === 'kits');
const inheritedItems = buildCurrentNavigationItems(
inheritedTerms, inheritedRoot, 'http://dhs.contoso.local/organisation/cir', 1031, 3, false
);
assert('top-navigation hidden does not hide current navigation terms',
inheritedItems.length === 1 && inheritedItems[0].id === 'team' &&
inheritedItems[0].items.length === 1 && inheritedItems[0].items[0].id === 'games');
console.log('CurrentNavigation core tests passed:', passed);
+4 -4
View File
@@ -9,8 +9,8 @@ var manifest = json('src/extensions/currentNavigation/CurrentNavigationApplicati
var app = read('src/extensions/currentNavigation/CurrentNavigationApplicationCustomizer.ts');
var renderer = read('src/extensions/currentNavigation/CurrentNavigationRenderer.ts');
var service = read('src/services/CurrentNavigationTaxonomyService.ts');
assert('package version', packageJson.version === '1.1.3');
assert('solution version', solution.version === '1.1.3.0');
assert('package version', packageJson.version === '1.1.4');
assert('solution version', solution.version === '1.1.4.0');
assert('tenant deployment', solution.skipFeatureDeployment === true);
assert('component type', manifest.extensionType === 'ApplicationCustomizer');
assert('safe renderer', renderer.indexOf('innerHTML') < 0 && app.indexOf('innerHTML') < 0);
@@ -20,8 +20,8 @@ assert('targets SharePoint ms-Nav fallback', app.indexOf('nav[role="navigation"]
app.indexOf("querySelector('.ms-Nav-navItems')") >= 0);
assert('native navigation is not hidden', app.indexOf('current-navigation-managed') < 0 &&
read('src/extensions/currentNavigation/CurrentNavigation.module.scss').indexOf(':not(#CurrentNavigationHost)') < 0);
assert('row hover and neutral border', read('src/extensions/currentNavigation/CurrentNavigation.module.scss').indexOf('.current-navigation-row:hover') >= 0 &&
read('src/extensions/currentNavigation/CurrentNavigation.module.scss').indexOf('default: #edebe9') >= 0);
assert('whole row hover',
read('src/extensions/currentNavigation/CurrentNavigation.module.scss').indexOf('.current-navigation-row:hover') >= 0);
assert('custom navigation is appended below SiteNavigation', app.indexOf('this.siteNavigation.appendChild(this.host)') >= 0);
assert('observer cleanup', app.indexOf('observer.disconnect()') >= 0);
assert('treeview semantics', renderer.indexOf("setAttribute('role', root ? 'tree' : 'group')") >= 0 &&