Update Current Navigation to version 1.1.4 with new features and improvements
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# CurrentNavigation
|
# 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
|
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.
|
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.
|
- 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
|
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
|
Homepage unterhalb der Web-URL werden erkannt. Der nächstgelegene Root-Term gilt außerdem für alle darunterliegenden
|
||||||
Kinder bilden die erste Navigationsebene.
|
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,
|
Der Treeview unterstützt auf- und zuklappbare Zweige, aktive Seitenpfade sowie die Navigation mit den Pfeiltasten,
|
||||||
Pos1 und Ende.
|
Pos1 und Ende.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CurrentNavigation – Umsetzungsstand
|
# CurrentNavigation – Umsetzungsstand
|
||||||
|
|
||||||
Zielversion: 1.1.3
|
Zielversion: 1.1.4
|
||||||
Status: Implementierung und lokale Tests abgeschlossen; Ship-Build auf Node 8 ausstehend
|
Status: Implementierung und lokale Tests abgeschlossen; Ship-Build auf Node 8 ausstehend
|
||||||
|
|
||||||
- [x] SPFx Application Customizer für moderne Seiten
|
- [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] Cache-Dauer, Cache-Version und Debug-Modus
|
||||||
- [x] CurrentNavigation.Root und CurrentNavigation.Hidden
|
- [x] CurrentNavigation.Root und CurrentNavigation.Hidden
|
||||||
- [x] URL-basierte Zuordnung zum aktuellen Web
|
- [x] URL-basierte Zuordnung zum aktuellen Web
|
||||||
|
- [x] Vererbung des nächstgelegenen CurrentNavigation.Root auf darunterliegende Webs
|
||||||
- [x] Begrenzbare Navigationstiefe
|
- [x] Begrenzbare Navigationstiefe
|
||||||
- [x] Sicherer DOM-Renderer ohne innerHTML
|
- [x] Sicherer DOM-Renderer ohne innerHTML
|
||||||
- [x] Session-Cache mit Stale-Fallback
|
- [x] Session-Cache mit Stale-Fallback
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"solution": {
|
"solution": {
|
||||||
"name": "current-navigation-client-side-solution",
|
"name": "current-navigation-client-side-solution",
|
||||||
"id": "bb8f9af5-b318-4111-8401-ce189cfe1b1a",
|
"id": "bb8f9af5-b318-4111-8401-ce189cfe1b1a",
|
||||||
"version": "1.1.3.0",
|
"version": "1.1.4.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": true
|
"skipFeatureDeployment": true
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "current-navigation",
|
"name": "current-navigation",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "current-navigation",
|
"name": "current-navigation",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
:global {
|
:global {
|
||||||
#CurrentNavigationHost {
|
#CurrentNavigationHost {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid "[theme: neutralLight, default: #edebe9]";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation {
|
.current-navigation {
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ export function resolveTermUrl(term: ICurrentNavigationTerm, siteCollectionUrl:
|
|||||||
export function normalizeNavigationPath(value: string): string {
|
export function normalizeNavigationPath(value: string): string {
|
||||||
let path: string = String(value || '').trim().toLowerCase();
|
let path: string = String(value || '').trim().toLowerCase();
|
||||||
path = path.replace(/^https?:\/\/[^/]+/i, '').split('#')[0].split('?')[0].replace(/\/+$/, '');
|
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 || '/';
|
return path || '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,26 +53,58 @@ export function selectCurrentNavigationRoot(
|
|||||||
siteCollectionUrl: string
|
siteCollectionUrl: string
|
||||||
): ICurrentNavigationTerm | undefined {
|
): ICurrentNavigationTerm | undefined {
|
||||||
const webPath: string = normalizeNavigationPath(currentWebUrl);
|
const webPath: string = normalizeNavigationPath(currentWebUrl);
|
||||||
|
const hierarchyRoot: ICurrentNavigationTerm = selectRootByTermHierarchy(
|
||||||
|
terms, webPath, siteCollectionUrl
|
||||||
|
);
|
||||||
|
if (hierarchyRoot) { return hierarchyRoot; }
|
||||||
|
|
||||||
let selected: ICurrentNavigationTerm | undefined;
|
let selected: ICurrentNavigationTerm | undefined;
|
||||||
let selectedScore: number = -1;
|
let selectedScore: number = -1;
|
||||||
for (let i: number = 0; i < terms.length; i++) {
|
for (let i: number = 0; i < terms.length; i++) {
|
||||||
if (!parseBoolean(getLocalProperty(terms[i], CurrentNavigationProperties.root))) { continue; }
|
if (!parseBoolean(getLocalProperty(terms[i], CurrentNavigationProperties.root))) { continue; }
|
||||||
const url: string = resolveTermUrl(terms[i], siteCollectionUrl);
|
const url: string = resolveTermUrl(terms[i], siteCollectionUrl);
|
||||||
if (!url) { continue; }
|
if (!url) { continue; }
|
||||||
const candidatePath: string = normalizeNavigationPath(url);
|
const candidatePath: string = normalizeRootWebPath(url);
|
||||||
let score: number = -1;
|
let score: number = -1;
|
||||||
if (candidatePath === webPath) {
|
if (candidatePath === webPath) {
|
||||||
score = 30000 + candidatePath.length;
|
score = 30000 + candidatePath.length;
|
||||||
} else if (candidatePath.indexOf(webPath + '/') === 0) {
|
} else if (isDescendantPath(webPath, candidatePath)) {
|
||||||
score = 20000 + webPath.length;
|
score = 20000 + candidatePath.length;
|
||||||
} else if (webPath.indexOf(candidatePath + '/') === 0) {
|
|
||||||
score = 10000 + candidatePath.length;
|
|
||||||
}
|
}
|
||||||
if (score > selectedScore) { selected = terms[i]; selectedScore = score; }
|
if (score > selectedScore) { selected = terms[i]; selectedScore = score; }
|
||||||
}
|
}
|
||||||
return selected;
|
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(
|
export function buildCurrentNavigationItems(
|
||||||
terms: ICurrentNavigationTerm[],
|
terms: ICurrentNavigationTerm[],
|
||||||
root: ICurrentNavigationTerm,
|
root: ICurrentNavigationTerm,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
buildCurrentNavigationItems, getLocalProperty, parseBoolean, resolveTermUrl, selectCurrentNavigationRoot
|
buildCurrentNavigationItems, getLocalProperty, normalizeRootWebPath, parseBoolean, resolveTermUrl,
|
||||||
|
selectCurrentNavigationRoot
|
||||||
} from '../src/services/CurrentNavigationCore';
|
} from '../src/services/CurrentNavigationCore';
|
||||||
import { ICurrentNavigationTerm } from '../src/services/CurrentNavigationTypes';
|
import { ICurrentNavigationTerm } from '../src/services/CurrentNavigationTypes';
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ const root: ICurrentNavigationTerm = selectCurrentNavigationRoot(
|
|||||||
terms, 'https://portal/sites/portal/hr', 'https://portal/sites/portal'
|
terms, 'https://portal/sites/portal/hr', 'https://portal/sites/portal'
|
||||||
);
|
);
|
||||||
assert('root is selected through its site home URL', !!root && root.Id === 'root');
|
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);
|
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('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');
|
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',
|
assert('hierarchy is independent of taxonomy response order',
|
||||||
unordered.length === 1 && unordered[0].id === 'docs' && unordered[0].items[0].id === 'forms');
|
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);
|
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 app = read('src/extensions/currentNavigation/CurrentNavigationApplicationCustomizer.ts');
|
||||||
var renderer = read('src/extensions/currentNavigation/CurrentNavigationRenderer.ts');
|
var renderer = read('src/extensions/currentNavigation/CurrentNavigationRenderer.ts');
|
||||||
var service = read('src/services/CurrentNavigationTaxonomyService.ts');
|
var service = read('src/services/CurrentNavigationTaxonomyService.ts');
|
||||||
assert('package version', packageJson.version === '1.1.3');
|
assert('package version', packageJson.version === '1.1.4');
|
||||||
assert('solution version', solution.version === '1.1.3.0');
|
assert('solution version', solution.version === '1.1.4.0');
|
||||||
assert('tenant deployment', solution.skipFeatureDeployment === true);
|
assert('tenant deployment', solution.skipFeatureDeployment === true);
|
||||||
assert('component type', manifest.extensionType === 'ApplicationCustomizer');
|
assert('component type', manifest.extensionType === 'ApplicationCustomizer');
|
||||||
assert('safe renderer', renderer.indexOf('innerHTML') < 0 && app.indexOf('innerHTML') < 0);
|
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);
|
app.indexOf("querySelector('.ms-Nav-navItems')") >= 0);
|
||||||
assert('native navigation is not hidden', app.indexOf('current-navigation-managed') < 0 &&
|
assert('native navigation is not hidden', app.indexOf('current-navigation-managed') < 0 &&
|
||||||
read('src/extensions/currentNavigation/CurrentNavigation.module.scss').indexOf(':not(#CurrentNavigationHost)') < 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 &&
|
assert('whole row hover',
|
||||||
read('src/extensions/currentNavigation/CurrentNavigation.module.scss').indexOf('default: #edebe9') >= 0);
|
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('custom navigation is appended below SiteNavigation', app.indexOf('this.siteNavigation.appendChild(this.host)') >= 0);
|
||||||
assert('observer cleanup', app.indexOf('observer.disconnect()') >= 0);
|
assert('observer cleanup', app.indexOf('observer.disconnect()') >= 0);
|
||||||
assert('treeview semantics', renderer.indexOf("setAttribute('role', root ? 'tree' : 'group')") >= 0 &&
|
assert('treeview semantics', renderer.indexOf("setAttribute('role', root ? 'tree' : 'group')") >= 0 &&
|
||||||
|
|||||||
Reference in New Issue
Block a user