Skip to content

Commit ddddfdd

Browse files
committed
Add support for categories/groups in the navigation
Resolves #1532
1 parent 497ddba commit ddddfdd

File tree

5 files changed

+146
-99
lines changed

5 files changed

+146
-99
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: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ProjectReflection,
44
Reflection,
55
ReflectionCategory,
6+
ReflectionGroup,
67
ReflectionKind,
78
} from "../../../../models";
89
import { JSX } from "../../../../utils";
@@ -104,77 +105,94 @@ export function settings(context: DefaultThemeRenderContext) {
104105
);
105106
}
106107

107-
type NavigationElement = ReflectionCategory | DeclarationReflection;
108+
type NavigationElement = ReflectionCategory | ReflectionGroup | DeclarationReflection;
108109

109-
function getNavigationElements(parent: NavigationElement | ProjectReflection): NavigationElement[] {
110+
function getNavigationElements(
111+
parent: NavigationElement | ProjectReflection,
112+
opts: { includeCategories: boolean; includeGroups: boolean }
113+
): NavigationElement[] {
110114
if (parent instanceof ReflectionCategory) {
111115
return parent.children;
112116
}
113117

118+
if (parent instanceof ReflectionGroup) {
119+
if (opts.includeCategories && parent.categories) {
120+
return parent.categories;
121+
}
122+
return parent.children;
123+
}
124+
114125
if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)) {
115126
return [];
116127
}
117128

118-
if (parent.categories) {
129+
if (parent.categories && opts.includeCategories) {
119130
return parent.categories;
120131
}
121132

133+
if (parent.groups && opts.includeGroups) {
134+
return parent.groups;
135+
}
136+
122137
return parent.children || [];
123138
}
124139

125140
export function navigation(context: DefaultThemeRenderContext, props: PageEvent<Reflection>) {
141+
const opts = context.options.getValue("navigation");
126142
// Create the navigation for the current page
127143
// Recurse to children if the parent is some kind of module
128144

129145
return (
130146
<nav class="tsd-navigation">
131-
{createNavElement(props.project, false)}
132-
<ul class="tsd-nested-navigation">
133-
{getNavigationElements(props.project).map((c) => (
134-
<li>{links(c)}</li>
147+
{createNavElement(props.project)}
148+
<ul class="tsd-small-nested-navigation">
149+
{getNavigationElements(props.project, opts).map((c) => (
150+
<li>{links(c, [])}</li>
135151
))}
136152
</ul>
137153
</nav>
138154
);
139155

140-
function links(mod: NavigationElement) {
156+
function links(mod: NavigationElement, parents: string[]) {
141157
const nameClasses = classNames(
142158
{ deprecated: mod instanceof Reflection && mod.isDeprecated() },
143159
!(mod instanceof Reflection) || mod.isProject() ? void 0 : context.getReflectionClasses(mod)
144160
);
145161

146-
const children = getNavigationElements(mod);
162+
const children = getNavigationElements(mod, opts);
147163

148164
if (!children.length) {
149-
return createNavElement(mod, true, nameClasses);
165+
return createNavElement(mod, nameClasses);
150166
}
151167

152168
return (
153169
<details
154170
class={classNames({ "tsd-index-accordion": true }, nameClasses)}
155171
open={mod instanceof Reflection && inPath(mod)}
156-
data-key={mod instanceof Reflection ? mod.getFullName() : mod.title}
172+
data-key={mod instanceof Reflection ? mod.getFullName() : [...parents, mod.title].join("$")}
157173
>
158174
<summary class="tsd-accordion-summary">
159175
{context.icons.chevronDown()}
160-
{createNavElement(mod, false)}
176+
{createNavElement(mod)}
161177
</summary>
162178
<div class="tsd-accordion-details">
163179
<ul class="tsd-nested-navigation">
164180
{children.map((c) => (
165-
<li>{links(c)}</li>
181+
<li>
182+
{links(c, mod instanceof Reflection ? [mod.getFullName()] : [...parents, mod.title])}
183+
</li>
166184
))}
167185
</ul>
168186
</div>
169187
</details>
170188
);
171189
}
172190

173-
function createNavElement(child: NavigationElement | ProjectReflection, icon: boolean, nameClasses?: string) {
191+
function createNavElement(child: NavigationElement | ProjectReflection, nameClasses?: string) {
174192
if (child instanceof Reflection) {
175193
return (
176194
<a href={context.urlTo(child)} class={classNames({ current: child === props.model }, nameClasses)}>
177-
{icon && context.icons[child.kind]()}
195+
{context.icons[child.kind]()}
178196
<span>{wbr(getDisplayName(child))}</span>
179197
</a>
180198
);

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
///////////////////////////

static/style.css

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,8 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark {
720720
}
721721
.tsd-navigation ul,
722722
.tsd-page-navigation ul {
723-
margin: 0;
723+
margin-top: 0;
724+
margin-bottom: 0;
724725
padding: 0;
725726
list-style: none;
726727
}
@@ -729,14 +730,24 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark {
729730
padding: 0;
730731
max-width: 100%;
731732
}
733+
.tsd-nested-navigation {
734+
margin-left: 3rem;
735+
}
736+
.tsd-nested-navigation > li > details {
737+
margin-left: -1.5rem;
738+
}
739+
.tsd-small-nested-navigation {
740+
margin-left: 1.5rem;
741+
}
742+
.tsd-small-nested-navigation > li > details {
743+
margin-left: -1.5rem;
744+
}
745+
732746
.tsd-nested-navigation > li > a,
733747
.tsd-nested-navigation > li > span {
734748
width: calc(100% - 1.75rem - 0.5rem);
735-
margin-left: 1.75rem;
736-
}
737-
.tsd-nested-navigation > li > details {
738-
margin-left: 1.75rem;
739749
}
750+
740751
.tsd-page-navigation ul {
741752
padding-left: 1.75rem;
742753
}
@@ -781,8 +792,7 @@ a.tsd-index-link {
781792
padding-top: 0;
782793
padding-bottom: 0;
783794
}
784-
.tsd-index-accordion .tsd-accordion-summary svg {
785-
margin-right: 0.25rem;
795+
.tsd-index-accordion .tsd-accordion-summary > svg {
786796
margin-left: 0.25rem;
787797
}
788798
.tsd-index-content > :not(:first-child) {

0 commit comments

Comments
 (0)