Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,37 @@ window.$docsify = {

Determines if Docsify should handle uncaught _synchronous_ plugin errors automatically. This can prevent plugin errors from affecting docsify's ability to properly render live site content.

## collapseSidebarGroups

- Type: `Boolean`
- Default: `false`

Initially collapses all root sidebar groups when `collapsibleSidebarGroups` is
enabled. Visitors can still expand and collapse each group by selecting its
title. Their choices are preserved while navigating between pages.

```js
window.$docsify = {
collapseSidebarGroups: true,
collapsibleSidebarGroups: true,
};
```

## collapsibleSidebarGroups

- Type: `Boolean`
- Default: `false`

Enables visitors to expand and collapse root sidebar groups by selecting their
titles or using the <kbd>Enter</kbd> and <kbd>Space</kbd> keys. Enabling a
sidebar chevron theme class also displays chevrons on these group titles.

```js
window.$docsify = {
collapsibleSidebarGroups: true,
};
```

## cornerExternalLinkTarget

- Type: `String`
Expand Down Expand Up @@ -306,21 +337,6 @@ window.$docsify = {
};
```

## collapseSidebarGroups

- Type: `Boolean`
- Default: `false`

Initially collapses all root sidebar groups. Visitors can still expand and
collapse each group by selecting its title. Their choices are preserved while
navigating between pages.

```js
window.$docsify = {
collapseSidebarGroups: true,
};
```

## sidebarPosition

- Type: `String`
Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
'https://cdn.jsdelivr.net/gh/docsifyjs/docs-zh@main/$1',
},
auto2top: true,
collapsibleSidebarGroups: true,
coverpage: true,
executeScript: true,
// hideSidebar: true,
Expand Down
16 changes: 16 additions & 0 deletions docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,21 @@ Display a loading animation while waiting for Docsify to initialize.

Display expand/collapse icons on page links in the sidebar.

Root group titles display chevrons only when
[`collapsibleSidebarGroups`](configuration.md?id=collapsiblesidebargroups) is
enabled.

<label>
<input class="toggle" type="checkbox" value="sidebar-chevron-right" data-class data-group="sidebar-chevron"> Preview <code>sidebar-chevron-right</code>
</label>
<br>
<label>
<input class="toggle" type="checkbox" value="sidebar-chevron-left" data-class data-group="sidebar-chevron"> Preview <code>sidebar-chevron-left</code>
</label>
<br>
<label>
<input class="toggle" type="checkbox" value="sidebar-chevron-root-hidden" data-class> Hide root-level chevrons with <code>sidebar-chevron-root-hidden</code>
</label>

<!-- prettier-ignore -->
```html
Expand All @@ -128,6 +136,14 @@ Display expand/collapse icons on page links in the sidebar.
<body class="sidebar-chevron-left">
```

To hide chevrons on all root-level page links and group titles while retaining
chevrons on nested page links, add the `sidebar-chevron-root-hidden` class:

<!-- prettier-ignore -->
```html
<body class="sidebar-chevron-right sidebar-chevron-root-hidden">
```

To prevent chevrons from displaying for specific page links, add a `no-chevron` class as follows:

```md
Expand Down
1 change: 1 addition & 0 deletions src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const defaultDocsifyConfig = () => ({
basePath: '',
catchPluginErrors: true,
collapseSidebarGroups: false,
collapsibleSidebarGroups: false,
cornerExternalLinkTarget:
/** @type {'_blank' | '_self' | '_parent' | '_top' | '_unfencedTop'} */ (
'_blank'
Expand Down
24 changes: 12 additions & 12 deletions src/core/event/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,14 @@ export function Events(Base) {

// Collapse toggle
dom.on(sidebarElm, 'click', (/** @type {MouseEvent} */ { target }) => {
const groupTitle = /** @type {HTMLElement | null} */ (
const groupToggle = /** @type {HTMLElement | null} */ (
/** @type {HTMLElement} */ (target).closest(
'.group-title[role="button"]',
'.group-toggle[role="button"]',
)
);

if (groupTitle) {
this.#toggleSidebarGroup(groupTitle);
if (groupToggle) {
this.#toggleSidebarGroup(groupToggle);
return;
}

Expand All @@ -275,36 +275,36 @@ export function Events(Base) {
});

dom.on(sidebarElm, 'keydown', (/** @type {KeyboardEvent} */ event) => {
const groupTitle = /** @type {HTMLElement | null} */ (
const groupToggle = /** @type {HTMLElement | null} */ (
/** @type {HTMLElement} */ (event.target).closest(
'.group-title[role="button"]',
'.group-toggle[role="button"]',
)
);

if (groupTitle && (event.key === 'Enter' || event.key === ' ')) {
if (groupToggle && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
this.#toggleSidebarGroup(groupTitle);
this.#toggleSidebarGroup(groupToggle);
}
});
}

/**
* Toggle a root sidebar group and keep its accessible state in sync.
*
* @param {HTMLElement} groupTitle
* @param {HTMLElement} groupToggle
* @void
*/
#toggleSidebarGroup(groupTitle) {
#toggleSidebarGroup(groupToggle) {
const group = /** @type {HTMLLIElement | null} */ (
groupTitle.closest('li')
groupToggle.closest('li')
);

if (!group) {
return;
}

const isCollapsed = group.classList.toggle('collapse');
groupTitle.setAttribute('aria-expanded', String(!isCollapsed));
groupToggle.setAttribute('aria-expanded', String(!isCollapsed));
}

/**
Expand Down
49 changes: 43 additions & 6 deletions src/core/render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,32 @@ export function Render(Base) {
});
}

/**
* Normalize links in loose Markdown lists from `<li><p><a>` to
* `<li><a>` so sidebar behavior and styling do not depend on list
* tightness.
*
* @param {Element} sidebarNavEl
*/
#normalizeSidebarPageLinks(sidebarNavEl) {
dom.findAll(sidebarNavEl, 'li > p').forEach(paragraph => {
const link = paragraph.firstElementChild;
const onlyContainsLink = [...paragraph.childNodes].every(
node =>
node === link || (node.nodeType === 3 && !node.textContent?.trim()),
);

if (
!paragraph.attributes.length &&
paragraph.children.length === 1 &&
link?.tagName === 'A' &&
onlyContainsLink
) {
paragraph.replaceWith(link);
}
});
}

#executeScript() {
const script = dom
.findAll('.markdown-section>script')
Expand Down Expand Up @@ -296,6 +322,7 @@ export function Render(Base) {
_renderSidebar(text) {
const {
collapseSidebarGroups,
collapsibleSidebarGroups,
maxLevel,
subMaxLevel,
loadSidebar,
Expand All @@ -320,7 +347,7 @@ export function Render(Base) {
dom
.findAll(
sidebarNavEl,
'li.group > .group-title[role="button"][data-group-id]',
'li.group > .group-toggle[role="button"][data-group-id]',
)
.map(elm => [
elm.getAttribute('data-group-id'),
Expand All @@ -329,6 +356,7 @@ export function Render(Base) {
);

dom.setHTML('.sidebar-nav', this.compiler.sidebar(text, maxLevel));
this.#normalizeSidebarPageLinks(sidebarNavEl);

sidebarToggleEl.setAttribute('aria-expanded', String(!isMobile()));

Expand Down Expand Up @@ -358,18 +386,18 @@ export function Render(Base) {
// Mark page links and groups
const pageLinks = dom.findAll(
sidebarNavEl,
'a:is(li > a, li > p > a):not(.section-link, [target="_blank"])',
'li > a:not(.section-link, [target="_blank"])',
);
const pageLinkGroups = dom
// NOTE: Using filter() method as a replacement for :has() selector. It
// would be preferable to use only 'li:not(:has(> a, > p > a))' selector
// would be preferable to use only 'li:not(:has(> a))' selector
// but the :has() selector is not supported by our Jest test environment
// See: https://github.com/jsdom/jsdom/issues/3506#issuecomment-1769782333
.findAll(sidebarEl, 'li')
.filter(
elm =>
elm.querySelector(':scope > ul') &&
!elm.querySelectorAll(':scope > a, :scope > p > a').length,
!elm.querySelector(':scope > a'),
);

pageLinks.forEach(elm => {
Expand All @@ -382,6 +410,10 @@ export function Render(Base) {
let groupTitle = [...elm.children].find(
child => child.tagName === 'P' && !child.querySelector('a'),
);
// Preserve the original styling behavior: only text-only paragraphs
// produced by Markdown receive the group-title class.
const styledGroupTitle =
groupTitle && !groupTitle.children.length ? groupTitle : null;

if (!groupTitle) {
const sublist = [...elm.children].find(
Expand All @@ -405,16 +437,21 @@ export function Render(Base) {
}
}

groupTitle?.classList.add('group-title');
styledGroupTitle?.classList.add('group-title');

const rootList = elm.parentElement;

if (groupTitle && rootList?.parentElement === sidebarNavEl) {
if (
collapsibleSidebarGroups &&
groupTitle &&
rootList?.parentElement === sidebarNavEl
) {
const groupId = `${[...sidebarNavEl.children].indexOf(rootList)}:${[...rootList.children].indexOf(elm)}`;
const isCollapsed =
sidebarGroupStates.get(groupId) ?? collapseSidebarGroups;

elm.classList.toggle('collapse', isCollapsed);
groupTitle.classList.add('group-toggle');
groupTitle.setAttribute('data-group-id', groupId);
groupTitle.setAttribute('role', 'button');
groupTitle.setAttribute('tabindex', '0');
Expand Down
19 changes: 14 additions & 5 deletions src/themes/shared/_classes.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@
}

body[class*='sidebar-chevron'] {
.sidebar-nav :is(a.page-link, p.group-title[role='button']).no-chevron {
.sidebar-nav :is(a.page-link, p.group-toggle).no-chevron {
background: none;
}

.sidebar-nav p.group-title[role='button'] {
.sidebar-nav p.group-toggle {
background: var(--sidebar-pagelink-bg);

&[aria-expanded='true'] {
Expand All @@ -94,6 +94,15 @@ body[class*='sidebar-chevron'] {
}
}

body.sidebar-chevron-root-hidden {
.sidebar-nav > ul > li {
> a.page-link,
> p.group-toggle[aria-expanded] {
background: none;
}
}
}

/* Left */
/* -------------------------------------------------------------------------- */
:root:has(body.sidebar-chevron-left) {
Expand All @@ -105,7 +114,7 @@ body.sidebar-chevron-left {
--_inset: 18px;

li {
:is(a.page-link, p.group-title[role='button']) {
:is(a.page-link, p.group-toggle) {
padding-left: var(--_inset);
}

Expand All @@ -128,12 +137,12 @@ body.sidebar-chevron-left {

body.sidebar-chevron-right {
.sidebar-nav {
p.group-title[role='button'] {
p.group-toggle {
margin-right: 0;
}

li {
:is(a, p.group-title[role='button']) {
:is(a, p.group-toggle) {
padding-right: calc(var(--_sidebar-inset) + 15px);
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/themes/shared/_sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@
color: var(--sidebar-group-title-color);
font-size: var(--sidebar-group-title-font-size);
font-weight: var(--sidebar-group-title-font-weight);
}

&[role='button'] {
cursor: pointer;
user-select: none;
&.group-toggle {
cursor: pointer;
user-select: none;

&:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
&:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
}
}
Expand All @@ -99,7 +99,7 @@
}

&.collapse {
> :not(a, p:has(> a.page-link)):not(.group-title) {
> :not(a, .group-toggle) {
display: none;
}
}
Expand Down
1 change: 1 addition & 0 deletions test/consume-types/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const d = new Docsify({
themeColor: 'deeppink',
hideSidebar: false,
collapseSidebarGroups: true,
collapsibleSidebarGroups: true,

// @ts-expect-error invalid property to test that type checking works
blahblah: 123,
Expand Down
Loading