Update Current Navigation to version 1.1.4 with new features and improvements
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "current-navigation",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user