diff --git a/README.md b/README.md index 87598eb..529f440 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Zentrale, barrierearme Portalnavigation aus SharePoint Managed Metadata für Sha | Eigenschaft | Wert | |---|---| -| Version | 2.2.0 | +| Version | 2.2.2 | | SharePoint Framework | 1.4.1 | | Node.js für den Build | 8.17.0 | | Moderne Seiten | SPFx Application Customizer | @@ -272,6 +272,12 @@ MegaMenu/ ## Versionshistorie +### 2.2.1 + +- Terms mit `_Sys_Nav_ExcludedProviders = GlobalNavigationTaxonomyProvider` werden einschließlich ihres Unterbaums + aus der modernen und klassischen globalen Navigation ausgeblendet. +- Das Cache-Schema wurde erhöht, damit die neue Sichtbarkeitsregel sofort greift. + ### 2.2.0 - mobiles Hamburger-/Accordion-Menü für Modern und Classic diff --git a/ToDo.md b/ToDo.md index a1f7f50..91e7531 100644 --- a/ToDo.md +++ b/ToDo.md @@ -1,4 +1,4 @@ -# MegaMenu – Arbeitsplan Version 2.2 +# MegaMenu – Arbeitsplan Version 2.2.2 Status: Implementierung abgeschlossen; Ship-Build auf der vorgesehenen Node-8-Buildumgebung ausstehend diff --git a/classic/megamenu-classic.css b/classic/megamenu-classic.css index 6729724..edda630 100644 --- a/classic/megamenu-classic.css +++ b/classic/megamenu-classic.css @@ -59,6 +59,15 @@ padding: 0; } +.mega-menu-top-item:hover, +.mega-menu-top-item:focus-within, +.mega-menu-top-item:active, +.mega-menu-top-item.is-open, +.mega-menu-top-item.is-active { + background: #f3f2f1; + color: #323130; +} + .mega-menu-mode-flyout .mega-menu-top-item { position: relative; } @@ -114,8 +123,8 @@ .menu-item-text:hover, .menu-item-toggle:hover, .menu-item-toggle[aria-expanded="true"] { - background: #f3f2f1; - color: #323130; + background: transparent; + color: inherit; text-decoration: none; } @@ -258,8 +267,9 @@ outline: none; } -.flyout-link:hover, -.flyout-item:focus-within > .flyout-link { +.flyout-item:hover > .flyout-link, +.flyout-item:focus-within > .flyout-link, +.flyout-item:active > .flyout-link { background: #f3f2f1; color: #323130; } @@ -368,7 +378,7 @@ border-bottom: 1px solid #edebe9; } - .menu-item-toggle { width: 44px; margin: 0; border-left: 1px solid #edebe9; } + .menu-item-toggle { width: 44px; margin: 0; border-left: 0; } .mega-menu { position: static; diff --git a/classic/megamenu-services-standalone.js b/classic/megamenu-services-standalone.js index c0ff4ae..b7d0930 100644 --- a/classic/megamenu-services-standalone.js +++ b/classic/megamenu-services-standalone.js @@ -122,7 +122,7 @@ _cacheKey() { const normalize = value => encodeURIComponent(String(value || '').trim().toLowerCase()); - return 'MegaMenu:TermSet:v3:' + normalize(this._siteCollectionUrl) + ':' + normalize(this.termSetIdentifier) + ':' + this._languageLcid + ':' + normalize(this._cacheVersion); + return 'MegaMenu:TermSet:v4:' + normalize(this._siteCollectionUrl) + ':' + normalize(this.termSetIdentifier) + ':' + this._languageLcid + ':' + normalize(this._cacheVersion); } _getCache(allowExpired) { @@ -190,7 +190,9 @@ while (enumerator.moveNext()) { const term = enumerator.get_current(); const properties = this._getCustomProperties(term); - if (!term.get_isAvailableForTagging() || term.get_isDeprecated() || this._asBoolean(properties['MegaMenu.Hidden'])) { + if (!term.get_isAvailableForTagging() || term.get_isDeprecated() || + this._asBoolean(properties['MegaMenu.Hidden']) || + this._isExcludedFromGlobalNavigation(properties)) { continue; } processedTerms.push({ @@ -215,6 +217,7 @@ _Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null, _Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null, _Sys_Nav_HoverText: props._Sys_Nav_HoverText || null, + _Sys_Nav_ExcludedProviders: props._Sys_Nav_ExcludedProviders || null, 'MegaMenu.Hidden': props['MegaMenu.Hidden'] || null, 'MegaMenu.OpenInNewWindow': props['MegaMenu.OpenInNewWindow'] || null, 'MegaMenu.Description': props['MegaMenu.Description'] || null @@ -228,13 +231,24 @@ return value === true || ['true', '1', 'yes', 'ja'].indexOf(String(value || '').toLowerCase()) >= 0; } + _isExcludedFromGlobalNavigation(properties) { + const value = String((properties || {})._Sys_Nav_ExcludedProviders || '').replace(/["']/g, ''); + return value.split(/[;,|]/).some(provider => + provider.trim().toLowerCase() === 'globalnavigationtaxonomyprovider' + ); + } + _processTerms(termsData) { const itemsDict = new ItemDictionary(); const itemsByPath = {}; const menuItems = []; + const visibleTerms = termsData.filter(termData => + !this._asBoolean((termData.LocalCustomProperties || {})['MegaMenu.Hidden']) && + !this._isExcludedFromGlobalNavigation(termData.LocalCustomProperties) + ); // Create MenuItem objects - termsData.forEach(termData => { + visibleTerms.forEach(termData => { const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl); itemsDict.Add(termData.Id, menuItem); itemsByPath[termData.PathOfTerm] = menuItem; @@ -245,7 +259,7 @@ }); // Build hierarchy - termsData.forEach(termData => { + visibleTerms.forEach(termData => { if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) { const menuItem = itemsDict.Get(termData.Id); const pathParts = termData.PathOfTerm.split(';'); @@ -484,6 +498,7 @@ renderer.closeAll(panel); toggle.setAttribute('aria-expanded', String(open)); toggle.setAttribute('aria-label', (open ? renderer.strings.closeSubmenu : renderer.strings.openSubmenu) + ' ' + renderer.getToggleLabel(toggle)); + parent.classList.toggle('is-open', open); panel.setAttribute('aria-expanded', String(open)); panel.classList.toggle('js-open', open); }; @@ -499,7 +514,7 @@ }); let closeTimer; parent.addEventListener('mouseenter', () => { window.clearTimeout(closeTimer); setOpen(true); }); - parent.addEventListener('mouseleave', () => { closeTimer = window.setTimeout(() => setOpen(false), 180); }); + parent.addEventListener('mouseleave', () => { closeTimer = window.setTimeout(() => setOpen(false), 0); }); }); const flyoutToggles = document.querySelectorAll('#Mega-Menu .flyout-toggle'); diff --git a/config/package-solution.json b/config/package-solution.json index 9afec36..e2058b8 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "mega-menu-client-side-solution", "id": "f4660e06-ce08-43ee-bfb7-5c4464e01133", - "version": "2.2.0.0", + "version": "2.2.2.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package-lock.json b/package-lock.json index 90a690d..e6f19a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mega-menu", - "version": "2.2.0", + "version": "2.2.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d0795c9..eac8c7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mega-menu", - "version": "2.2.0", + "version": "2.2.2", "private": true, "main": "lib/index.js", "engines": { diff --git a/src/extensions/megaMenu/MegaMenu.module.scss b/src/extensions/megaMenu/MegaMenu.module.scss index 0360061..a00a09d 100644 --- a/src/extensions/megaMenu/MegaMenu.module.scss +++ b/src/extensions/megaMenu/MegaMenu.module.scss @@ -70,6 +70,15 @@ $menuLayer: var(--megaMenuZIndex, 6000); padding: 0; } + .mega-menu-top-item:hover, + .mega-menu-top-item:focus-within, + .mega-menu-top-item:active, + .mega-menu-top-item.is-open, + .mega-menu-top-item.is-active { + background: $neutralLighter; + color: $neutralPrimary; + } + .mega-menu-mobile-toggle { display: none; } @@ -132,8 +141,8 @@ $menuLayer: var(--megaMenuZIndex, 6000); .menu-item-text:hover, .menu-item-toggle:hover, .menu-item-toggle[aria-expanded='true'] { - background: $neutralLighter; - color: $neutralPrimary; + background: transparent; + color: inherit; text-decoration: none; } @@ -309,8 +318,9 @@ $menuLayer: var(--megaMenuZIndex, 6000); outline: none; } - .flyout-link:hover, + .flyout-item:hover > .flyout-link, .flyout-item:focus-within > .flyout-link, + .flyout-item:active > .flyout-link, .flyout-item.is-active > .flyout-link { background: $neutralLighter; color: $neutralPrimary; @@ -461,7 +471,7 @@ $menuLayer: var(--megaMenuZIndex, 6000); .menu-item-toggle { width: 44px; margin: 0; - border-left: 1px solid $neutralLight; + border-left: 0; } .mega-menu { diff --git a/src/extensions/megaMenu/MegaMenuRenderer.ts b/src/extensions/megaMenu/MegaMenuRenderer.ts index 20aa9ff..8c6f6f7 100644 --- a/src/extensions/megaMenu/MegaMenuRenderer.ts +++ b/src/extensions/megaMenu/MegaMenuRenderer.ts @@ -23,8 +23,8 @@ export interface IMegaMenuRendererStrings { } export class MegaMenuRenderer { - private static readonly hoverOpenDelayMs: number = 140; - private static readonly hoverCloseDelayMs: number = 180; + private static readonly hoverOpenDelayMs: number = 0; + private static readonly hoverCloseDelayMs: number = 0; private _container: HTMLElement | undefined; private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined; private _documentClickHandler: ((event: MouseEvent) => void) | undefined; @@ -657,6 +657,7 @@ export class MegaMenuRenderer { const label: string = this.getTriggerLabel(trigger); trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-label', this.strings.closeSubmenu + ' ' + label); + (trigger.parentElement as HTMLElement).classList.add('is-open'); menu.setAttribute('aria-expanded', 'true'); menu.classList.add('js-open'); this.announce(label + ' ' + this.strings.opened); @@ -667,6 +668,7 @@ export class MegaMenuRenderer { const label: string = this.getTriggerLabel(trigger); trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-label', this.strings.openSubmenu + ' ' + label); + (trigger.parentElement as HTMLElement).classList.remove('is-open'); menu.setAttribute('aria-expanded', 'false'); menu.classList.remove('js-open'); this.announce(label + ' ' + this.strings.closed); diff --git a/src/services/MegaMenuCore.ts b/src/services/MegaMenuCore.ts index fae4f29..34276c9 100644 --- a/src/services/MegaMenuCore.ts +++ b/src/services/MegaMenuCore.ts @@ -1,13 +1,16 @@ import { ITerm, ITermLabelCollection } from './ISPTermStorePickerService'; -export const MegaMenuCacheSchemaVersion: string = '3'; +export const MegaMenuCacheSchemaVersion: string = '4'; export const MegaMenuTermProperties = { hidden: 'MegaMenu.Hidden', openInNewWindow: 'MegaMenu.OpenInNewWindow', - description: 'MegaMenu.Description' + description: 'MegaMenu.Description', + excludedProviders: '_Sys_Nav_ExcludedProviders' }; +export const GlobalNavigationTaxonomyProvider: string = 'GlobalNavigationTaxonomyProvider'; + export interface IMegaMenuCacheEnvelope { expiresAt: number; value: T; @@ -48,6 +51,16 @@ export function isTermHidden(term: ITerm): boolean { return parseBooleanProperty(getTermProperty(term, MegaMenuTermProperties.hidden)); } +export function isTermExcludedFromGlobalNavigation(term: ITerm): boolean { + const value: string = getTermProperty(term, MegaMenuTermProperties.excludedProviders); + if (!value) { + return false; + } + const providers: string[] = value.replace(/["']/g, '').split(/[;,|]/); + const expected: string = GlobalNavigationTaxonomyProvider.toLowerCase(); + return providers.some((provider: string): boolean => provider.trim().toLowerCase() === expected); +} + export function getLocalizedTermLabel(term: ITerm, languageLcid: number): string { const labels: ITermLabelCollection | undefined = term && term.Labels; const items = labels && labels._Child_Items_ ? labels._Child_Items_ : []; diff --git a/src/services/TaxonomyNavigationService.ts b/src/services/TaxonomyNavigationService.ts index 821cdf2..eab6c6c 100644 --- a/src/services/TaxonomyNavigationService.ts +++ b/src/services/TaxonomyNavigationService.ts @@ -6,7 +6,12 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; import SPTermStorePickerService from './SPTermStorePickerService'; import { ITerm, ITermSet } from './ISPTermStorePickerService'; import { debugLog, debugWarn } from './MegaMenuDebug'; -import { buildMenuHierarchy, IMegaMenuHierarchyEntry, isTermHidden } from './MegaMenuCore'; +import { + buildMenuHierarchy, + IMegaMenuHierarchyEntry, + isTermExcludedFromGlobalNavigation, + isTermHidden +} from './MegaMenuCore'; const LOG_SOURCE: string = 'TaxonomyNavigationService'; @@ -70,7 +75,9 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService { throw new Error('The configured navigation term set was not found or contains no terms.'); } - termset.Terms.filter((term: ITerm) => !isTermHidden(term)).forEach((term: ITerm) => { + termset.Terms.filter((term: ITerm) => + !isTermHidden(term) && !isTermExcludedFromGlobalNavigation(term) + ).forEach((term: ITerm) => { const menuItem: IMenuItem = new MenuItem(term, 0, siteCollectionUrl, this.languageLcid); entries.push({ id: term.Id, diff --git a/tests/MegaMenuCore.test.ts b/tests/MegaMenuCore.test.ts index 6815471..43340ff 100644 --- a/tests/MegaMenuCore.test.ts +++ b/tests/MegaMenuCore.test.ts @@ -6,6 +6,7 @@ import { getTermProperty, isExternalNavigationUrl, isNavigationUrlActive, + isTermExcludedFromGlobalNavigation, isTermHidden, MegaMenuTermProperties, parseBooleanProperty, @@ -31,6 +32,21 @@ function term(values: any): ITerm { // tslint:disable-line:no-any assert('boolean property accepts German yes', parseBooleanProperty('ja')); assert('boolean property rejects false', !parseBooleanProperty('false')); assert('hidden local term property', isTermHidden(term({ LocalCustomProperties: { 'MegaMenu.Hidden': 'true' } }))); +assert('global navigation provider excludes term', isTermExcludedFromGlobalNavigation(term({ + LocalCustomProperties: { '_Sys_Nav_ExcludedProviders': 'GlobalNavigationTaxonomyProvider' } +}))); +assert('global navigation provider is matched case-insensitively in provider list', + isTermExcludedFromGlobalNavigation(term({ + LocalCustomProperties: { + '_Sys_Nav_ExcludedProviders': 'CurrentNavigationTaxonomyProvider; globalnavigationtaxonomyprovider' + } + }))); +assert('different excluded provider keeps term visible', !isTermExcludedFromGlobalNavigation(term({ + LocalCustomProperties: { '_Sys_Nav_ExcludedProviders': 'CurrentNavigationTaxonomyProvider' } +}))); +assert('missing excluded provider keeps term visible', !isTermExcludedFromGlobalNavigation(term({ + LocalCustomProperties: {} +}))); assert('description local term property', getTermProperty(term({ LocalCustomProperties: { 'MegaMenu.Description': 'Forms' } }), MegaMenuTermProperties.description) === 'Forms'); assert('open-in-new-window local term property', parseBooleanProperty(getTermProperty(term({ LocalCustomProperties: { 'MegaMenu.OpenInNewWindow': '1' } }), MegaMenuTermProperties.openInNewWindow)));