Skip to content

Commit

Permalink
Add support for categories/groups in the navigation
Browse files Browse the repository at this point in the history
Resolves #1532
  • Loading branch information
Gerrit0 committed Apr 22, 2023
1 parent 497ddba commit ddddfdd
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 99 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

### Features

- Categories and groups can now be shown in the navigation, added `--navigation.includeCategories`
and `--navigation.includeGroups` to control this behavior. The `--categorizeByGroup` option also
effects this behavior. If `categorizeByGroup` is set (the default) and `navigation.includeGroups` is
_not_ set, the value of `navigation.includeCategories` will be effectively ignored since categories
will be created only within groups, #1532.
- Added support for discovering a "module" comment on global files, #2165.
- Added copy code to clipboard button, #2153.
- Function `@returns` blocks will now be rendered with the return type, #2180.
Expand Down
48 changes: 33 additions & 15 deletions src/lib/output/themes/default/partials/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ProjectReflection,
Reflection,
ReflectionCategory,
ReflectionGroup,
ReflectionKind,
} from "../../../../models";
import { JSX } from "../../../../utils";
Expand Down Expand Up @@ -104,77 +105,94 @@ export function settings(context: DefaultThemeRenderContext) {
);
}

type NavigationElement = ReflectionCategory | DeclarationReflection;
type NavigationElement = ReflectionCategory | ReflectionGroup | DeclarationReflection;

function getNavigationElements(parent: NavigationElement | ProjectReflection): NavigationElement[] {
function getNavigationElements(
parent: NavigationElement | ProjectReflection,
opts: { includeCategories: boolean; includeGroups: boolean }
): NavigationElement[] {
if (parent instanceof ReflectionCategory) {
return parent.children;
}

if (parent instanceof ReflectionGroup) {
if (opts.includeCategories && parent.categories) {
return parent.categories;
}
return parent.children;
}

if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)) {
return [];
}

if (parent.categories) {
if (parent.categories && opts.includeCategories) {
return parent.categories;
}

if (parent.groups && opts.includeGroups) {
return parent.groups;
}

return parent.children || [];
}

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

