mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 18:58:36 +02:00
feat(v2): allow any type of NavbarItem
to be passed in a navbar dropdown (#5072)
* Initial work Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * More elegant `eslint-disable` Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Fix typing Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Allow doc links in dropdown Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Allow more dropdowns to use linklike items Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Finalize Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Dogfood Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Disallow nested dropdowns Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Better typing Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Complete type fix Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Use flatmap Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Patch Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Test Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Try fix Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Style change Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Revert (to test) Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Hmmm Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * I know what's wrong Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Does this work? Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Nope Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Wrong class name Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Pass prop to render dropdown item differently Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * This looks better Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Tests passed Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Separate dropdown from default Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Pourquois? Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Better prop typing Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Make code simpler Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * There's some extra className Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Test Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * More test Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * A-ha! Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Test again? * Add backward compatibility Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Incorporate my type fix Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Resolve conflict Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * minor refactor * minor refactors * allow usage of ES2019 in browser code * revert NavLink rename Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
38c6cb5faa
commit
007e901354
13 changed files with 298 additions and 238 deletions
|
@ -8,7 +8,7 @@
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
declare module '@docusaurus/plugin-content-docs-types' {
|
declare module '@docusaurus/plugin-content-docs-types' {
|
||||||
import type {VersionBanner} from './types';
|
type VersionBanner = import('./types').VersionBanner;
|
||||||
|
|
||||||
export type PropVersionMetadata = {
|
export type PropVersionMetadata = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
|
|
|
@ -58,7 +58,7 @@ function UnmaintainedVersionLabel({
|
||||||
}
|
}
|
||||||
|
|
||||||
const BannerLabelComponents: Record<
|
const BannerLabelComponents: Record<
|
||||||
Props['versionMetadata']['banner'],
|
Exclude<Props['versionMetadata']['banner'], 'none'>,
|
||||||
ComponentType<BannerLabelComponentProps>
|
ComponentType<BannerLabelComponentProps>
|
||||||
> = {
|
> = {
|
||||||
unreleased: UnreleasedVersionLabel,
|
unreleased: UnreleasedVersionLabel,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import useHideableNavbar from '@theme/hooks/useHideableNavbar';
|
import useHideableNavbar from '@theme/hooks/useHideableNavbar';
|
||||||
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
|
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
|
||||||
import useWindowSize from '@theme/hooks/useWindowSize';
|
import useWindowSize from '@theme/hooks/useWindowSize';
|
||||||
import NavbarItem from '@theme/NavbarItem';
|
import NavbarItem, {Props as NavbarItemConfig} from '@theme/NavbarItem';
|
||||||
import Logo from '@theme/Logo';
|
import Logo from '@theme/Logo';
|
||||||
import IconMenu from '@theme/IconMenu';
|
import IconMenu from '@theme/IconMenu';
|
||||||
|
|
||||||
|
@ -28,9 +28,14 @@ import styles from './styles.module.css';
|
||||||
// retrocompatible with v1
|
// retrocompatible with v1
|
||||||
const DefaultNavItemPosition = 'right';
|
const DefaultNavItemPosition = 'right';
|
||||||
|
|
||||||
|
function useNavbarItems() {
|
||||||
|
// TODO temporary casting until ThemeConfig type is improved
|
||||||
|
return useThemeConfig().navbar.items as NavbarItemConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
// If split links by left/right
|
// If split links by left/right
|
||||||
// if position is unspecified, fallback to right (as v1)
|
// if position is unspecified, fallback to right (as v1)
|
||||||
function splitNavItemsByPosition(items) {
|
function splitNavItemsByPosition(items: NavbarItemConfig[]) {
|
||||||
const leftItems = items.filter(
|
const leftItems = items.filter(
|
||||||
(item) => (item.position ?? DefaultNavItemPosition) === 'left',
|
(item) => (item.position ?? DefaultNavItemPosition) === 'left',
|
||||||
);
|
);
|
||||||
|
@ -132,9 +137,7 @@ function NavbarMobileSidebar({
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
}: NavbarMobileSidebarProps) {
|
}: NavbarMobileSidebarProps) {
|
||||||
useLockBodyScroll(sidebarShown);
|
useLockBodyScroll(sidebarShown);
|
||||||
const {
|
const items = useNavbarItems();
|
||||||
navbar: {items},
|
|
||||||
} = useThemeConfig();
|
|
||||||
|
|
||||||
const colorModeToggle = useColorModeToggle();
|
const colorModeToggle = useColorModeToggle();
|
||||||
|
|
||||||
|
@ -166,12 +169,7 @@ function NavbarMobileSidebar({
|
||||||
<div className="menu">
|
<div className="menu">
|
||||||
<ul className="menu__list">
|
<ul className="menu__list">
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<NavbarItem
|
<NavbarItem mobile {...item} onClick={toggleSidebar} key={i} />
|
||||||
mobile
|
|
||||||
{...(item as any)} // TODO fix typing
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
key={i}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -196,7 +194,7 @@ function NavbarMobileSidebar({
|
||||||
|
|
||||||
function Navbar(): JSX.Element {
|
function Navbar(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
navbar: {items, hideOnScroll, style},
|
navbar: {hideOnScroll, style},
|
||||||
} = useThemeConfig();
|
} = useThemeConfig();
|
||||||
|
|
||||||
const mobileSidebar = useMobileSidebar();
|
const mobileSidebar = useMobileSidebar();
|
||||||
|
@ -204,6 +202,7 @@ function Navbar(): JSX.Element {
|
||||||
|
|
||||||
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
|
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
|
||||||
|
|
||||||
|
const items = useNavbarItems();
|
||||||
const hasSearchNavbarItem = items.some((item) => item.type === 'search');
|
const hasSearchNavbarItem = items.some((item) => item.type === 'search');
|
||||||
const {leftItems, rightItems} = splitNavItemsByPosition(items);
|
const {leftItems, rightItems} = splitNavItemsByPosition(items);
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,10 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState, useRef, useEffect} from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
import {useLocation} from '@docusaurus/router';
|
|
||||||
import {
|
|
||||||
isSamePath,
|
|
||||||
useCollapsible,
|
|
||||||
Collapsible,
|
|
||||||
} from '@docusaurus/theme-common';
|
|
||||||
import type {
|
import type {
|
||||||
NavLinkProps,
|
NavLinkProps,
|
||||||
DesktopOrMobileNavBarItemProps,
|
DesktopOrMobileNavBarItemProps,
|
||||||
|
@ -25,7 +19,7 @@ import isInternalUrl from '@docusaurus/isInternalUrl';
|
||||||
|
|
||||||
const dropdownLinkActiveClass = 'dropdown__link--active';
|
const dropdownLinkActiveClass = 'dropdown__link--active';
|
||||||
|
|
||||||
function NavLink({
|
export function NavLink({
|
||||||
activeBasePath,
|
activeBasePath,
|
||||||
activeBaseRegex,
|
activeBaseRegex,
|
||||||
to,
|
to,
|
||||||
|
@ -34,7 +28,7 @@ function NavLink({
|
||||||
activeClassName = 'navbar__link--active',
|
activeClassName = 'navbar__link--active',
|
||||||
prependBaseUrlToHref,
|
prependBaseUrlToHref,
|
||||||
...props
|
...props
|
||||||
}: NavLinkProps) {
|
}: NavLinkProps): JSX.Element {
|
||||||
// TODO all this seems hacky
|
// TODO all this seems hacky
|
||||||
// {to: 'version'} should probably be forbidden, in favor of {to: '/version'}
|
// {to: 'version'} should probably be forbidden, in favor of {to: '/version'}
|
||||||
const toUrl = useBaseUrl(to);
|
const toUrl = useBaseUrl(to);
|
||||||
|
@ -75,159 +69,36 @@ function NavLink({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavItemDesktop({
|
function DefaultNavbarItemDesktop({
|
||||||
items,
|
|
||||||
position,
|
|
||||||
className,
|
className,
|
||||||
|
isDropdownItem = false,
|
||||||
...props
|
...props
|
||||||
}: DesktopOrMobileNavBarItemProps) {
|
}: DesktopOrMobileNavBarItemProps) {
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const dropdownMenuRef = useRef<HTMLUListElement>(null);
|
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (!dropdownRef.current || dropdownRef.current.contains(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowDropdown(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
document.addEventListener('touchstart', handleClickOutside);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
document.removeEventListener('touchstart', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [dropdownRef]);
|
|
||||||
|
|
||||||
const navLinkClassNames = (extraClassName?: string, isDropdownItem = false) =>
|
|
||||||
clsx(
|
|
||||||
{
|
|
||||||
'navbar__item navbar__link': !isDropdownItem,
|
|
||||||
dropdown__link: isDropdownItem,
|
|
||||||
},
|
|
||||||
extraClassName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!items) {
|
|
||||||
return <NavLink className={navLinkClassNames(className)} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
ref={dropdownRef}
|
|
||||||
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
|
||||||
'dropdown--left': position === 'left',
|
|
||||||
'dropdown--right': position === 'right',
|
|
||||||
'dropdown--show': showDropdown,
|
|
||||||
})}>
|
|
||||||
<NavLink
|
<NavLink
|
||||||
className={navLinkClassNames(className)}
|
className={clsx(
|
||||||
|
isDropdownItem ? 'dropdown__link' : 'navbar__item navbar__link',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
onClick={props.to ? undefined : (e) => e.preventDefault()}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowDropdown(!showDropdown);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{props.children ?? props.label}
|
|
||||||
</NavLink>
|
|
||||||
<ul ref={dropdownMenuRef} className="dropdown__menu">
|
|
||||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<NavLink
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (i === items.length - 1 && e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setShowDropdown(false);
|
|
||||||
|
|
||||||
const nextNavbarItem = (dropdownRef.current as HTMLElement)
|
|
||||||
.nextElementSibling;
|
|
||||||
|
|
||||||
if (nextNavbarItem) {
|
|
||||||
(nextNavbarItem as HTMLElement).focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
activeClassName={dropdownLinkActiveClass}
|
|
||||||
className={navLinkClassNames(childItemClassName, true)}
|
|
||||||
{...childItemProps}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavItemMobile({
|
function DefaultNavbarItemMobile({
|
||||||
items,
|
|
||||||
className,
|
className,
|
||||||
position: _position, // Need to destructure position from props so that it doesn't get passed on.
|
isDropdownItem: _isDropdownItem,
|
||||||
...props
|
...props
|
||||||
}: DesktopOrMobileNavBarItemProps) {
|
}: DesktopOrMobileNavBarItemProps) {
|
||||||
const {pathname} = useLocation();
|
|
||||||
const {collapsed, toggleCollapsed} = useCollapsible({
|
|
||||||
initialState: () =>
|
|
||||||
!items?.some((item) => isSamePath(item.to, pathname)) ?? true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const navLinkClassNames = (extraClassName?: string, isSubList = false) =>
|
|
||||||
clsx(
|
|
||||||
'menu__link',
|
|
||||||
{
|
|
||||||
'menu__link--sublist': isSubList,
|
|
||||||
},
|
|
||||||
extraClassName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!items) {
|
|
||||||
return (
|
return (
|
||||||
<li className="menu__list-item">
|
<li className="menu__list-item">
|
||||||
<NavLink className={navLinkClassNames(className)} {...props} />
|
<NavLink className={clsx('menu__link', className)} {...props} />
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
className={clsx('menu__list-item', {
|
|
||||||
'menu__list-item--collapsed': collapsed,
|
|
||||||
})}>
|
|
||||||
<NavLink
|
|
||||||
role="button"
|
|
||||||
className={navLinkClassNames(className, true)}
|
|
||||||
{...props}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleCollapsed();
|
|
||||||
}}>
|
|
||||||
{props.children ?? props.label}
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
|
|
||||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
|
||||||
<li className="menu__list-item" key={i}>
|
|
||||||
<NavLink
|
|
||||||
activeClassName="menu__link--active"
|
|
||||||
className={navLinkClassNames(childItemClassName)}
|
|
||||||
{...childItemProps}
|
|
||||||
onClick={props.onClick}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</Collapsible>
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DefaultNavbarItem({mobile = false, ...props}: Props): JSX.Element {
|
function DefaultNavbarItem({mobile = false, ...props}: Props): JSX.Element {
|
||||||
const Comp = mobile ? NavItemMobile : NavItemDesktop;
|
const Comp = mobile ? DefaultNavbarItemMobile : DefaultNavbarItemDesktop;
|
||||||
return <Comp {...props} />;
|
return <Comp {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,17 +12,10 @@ import clsx from 'clsx';
|
||||||
import type {Props} from '@theme/NavbarItem/DocNavbarItem';
|
import type {Props} from '@theme/NavbarItem/DocNavbarItem';
|
||||||
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
|
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
|
||||||
import {uniq} from '@docusaurus/utils-common';
|
import {uniq} from '@docusaurus/utils-common';
|
||||||
import type {
|
import type {GlobalDataVersion} from '@docusaurus/plugin-content-docs-types';
|
||||||
GlobalDataVersion,
|
|
||||||
GlobalDataDoc,
|
|
||||||
} from '@docusaurus/plugin-content-docs-types';
|
|
||||||
|
|
||||||
function getDocInVersions(versions: GlobalDataVersion[], docId: string) {
|
function getDocInVersions(versions: GlobalDataVersion[], docId: string) {
|
||||||
// vanilla-js flatten, TODO replace soon by ES flat() / flatMap()
|
const allDocs = versions.flatMap((version) => version.docs);
|
||||||
const allDocs: GlobalDataDoc[] = [].concat(
|
|
||||||
...versions.map((version) => version.docs),
|
|
||||||
);
|
|
||||||
|
|
||||||
const doc = allDocs.find((versionDoc) => versionDoc.id === docId);
|
const doc = allDocs.find((versionDoc) => versionDoc.id === docId);
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
const docIds = allDocs.map((versionDoc) => versionDoc.id).join('\n- ');
|
const docIds = allDocs.map((versionDoc) => versionDoc.id).join('\n- ');
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
import {
|
import {
|
||||||
useVersions,
|
useVersions,
|
||||||
useLatestVersion,
|
useLatestVersion,
|
||||||
|
@ -52,20 +53,7 @@ export default function DocsVersionDropdownNavbarItem({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = [
|
return [...dropdownItemsBefore, ...versionLinks, ...dropdownItemsAfter];
|
||||||
...dropdownItemsBefore,
|
|
||||||
...versionLinks,
|
|
||||||
...dropdownItemsAfter,
|
|
||||||
];
|
|
||||||
|
|
||||||
// We don't want to render a version dropdown with 0 or 1 item
|
|
||||||
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0'])
|
|
||||||
// We'd rather render a button instead of a dropdown
|
|
||||||
if (items.length <= 1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = getItems();
|
const items = getItems();
|
||||||
|
@ -78,8 +66,23 @@ export default function DocsVersionDropdownNavbarItem({
|
||||||
const dropdownTo =
|
const dropdownTo =
|
||||||
mobile && items ? undefined : getVersionMainDoc(dropdownVersion).path;
|
mobile && items ? undefined : getVersionMainDoc(dropdownVersion).path;
|
||||||
|
|
||||||
|
// We don't want to render a version dropdown with 0 or 1 item
|
||||||
|
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0'])
|
||||||
|
// We'd rather render a button instead of a dropdown
|
||||||
|
if (items.length <= 1) {
|
||||||
return (
|
return (
|
||||||
<DefaultNavbarItem
|
<DefaultNavbarItem
|
||||||
|
{...props}
|
||||||
|
mobile={mobile}
|
||||||
|
label={dropdownLabel}
|
||||||
|
to={dropdownTo}
|
||||||
|
isActive={dropdownActiveClassDisabled ? () => false : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownNavbarItem
|
||||||
{...props}
|
{...props}
|
||||||
mobile={mobile}
|
mobile={mobile}
|
||||||
label={dropdownLabel}
|
label={dropdownLabel}
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useState, useRef, useEffect} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {useLocation} from '@docusaurus/router';
|
||||||
|
import {
|
||||||
|
isSamePath,
|
||||||
|
useCollapsible,
|
||||||
|
Collapsible,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
import type {
|
||||||
|
DesktopOrMobileNavBarItemProps,
|
||||||
|
Props,
|
||||||
|
} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
|
import {NavLink} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import NavbarItem from '@theme/NavbarItem';
|
||||||
|
|
||||||
|
const dropdownLinkActiveClass = 'dropdown__link--active';
|
||||||
|
|
||||||
|
function DropdownNavbarItemDesktop({
|
||||||
|
items,
|
||||||
|
position,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DesktopOrMobileNavBarItemProps) {
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dropdownMenuRef = useRef<HTMLUListElement>(null);
|
||||||
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (
|
||||||
|
!dropdownRef.current ||
|
||||||
|
dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('touchstart', handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('touchstart', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [dropdownRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
||||||
|
'dropdown--left': position === 'left',
|
||||||
|
'dropdown--right': position === 'right',
|
||||||
|
'dropdown--show': showDropdown,
|
||||||
|
})}>
|
||||||
|
<NavLink
|
||||||
|
className={clsx('navbar__item navbar__link', className)}
|
||||||
|
{...props}
|
||||||
|
onClick={props.to ? undefined : (e) => e.preventDefault()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowDropdown(!showDropdown);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{props.children ?? props.label}
|
||||||
|
</NavLink>
|
||||||
|
<ul ref={dropdownMenuRef} className="dropdown__menu">
|
||||||
|
{items.map((childItemProps, i) => (
|
||||||
|
<NavbarItem
|
||||||
|
isDropdownItem
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (i === items.length - 1 && e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowDropdown(false);
|
||||||
|
const nextNavbarItem = dropdownRef.current!.nextElementSibling;
|
||||||
|
if (nextNavbarItem) {
|
||||||
|
(nextNavbarItem as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
activeClassName={dropdownLinkActiveClass}
|
||||||
|
{...childItemProps}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownNavbarItemMobile({
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
position: _position, // Need to destructure position from props so that it doesn't get passed on.
|
||||||
|
...props
|
||||||
|
}: DesktopOrMobileNavBarItemProps) {
|
||||||
|
const {pathname} = useLocation();
|
||||||
|
const {collapsed, toggleCollapsed} = useCollapsible({
|
||||||
|
initialState: () =>
|
||||||
|
!items?.some((item) => isSamePath(item.to, pathname)) ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={clsx('menu__list-item', {
|
||||||
|
'menu__list-item--collapsed': collapsed,
|
||||||
|
})}>
|
||||||
|
<NavLink
|
||||||
|
role="button"
|
||||||
|
className={clsx('menu__link menu__link--sublist', className)}
|
||||||
|
{...props}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleCollapsed();
|
||||||
|
}}>
|
||||||
|
{props.children ?? props.label}
|
||||||
|
</NavLink>
|
||||||
|
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
|
||||||
|
{items.map((childItemProps, i) => (
|
||||||
|
<NavbarItem
|
||||||
|
mobile
|
||||||
|
isDropdownItem
|
||||||
|
onClick={props.onClick}
|
||||||
|
activeClassName="menu__link--active"
|
||||||
|
{...childItemProps}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Collapsible>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownNavbarItem({mobile = false, ...props}: Props): JSX.Element {
|
||||||
|
const Comp = mobile ? DropdownNavbarItemMobile : DropdownNavbarItemDesktop;
|
||||||
|
return <Comp {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DropdownNavbarItem;
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
import IconLanguage from '@theme/IconLanguage';
|
import IconLanguage from '@theme/IconLanguage';
|
||||||
import type {Props} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
import type {Props} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
@ -49,7 +49,7 @@ export default function LocaleDropdownNavbarItem({
|
||||||
const dropdownLabel = mobile ? 'Languages' : getLocaleLabel(currentLocale);
|
const dropdownLabel = mobile ? 'Languages' : getLocaleLabel(currentLocale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultNavbarItem
|
<DropdownNavbarItem
|
||||||
{...props}
|
{...props}
|
||||||
href="#"
|
href="#"
|
||||||
mobile={mobile}
|
mobile={mobile}
|
||||||
|
|
|
@ -7,39 +7,54 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import DropdownNavbarItem, {
|
||||||
|
Props as DropdownNavbarItemProps,
|
||||||
|
} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
||||||
import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
|
import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
|
||||||
import type {Props} from '@theme/NavbarItem';
|
import type {Types, Props} from '@theme/NavbarItem';
|
||||||
|
|
||||||
const NavbarItemComponents = {
|
const NavbarItemComponents: Record<
|
||||||
|
Exclude<Types, undefined>,
|
||||||
|
() => (props) => JSX.Element
|
||||||
|
> = {
|
||||||
default: () => DefaultNavbarItem,
|
default: () => DefaultNavbarItem,
|
||||||
localeDropdown: () => LocaleDropdownNavbarItem,
|
localeDropdown: () => LocaleDropdownNavbarItem,
|
||||||
search: () => SearchNavbarItem,
|
search: () => SearchNavbarItem,
|
||||||
|
dropdown: () => DropdownNavbarItem,
|
||||||
|
|
||||||
// Need to lazy load these items as we don't know for sure the docs plugin is loaded
|
// Need to lazy load these items as we don't know for sure the docs plugin is loaded
|
||||||
// See https://github.com/facebook/docusaurus/issues/3360
|
// See https://github.com/facebook/docusaurus/issues/3360
|
||||||
docsVersion: () =>
|
/* eslint-disable @typescript-eslint/no-var-requires, global-require */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
docsVersion: () => require('@theme/NavbarItem/DocsVersionNavbarItem').default,
|
||||||
require('@theme/NavbarItem/DocsVersionNavbarItem').default,
|
|
||||||
docsVersionDropdown: () =>
|
docsVersionDropdown: () =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
||||||
require('@theme/NavbarItem/DocsVersionDropdownNavbarItem').default,
|
require('@theme/NavbarItem/DocsVersionDropdownNavbarItem').default,
|
||||||
doc: () =>
|
doc: () => require('@theme/NavbarItem/DocNavbarItem').default,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
/* eslint-enable @typescript-eslint/no-var-requires, global-require */
|
||||||
require('@theme/NavbarItem/DocNavbarItem').default,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const getNavbarItemComponent = (
|
type NavbarItemComponentType = keyof typeof NavbarItemComponents;
|
||||||
type: keyof typeof NavbarItemComponents = 'default',
|
|
||||||
) => {
|
const getNavbarItemComponent = (type: NavbarItemComponentType) => {
|
||||||
const navbarItemComponent = NavbarItemComponents[type];
|
const navbarItemComponentFn = NavbarItemComponents[type];
|
||||||
if (!navbarItemComponent) {
|
if (!navbarItemComponentFn) {
|
||||||
throw new Error(`No NavbarItem component found for type "${type}".`);
|
throw new Error(`No NavbarItem component found for type "${type}".`);
|
||||||
}
|
}
|
||||||
return navbarItemComponent();
|
return navbarItemComponentFn();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NavbarItem({type, ...props}: Props): JSX.Element {
|
function getComponentType({type, ...props}: Props): NavbarItemComponentType {
|
||||||
const NavbarItemComponent = getNavbarItemComponent(type);
|
// Backward compatibility: navbar item with no type set
|
||||||
|
// but containing dropdown items should use the type "dropdown"
|
||||||
|
if (!type || type === 'default') {
|
||||||
|
const isDropdown = (props as DropdownNavbarItemProps).items !== undefined;
|
||||||
|
return isDropdown ? 'dropdown' : 'default';
|
||||||
|
}
|
||||||
|
return type as NavbarItemComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarItem(props: Props): JSX.Element {
|
||||||
|
const componentType = getComponentType(props);
|
||||||
|
const NavbarItemComponent = getNavbarItemComponent(componentType);
|
||||||
return <NavbarItemComponent {...props} />;
|
return <NavbarItemComponent {...props} />;
|
||||||
}
|
}
|
||||||
|
|
90
packages/docusaurus-theme-classic/src/types.d.ts
vendored
90
packages/docusaurus-theme-classic/src/types.d.ts
vendored
|
@ -334,23 +334,18 @@ declare module '@theme/Navbar' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
||||||
import type {ComponentProps, ReactNode} from 'react';
|
import type {LinkProps} from '@docusaurus/Link';
|
||||||
|
|
||||||
export type NavLinkProps = {
|
export type NavLinkProps = LinkProps & {
|
||||||
activeBasePath?: string;
|
readonly activeBasePath?: string;
|
||||||
activeBaseRegex?: string;
|
readonly activeBaseRegex?: string;
|
||||||
to?: string;
|
readonly exact?: boolean;
|
||||||
exact?: boolean;
|
readonly label?: ReactNode;
|
||||||
href?: string;
|
readonly prependBaseUrlToHref?: string;
|
||||||
label?: ReactNode;
|
};
|
||||||
activeClassName?: string;
|
|
||||||
prependBaseUrlToHref?: string;
|
|
||||||
isActive?: () => boolean;
|
|
||||||
} & ComponentProps<'a'>;
|
|
||||||
|
|
||||||
export type DesktopOrMobileNavBarItemProps = NavLinkProps & {
|
export type DesktopOrMobileNavBarItemProps = NavLinkProps & {
|
||||||
readonly items?: readonly NavLinkProps[];
|
readonly isDropdownItem?: boolean;
|
||||||
readonly position?: 'left' | 'right';
|
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -358,10 +353,30 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
||||||
readonly mobile?: boolean;
|
readonly mobile?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NavLink: (props: NavLinkProps) => JSX.Element;
|
||||||
|
|
||||||
const DefaultNavbarItem: (props: Props) => JSX.Element;
|
const DefaultNavbarItem: (props: Props) => JSX.Element;
|
||||||
export default DefaultNavbarItem;
|
export default DefaultNavbarItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/NavbarItem/DropdownNavbarItem' {
|
||||||
|
import type {NavLinkProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||||
|
|
||||||
|
export type DesktopOrMobileNavBarItemProps = NavLinkProps & {
|
||||||
|
readonly position?: 'left' | 'right';
|
||||||
|
readonly items: readonly LinkLikeNavbarItemProps[];
|
||||||
|
readonly className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = DesktopOrMobileNavBarItemProps & {
|
||||||
|
readonly mobile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownNavbarItem: (props: Props) => JSX.Element;
|
||||||
|
export default DropdownNavbarItem;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem/SearchNavbarItem' {
|
declare module '@theme/NavbarItem/SearchNavbarItem' {
|
||||||
export type Props = {readonly mobile?: boolean};
|
export type Props = {readonly mobile?: boolean};
|
||||||
|
|
||||||
|
@ -370,12 +385,12 @@ declare module '@theme/NavbarItem/SearchNavbarItem' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem/LocaleDropdownNavbarItem' {
|
declare module '@theme/NavbarItem/LocaleDropdownNavbarItem' {
|
||||||
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
import type {Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
import type {NavLinkProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||||
|
|
||||||
export type Props = DefaultNavbarItemProps & {
|
export type Props = DropdownNavbarItemProps & {
|
||||||
readonly dropdownItemsBefore: NavLinkProps[];
|
readonly dropdownItemsBefore: LinkLikeNavbarItemProps[];
|
||||||
readonly dropdownItemsAfter: NavLinkProps[];
|
readonly dropdownItemsAfter: LinkLikeNavbarItemProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const LocaleDropdownNavbarItem: (props: Props) => JSX.Element;
|
const LocaleDropdownNavbarItem: (props: Props) => JSX.Element;
|
||||||
|
@ -383,14 +398,14 @@ declare module '@theme/NavbarItem/LocaleDropdownNavbarItem' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem/DocsVersionDropdownNavbarItem' {
|
declare module '@theme/NavbarItem/DocsVersionDropdownNavbarItem' {
|
||||||
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
import type {Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
import type {NavLinkProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||||
|
|
||||||
export type Props = DefaultNavbarItemProps & {
|
export type Props = DropdownNavbarItemProps & {
|
||||||
readonly docsPluginId?: string;
|
readonly docsPluginId?: string;
|
||||||
readonly dropdownActiveClassDisabled?: boolean;
|
readonly dropdownActiveClassDisabled?: boolean;
|
||||||
readonly dropdownItemsBefore: NavLinkProps[];
|
readonly dropdownItemsBefore: LinkLikeNavbarItemProps[];
|
||||||
readonly dropdownItemsAfter: NavLinkProps[];
|
readonly dropdownItemsAfter: LinkLikeNavbarItemProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocsVersionDropdownNavbarItem: (props: Props) => JSX.Element;
|
const DocsVersionDropdownNavbarItem: (props: Props) => JSX.Element;
|
||||||
|
@ -420,18 +435,35 @@ declare module '@theme/NavbarItem/DocNavbarItem' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem' {
|
declare module '@theme/NavbarItem' {
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
import type {Props as DocsVersionDropdownNavbarItemProps} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
|
import type {Props as DocNavbarItemProps} from '@theme/NavbarItem/DocNavbarItem';
|
||||||
import type {Props as DocsVersionNavbarItemProps} from '@theme/NavbarItem/DocsVersionNavbarItem';
|
import type {Props as DocsVersionNavbarItemProps} from '@theme/NavbarItem/DocsVersionNavbarItem';
|
||||||
|
import type {Props as DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||||
|
import type {Props as DocsVersionDropdownNavbarItemProps} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
|
||||||
|
import type {Props as LocaleDropdownNavbarItemProps} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
||||||
import type {Props as SearchNavbarItemProps} from '@theme/NavbarItem/SearchNavbarItem';
|
import type {Props as SearchNavbarItemProps} from '@theme/NavbarItem/SearchNavbarItem';
|
||||||
|
|
||||||
export type Props =
|
export type LinkLikeNavbarItemProps =
|
||||||
| ({readonly type?: 'default' | undefined} & DefaultNavbarItemProps)
|
| ({readonly type?: 'default'} & DefaultNavbarItemProps)
|
||||||
|
| ({readonly type: 'doc'} & DocNavbarItemProps)
|
||||||
|
| ({readonly type: 'docsVersion'} & DocsVersionNavbarItemProps);
|
||||||
|
|
||||||
|
export type Props = ComponentProps<'a'> & {
|
||||||
|
readonly position?: 'left' | 'right';
|
||||||
|
} & (
|
||||||
|
| LinkLikeNavbarItemProps
|
||||||
|
| ({readonly type?: 'dropdown'} & DropdownNavbarItemProps)
|
||||||
| ({
|
| ({
|
||||||
readonly type: 'docsVersionDropdown';
|
readonly type: 'docsVersionDropdown';
|
||||||
} & DocsVersionDropdownNavbarItemProps)
|
} & DocsVersionDropdownNavbarItemProps)
|
||||||
| ({readonly type: 'docsVersion'} & DocsVersionNavbarItemProps)
|
| ({readonly type: 'localeDropdown'} & LocaleDropdownNavbarItemProps)
|
||||||
| ({readonly type: 'search'} & SearchNavbarItemProps);
|
| ({
|
||||||
|
readonly type: 'search';
|
||||||
|
} & SearchNavbarItemProps)
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Types = Props['type'];
|
||||||
|
|
||||||
const NavbarItem: (props: Props) => JSX.Element;
|
const NavbarItem: (props: Props) => JSX.Element;
|
||||||
export default NavbarItem;
|
export default NavbarItem;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["DOM"],
|
"lib": ["DOM", "ES2019"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"lib": ["DOM"],
|
"lib": ["DOM", "ES2019"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"tsBuildInfoFile": "./lib/client/.tsbuildinfo",
|
"tsBuildInfoFile": "./lib/client/.tsbuildinfo",
|
||||||
"outDir": "lib/client",
|
"outDir": "lib/client",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"lib": ["DOM"],
|
"lib": ["DOM", "ES2019"],
|
||||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
|
|
Loading…
Add table
Reference in a new issue