2 Commits
13 changed files with 87 additions and 19 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
# CurrentNavigation # CurrentNavigation
CurrentNavigation 1.1.4 ist ein SPFx-1.4.1-Application-Customizer für moderne Seiten in SharePoint Server CurrentNavigation 1.1.6 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.
@@ -21,6 +21,11 @@ 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.
Der initiale Aufklappzustand ist über `initialExpansion` steuerbar: `activePath` öffnet wie bisher nur den aktiven
Pfad, `expanded` öffnet alle Zweige und `collapsed` startet mit vollständig geschlossenen Zweigen.
Für `activePath` wird die Term-URL sowohl mit der aktuellen Seite als auch mit der aktuellen SharePoint-Web-URL
verglichen. Dadurch wird der Pfad auch auf modernen `SitePages` und in Listen oder Bibliotheken korrekt geöffnet.
Wenn kein passender Root-Term gefunden wird oder das Termset nicht geladen werden kann, bleibt ausschließlich die Wenn kein passender Root-Term gefunden wird oder das Termset nicht geladen werden kann, bleibt ausschließlich die
native SharePoint-Sitenavigation sichtbar. native SharePoint-Sitenavigation sichtbar.
+2 -1
View File
@@ -1,11 +1,12 @@
# CurrentNavigation – Umsetzungsstand # CurrentNavigation – Umsetzungsstand
Zielversion: 1.1.4 Zielversion: 1.1.6
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
- [x] Ergänzendes Rendering unterhalb der nativen Einträge im modernen SiteNavigation-Container - [x] Ergänzendes Rendering unterhalb der nativen Einträge im modernen SiteNavigation-Container
- [x] Barrierearmer Treeview mit Tastatursteuerung - [x] Barrierearmer Treeview mit Tastatursteuerung
- [x] `activePath` über aktuelle Seiten- und Web-URL zuverlässig erkennen
- [x] Site-Collection-weite Registrierung - [x] Site-Collection-weite Registrierung
- [x] Termset-Picker über PortalSettings - [x] Termset-Picker über PortalSettings
- [x] Cache-Dauer, Cache-Version und Debug-Modus - [x] Cache-Dauer, Cache-Version und Debug-Modus
+1 -1
View File
@@ -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.4.0", "version": "1.1.6.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+1 -1
View File
@@ -11,7 +11,7 @@
"properties": { "properties": {
"termSetId": "00000000-0000-0000-0000-000000000000", "termSetId": "00000000-0000-0000-0000-000000000000",
"cacheMinutes": 15, "cacheVersion": "1", "maxDepth": 3, "cacheMinutes": 15, "cacheVersion": "1", "maxDepth": 3,
"showRoot": false, "debug": true "showRoot": false, "initialExpansion": "activePath", "debug": true
} }
} }
} }
+2
View File
@@ -5,6 +5,7 @@ param(
[string]$CacheVersion = '1', [string]$CacheVersion = '1',
[ValidateRange(1, 8)][int]$MaxDepth = 3, [ValidateRange(1, 8)][int]$MaxDepth = 3,
[switch]$ShowRoot, [switch]$ShowRoot,
[ValidateSet('activePath', 'expanded', 'collapsed')][string]$InitialExpansion = 'activePath',
[switch]$EnableDebug [switch]$EnableDebug
) )
@@ -28,6 +29,7 @@ $properties = @{
cacheVersion = $(if ([string]::IsNullOrWhiteSpace($CacheVersion)) { '1' } else { $CacheVersion.Trim() }) cacheVersion = $(if ([string]::IsNullOrWhiteSpace($CacheVersion)) { '1' } else { $CacheVersion.Trim() })
maxDepth = $MaxDepth maxDepth = $MaxDepth
showRoot = [bool]$ShowRoot.IsPresent showRoot = [bool]$ShowRoot.IsPresent
initialExpansion = $InitialExpansion
debug = [bool]$EnableDebug.IsPresent debug = [bool]$EnableDebug.IsPresent
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "current-navigation", "name": "current-navigation",
"version": "1.1.4", "version": "1.1.6",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "current-navigation", "name": "current-navigation",
"version": "1.1.4", "version": "1.1.6",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -5,7 +5,7 @@ import * as strings from 'CurrentNavigationApplicationCustomizerStrings';
import { buildCurrentNavigationItems, selectCurrentNavigationRoot } from '../../services/CurrentNavigationCore'; import { buildCurrentNavigationItems, selectCurrentNavigationRoot } from '../../services/CurrentNavigationCore';
import { CurrentNavigationTaxonomyService } from '../../services/CurrentNavigationTaxonomyService'; import { CurrentNavigationTaxonomyService } from '../../services/CurrentNavigationTaxonomyService';
import { ICurrentNavigationItem, ICurrentNavigationTerm, ITermSetResult } from '../../services/CurrentNavigationTypes'; import { ICurrentNavigationItem, ICurrentNavigationTerm, ITermSetResult } from '../../services/CurrentNavigationTypes';
import { CurrentNavigationRenderer } from './CurrentNavigationRenderer'; import { CurrentNavigationInitialExpansion, CurrentNavigationRenderer } from './CurrentNavigationRenderer';
import './CurrentNavigation.module.scss'; import './CurrentNavigation.module.scss';
export interface ICurrentNavigationApplicationCustomizerProperties { export interface ICurrentNavigationApplicationCustomizerProperties {
@@ -15,6 +15,7 @@ export interface ICurrentNavigationApplicationCustomizerProperties {
cacheVersion?: string; cacheVersion?: string;
maxDepth?: number; maxDepth?: number;
showRoot?: boolean; showRoot?: boolean;
initialExpansion?: CurrentNavigationInitialExpansion;
debug?: boolean; debug?: boolean;
} }
@@ -123,7 +124,7 @@ export default class CurrentNavigationApplicationCustomizer
navigationLabel: strings.NavigationLabel, navigationLabel: strings.NavigationLabel,
expand: strings.Expand, expand: strings.Expand,
collapse: strings.Collapse collapse: strings.Collapse
}); }, this.initialExpansion(), this.context.pageContext.web.absoluteUrl);
} }
} catch (error) { this.debug(strings.LoadError, error); } } catch (error) { this.debug(strings.LoadError, error); }
} }
@@ -141,6 +142,11 @@ export default class CurrentNavigationApplicationCustomizer
? String(this.properties.cacheVersion).trim() : '1'; ? String(this.properties.cacheVersion).trim() : '1';
return value || '1'; return value || '1';
} }
private initialExpansion(): CurrentNavigationInitialExpansion {
const value: string = this.properties && this.properties.initialExpansion
? String(this.properties.initialExpansion) : 'activePath';
return value === 'expanded' || value === 'collapsed' ? value : 'activePath';
}
private languageLcid(): number { private languageLcid(): number {
const legacy: any = (this.context.pageContext as any).legacyPageContext; // tslint:disable-line:no-any const legacy: any = (this.context.pageContext as any).legacyPageContext; // tslint:disable-line:no-any
const value: number = legacy ? Number(legacy.currentLanguage) : 0; const value: number = legacy ? Number(legacy.currentLanguage) : 0;
@@ -1,4 +1,4 @@
import { normalizeNavigationPath } from '../../services/CurrentNavigationCore'; import { isCurrentNavigationUrl } from '../../services/CurrentNavigationCore';
import { ICurrentNavigationItem } from '../../services/CurrentNavigationTypes'; import { ICurrentNavigationItem } from '../../services/CurrentNavigationTypes';
export interface ICurrentNavigationRendererStrings { export interface ICurrentNavigationRendererStrings {
@@ -7,19 +7,35 @@ export interface ICurrentNavigationRendererStrings {
collapse: string; collapse: string;
} }
export type CurrentNavigationInitialExpansion = 'activePath' | 'expanded' | 'collapsed';
export function isInitiallyExpanded(
initialExpansion: CurrentNavigationInitialExpansion,
containsCurrentItem: boolean
): boolean {
return initialExpansion === 'expanded' ||
(initialExpansion === 'activePath' && containsCurrentItem);
}
export class CurrentNavigationRenderer { export class CurrentNavigationRenderer {
private container: HTMLElement | undefined; private container: HTMLElement | undefined;
private keydownHandler: ((event: KeyboardEvent) => void) | undefined; private keydownHandler: ((event: KeyboardEvent) => void) | undefined;
private strings: ICurrentNavigationRendererStrings; private strings: ICurrentNavigationRendererStrings;
private initialExpansion: CurrentNavigationInitialExpansion = 'activePath';
private currentWebUrl: string = '';
public render( public render(
container: HTMLElement, container: HTMLElement,
items: ICurrentNavigationItem[], items: ICurrentNavigationItem[],
strings: ICurrentNavigationRendererStrings strings: ICurrentNavigationRendererStrings,
initialExpansion: CurrentNavigationInitialExpansion,
currentWebUrl?: string
): void { ): void {
this.clear(container); this.clear(container);
this.container = container; this.container = container;
this.strings = strings; this.strings = strings;
this.initialExpansion = initialExpansion;
this.currentWebUrl = currentWebUrl || window.location.href;
const nav: HTMLElement = document.createElement('nav'); const nav: HTMLElement = document.createElement('nav');
nav.className = 'current-navigation'; nav.className = 'current-navigation';
nav.setAttribute('aria-label', strings.navigationLabel); nav.setAttribute('aria-label', strings.navigationLabel);
@@ -52,7 +68,7 @@ export class CurrentNavigationRenderer {
private createItem(item: ICurrentNavigationItem, depth: number): HTMLLIElement { private createItem(item: ICurrentNavigationItem, depth: number): HTMLLIElement {
const entry: HTMLLIElement = document.createElement('li'); const entry: HTMLLIElement = document.createElement('li');
const hasChildren: boolean = item.items.length > 0; const hasChildren: boolean = item.items.length > 0;
const activeBranch: boolean = this.containsCurrentItem(item); const expanded: boolean = isInitiallyExpanded(this.initialExpansion, this.containsCurrentItem(item));
entry.className = hasChildren ? 'current-navigation-item has-children' : 'current-navigation-item'; entry.className = hasChildren ? 'current-navigation-item has-children' : 'current-navigation-item';
entry.setAttribute('role', 'treeitem'); entry.setAttribute('role', 'treeitem');
entry.setAttribute('aria-level', String(depth)); entry.setAttribute('aria-level', String(depth));
@@ -64,8 +80,8 @@ export class CurrentNavigationRenderer {
toggle.type = 'button'; toggle.type = 'button';
toggle.className = 'current-navigation-toggle'; toggle.className = 'current-navigation-toggle';
toggle.tabIndex = -1; toggle.tabIndex = -1;
toggle.setAttribute('aria-expanded', activeBranch ? 'true' : 'false'); toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
toggle.setAttribute('aria-label', (activeBranch ? this.strings.collapse : this.strings.expand) + ' ' + item.label); toggle.setAttribute('aria-label', (expanded ? this.strings.collapse : this.strings.expand) + ' ' + item.label);
const chevron: HTMLSpanElement = document.createElement('span'); const chevron: HTMLSpanElement = document.createElement('span');
chevron.className = 'current-navigation-chevron'; chevron.className = 'current-navigation-chevron';
chevron.setAttribute('aria-hidden', 'true'); chevron.setAttribute('aria-hidden', 'true');
@@ -74,7 +90,7 @@ export class CurrentNavigationRenderer {
this.setExpanded(entry, entry.getAttribute('aria-expanded') !== 'true', item.label); this.setExpanded(entry, entry.getAttribute('aria-expanded') !== 'true', item.label);
}); });
row.appendChild(toggle); row.appendChild(toggle);
entry.setAttribute('aria-expanded', activeBranch ? 'true' : 'false'); entry.setAttribute('aria-expanded', expanded ? 'true' : 'false');
} else { } else {
const spacer: HTMLSpanElement = document.createElement('span'); const spacer: HTMLSpanElement = document.createElement('span');
spacer.className = 'current-navigation-toggle-spacer'; spacer.className = 'current-navigation-toggle-spacer';
@@ -99,7 +115,7 @@ export class CurrentNavigationRenderer {
entry.appendChild(row); entry.appendChild(row);
if (hasChildren) { if (hasChildren) {
const children: HTMLUListElement = this.createList(item.items, depth + 1, false); const children: HTMLUListElement = this.createList(item.items, depth + 1, false);
children.hidden = !activeBranch; children.hidden = !expanded;
entry.appendChild(children); entry.appendChild(children);
} }
return entry; return entry;
@@ -195,6 +211,6 @@ export class CurrentNavigationRenderer {
return false; return false;
} }
private isCurrentUrl(url: string): boolean { private isCurrentUrl(url: string): boolean {
return normalizeNavigationPath(window.location.href) === normalizeNavigationPath(url); return isCurrentNavigationUrl(url, window.location.href, this.currentWebUrl);
} }
} }
+12
View File
@@ -47,6 +47,18 @@ export function normalizeRootWebPath(value: string): string {
return path || '/'; return path || '/';
} }
export function isCurrentNavigationUrl(
navigationUrl: string,
currentPageUrl: string,
currentWebUrl: string
): boolean {
const navigationPath: string = normalizeNavigationPath(navigationUrl);
const pagePath: string = normalizeNavigationPath(currentPageUrl);
const webPath: string = normalizeNavigationPath(currentWebUrl);
return navigationPath === pagePath || navigationPath === webPath ||
normalizeRootWebPath(navigationUrl) === webPath;
}
export function selectCurrentNavigationRoot( export function selectCurrentNavigationRoot(
terms: ICurrentNavigationTerm[], terms: ICurrentNavigationTerm[],
currentWebUrl: string, currentWebUrl: string,
+21 -1
View File
@@ -1,8 +1,9 @@
import { import {
buildCurrentNavigationItems, getLocalProperty, normalizeRootWebPath, parseBoolean, resolveTermUrl, buildCurrentNavigationItems, getLocalProperty, isCurrentNavigationUrl, normalizeRootWebPath, parseBoolean, resolveTermUrl,
selectCurrentNavigationRoot selectCurrentNavigationRoot
} from '../src/services/CurrentNavigationCore'; } from '../src/services/CurrentNavigationCore';
import { ICurrentNavigationTerm } from '../src/services/CurrentNavigationTypes'; import { ICurrentNavigationTerm } from '../src/services/CurrentNavigationTypes';
import { isInitiallyExpanded } from '../src/extensions/currentNavigation/CurrentNavigationRenderer';
let passed: number = 0; let passed: number = 0;
function assert(name: string, condition: boolean): void { function assert(name: string, condition: boolean): void {
@@ -27,6 +28,10 @@ const terms: ICurrentNavigationTerm[] = [
assert('German boolean is accepted', parseBoolean('ja')); assert('German boolean is accepted', parseBoolean('ja'));
assert('local property is read', getLocalProperty(terms[0], 'CurrentNavigation.Root') === 'true'); assert('local property is read', getLocalProperty(terms[0], 'CurrentNavigation.Root') === 'true');
assert('expanded opens every branch', isInitiallyExpanded('expanded', false));
assert('collapsed closes even the active branch', !isInitiallyExpanded('collapsed', true));
assert('activePath keeps the previous active branch behavior', isInitiallyExpanded('activePath', true));
assert('activePath keeps inactive branches closed', !isInitiallyExpanded('activePath', false));
assert('site collection token resolves', resolveTermUrl(term({ assert('site collection token resolves', resolveTermUrl(term({
LocalCustomProperties: { '_Sys_Nav_SimpleLinkUrl': '~sitecollection/start' } LocalCustomProperties: { '_Sys_Nav_SimpleLinkUrl': '~sitecollection/start' }
}), 'https://portal/sites/a') === 'https://portal/sites/a/start'); }), 'https://portal/sites/a') === 'https://portal/sites/a/start');
@@ -36,6 +41,21 @@ const root: ICurrentNavigationTerm = selectCurrentNavigationRoot(
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', assert('site home URL is normalized to its web',
normalizeRootWebPath('/sites/portal/hr/SitePages/Home.aspx') === '/sites/portal/hr'); normalizeRootWebPath('/sites/portal/hr/SitePages/Home.aspx') === '/sites/portal/hr');
assert('site URL is current while a modern home page is open', isCurrentNavigationUrl(
'http://dhs.contoso.local/organisation/cir/CIR_13/',
'http://dhs.contoso.local/organisation/cir/CIR_13/SitePages/Home.aspx',
'http://dhs.contoso.local/organisation/cir/CIR_13'
));
assert('configured site home URL matches another page in the same web', isCurrentNavigationUrl(
'http://dhs.contoso.local/organisation/cir/CIR_13/SitePages/Home.aspx',
'http://dhs.contoso.local/organisation/cir/CIR_13/SitePages/News.aspx',
'http://dhs.contoso.local/organisation/cir/CIR_13/'
));
assert('different web is not current', !isCurrentNavigationUrl(
'http://dhs.contoso.local/organisation/cir/CIR_3/',
'http://dhs.contoso.local/organisation/cir/CIR_13/SitePages/Home.aspx',
'http://dhs.contoso.local/organisation/cir/CIR_13/'
));
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');
+1
View File
@@ -7,6 +7,7 @@
"files": [ "files": [
"../src/services/CurrentNavigationTypes.ts", "../src/services/CurrentNavigationTypes.ts",
"../src/services/CurrentNavigationCore.ts", "../src/services/CurrentNavigationCore.ts",
"../src/extensions/currentNavigation/CurrentNavigationRenderer.ts",
"CurrentNavigationCore.test.ts" "CurrentNavigationCore.test.ts"
] ]
} }
+7 -2
View File
@@ -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.4'); assert('package version', packageJson.version === '1.1.6');
assert('solution version', solution.version === '1.1.4.0'); assert('solution version', solution.version === '1.1.6.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);
@@ -26,6 +26,11 @@ assert('custom navigation is appended below SiteNavigation', app.indexOf('this.s
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 &&
renderer.indexOf("setAttribute('role', 'treeitem')") >= 0); renderer.indexOf("setAttribute('role', 'treeitem')") >= 0);
assert('configurable initial expansion', renderer.indexOf('isInitiallyExpanded(this.initialExpansion') >= 0 &&
renderer.indexOf("initialExpansion === 'expanded'") >= 0 && app.indexOf('this.initialExpansion()') >= 0);
assert('active path receives the current SharePoint web URL',
app.indexOf('this.context.pageContext.web.absoluteUrl') >= 0 &&
renderer.indexOf('isCurrentNavigationUrl(url, window.location.href, this.currentWebUrl)') >= 0);
assert('legacy selector compatibility', renderer.indexOf(':scope') < 0); assert('legacy selector compatibility', renderer.indexOf(':scope') < 0);
assert('local properties loaded', service.indexOf('LocalCustomProperties') >= 0); assert('local properties loaded', service.indexOf('LocalCustomProperties') >= 0);
assert('root property present', read('src/services/CurrentNavigationCore.ts').indexOf('CurrentNavigation.Root') >= 0); assert('root property present', read('src/services/CurrentNavigationCore.ts').indexOf('CurrentNavigation.Root') >= 0);