return (
<nav class="tsd-navigation">
{createNavElement(props.project, false)}
<ul class="tsd-nested-navigation">
{getNavigationElements(props.project).map((c) => (
<li>{links(c)}</li>
{createNavElement(props.project)}
<ul class="tsd-small-nested-navigation">
{getNavigationElements(props.project, opts).map((c) => (
<li>{links(c, [])}</li>
))}
</ul>
</nav>
);

function links(mod: NavigationElement) {
function links(mod: NavigationElement, parents: string[]) {
const nameClasses = classNames(
{ deprecated: mod instanceof Reflection && mod.isDeprecated() },
!(mod instanceof Reflection) || mod.isProject() ? void 0 : context.getReflectionClasses(mod)
);

const children = getNavigationElements(mod);
const children = getNavigationElements(mod, opts);

if (!children.length) {
return createNavElement(mod, true, nameClasses);
return createNavElement(mod, nameClasses);
}

return (
<details
class={classNames({ "tsd-index-accordion": true }, nameClasses)}
open={mod instanceof Reflection && inPath(mod)}
data-key={mod instanceof Reflection ? mod.getFullName() : mod.title}
data-key={mod instanceof Reflection ? mod.getFullName() : [...parents, mod.title].join("$")}
>
<summary class="tsd-accordion-summary">
{context.icons.chevronDown()}
{createNavElement(mod, false)}
{createNavElement(mod)}
</summary>
<div class="tsd-accordion-details">
<ul class="tsd-nested-navigation">
{children.map((c) => (
<li>{links(c)}</li>
<li>
{links(c, mod instanceof Reflection ? [mod.getFullName()] : [...parents, mod.title])}
</li>
))}
</ul>
</div>
</details>
);
}

function createNavElement(child: NavigationElement | ProjectReflection, icon: boolean, nameClasses?: string) {
function createNavElement(child: NavigationElement | ProjectReflection, nameClasses?: string) {
if (child instanceof Reflection) {
return (
<a href={context.urlTo(child)} class={classNames({ current: child === props.model }, nameClasses)}>
{icon && context.icons[child.kind]()}
{context.icons[child.kind]()}
<span>{wbr(getDisplayName(child))}</span>
</a>
);
Expand Down
4 changes: 4 additions & 0 deletions src/lib/utils/options/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ export interface TypeDocOptionMap {
titleLink: string;
navigationLinks: ManuallyValidatedOption<Record<string, string>>;
sidebarLinks: ManuallyValidatedOption<Record<string, string>>;
navigation: {
includeCategories: boolean;
includeGroups: boolean;
};
visibilityFilters: ManuallyValidatedOption<{
protected?: boolean;
private?: boolean;
Expand Down
164 changes: 87 additions & 77 deletions src/lib/utils/options/sources/typedoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,92 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
},
});

options.addDeclaration({
name: "navigation",
help: "Determines how the navigation sidebar is organized.",
type: ParameterType.Flags,
defaults: {
includeCategories: false,
includeGroups: false,
},
});

options.addDeclaration({
name: "visibilityFilters",
help: "Specify the default visibility for builtin filters and additional filters according to modifier tags.",
type: ParameterType.Mixed,
configFileOnly: true,
defaultValue: {
protected: false,
private: false,
inherited: true,
external: false,
},
validate(value) {
const knownKeys = ["protected", "private", "inherited", "external"];
if (!value || typeof value !== "object") {
throw new Error("visibilityFilters must be an object.");
}

for (const [key, val] of Object.entries(value)) {
if (!key.startsWith("@") && !knownKeys.includes(key)) {
throw new Error(
`visibilityFilters can only include the following non-@ keys: ${knownKeys.join(
", "
)}`
);
}

if (typeof val !== "boolean") {
throw new Error(
`All values of visibilityFilters must be booleans.`
);
}
}
},
});

options.addDeclaration({
name: "searchCategoryBoosts",
help: "Configure search to give a relevance boost to selected categories",
type: ParameterType.Mixed,
configFileOnly: true,
defaultValue: {},
validate(value) {
if (!isObject(value)) {
throw new Error(
"The 'searchCategoryBoosts' option must be a non-array object."
);
}

if (Object.values(value).some((x) => typeof x !== "number")) {
throw new Error(
"All values of 'searchCategoryBoosts' must be numbers."
);
}
},
});
options.addDeclaration({
name: "searchGroupBoosts",
help: 'Configure search to give a relevance boost to selected kinds (eg "class")',
type: ParameterType.Mixed,
configFileOnly: true,
defaultValue: {},
validate(value: unknown) {
if (!isObject(value)) {
throw new Error(
"The 'searchGroupBoosts' option must be a non-array object."
);
}

if (Object.values(value).some((x) => typeof x !== "number")) {
throw new Error(
"All values of 'searchGroupBoosts' must be numbers."
);
}
},
});

///////////////////////////
///// Comment Options /////
///////////////////////////
Expand Down Expand Up @@ -510,7 +596,7 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
name: "categorizeByGroup",
help: "Specify whether categorization will be done at the group level.",
type: ParameterType.Boolean,
defaultValue: true,
defaultValue: true, // 0.25, change this to false.
});
options.addDeclaration({
name: "defaultCategory",
Expand Down Expand Up @@ -594,82 +680,6 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
},
});

options.addDeclaration({
name: "visibilityFilters",
help: "Specify the default visibility for builtin filters and additional filters according to modifier tags.",
type: ParameterType.Mixed,
configFileOnly: true,
defaultValue: {
protected: false,
private: false,
inherited: true,
external: false,
},
validate(value) {
const knownKeys = ["protected", "private", "inherited", "external"];
if (!value || typeof value !== "object") {
throw new Error("visibilityFilters must be an object.");
}

for (const [key, val] of Object.entries(value)) {
if (!key.startsWith("@") && !knownKeys.includes(key)) {
throw new Error(
`visibilityFilters can only include the following non-@ keys: ${knownKeys.join(
", "
)}`
);
}

if (typeof val !== "boolean") {
throw new Error(
`All values of visibilityFilters must be booleans.`
);
}
}
},
});

options.addDeclaration({
name: "searchCategoryBoosts",
help: "Configure search to give a relevance boost to selected categories",
type: ParameterType.Mixed,
configFileOnly: true,
defaultValue: {},
validate(value) {
if (!isObject(value)) {
throw new Error(
"The 'searchCategoryBoosts' option must be a non-array object."
);
}

if (Object.values(value).some((x) => typeof x !== "number")) {
throw new Error(
"All values of 'searchCategoryBoosts' must be numbers."
);
}
},
});
options.addDeclaration({
name: "searchGroupBoosts",
help: 'Configure search to give a relevance boost to selected kinds (eg "class")',
type: ParameterType.Mixed,
configFileOnly: true,
defaultValue: {},
validate(value: unknown) {
if (!isObject(value)) {
throw new Error(
"The 'searchGroupBoosts' option must be a non-array object."
);
}

if (Object.values(value).some((x) => typeof x !== "number")) {
throw new Error(
"All values of 'searchGroupBoosts' must be numbers."
);
}
},
});

///////////////////////////
///// General Options /////
///////////////////////////
Expand Down
24 changes: 17 additions & 7 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,8 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark {
}
.tsd-navigation ul,
.tsd-page-navigation ul {
margin: 0;
margin-top: 0;
margin-bottom: 0;
padding: 0;
list-style: none;
}
Expand All @@ -729,14 +730,24 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark {
padding: 0;
max-width: 100%;
}
.tsd-nested-navigation {
margin-left: 3rem;
}
.tsd-nested-navigation > li > details {
margin-left: -1.5rem;
}
.tsd-small-nested-navigation {
margin-left: 1.5rem;
}
.tsd-small-nested-navigation > li > details {
margin-left: -1.5rem;
}

.tsd-nested-navigation > li > a,
.tsd-nested-navigation > li > span {
width: calc(100% - 1.75rem - 0.5rem);
margin-left: 1.75rem;
}
.tsd-nested-navigation > li > details {
margin-left: 1.75rem;
}

.tsd-page-navigation ul {
padding-left: 1.75rem;
}
Expand Down Expand Up @@ -781,8 +792,7 @@ a.tsd-index-link {
padding-top: 0;
padding-bottom: 0;
}
.tsd-index-accordion .tsd-accordion-summary svg {
margin-right: 0.25rem;
.tsd-index-accordion .tsd-accordion-summary > svg {
margin-left: 0.25rem;
}
.tsd-index-content > :not(:first-child) {
Expand Down

0 comments on commit ddddfdd

Please sign in to comment.