Skip to content

Commit 44badfd

Browse files
authored
Merge pull request #2255 from TypeStrong/feat/1532
Categorize sidebar
2 parents 69a2b60 + ddddfdd commit 44badfd

File tree

5 files changed

+190
-118
lines changed

5 files changed

+190
-118
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
### Features
44

5+
- Categories and groups can now be shown in the navigation, added `--navigation.includeCategories`
6+
and `--navigation.includeGroups` to control this behavior. The `--categorizeByGroup` option also
7+
effects this behavior. If `categorizeByGroup` is set (the default) and `navigation.includeGroups` is
8+
_not_ set, the value of `navigation.includeCategories` will be effectively ignored since categories
9+
will be created only within groups, #1532.
510
- Added support for discovering a "module" comment on global files, #2165.
611
- Added copy code to clipboard button, #2153.
712
- Function `@returns` blocks will now be rendered with the return type, #2180.

src/lib/output/themes/default/partials/navigation.tsx

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { DeclarationReflection, ProjectReflection, Reflection, ReflectionKind } from "../../../../models";
1+
import {
2+
DeclarationReflection,
3+
ProjectReflection,
4+
Reflection,
5+
ReflectionCategory,
6+
ReflectionGroup,
7+
ReflectionKind,
8+
} from "../../../../models";
29
import { JSX } from "../../../../utils";
310
import type { PageEvent } from "../../../events";
411
import { camelToTitleCase, classNames, getDisplayName, wbr } from "../../lib";
@@ -98,61 +105,100 @@ export function settings(context: DefaultThemeRenderContext) {
98105
);
99106
}
100107

