Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61a968562d | ||
|
|
9b73bb4614 | ||
|
|
0931257aa6 | ||
|
|
5725e1fe35 |
@@ -1,7 +1,8 @@
|
|||||||
# CurrentNavigation
|
# CurrentNavigation
|
||||||
|
|
||||||
CurrentNavigation 1.0.0 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 rendert im oberen Placeholder eine Navigation für das aktuelle Web.
|
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.
|
||||||
|
|
||||||
## Term-Konfiguration
|
## Term-Konfiguration
|
||||||
|
|
||||||
@@ -11,8 +12,21 @@ 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,
|
||||||
|
Pos1 und Ende.
|
||||||
|
|
||||||
|
Wenn kein passender Root-Term gefunden wird oder das Termset nicht geladen werden kann, bleibt ausschließlich die
|
||||||
|
native SharePoint-Sitenavigation sichtbar.
|
||||||
|
|
||||||
|
Für moderne Teamwebsites wird unter anderem das stabile Fabric-Muster
|
||||||
|
nav[role="navigation"].ms-Nav mit enthaltener ms-Nav-navItems-Liste erkannt. Dynamisch erzeugte Klassen wie root-70
|
||||||
|
werden nicht verwendet.
|
||||||
|
|
||||||
## Build und Bereitstellung
|
## Build und Bereitstellung
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
# CurrentNavigation – Umsetzungsstand
|
# CurrentNavigation – Umsetzungsstand
|
||||||
|
|
||||||
Zielversion: 1.0.0
|
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
|
||||||
|
- [x] Ergänzendes Rendering unterhalb der nativen Einträge im modernen SiteNavigation-Container
|
||||||
|
- [x] Barrierearmer Treeview mit Tastatursteuerung
|
||||||
- [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
|
||||||
- [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.0.0.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.0.0",
|
"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.0.0",
|
"version": "1.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
.current-navigation {
|
#CurrentNavigationHost {
|
||||||
position: relative;
|
width: 100%;
|
||||||
z-index: 299;
|
}
|
||||||
|
|
||||||
|
.current-navigation {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid $ms-color-neutralLight;
|
|
||||||
background: $ms-color-white;
|
|
||||||
color: $ms-color-neutralPrimary;
|
color: $ms-color-neutralPrimary;
|
||||||
font-family: $ms-font-family-fallbacks;
|
font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
|
||||||
font-size: $ms-font-size-s;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation ul {
|
.current-navigation ul {
|
||||||
@@ -18,74 +18,99 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation-root {
|
.current-navigation-group {
|
||||||
display: flex;
|
padding-left: 20px !important;
|
||||||
min-height: 40px;
|
|
||||||
padding: 0 16px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation li {
|
.current-navigation-row {
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-navigation a,
|
|
||||||
.current-navigation span {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 40px;
|
min-height: 36px;
|
||||||
padding: 0 12px;
|
}
|
||||||
|
|
||||||
|
.current-navigation-row:hover,
|
||||||
|
.current-navigation-row:focus-within {
|
||||||
|
background: $ms-color-neutralLighter;
|
||||||
|
color: $ms-color-themePrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-row:active {
|
||||||
|
background: $ms-color-neutralLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-toggle,
|
||||||
|
.current-navigation-toggle-spacer {
|
||||||
|
flex: 0 0 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-toggle:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-chevron {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-right: 1px solid currentColor;
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-item[aria-expanded='true'] > .current-navigation-row .current-navigation-chevron {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-label {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 12px 0 4px;
|
||||||
|
overflow: hidden;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation a:hover,
|
.current-navigation-label:hover,
|
||||||
.current-navigation a:focus,
|
.current-navigation-label:focus {
|
||||||
.current-navigation a.is-current {
|
background: transparent;
|
||||||
color: $ms-color-themePrimary;
|
color: inherit;
|
||||||
background: $ms-color-neutralLighter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation a:focus {
|
.current-navigation-label:focus {
|
||||||
outline: 2px solid $ms-color-themePrimary;
|
outline: 2px solid $ms-color-themePrimary;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation-children {
|
.current-navigation-label.is-current {
|
||||||
position: absolute;
|
background: $ms-color-neutralLighter;
|
||||||
top: 100%;
|
color: $ms-color-themePrimary;
|
||||||
left: 0;
|
font-weight: $ms-font-weight-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-navigation-group[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
min-width: 220px;
|
|
||||||
border: 1px solid $ms-color-neutralLight;
|
|
||||||
background: $ms-color-white;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-navigation-children .current-navigation-children {
|
@media (forced-colors: active) {
|
||||||
top: 0;
|
.current-navigation-label:focus { outline: 2px solid CanvasText; }
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-navigation li:hover > .current-navigation-children,
|
|
||||||
.current-navigation li:focus-within > .current-navigation-children {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.current-navigation-root {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-navigation-children,
|
|
||||||
.current-navigation-children .current-navigation-children {
|
|
||||||
position: static;
|
|
||||||
display: block;
|
|
||||||
min-width: 0;
|
|
||||||
border: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
padding-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// tslint:disable:max-line-length variable-name
|
// tslint:disable:max-line-length variable-name
|
||||||
import { override } from '@microsoft/decorators';
|
import { override } from '@microsoft/decorators';
|
||||||
import { BaseApplicationCustomizer, PlaceholderContent, PlaceholderName } from '@microsoft/sp-application-base';
|
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
|
||||||
import * as strings from 'CurrentNavigationApplicationCustomizerStrings';
|
import * as strings from 'CurrentNavigationApplicationCustomizerStrings';
|
||||||
import {
|
import { buildCurrentNavigationItems, selectCurrentNavigationRoot } from '../../services/CurrentNavigationCore';
|
||||||
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 { CurrentNavigationRenderer } from './CurrentNavigationRenderer';
|
||||||
@@ -23,38 +20,79 @@ export interface ICurrentNavigationApplicationCustomizerProperties {
|
|||||||
|
|
||||||
export default class CurrentNavigationApplicationCustomizer
|
export default class CurrentNavigationApplicationCustomizer
|
||||||
extends BaseApplicationCustomizer<ICurrentNavigationApplicationCustomizerProperties> {
|
extends BaseApplicationCustomizer<ICurrentNavigationApplicationCustomizerProperties> {
|
||||||
private topPlaceholder: PlaceholderContent | undefined;
|
private siteNavigation: HTMLElement | undefined;
|
||||||
private host: HTMLElement | undefined;
|
private host: HTMLElement | undefined;
|
||||||
|
private observer: MutationObserver | undefined;
|
||||||
|
private mountPending: boolean = false;
|
||||||
private renderer: CurrentNavigationRenderer = new CurrentNavigationRenderer();
|
private renderer: CurrentNavigationRenderer = new CurrentNavigationRenderer();
|
||||||
private renderVersion: number = 0;
|
private renderVersion: number = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
public onInit(): Promise<void> {
|
public onInit(): Promise<void> {
|
||||||
this.context.placeholderProvider.changedEvent.add(this, this.renderPlaceholders);
|
|
||||||
const application: any = this.context.application as any; // tslint:disable-line:no-any
|
const application: any = this.context.application as any; // tslint:disable-line:no-any
|
||||||
if (application && application.navigatedEvent) {
|
if (application && application.navigatedEvent) { application.navigatedEvent.add(this, this.onNavigated); }
|
||||||
application.navigatedEvent.add(this, this.onNavigated);
|
this.observer = new MutationObserver(this.onDomChanged);
|
||||||
}
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
||||||
this.renderPlaceholders();
|
this.mountInSiteNavigation(true);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPlaceholders = (): void => {
|
private onNavigated = (): void => { this.scheduleMount(true); }
|
||||||
if (!this.topPlaceholder) {
|
|
||||||
this.topPlaceholder = this.context.placeholderProvider.tryCreateContent(
|
private onDomChanged = (): void => {
|
||||||
PlaceholderName.Top, { onDispose: this.onPlaceholderDisposed }
|
if (!this.host || !document.body.contains(this.host)) { this.scheduleMount(true); }
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!this.topPlaceholder) { return; }
|
|
||||||
if (!this.host) {
|
|
||||||
this.host = document.createElement('div');
|
|
||||||
this.host.id = 'CurrentNavigationHost';
|
|
||||||
this.topPlaceholder.domElement.appendChild(this.host);
|
|
||||||
}
|
|
||||||
this.renderNavigation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNavigated = (): void => { this.renderNavigation(); }
|
private scheduleMount(forceRender: boolean): void {
|
||||||
|
if (this.mountPending) { return; }
|
||||||
|
this.mountPending = true;
|
||||||
|
window.setTimeout((): void => {
|
||||||
|
this.mountPending = false;
|
||||||
|
this.mountInSiteNavigation(forceRender);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mountInSiteNavigation(forceRender: boolean): void {
|
||||||
|
const container: HTMLElement = this.findSiteNavigation();
|
||||||
|
if (!container) {
|
||||||
|
this.debug('The modern SiteNavigation container was not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.siteNavigation !== container || !this.host || !container.contains(this.host)) {
|
||||||
|
this.detachHost();
|
||||||
|
this.siteNavigation = container;
|
||||||
|
this.host = document.createElement('div');
|
||||||
|
this.host.id = 'CurrentNavigationHost';
|
||||||
|
this.siteNavigation.appendChild(this.host);
|
||||||
|
forceRender = true;
|
||||||
|
}
|
||||||
|
if (forceRender) { this.renderNavigation(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private findSiteNavigation(): HTMLElement | undefined {
|
||||||
|
const selectors: string[] = [
|
||||||
|
'[data-automationid="SiteNavigation"]',
|
||||||
|
'[data-automation-id="SiteNavigation"]',
|
||||||
|
'nav[role="navigation"].ms-Nav',
|
||||||
|
'#spLeftNav',
|
||||||
|
'nav[aria-label="Site navigation"]',
|
||||||
|
'nav[aria-label="Websitenavigation"]'
|
||||||
|
];
|
||||||
|
for (let i: number = 0; i < selectors.length; i++) {
|
||||||
|
const candidates: NodeListOf<Element> = document.querySelectorAll(selectors[i]);
|
||||||
|
for (let j: number = 0; j < candidates.length; j++) {
|
||||||
|
const candidate: HTMLElement = candidates[j] as HTMLElement;
|
||||||
|
if (selectors[i] === 'nav[role="navigation"].ms-Nav' &&
|
||||||
|
!candidate.querySelector('.ms-Nav-navItems')) { continue; }
|
||||||
|
if (candidate.id !== 'CurrentNavigationHost' && !this.hostContains(candidate)) { return candidate; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hostContains(element: HTMLElement): boolean {
|
||||||
|
return !!(this.host && this.host.contains(element));
|
||||||
|
}
|
||||||
|
|
||||||
private async renderNavigation(): Promise<void> {
|
private async renderNavigation(): Promise<void> {
|
||||||
const version: number = ++this.renderVersion;
|
const version: number = ++this.renderVersion;
|
||||||
@@ -68,8 +106,7 @@ export default class CurrentNavigationApplicationCustomizer
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const service: CurrentNavigationTaxonomyService = new CurrentNavigationTaxonomyService(
|
const service: CurrentNavigationTaxonomyService = new CurrentNavigationTaxonomyService(
|
||||||
this.context, termSetId, this.cacheMinutes(), this.cacheVersion(),
|
this.context, termSetId, this.cacheMinutes(), this.cacheVersion(), this.languageLcid(), this.isDebug()
|
||||||
this.languageLcid(), this.isDebug()
|
|
||||||
);
|
);
|
||||||
const termSet: ITermSetResult = await service.getTerms();
|
const termSet: ITermSetResult = await service.getTerms();
|
||||||
if (version !== this.renderVersion || !this.host) { return; }
|
if (version !== this.renderVersion || !this.host) { return; }
|
||||||
@@ -81,10 +118,14 @@ export default class CurrentNavigationApplicationCustomizer
|
|||||||
termSet.Terms, root, this.context.pageContext.site.absoluteUrl,
|
termSet.Terms, root, this.context.pageContext.site.absoluteUrl,
|
||||||
this.languageLcid(), this.maxDepth(), !!(this.properties && this.properties.showRoot)
|
this.languageLcid(), this.maxDepth(), !!(this.properties && this.properties.showRoot)
|
||||||
);
|
);
|
||||||
if (items.length) { this.renderer.render(this.host, items, strings.NavigationLabel); }
|
if (items.length && this.host) {
|
||||||
} catch (error) {
|
this.renderer.render(this.host, items, {
|
||||||
this.debug(strings.LoadError, error);
|
navigationLabel: strings.NavigationLabel,
|
||||||
}
|
expand: strings.Expand,
|
||||||
|
collapse: strings.Collapse
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) { this.debug(strings.LoadError, error); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private cacheMinutes(): number {
|
private cacheMinutes(): number {
|
||||||
@@ -109,17 +150,21 @@ export default class CurrentNavigationApplicationCustomizer
|
|||||||
private debug(message: string, data?: any): void { // tslint:disable-line:no-any
|
private debug(message: string, data?: any): void { // tslint:disable-line:no-any
|
||||||
if (this.isDebug() && window.console) { console.log('[CurrentNavigation] ' + message, data || ''); }
|
if (this.isDebug() && window.console) { console.log('[CurrentNavigation] ' + message, data || ''); }
|
||||||
}
|
}
|
||||||
private onPlaceholderDisposed = (): void => {
|
private detachHost(): void {
|
||||||
if (this.host) { this.renderer.clear(this.host); }
|
this.renderVersion++;
|
||||||
|
if (this.host) {
|
||||||
|
this.renderer.clear(this.host);
|
||||||
|
if (this.host.parentElement) { this.host.parentElement.removeChild(this.host); }
|
||||||
|
}
|
||||||
this.host = undefined;
|
this.host = undefined;
|
||||||
this.topPlaceholder = undefined;
|
this.siteNavigation = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
protected onDispose(): void {
|
protected onDispose(): void {
|
||||||
this.context.placeholderProvider.changedEvent.remove(this, this.renderPlaceholders);
|
|
||||||
const application: any = this.context.application as any; // tslint:disable-line:no-any
|
const application: any = this.context.application as any; // tslint:disable-line:no-any
|
||||||
if (application && application.navigatedEvent) { application.navigatedEvent.remove(this, this.onNavigated); }
|
if (application && application.navigatedEvent) { application.navigatedEvent.remove(this, this.onNavigated); }
|
||||||
this.onPlaceholderDisposed();
|
if (this.observer) { this.observer.disconnect(); this.observer = undefined; }
|
||||||
|
this.detachHost();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,200 @@
|
|||||||
import { ICurrentNavigationItem } from '../../services/CurrentNavigationTypes';
|
|
||||||
import { normalizeNavigationPath } from '../../services/CurrentNavigationCore';
|
import { normalizeNavigationPath } from '../../services/CurrentNavigationCore';
|
||||||
|
import { ICurrentNavigationItem } from '../../services/CurrentNavigationTypes';
|
||||||
|
|
||||||
|
export interface ICurrentNavigationRendererStrings {
|
||||||
|
navigationLabel: string;
|
||||||
|
expand: string;
|
||||||
|
collapse: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class CurrentNavigationRenderer {
|
export class CurrentNavigationRenderer {
|
||||||
public render(container: HTMLElement, items: ICurrentNavigationItem[], navigationLabel: string): void {
|
private container: HTMLElement | undefined;
|
||||||
|
private keydownHandler: ((event: KeyboardEvent) => void) | undefined;
|
||||||
|
private strings: ICurrentNavigationRendererStrings;
|
||||||
|
|
||||||
|
public render(
|
||||||
|
container: HTMLElement,
|
||||||
|
items: ICurrentNavigationItem[],
|
||||||
|
strings: ICurrentNavigationRendererStrings
|
||||||
|
): void {
|
||||||
this.clear(container);
|
this.clear(container);
|
||||||
|
this.container = container;
|
||||||
|
this.strings = strings;
|
||||||
const nav: HTMLElement = document.createElement('nav');
|
const nav: HTMLElement = document.createElement('nav');
|
||||||
nav.className = 'current-navigation';
|
nav.className = 'current-navigation';
|
||||||
nav.setAttribute('aria-label', navigationLabel);
|
nav.setAttribute('aria-label', strings.navigationLabel);
|
||||||
nav.appendChild(this.createList(items, 1));
|
nav.appendChild(this.createList(items, 1, true));
|
||||||
|
this.keydownHandler = this.onKeydown;
|
||||||
|
nav.addEventListener('keydown', this.keydownHandler);
|
||||||
container.appendChild(nav);
|
container.appendChild(nav);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(container: HTMLElement): void {
|
public clear(container: HTMLElement): void {
|
||||||
|
if (this.keydownHandler && this.container) {
|
||||||
|
const nav: HTMLElement = this.container.querySelector('.current-navigation') as HTMLElement;
|
||||||
|
if (nav) { nav.removeEventListener('keydown', this.keydownHandler); }
|
||||||
|
}
|
||||||
while (container && container.firstChild) { container.removeChild(container.firstChild); }
|
while (container && container.firstChild) { container.removeChild(container.firstChild); }
|
||||||
|
this.keydownHandler = undefined;
|
||||||
|
this.container = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createList(items: ICurrentNavigationItem[], depth: number): HTMLUListElement {
|
private createList(items: ICurrentNavigationItem[], depth: number, root: boolean): HTMLUListElement {
|
||||||
const list: HTMLUListElement = document.createElement('ul');
|
const list: HTMLUListElement = document.createElement('ul');
|
||||||
list.className = depth === 1 ? 'current-navigation-root' : 'current-navigation-children';
|
list.className = root ? 'current-navigation-tree' : 'current-navigation-group';
|
||||||
|
list.setAttribute('role', root ? 'tree' : 'group');
|
||||||
for (let i: number = 0; i < items.length; i++) {
|
for (let i: number = 0; i < items.length; i++) {
|
||||||
const item: ICurrentNavigationItem = items[i];
|
list.appendChild(this.createItem(items[i], depth));
|
||||||
const entry: HTMLLIElement = document.createElement('li');
|
|
||||||
entry.className = item.items.length ? 'has-children' : '';
|
|
||||||
const content: HTMLElement = item.url ? document.createElement('a') : document.createElement('span');
|
|
||||||
content.textContent = item.label;
|
|
||||||
if (item.url) {
|
|
||||||
const link: HTMLAnchorElement = content as HTMLAnchorElement;
|
|
||||||
link.href = item.url;
|
|
||||||
if (normalizeNavigationPath(window.location.href) === normalizeNavigationPath(item.url)) {
|
|
||||||
link.setAttribute('aria-current', 'page');
|
|
||||||
link.className = 'is-current';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry.appendChild(content);
|
|
||||||
if (item.items.length) { entry.appendChild(this.createList(item.items, depth + 1)); }
|
|
||||||
list.appendChild(entry);
|
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createItem(item: ICurrentNavigationItem, depth: number): HTMLLIElement {
|
||||||
|
const entry: HTMLLIElement = document.createElement('li');
|
||||||
|
const hasChildren: boolean = item.items.length > 0;
|
||||||
|
const activeBranch: boolean = this.containsCurrentItem(item);
|
||||||
|
entry.className = hasChildren ? 'current-navigation-item has-children' : 'current-navigation-item';
|
||||||
|
entry.setAttribute('role', 'treeitem');
|
||||||
|
entry.setAttribute('aria-level', String(depth));
|
||||||
|
|
||||||
|
const row: HTMLDivElement = document.createElement('div');
|
||||||
|
row.className = 'current-navigation-row';
|
||||||
|
if (hasChildren) {
|
||||||
|
const toggle: HTMLButtonElement = document.createElement('button');
|
||||||
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'current-navigation-toggle';
|
||||||
|
toggle.tabIndex = -1;
|
||||||
|
toggle.setAttribute('aria-expanded', activeBranch ? 'true' : 'false');
|
||||||
|
toggle.setAttribute('aria-label', (activeBranch ? this.strings.collapse : this.strings.expand) + ' ' + item.label);
|
||||||
|
const chevron: HTMLSpanElement = document.createElement('span');
|
||||||
|
chevron.className = 'current-navigation-chevron';
|
||||||
|
chevron.setAttribute('aria-hidden', 'true');
|
||||||
|
toggle.appendChild(chevron);
|
||||||
|
toggle.addEventListener('click', (): void => {
|
||||||
|
this.setExpanded(entry, entry.getAttribute('aria-expanded') !== 'true', item.label);
|
||||||
|
});
|
||||||
|
row.appendChild(toggle);
|
||||||
|
entry.setAttribute('aria-expanded', activeBranch ? 'true' : 'false');
|
||||||
|
} else {
|
||||||
|
const spacer: HTMLSpanElement = document.createElement('span');
|
||||||
|
spacer.className = 'current-navigation-toggle-spacer';
|
||||||
|
spacer.setAttribute('aria-hidden', 'true');
|
||||||
|
row.appendChild(spacer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: HTMLElement = item.url ? document.createElement('a') : document.createElement('span');
|
||||||
|
content.className = 'current-navigation-label';
|
||||||
|
content.textContent = item.label;
|
||||||
|
if (item.url) {
|
||||||
|
const link: HTMLAnchorElement = content as HTMLAnchorElement;
|
||||||
|
link.href = item.url;
|
||||||
|
if (this.isCurrentUrl(item.url)) {
|
||||||
|
link.setAttribute('aria-current', 'page');
|
||||||
|
link.className += ' is-current';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.setAttribute('tabindex', '0');
|
||||||
|
}
|
||||||
|
row.appendChild(content);
|
||||||
|
entry.appendChild(row);
|
||||||
|
if (hasChildren) {
|
||||||
|
const children: HTMLUListElement = this.createList(item.items, depth + 1, false);
|
||||||
|
children.hidden = !activeBranch;
|
||||||
|
entry.appendChild(children);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setExpanded(entry: HTMLLIElement, expanded: boolean, label?: string): void {
|
||||||
|
const row: HTMLElement = entry.firstElementChild as HTMLElement;
|
||||||
|
const group: HTMLUListElement = row && row.nextElementSibling as HTMLUListElement;
|
||||||
|
const toggle: HTMLButtonElement = row
|
||||||
|
? row.querySelector('.current-navigation-toggle') as HTMLButtonElement
|
||||||
|
: undefined;
|
||||||
|
entry.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||||
|
if (group && group.className.indexOf('current-navigation-group') >= 0) { group.hidden = !expanded; }
|
||||||
|
if (toggle) {
|
||||||
|
toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||||
|
toggle.setAttribute('aria-label', (expanded ? this.strings.collapse : this.strings.expand) + ' ' + (label || ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeydown = (event: KeyboardEvent): void => {
|
||||||
|
const target: HTMLElement = event.target as HTMLElement;
|
||||||
|
if (!target || target.className.indexOf('current-navigation-label') < 0) { return; }
|
||||||
|
const entry: HTMLLIElement = this.closestTreeItem(target);
|
||||||
|
if (!entry) { return; }
|
||||||
|
if (event.key === 'ArrowRight' && entry.hasAttribute('aria-expanded')) {
|
||||||
|
if (entry.getAttribute('aria-expanded') !== 'true') { this.setExpanded(entry, true, target.textContent); }
|
||||||
|
else { this.focusRelative(target, 1); }
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.key === 'ArrowLeft') {
|
||||||
|
if (entry.getAttribute('aria-expanded') === 'true') { this.setExpanded(entry, false, target.textContent); }
|
||||||
|
else {
|
||||||
|
const parent: HTMLLIElement = this.parentTreeItem(entry);
|
||||||
|
if (parent) { (parent.querySelector('.current-navigation-label') as HTMLElement).focus(); }
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.key === 'ArrowDown') {
|
||||||
|
this.focusRelative(target, 1); event.preventDefault();
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
this.focusRelative(target, -1); event.preventDefault();
|
||||||
|
} else if (event.key === 'Home' || event.key === 'End') {
|
||||||
|
this.focusBoundary(event.key === 'Home'); event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private visibleLabels(): HTMLElement[] {
|
||||||
|
if (!this.container) { return []; }
|
||||||
|
const labels: NodeListOf<Element> = this.container.querySelectorAll('.current-navigation-label');
|
||||||
|
const result: HTMLElement[] = [];
|
||||||
|
for (let i: number = 0; i < labels.length; i++) {
|
||||||
|
const label: HTMLElement = labels[i] as HTMLElement;
|
||||||
|
if (!this.hasHiddenParent(label)) { result.push(label); }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private focusRelative(current: HTMLElement, offset: number): void {
|
||||||
|
const labels: HTMLElement[] = this.visibleLabels();
|
||||||
|
const index: number = labels.indexOf(current);
|
||||||
|
if (index >= 0 && labels[index + offset]) { labels[index + offset].focus(); }
|
||||||
|
}
|
||||||
|
private focusBoundary(first: boolean): void {
|
||||||
|
const labels: HTMLElement[] = this.visibleLabels();
|
||||||
|
if (labels.length) { labels[first ? 0 : labels.length - 1].focus(); }
|
||||||
|
}
|
||||||
|
private hasHiddenParent(element: HTMLElement): boolean {
|
||||||
|
let current: HTMLElement = element.parentElement;
|
||||||
|
while (current && current !== this.container) {
|
||||||
|
if (current.hidden) { return true; }
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private closestTreeItem(element: HTMLElement): HTMLLIElement {
|
||||||
|
let current: HTMLElement = element;
|
||||||
|
while (current && current !== this.container) {
|
||||||
|
if (current.getAttribute('role') === 'treeitem') { return current as HTMLLIElement; }
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
private parentTreeItem(entry: HTMLLIElement): HTMLLIElement {
|
||||||
|
let current: HTMLElement = entry.parentElement ? entry.parentElement.parentElement : undefined;
|
||||||
|
while (current && current !== this.container) {
|
||||||
|
if (current.getAttribute('role') === 'treeitem') { return current as HTMLLIElement; }
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
private containsCurrentItem(item: ICurrentNavigationItem): boolean {
|
||||||
|
if (item.url && this.isCurrentUrl(item.url)) { return true; }
|
||||||
|
for (let i: number = 0; i < item.items.length; i++) {
|
||||||
|
if (this.containsCurrentItem(item.items[i])) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private isCurrentUrl(url: string): boolean {
|
||||||
|
return normalizeNavigationPath(window.location.href) === normalizeNavigationPath(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ define([], function() {
|
|||||||
"Title": "CurrentNavigationApplicationCustomizer",
|
"Title": "CurrentNavigationApplicationCustomizer",
|
||||||
"NavigationLabel": "Navigation der aktuellen Website",
|
"NavigationLabel": "Navigation der aktuellen Website",
|
||||||
"ConfigurationMissing": "Die aktuelle Navigation ist nicht konfiguriert.",
|
"ConfigurationMissing": "Die aktuelle Navigation ist nicht konfiguriert.",
|
||||||
"LoadError": "Die aktuelle Navigation konnte nicht geladen werden."
|
"LoadError": "Die aktuelle Navigation konnte nicht geladen werden.",
|
||||||
|
"Expand": "Aufklappen",
|
||||||
|
"Collapse": "Zuklappen"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ define([], function() {
|
|||||||
"Title": "CurrentNavigationApplicationCustomizer",
|
"Title": "CurrentNavigationApplicationCustomizer",
|
||||||
"NavigationLabel": "Current site navigation",
|
"NavigationLabel": "Current site navigation",
|
||||||
"ConfigurationMissing": "Current navigation is not configured.",
|
"ConfigurationMissing": "Current navigation is not configured.",
|
||||||
"LoadError": "Current navigation could not be loaded."
|
"LoadError": "Current navigation could not be loaded.",
|
||||||
|
"Expand": "Expand",
|
||||||
|
"Collapse": "Collapse"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ declare interface ICurrentNavigationApplicationCustomizerStrings {
|
|||||||
NavigationLabel: string;
|
NavigationLabel: string;
|
||||||
ConfigurationMissing: string;
|
ConfigurationMissing: string;
|
||||||
LoadError: string;
|
LoadError: string;
|
||||||
|
Expand: string;
|
||||||
|
Collapse: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'CurrentNavigationApplicationCustomizerStrings' {
|
declare module 'CurrentNavigationApplicationCustomizerStrings' {
|
||||||
|
|||||||
@@ -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,12 +9,24 @@ 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.0.0');
|
assert('package version', packageJson.version === '1.1.4');
|
||||||
assert('solution version', solution.version === '1.0.0.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);
|
||||||
assert('placeholder lifecycle cleanup', app.indexOf('changedEvent.remove') >= 0);
|
assert('does not use Top placeholder', app.indexOf('PlaceholderName.Top') < 0);
|
||||||
|
assert('targets modern SiteNavigation', app.indexOf('data-automationid="SiteNavigation"') >= 0);
|
||||||
|
assert('targets SharePoint ms-Nav fallback', app.indexOf('nav[role="navigation"].ms-Nav') >= 0 &&
|
||||||
|
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('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 &&
|
||||||
|
renderer.indexOf("setAttribute('role', 'treeitem')") >= 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);
|
||||||
assert('hidden property present', read('src/services/CurrentNavigationCore.ts').indexOf('CurrentNavigation.Hidden') >= 0);
|
assert('hidden property present', read('src/services/CurrentNavigationCore.ts').indexOf('CurrentNavigation.Hidden') >= 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user