dev-3.0 #2

Merged
Torsten merged 6 commits from dev-3.0 into main 2026-08-18 20:49:53 +00:00
12 changed files with 104 additions and 25 deletions
Showing only changes of commit e977fd7365 - Show all commits
+7 -1
View File
@@ -4,7 +4,7 @@ Zentrale, barrierearme Portalnavigation aus SharePoint Managed Metadata für Sha
| Eigenschaft | Wert | | Eigenschaft | Wert |
|---|---| |---|---|
| Version | 2.2.0 | | Version | 2.2.2 |
| SharePoint Framework | 1.4.1 | | SharePoint Framework | 1.4.1 |
| Node.js für den Build | 8.17.0 | | Node.js für den Build | 8.17.0 |
| Moderne Seiten | SPFx Application Customizer | | Moderne Seiten | SPFx Application Customizer |
@@ -272,6 +272,12 @@ MegaMenu/
## Versionshistorie ## 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 ### 2.2.0
- mobiles Hamburger-/Accordion-Menü für Modern und Classic - mobiles Hamburger-/Accordion-Menü für Modern und Classic
+1 -1
View File
@@ -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 Status: Implementierung abgeschlossen; Ship-Build auf der vorgesehenen Node-8-Buildumgebung ausstehend
+15 -5
View File
@@ -59,6 +59,15 @@
padding: 0; 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 { .mega-menu-mode-flyout .mega-menu-top-item {
position: relative; position: relative;
} }
@@ -114,8 +123,8 @@
.menu-item-text:hover, .menu-item-text:hover,
.menu-item-toggle:hover, .menu-item-toggle:hover,
.menu-item-toggle[aria-expanded="true"] { .menu-item-toggle[aria-expanded="true"] {
background: #f3f2f1; background: transparent;
color: #323130; color: inherit;
text-decoration: none; text-decoration: none;
} }
@@ -258,8 +267,9 @@
outline: none; outline: none;
} }
.flyout-link:hover, .flyout-item:hover > .flyout-link,
.flyout-item:focus-within > .flyout-link { .flyout-item:focus-within > .flyout-link,
.flyout-item:active > .flyout-link {
background: #f3f2f1; background: #f3f2f1;
color: #323130; color: #323130;
} }
@@ -368,7 +378,7 @@
border-bottom: 1px solid #edebe9; 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 { .mega-menu {
position: static; position: static;
+20 -5
View File
@@ -122,7 +122,7 @@
_cacheKey() { _cacheKey() {
const normalize = value => encodeURIComponent(String(value || '').trim().toLowerCase()); 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) { _getCache(allowExpired) {
@@ -190,7 +190,9 @@
while (enumerator.moveNext()) { while (enumerator.moveNext()) {
const term = enumerator.get_current(); const term = enumerator.get_current();
const properties = this._getCustomProperties(term); 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; continue;
} }
processedTerms.push({ processedTerms.push({
@@ -215,6 +217,7 @@
_Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null, _Sys_Nav_SimpleLinkUrl: props._Sys_Nav_SimpleLinkUrl || null,
_Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null, _Sys_Nav_TargetUrl: props._Sys_Nav_TargetUrl || null,
_Sys_Nav_HoverText: props._Sys_Nav_HoverText || null, _Sys_Nav_HoverText: props._Sys_Nav_HoverText || null,
_Sys_Nav_ExcludedProviders: props._Sys_Nav_ExcludedProviders || null,
'MegaMenu.Hidden': props['MegaMenu.Hidden'] || null, 'MegaMenu.Hidden': props['MegaMenu.Hidden'] || null,
'MegaMenu.OpenInNewWindow': props['MegaMenu.OpenInNewWindow'] || null, 'MegaMenu.OpenInNewWindow': props['MegaMenu.OpenInNewWindow'] || null,
'MegaMenu.Description': props['MegaMenu.Description'] || null 'MegaMenu.Description': props['MegaMenu.Description'] || null
@@ -228,13 +231,24 @@
return value === true || ['true', '1', 'yes', 'ja'].indexOf(String(value || '').toLowerCase()) >= 0; 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) { _processTerms(termsData) {
const itemsDict = new ItemDictionary(); const itemsDict = new ItemDictionary();
const itemsByPath = {}; const itemsByPath = {};
const menuItems = []; const menuItems = [];
const visibleTerms = termsData.filter(termData =>
!this._asBoolean((termData.LocalCustomProperties || {})['MegaMenu.Hidden']) &&
!this._isExcludedFromGlobalNavigation(termData.LocalCustomProperties)
);
// Create MenuItem objects // Create MenuItem objects
termsData.forEach(termData => { visibleTerms.forEach(termData => {
const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl); const menuItem = new MenuItem(termData, 0, this._siteCollectionUrl);
itemsDict.Add(termData.Id, menuItem); itemsDict.Add(termData.Id, menuItem);
itemsByPath[termData.PathOfTerm] = menuItem; itemsByPath[termData.PathOfTerm] = menuItem;
@@ -245,7 +259,7 @@
}); });
// Build hierarchy // Build hierarchy
termsData.forEach(termData => { visibleTerms.forEach(termData => {
if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) { if (termData.PathOfTerm && termData.PathOfTerm.split(';').length > 1) {
const menuItem = itemsDict.Get(termData.Id); const menuItem = itemsDict.Get(termData.Id);
const pathParts = termData.PathOfTerm.split(';'); const pathParts = termData.PathOfTerm.split(';');
@@ -484,6 +498,7 @@
renderer.closeAll(panel); renderer.closeAll(panel);
toggle.setAttribute('aria-expanded', String(open)); toggle.setAttribute('aria-expanded', String(open));
toggle.setAttribute('aria-label', (open ? renderer.strings.closeSubmenu : renderer.strings.openSubmenu) + ' ' + renderer.getToggleLabel(toggle)); 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.setAttribute('aria-expanded', String(open));
panel.classList.toggle('js-open', open); panel.classList.toggle('js-open', open);
}; };
@@ -499,7 +514,7 @@
}); });
let closeTimer; let closeTimer;
parent.addEventListener('mouseenter', () => { window.clearTimeout(closeTimer); setOpen(true); }); 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'); const flyoutToggles = document.querySelectorAll('#Mega-Menu .flyout-toggle');
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "mega-menu-client-side-solution", "name": "mega-menu-client-side-solution",
"id": "f4660e06-ce08-43ee-bfb7-5c4464e01133", "id": "f4660e06-ce08-43ee-bfb7-5c4464e01133",
"version": "2.2.0.0", "version": "2.2.2.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "2.2.0", "version": "2.2.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mega-menu", "name": "mega-menu",
"version": "2.2.0", "version": "2.2.2",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
+14 -4
View File
@@ -70,6 +70,15 @@ $menuLayer: var(--megaMenuZIndex, 6000);
padding: 0; 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 { .mega-menu-mobile-toggle {
display: none; display: none;
} }
@@ -132,8 +141,8 @@ $menuLayer: var(--megaMenuZIndex, 6000);
.menu-item-text:hover, .menu-item-text:hover,
.menu-item-toggle:hover, .menu-item-toggle:hover,
.menu-item-toggle[aria-expanded='true'] { .menu-item-toggle[aria-expanded='true'] {
background: $neutralLighter; background: transparent;
color: $neutralPrimary; color: inherit;
text-decoration: none; text-decoration: none;
} }
@@ -309,8 +318,9 @@ $menuLayer: var(--megaMenuZIndex, 6000);
outline: none; outline: none;
} }
.flyout-link:hover, .flyout-item:hover > .flyout-link,
.flyout-item:focus-within > .flyout-link, .flyout-item:focus-within > .flyout-link,
.flyout-item:active > .flyout-link,
.flyout-item.is-active > .flyout-link { .flyout-item.is-active > .flyout-link {
background: $neutralLighter; background: $neutralLighter;
color: $neutralPrimary; color: $neutralPrimary;
@@ -461,7 +471,7 @@ $menuLayer: var(--megaMenuZIndex, 6000);
.menu-item-toggle { .menu-item-toggle {
width: 44px; width: 44px;
margin: 0; margin: 0;
border-left: 1px solid $neutralLight; border-left: 0;
} }
.mega-menu { .mega-menu {
+4 -2
View File
@@ -23,8 +23,8 @@ export interface IMegaMenuRendererStrings {
} }
export class MegaMenuRenderer { export class MegaMenuRenderer {
private static readonly hoverOpenDelayMs: number = 140; private static readonly hoverOpenDelayMs: number = 0;
private static readonly hoverCloseDelayMs: number = 180; private static readonly hoverCloseDelayMs: number = 0;
private _container: HTMLElement | undefined; private _container: HTMLElement | undefined;
private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined; private _documentKeydownHandler: ((event: KeyboardEvent) => void) | undefined;
private _documentClickHandler: ((event: MouseEvent) => void) | undefined; private _documentClickHandler: ((event: MouseEvent) => void) | undefined;
@@ -657,6 +657,7 @@ export class MegaMenuRenderer {
const label: string = this.getTriggerLabel(trigger); const label: string = this.getTriggerLabel(trigger);
trigger.setAttribute('aria-expanded', 'true'); trigger.setAttribute('aria-expanded', 'true');
trigger.setAttribute('aria-label', this.strings.closeSubmenu + ' ' + label); trigger.setAttribute('aria-label', this.strings.closeSubmenu + ' ' + label);
(trigger.parentElement as HTMLElement).classList.add('is-open');
menu.setAttribute('aria-expanded', 'true'); menu.setAttribute('aria-expanded', 'true');
menu.classList.add('js-open'); menu.classList.add('js-open');
this.announce(label + ' ' + this.strings.opened); this.announce(label + ' ' + this.strings.opened);
@@ -667,6 +668,7 @@ export class MegaMenuRenderer {
const label: string = this.getTriggerLabel(trigger); const label: string = this.getTriggerLabel(trigger);
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-expanded', 'false');
trigger.setAttribute('aria-label', this.strings.openSubmenu + ' ' + label); trigger.setAttribute('aria-label', this.strings.openSubmenu + ' ' + label);
(trigger.parentElement as HTMLElement).classList.remove('is-open');
menu.setAttribute('aria-expanded', 'false'); menu.setAttribute('aria-expanded', 'false');
menu.classList.remove('js-open'); menu.classList.remove('js-open');
this.announce(label + ' ' + this.strings.closed); this.announce(label + ' ' + this.strings.closed);
+15 -2
View File
@@ -1,13 +1,16 @@
import { ITerm, ITermLabelCollection } from './ISPTermStorePickerService'; import { ITerm, ITermLabelCollection } from './ISPTermStorePickerService';
export const MegaMenuCacheSchemaVersion: string = '3'; export const MegaMenuCacheSchemaVersion: string = '4';
export const MegaMenuTermProperties = { export const MegaMenuTermProperties = {
hidden: 'MegaMenu.Hidden', hidden: 'MegaMenu.Hidden',
openInNewWindow: 'MegaMenu.OpenInNewWindow', openInNewWindow: 'MegaMenu.OpenInNewWindow',
description: 'MegaMenu.Description' description: 'MegaMenu.Description',
excludedProviders: '_Sys_Nav_ExcludedProviders'
}; };
export const GlobalNavigationTaxonomyProvider: string = 'GlobalNavigationTaxonomyProvider';
export interface IMegaMenuCacheEnvelope<T> { export interface IMegaMenuCacheEnvelope<T> {
expiresAt: number; expiresAt: number;
value: T; value: T;
@@ -48,6 +51,16 @@ export function isTermHidden(term: ITerm): boolean {
return parseBooleanProperty(getTermProperty(term, MegaMenuTermProperties.hidden)); 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 { export function getLocalizedTermLabel(term: ITerm, languageLcid: number): string {
const labels: ITermLabelCollection | undefined = term && term.Labels; const labels: ITermLabelCollection | undefined = term && term.Labels;
const items = labels && labels._Child_Items_ ? labels._Child_Items_ : []; const items = labels && labels._Child_Items_ ? labels._Child_Items_ : [];
+9 -2
View File
@@ -6,7 +6,12 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import SPTermStorePickerService from './SPTermStorePickerService'; import SPTermStorePickerService from './SPTermStorePickerService';
import { ITerm, ITermSet } from './ISPTermStorePickerService'; import { ITerm, ITermSet } from './ISPTermStorePickerService';
import { debugLog, debugWarn } from './MegaMenuDebug'; import { debugLog, debugWarn } from './MegaMenuDebug';
import { buildMenuHierarchy, IMegaMenuHierarchyEntry, isTermHidden } from './MegaMenuCore'; import {
buildMenuHierarchy,
IMegaMenuHierarchyEntry,
isTermExcludedFromGlobalNavigation,
isTermHidden
} from './MegaMenuCore';
const LOG_SOURCE: string = 'TaxonomyNavigationService'; 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.'); 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); const menuItem: IMenuItem = new MenuItem(term, 0, siteCollectionUrl, this.languageLcid);
entries.push({ entries.push({
id: term.Id, id: term.Id,
+16
View File
@@ -6,6 +6,7 @@ import {
getTermProperty, getTermProperty,
isExternalNavigationUrl, isExternalNavigationUrl,
isNavigationUrlActive, isNavigationUrlActive,
isTermExcludedFromGlobalNavigation,
isTermHidden, isTermHidden,
MegaMenuTermProperties, MegaMenuTermProperties,
parseBooleanProperty, 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 accepts German yes', parseBooleanProperty('ja'));
assert('boolean property rejects false', !parseBooleanProperty('false')); assert('boolean property rejects false', !parseBooleanProperty('false'));
assert('hidden local term property', isTermHidden(term({ LocalCustomProperties: { 'MegaMenu.Hidden': 'true' } }))); 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('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))); assert('open-in-new-window local term property', parseBooleanProperty(getTermProperty(term({ LocalCustomProperties: { 'MegaMenu.OpenInNewWindow': '1' } }), MegaMenuTermProperties.openInNewWindow)));