mirror of
https://github.com/expressjs/expressjs.com.git
synced 2026-02-21 19:41:33 +00:00
feat: version switcher controlling menu only
This commit is contained in:
@@ -214,6 +214,46 @@
|
||||
background-color: var(--color-bg-inverse);
|
||||
}
|
||||
|
||||
.sidebar-nav-item--nested.sidebar-nav-item--inactive::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-nav-item--nested.sidebar-nav-item--inactive {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.sidebar-nav-item--hidden,
|
||||
.sidebar-section--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sidebar-version-warning {
|
||||
background-color: var(--color-bg-warning, #fff3cd);
|
||||
border: 1px solid var(--color-border-warning, #ffc107);
|
||||
border-radius: var(--radius-base);
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
|
||||
p {
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: underline;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav-icon {
|
||||
padding: var(--space-2);
|
||||
border-radius: var(--radius-base);
|
||||
|
||||
@@ -71,6 +71,15 @@ export class SidebarController {
|
||||
this.initialActiveSubmenuPath = [...this.activeSubmenuPath];
|
||||
this.versionManager?.updatePath(this.activeSubmenuPath);
|
||||
|
||||
// Check version compatibility for the active submenu
|
||||
const activeSubmenuId = this.activeSubmenuPath[this.activeSubmenuPath.length - 1];
|
||||
const activeSubmenuColumn = this.sidebar?.querySelector(
|
||||
`[data-parent-id="${activeSubmenuId}"]`
|
||||
) as HTMLElement;
|
||||
if (activeSubmenuColumn) {
|
||||
this.checkVersionCompatibility(activeSubmenuColumn);
|
||||
}
|
||||
|
||||
if (this.navContainer) {
|
||||
this.navContainer.dataset.currentNavLevel = String(initialActiveLevel);
|
||||
}
|
||||
@@ -117,6 +126,9 @@ export class SidebarController {
|
||||
this.activeLevel = level;
|
||||
this.versionManager?.updatePath(this.activeSubmenuPath);
|
||||
|
||||
// Check version compatibility for this submenu
|
||||
this.checkVersionCompatibility(submenuColumn);
|
||||
|
||||
this.updateActiveColumns();
|
||||
|
||||
if (this.navContainer) {
|
||||
@@ -129,6 +141,41 @@ export class SidebarController {
|
||||
}, TRANSITION_DURATION);
|
||||
}
|
||||
|
||||
private checkVersionCompatibility(submenuColumn: HTMLElement): void {
|
||||
const supportedVersions = submenuColumn.dataset.supportedVersions?.split(',') || [];
|
||||
const currentMenuVersion = this.versionManager?.getVersion() || '';
|
||||
const urlVersion = this.versionManager?.getUrlVersion() || '';
|
||||
const warningElement = submenuColumn.querySelector('[data-version-warning]') as HTMLElement;
|
||||
|
||||
if (!warningElement) return;
|
||||
|
||||
// If no versions are supported (non-versioned submenu), hide warning
|
||||
if (supportedVersions.length === 0 || supportedVersions[0] === '') {
|
||||
warningElement.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// If current menu version is not supported by this submenu
|
||||
if (!supportedVersions.includes(currentMenuVersion)) {
|
||||
// Auto-revert to URL version if supported, otherwise to latest version
|
||||
const targetVersion = supportedVersions.includes(urlVersion)
|
||||
? urlVersion
|
||||
: supportedVersions[0]; // First version is the latest
|
||||
|
||||
// Only show warning if we're switching from v3
|
||||
if (currentMenuVersion === '3x') {
|
||||
warningElement.style.display = 'block';
|
||||
} else {
|
||||
warningElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Auto-switch to compatible version
|
||||
this.versionManager?.setVersion(targetVersion);
|
||||
} else {
|
||||
warningElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
private navigateBack(): void {
|
||||
if (this.activeSubmenuPath.length <= 1) return;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import type { MenuItem, VersionPrefix } from '@/config/types';
|
||||
import SidebarNavItem from './SidebarNavItem.astro';
|
||||
import { isLink, hasSubmenu, filterItems, getItemId, resolveHref, normalizePath } from './utils';
|
||||
import { isLink, hasSubmenu, getItemId, resolveHref, normalizePath } from './utils';
|
||||
|
||||
interface Props {
|
||||
items: MenuItem[];
|
||||
@@ -31,7 +31,8 @@ const {
|
||||
targetLevel,
|
||||
} = Astro.props;
|
||||
|
||||
const filteredItems = filterItems(items, version);
|
||||
// Don't filter items server-side - render all items and filter client-side based on selected version
|
||||
const filteredItems = items;
|
||||
---
|
||||
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
import type { Menu, VersionPrefix } from '@/config/types';
|
||||
import type { VersionConfig } from '@/components/patterns/VersionSwitcher/types';
|
||||
import { Body } from '@/components/primitives';
|
||||
import { Body, BodyMd } from '@/components/primitives';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { VersionSwitcher } from '@/components/patterns';
|
||||
import SidebarItemsList from './SidebarItemsList.astro';
|
||||
import {
|
||||
shouldOmitSection,
|
||||
collectAllSubmenus,
|
||||
submenuContainsCurrentPath,
|
||||
calculateInitialActiveLevel,
|
||||
@@ -63,28 +62,26 @@ const initialActiveLevel =
|
||||
>
|
||||
<nav class="sidebar-nav" aria-label="Main navigation">
|
||||
<ul class="sidebar-nav-list">
|
||||
{menu.sections
|
||||
?.filter((section) => !shouldOmitSection(section, version))
|
||||
.map((section, sectionIndex) => (
|
||||
<li class="sidebar-section">
|
||||
{section.title && <Body as="h3">{section.title}</Body>}
|
||||
<ul class="sidebar-section-list">
|
||||
<SidebarItemsList
|
||||
items={section.items}
|
||||
parentId={parentId}
|
||||
sectionIndex={sectionIndex}
|
||||
currentPath={currentPath}
|
||||
lang={lang}
|
||||
basePath={basePath}
|
||||
versioned={versioned}
|
||||
version={version}
|
||||
variant="root"
|
||||
showIcon={true}
|
||||
targetLevel={level + 1}
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
{menu.sections?.map((section, sectionIndex) => (
|
||||
<li class="sidebar-section" data-omit-from={section.omitFrom?.join(',') || undefined}>
|
||||
{section.title && <Body as="h3">{section.title}</Body>}
|
||||
<ul class="sidebar-section-list">
|
||||
<SidebarItemsList
|
||||
items={section.items}
|
||||
parentId={parentId}
|
||||
sectionIndex={sectionIndex}
|
||||
currentPath={currentPath}
|
||||
lang={lang}
|
||||
basePath={basePath}
|
||||
versioned={versioned}
|
||||
version={version}
|
||||
variant="root"
|
||||
showIcon={true}
|
||||
targetLevel={level + 1}
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
<SidebarItemsList
|
||||
items={menu.items ?? []}
|
||||
parentId={parentId}
|
||||
@@ -133,6 +130,7 @@ const initialActiveLevel =
|
||||
panelHasActiveItem && 'sidebar-nav-panel--active',
|
||||
]}
|
||||
data-parent-id={submenu.id}
|
||||
data-supported-versions={availableVersions.map((v) => v.id).join(',')}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{availableVersions.length > 0 && (
|
||||
@@ -145,6 +143,20 @@ const initialActiveLevel =
|
||||
data-version-switcher
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
class="sidebar-version-warning"
|
||||
data-version-warning
|
||||
aria-live="polite"
|
||||
style="display: none;"
|
||||
>
|
||||
<BodyMd vMargin={false}>
|
||||
<strong>Note:</strong> There is no documentation available for v3.x.
|
||||
</BodyMd>
|
||||
<BodyMd vMargin={false}>
|
||||
Express v3 has reached end-of-life. For support information, visit the{' '}
|
||||
<a href={`/${lang}/support`}>support page</a>.
|
||||
</BodyMd>
|
||||
</div>
|
||||
<nav class="sidebar-nav" aria-label={`${submenu.title} navigation`}>
|
||||
<button
|
||||
class="sidebar-nav-back"
|
||||
@@ -164,32 +176,33 @@ const initialActiveLevel =
|
||||
</Body>
|
||||
</button>
|
||||
<div class="sidebar-nav-content">
|
||||
{submenu.menu.sections
|
||||
?.filter((section) => !shouldOmitSection(section, version))
|
||||
.map((section, sectionIndex) => (
|
||||
<div class="sidebar-section">
|
||||
{section.title && (
|
||||
<Body as="h3" weight="bold" aria-label={`Section: ${section.title}`}>
|
||||
{section.title}
|
||||
</Body>
|
||||
)}
|
||||
<ul class="sidebar-section-list">
|
||||
<SidebarItemsList
|
||||
items={section.items}
|
||||
parentId={submenu.id}
|
||||
sectionIndex={sectionIndex}
|
||||
currentPath={currentPath}
|
||||
lang={lang}
|
||||
basePath={submenu.basePath}
|
||||
versioned={submenu.versioned}
|
||||
version={version}
|
||||
variant="nested"
|
||||
showIcon={false}
|
||||
targetLevel={navLevel + 1}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
{submenu.menu.sections?.map((section, sectionIndex) => (
|
||||
<div
|
||||
class="sidebar-section"
|
||||
data-omit-from={section.omitFrom?.join(',') || undefined}
|
||||
>
|
||||
{section.title && (
|
||||
<Body as="h3" weight="bold" aria-label={`Section: ${section.title}`}>
|
||||
{section.title}
|
||||
</Body>
|
||||
)}
|
||||
<ul class="sidebar-section-list">
|
||||
<SidebarItemsList
|
||||
items={section.items}
|
||||
parentId={submenu.id}
|
||||
sectionIndex={sectionIndex}
|
||||
currentPath={currentPath}
|
||||
lang={lang}
|
||||
basePath={submenu.basePath}
|
||||
versioned={submenu.versioned}
|
||||
version={version}
|
||||
variant="nested"
|
||||
showIcon={false}
|
||||
targetLevel={navLevel + 1}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
{submenu.menu.items && submenu.menu.items.length > 0 && (
|
||||
<ul class="sidebar-section-list">
|
||||
<SidebarItemsList
|
||||
|
||||
@@ -65,6 +65,7 @@ const {
|
||||
data-submenu-trigger
|
||||
data-target-id={submenuId}
|
||||
data-target-level={targetLevel}
|
||||
data-omit-from={item.omitFrom?.join(',') || undefined}
|
||||
>
|
||||
{showIcon && item.icon && (
|
||||
<div class="sidebar-nav-icon">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
export class SidebarVersionManager {
|
||||
private sidebar: HTMLElement;
|
||||
private currentVersion: string;
|
||||
private urlVersion: string; // Version from the URL (content version)
|
||||
private menuVersion: string; // Version selected in the menu switcher
|
||||
private activeSubmenuPath: string[];
|
||||
|
||||
constructor(sidebar: HTMLElement, currentVersion: string, activeSubmenuPath: string[]) {
|
||||
this.sidebar = sidebar;
|
||||
this.currentVersion = currentVersion;
|
||||
this.urlVersion = currentVersion;
|
||||
this.menuVersion = currentVersion;
|
||||
this.activeSubmenuPath = activeSubmenuPath;
|
||||
}
|
||||
|
||||
@@ -20,11 +22,45 @@ export class SidebarVersionManager {
|
||||
this.handleVersionChange(select.value);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for in-page link clicks to revert menu to URL version
|
||||
this.setupInPageLinkListener();
|
||||
|
||||
// Initialize menu visibility for current URL version
|
||||
this.updateMenuForVersion(this.urlVersion);
|
||||
}
|
||||
|
||||
private setupInPageLinkListener(): void {
|
||||
// Listen for clicks on any links in the main content area
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const link = target.closest('a');
|
||||
|
||||
// Check if it's a link and not in the sidebar
|
||||
if (link && !this.sidebar.contains(link)) {
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
// If it's an internal link (not external, not hash-only)
|
||||
if (href && !href.startsWith('http') && !href.startsWith('#')) {
|
||||
// Revert menu to URL version when clicking internal links
|
||||
this.revertToUrlVersion();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleVersionChange(newVersion: string): void {
|
||||
const previousVersion = this.currentVersion;
|
||||
this.currentVersion = newVersion;
|
||||
const previousVersion = this.menuVersion;
|
||||
this.menuVersion = newVersion;
|
||||
|
||||
// Sync all version switchers to the same value
|
||||
const versionSelects =
|
||||
this.sidebar.querySelectorAll<HTMLSelectElement>('[data-version-select]');
|
||||
versionSelects.forEach((select) => {
|
||||
if (select.value !== newVersion) {
|
||||
select.value = newVersion;
|
||||
}
|
||||
});
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('sidebar:versionChange', {
|
||||
@@ -32,82 +68,89 @@ export class SidebarVersionManager {
|
||||
})
|
||||
);
|
||||
|
||||
const currentPath = this.sidebar.dataset.currentPath || '';
|
||||
// Update menu visibility based on the new version
|
||||
this.updateMenuForVersion(newVersion);
|
||||
|
||||
if (currentPath.includes(`/${previousVersion}/`)) {
|
||||
if (this.isPathOmittedForVersion(currentPath, newVersion)) {
|
||||
const fallbackPath = this.getFirstAvailableLinkForVersion(newVersion, previousVersion);
|
||||
if (fallbackPath) {
|
||||
window.location.href = fallbackPath;
|
||||
return;
|
||||
}
|
||||
// Update link hrefs to point to the new version
|
||||
this.updateLinkVersions(previousVersion, newVersion);
|
||||
|
||||
// Update active states - remove them if menu version doesn't match URL version
|
||||
this.updateActiveStates();
|
||||
}
|
||||
|
||||
private updateMenuForVersion(version: string): void {
|
||||
// Show/hide menu items based on omitFrom attribute
|
||||
const allMenuItems = this.sidebar.querySelectorAll('[data-omit-from]');
|
||||
|
||||
allMenuItems.forEach((item) => {
|
||||
const omitFrom = (item as HTMLElement).dataset.omitFrom?.split(',') || [];
|
||||
const shouldHide = omitFrom.includes(version);
|
||||
|
||||
if (shouldHide) {
|
||||
item.classList.add('sidebar-nav-item--hidden');
|
||||
item.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
item.classList.remove('sidebar-nav-item--hidden');
|
||||
item.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
window.location.href = currentPath.replace(`/${previousVersion}/`, `/${newVersion}/`);
|
||||
return;
|
||||
}
|
||||
// Show/hide sections based on omitFrom attribute
|
||||
const allSections = this.sidebar.querySelectorAll('.sidebar-section[data-omit-from]');
|
||||
|
||||
if (this.isInVersionedSubmenu()) {
|
||||
const fallbackPath = this.getFirstAvailableLinkForVersion(newVersion, previousVersion);
|
||||
if (fallbackPath) {
|
||||
window.location.href = fallbackPath;
|
||||
allSections.forEach((section) => {
|
||||
const omitFrom = (section as HTMLElement).dataset.omitFrom?.split(',') || [];
|
||||
const shouldHide = omitFrom.includes(version);
|
||||
|
||||
if (shouldHide) {
|
||||
section.classList.add('sidebar-section--hidden');
|
||||
section.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
section.classList.remove('sidebar-section--hidden');
|
||||
section.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateLinkVersions(previousVersion: string, newVersion: string): void {
|
||||
// Update all versioned links to point to the new version
|
||||
const allLinks = this.sidebar.querySelectorAll<HTMLAnchorElement>('a.sidebar-nav-item[href]');
|
||||
|
||||
allLinks.forEach((link) => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && href.includes(`/${previousVersion}/`)) {
|
||||
const newHref = href.replace(`/${previousVersion}/`, `/${newVersion}/`);
|
||||
link.setAttribute('href', newHref);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateActiveStates(): void {
|
||||
// If menu version doesn't match URL version, remove all active states
|
||||
const allActiveItems = this.sidebar.querySelectorAll('.sidebar-nav-item--active');
|
||||
|
||||
if (this.menuVersion !== this.urlVersion) {
|
||||
allActiveItems.forEach((item) => {
|
||||
item.classList.add('sidebar-nav-item--inactive');
|
||||
item.classList.remove('sidebar-nav-item--active');
|
||||
});
|
||||
} else {
|
||||
allActiveItems.forEach((item) => {
|
||||
item.classList.remove('sidebar-nav-item--inactive');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isInVersionedSubmenu(): boolean {
|
||||
const activeSubmenuId = this.activeSubmenuPath[this.activeSubmenuPath.length - 1];
|
||||
if (activeSubmenuId === 'root') return false;
|
||||
|
||||
const activePanel = this.sidebar.querySelector(
|
||||
`[data-parent-id="${activeSubmenuId}"]`
|
||||
) as HTMLElement;
|
||||
|
||||
if (!activePanel) return false;
|
||||
|
||||
// Check if the active panel contains versioned links
|
||||
const versionedLink = activePanel.querySelector('a[href*="/4x/"], a[href*="/5x/"]');
|
||||
return versionedLink !== null;
|
||||
}
|
||||
|
||||
private isPathOmittedForVersion(currentPath: string, targetVersion: string): boolean {
|
||||
const activeLink = this.sidebar.querySelector(
|
||||
'a.sidebar-nav-item--active[data-omit-from]'
|
||||
) as HTMLAnchorElement;
|
||||
|
||||
if (!activeLink) return false;
|
||||
|
||||
const omitFrom = activeLink.dataset.omitFrom?.split(',') || [];
|
||||
return omitFrom.includes(targetVersion);
|
||||
}
|
||||
|
||||
private getFirstAvailableLinkForVersion(
|
||||
targetVersion: string,
|
||||
previousVersion: string
|
||||
): string | null {
|
||||
const activeSubmenuId = this.activeSubmenuPath[this.activeSubmenuPath.length - 1];
|
||||
const activePanel = this.sidebar.querySelector(
|
||||
`[data-parent-id="${activeSubmenuId}"]`
|
||||
) as HTMLElement;
|
||||
|
||||
if (!activePanel) return null;
|
||||
|
||||
const links = activePanel.querySelectorAll(
|
||||
'a.sidebar-nav-item[href]'
|
||||
) as NodeListOf<HTMLAnchorElement>;
|
||||
|
||||
for (const link of links) {
|
||||
const omitFrom = link.dataset.omitFrom?.split(',') || [];
|
||||
if (!omitFrom.includes(targetVersion)) {
|
||||
const href = link.getAttribute('href') || '';
|
||||
if (href.includes(`/${previousVersion}/`)) {
|
||||
return href.replace(`/${previousVersion}/`, `/${targetVersion}/`);
|
||||
}
|
||||
return href;
|
||||
}
|
||||
public revertToUrlVersion(): void {
|
||||
// Revert menu back to URL version
|
||||
if (this.menuVersion !== this.urlVersion) {
|
||||
const versionSelects =
|
||||
this.sidebar.querySelectorAll<HTMLSelectElement>('[data-version-select]');
|
||||
versionSelects.forEach((select) => {
|
||||
select.value = this.urlVersion;
|
||||
});
|
||||
this.handleVersionChange(this.urlVersion);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
updateVisibility(activeLevel: number): void {
|
||||
@@ -131,7 +174,11 @@ export class SidebarVersionManager {
|
||||
}
|
||||
|
||||
getVersion(): string {
|
||||
return this.currentVersion;
|
||||
return this.menuVersion;
|
||||
}
|
||||
|
||||
getUrlVersion(): string {
|
||||
return this.urlVersion;
|
||||
}
|
||||
|
||||
setVersion(version: string): void {
|
||||
|
||||
Reference in New Issue
Block a user