Skip to content

Commit

Permalink
Merge pull request #2255 from TypeStrong/feat/1532
Browse files Browse the repository at this point in the history
Categorize sidebar
  • Loading branch information
Gerrit0 authored Apr 22, 2023
2 parents 69a2b60 + ddddfdd commit 44badfd
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 118 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
88 changes: 67 additions & 21 deletions src/lib/output/themes/default/partials/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { DeclarationReflection, ProjectReflection, Reflection, ReflectionKind } from "../../../../models";
import {
DeclarationReflection,
ProjectReflection,
Reflection,
ReflectionCategory,
ReflectionGroup,
ReflectionKind,
} from "../../../../models";
import { JSX } from "../../../../utils";
import type { PageEvent } from "../../../events";
import { camelToTitleCase, classNames, getDisplayName, wbr } from "../../lib";
Expand Down Expand Up @@ -98,61 +105,100 @@ export function settings(context: DefaultThemeRenderContext) {
);
}

type NavigationElement = ReflectionCategory | ReflectionGroup | DeclarationReflection;

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 && 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">
{link(props.project)}
{createNavElement(props.project)}
<ul class="tsd-small-nested-navigation">
{props.project.children?.map((c) => (
<li>{links(c)}</li>
{getNavigationElements(props.project, opts).map((c) => (
<li>{links(c, [])}</li>
))}
</ul>
</nav>
);

function links(mod: DeclarationReflection) {
const children = (mod.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project) && mod.children) || [];

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

const children = getNavigationElements(mod, opts);

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

return (
<details
class={classNames({ "tsd-index-accordion": true }, nameClasses)}
open={inPath(mod)}
data-key={mod.getFullName()}
open={mod instanceof Reflection && inPath(mod)}
data-key={mod instanceof Reflection ? mod.getFullName() : [...parents, mod.title].join("$")}
>
<summary class="tsd-accordion-summary">
{context.icons.chevronDown()}
{link(mod)}
{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 link(child: DeclarationReflection | ProjectReflection, nameClasses?: string) {
return (
<a href={context.urlTo(child)} class={classNames({ current: child === props.model }, nameClasses)}>
{context.icons[child.kind]()}
<span>{wbr(getDisplayName(child))}</span>
</a>
);
function createNavElement(child: NavigationElement | ProjectReflection, nameClasses?: string) {
if (child instanceof Reflection) {
return (
<a href={context.urlTo(child)} class={classNames({ current: child === props.model }, nameClasses)}>
{context.icons[child.kind]()}
<span>{wbr(getDisplayName(child))}</span>
</a>
);
}

return <span>{child.title}</span>;
}

function inPath(mod: DeclarationReflection | ProjectReflection) {
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
Loading

0 comments on commit 44badfd

Please sign in to comment.