108+
type NavigationElement = ReflectionCategory | ReflectionGroup | DeclarationReflection;
109+
110+
function getNavigationElements(
111+
parent: NavigationElement | ProjectReflection,
112+
opts: { includeCategories: boolean; includeGroups: boolean }
113+
): NavigationElement[] {
114+
if (parent instanceof ReflectionCategory) {
115+
return parent.children;
116+
}
117+
118+
if (parent instanceof ReflectionGroup) {
119+
if (opts.includeCategories && parent.categories) {
120+
return parent.categories;
121+
}
122+
return parent.children;
123+
}
124+
125+
if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)) {
126+
return [];
127+
}
128+
129+
if (parent.categories && opts.includeCategories) {
130+
return parent.categories;
131+
}
132+
133+
if (parent.groups && opts.includeGroups) {
134+
return parent.groups;
135+
}
136+
137+
return parent.children || [];
138+
}
139+
101140
export function navigation(context: DefaultThemeRenderContext, props: PageEvent<Reflection>) {
141+
const opts = context.options.getValue("navigation");
102142
// Create the navigation for the current page
103143
// Recurse to children if the parent is some kind of module
104144

105145
return (
106146
<nav class="tsd-navigation">
107-
{link(props.project)}
147+
{createNavElement(props.project)}
108148
<ul class="tsd-small-nested-navigation">
109-
{props.project.children?.map((c) => (
110-
<li>{links(c)}</li>
149+
{getNavigationElements(props.project, opts).map((c) => (
150+
<li>{links(c, [])}</li>
111151
))}
112152
</ul>
113153
</nav>
114154
);
115155

116-
function links(mod: DeclarationReflection) {
117-
const children = (mod.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project) && mod.children) || [];
118-
156+
function links(mod: NavigationElement, parents: string[]) {
119157
const nameClasses = classNames(
120-
{ deprecated: mod.isDeprecated() },
121-
mod.isProject() ? void 0 : context.getReflectionClasses(mod)
158+
{ deprecated: mod instanceof Reflection && mod.isDeprecated() },
159+
!(mod instanceof Reflection) || mod.isProject() ? void 0 : context.getReflectionClasses(mod)
122160
);
123161

162+
const children = getNavigationElements(mod, opts);
163+
124164
if (!children.length) {
125-
return link(mod, nameClasses);
165+
return createNavElement(mod, nameClasses);
126166
}
127167

128168
return (
129169
<details
130170
class={classNames({ "tsd-index-accordion": true }, nameClasses)}
131-
open={inPath(mod)}
132-
data-key={mod.getFullName()}
171+
open={mod instanceof Reflection && inPath(mod)}
172+
data-key={mod instanceof Reflection ? mod.getFullName() : [...parents, mod.title].join("$")}
133173
>
134174
<summary class="tsd-accordion-summary">
135175
{context.icons.chevronDown()}
136-
{link(mod)}
176+
{createNavElement(mod)}
137177
</summary>
138178
<div class="tsd-accordion-details">
139179
<ul class="tsd-nested-navigation">
140180
{children.map((c) => (
141-
<li>{links(c)}</li>
181+
<li>
182+
{links(c, mod instanceof Reflection ? [mod.getFullName()] : [...parents, mod.title])}
183+
</li>
142184
))}
143185
</ul>
144186
</div>
145187
</details>
146188
);
147189
}
148190

149-
function link(child: DeclarationReflection | ProjectReflection, nameClasses?: string) {
150-
return (
151-
<a href={context.urlTo(child)} class={classNames({ current: child === props.model }, nameClasses)}>
152-
{context.icons[child.kind]()}
153-
<span>{wbr(getDisplayName(child))}</span>
154-
</a>
155-
);
191+
function createNavElement(child: NavigationElement | ProjectReflection, nameClasses?: string) {
192+
if (child instanceof Reflection) {
193+
return (
194+
<a href={context.urlTo(child)} class={classNames({ current: child === props.model }, nameClasses)}>
195+
{context.icons[child.kind]()}
196+
<span>{wbr(getDisplayName(child))}</span>
197+
</a>
198+
);
199+
}
200+
201+
return <span>{child.title}</span>;
156202
}
157203

158204
function inPath(mod: DeclarationReflection | ProjectReflection) {

src/lib/utils/options/declaration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ export interface TypeDocOptionMap {
136136
titleLink: string;
137137
navigationLinks: ManuallyValidatedOption<Record<string, string>>;
138138
sidebarLinks: ManuallyValidatedOption<Record<string, string>>;
139+
navigation: {
140+
includeCategories: boolean;
141+
includeGroups: boolean;
142+
};
139143
visibilityFilters: ManuallyValidatedOption<{
140144
protected?: boolean;
141145
private?: boolean;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 87 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,92 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
433433
},
434434
});
435435

436+
options.addDeclaration({
437+
name: "navigation",
438+
help: "Determines how the navigation sidebar is organized.",
439+
type: ParameterType.Flags,
440+
defaults: {
441+
includeCategories: false,
442+
includeGroups: false,
443+
},
444+
});
445+
446+
options.addDeclaration({
447+
name: "visibilityFilters",
448+
help: "Specify the default visibility for builtin filters and additional filters according to modifier tags.",
449+
type: ParameterType.Mixed,
450+
configFileOnly: true,
451+
defaultValue: {
452+
protected: false,
453+
private: false,
454+
inherited: true,
455+
external: false,
456+
},
457+
validate(value) {
458+
const knownKeys = ["protected", "private", "inherited", "external"];
459+
if (!value || typeof value !== "object") {
460+
throw new Error("visibilityFilters must be an object.");
461+
}
462+
463+
for (const [key, val] of Object.entries(value)) {
464+
if (!key.startsWith("@") && !knownKeys.includes(key)) {
465+
throw new Error(
466+
`visibilityFilters can only include the following non-@ keys: ${knownKeys.join(
467+
", "
468+
)}`
469+
);
470+
}
471+
472+
if (typeof val !== "boolean") {
473+
throw new Error(
474+
`All values of visibilityFilters must be booleans.`
475+
);
476+
}
477+
}
478+
},
479+
});
480+
481+
options.addDeclaration({
482+
name: "searchCategoryBoosts",
483+
help: "Configure search to give a relevance boost to selected categories",
484+
type: ParameterType.Mixed,
485+
configFileOnly: true,
486+
defaultValue: {},
487+
validate(value) {
488+
if (!isObject(value)) {
489+
throw new Error(
490+
"The 'searchCategoryBoosts' option must be a non-array object."
491+
);
492+
}
493+
494+
if (Object.values(value).some((x) => typeof x !== "number")) {
495+
throw new Error(
496+
"All values of 'searchCategoryBoosts' must be numbers."
497+
);
498+
}
499+
},
500+
});
501+
options.addDeclaration({
502+
name: "searchGroupBoosts",
503+
help: 'Configure search to give a relevance boost to selected kinds (eg "class")',
504+
type: ParameterType.Mixed,
505+
configFileOnly: true,
506+
defaultValue: {},
507+
validate(value: unknown) {
508+
if (!isObject(value)) {
509+
throw new Error(
510+
"The 'searchGroupBoosts' option must be a non-array object."
511+
);
512+
}
513+
514+
if (Object.values(value).some((x) => typeof x !== "number")) {
515+
throw new Error(
516+
"All values of 'searchGroupBoosts' must be numbers."
517+
);
518+
}
519+
},
520+
});
521+
436522
///////////////////////////
437523
///// Comment Options /////
438524
///////////////////////////
@@ -510,7 +596,7 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
510596
name: "categorizeByGroup",
511597
help: "Specify whether categorization will be done at the group level.",
512598
type: ParameterType.Boolean,
513-
defaultValue: true,
599+
defaultValue: true, // 0.25, change this to false.
514600
});
515601
options.addDeclaration({
516602
name: "defaultCategory",
@@ -594,82 +680,6 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
594680
},
595681
});
596682

597-
options.addDeclaration({
598-
name: "visibilityFilters",
599-
help: "Specify the default visibility for builtin filters and additional filters according to modifier tags.",
600-
type: ParameterType.Mixed,
601-
configFileOnly: true,
602-
defaultValue: {
603-
protected: false,
604-
private: false,
605-
inherited: true,
606-
external: false,
607-
},
608-
validate(value) {
609-
const knownKeys = ["protected", "private", "inherited", "external"];
610-
if (!value || typeof value !== "object") {
611-
throw new Error("visibilityFilters must be an object.");
612-
}
613-
614-
for (const [key, val] of Object.entries(value)) {
615-
if (!key.startsWith("@") && !knownKeys.includes(key)) {
616-
throw new Error(
617-
`visibilityFilters can only include the following non-@ keys: ${knownKeys.join(
618-
", "
619-
)}`
620-
);
621-
}
622-
623-
if (typeof val !== "boolean") {
624-
throw new Error(
625-
`All values of visibilityFilters must be booleans.`
626-
);
627-
}
628-
}
629-
},
630-
});
631-
632-
options.addDeclaration({
633-
name: "searchCategoryBoosts",
634-
help: "Configure search to give a relevance boost to selected categories",
635-
type: ParameterType.Mixed,
636-
configFileOnly: true,
637-
defaultValue: {},
638-
validate(value) {
639-
if (!isObject(value)) {
640-
throw new Error(
641-
"The 'searchCategoryBoosts' option must be a non-array object."
642-
);
643-
}
644-
645-
if (Object.values(value).some((x) => typeof x !== "number")) {
646-
throw new Error(
647-
"All values of 'searchCategoryBoosts' must be numbers."
648-
);
649-
}
650-
},
651-
});
652-
options.addDeclaration({
653-
name: "searchGroupBoosts",
654-
help: 'Configure search to give a relevance boost to selected kinds (eg "class")',
655-
type: ParameterType.Mixed,
656-
configFileOnly: true,
657-
defaultValue: {},
658-
validate(value: unknown) {
659-
if (!isObject(value)) {
660-
throw new Error(
661-
"The 'searchGroupBoosts' option must be a non-array object."
662-
);
663-
}
664-
665-
if (Object.values(value).some((x) => typeof x !== "number")) {
666-
throw new Error(
667-
"All values of 'searchGroupBoosts' must be numbers."
668-
);
669-
}
670-
},
671-
});
672-
673683
///////////////////////////
674684
///// General Options /////
675685
///////////////////////////

0 commit comments

Comments
 (0)