Table of contents
The table of contents plugin adds Notion's table of contents block, the document's headings listed live, each scrolling to its heading.
createTocPlugin() adds Notion’s table of contents: a toc block lists the document’s headings and toggle headings, live, in reading order, each indented by its level. A click scrolls to the heading. It is not a default plugin: list it.
<script lang="ts">
import { Edytor, createTocPlugin } from 'edytor';
</script>
<Edytor plugins={[createTocPlugin()]} />
A slash menu then lists “Table of contents” under Advanced blocks (keywords toc, outline, headings, contents). Like a divider it replaces an empty line, and goes after a line that holds text.
What it lists
- Every
headingandtoggle-headingof the document, nested ones included (in a toggle, a column, a list item), in reading order. Its list is drawn from the view, never stored: an edit anywhere (a heading typed, added, removed, turned into text, a peer’s change) updates it at once. - Each entry is the heading’s text (an inline atom, such as a mention, is left out), “Untitled” for an empty heading, and is indented one nesting step per level below the shallowest level listed: a document whose headings start at Heading 2 lists them flush.
- With no heading, it shows Notion’s hint, “Add headings to create a table of contents.”
A click scrolls the heading to the top of the view, smoothly, and opens the closed toggles it sits in. The caret and the selection stay where they were. A readonly view lists and scrolls too.
Options
createTocPlugin(options) takes a TocPluginOptions:
headingLevel?(block: { type: string; data: Record<string, unknown> }) => number | null
Which blocks the table lists, and how deep (1 the outermost), else `null`. By default the rich text headings and toggle headings by their `data.level`: `h1` 1, `h2` 2, any other 3, as they are drawn.
(block: { type: string; data: Record<string, unknown> }) => number | nullentry?Snippet<[TocEntryPayload]>
One entry's markup, replacing the default button (see [Your entries](#your-entries)).
Snippet<[TocEntryPayload]>empty?Snippet<[TocEmptyPayload]>
What shows while the document has no heading, replacing the default paragraph: `{ label }`, the view's `toc.empty`.
Snippet<[TocEmptyPayload]>The first table of contents plugin listed in a view gives all three.
import { createTocPlugin } from 'edytor';
// Your own heading kind beside the rich text ones.
export const toc = createTocPlugin({
headingLevel: ({ type, data }) =>
type === 'chapter' ? 1 : type === 'heading' ? (data.level === 'h1' ? 1 : 2) : null
});
Your entries
entry is drawn once per heading, in reading order, inside the block’s void wrapper ([data-edytor-toc]). It receives a TocEntryPayload: the heading’s block id, its level, its depth (its level less the shallowest shown: how many steps in it sits), its text ('' when it has none), its label (the text, else the view’s toc.untitled), go() (scroll to it, opening the closed toggles it sits in) and control. Give a button {@attach heading.control}, as a menu’s row takes its attachment: its marker (data-edytor-toc-entry), a press that keeps the caret and a click that runs go(). The indent is yours, from depth (the default markup sets --edytor-toc-depth, which the theme reads).
<script lang="ts">
import {
Edytor,
createTocPlugin,
type TocEmptyPayload,
type TocEntryPayload
} from 'edytor';
const toc = createTocPlugin({ entry, empty });
</script>
{#snippet entry(heading: TocEntryPayload)}
<button type="button" class="entry" style:padding-inline-start="{heading.depth}em" {@attach heading.control}>
{'·'.repeat(heading.depth)} {heading.label}
</button>
{/snippet}
{#snippet empty(none: TocEmptyPayload)}
<p class="entry-none">{none.label}</p>
{/snippet}
<Edytor plugins={[toc]} />
The kind
| Kind | Element | Data | Preset |
|---|---|---|---|
toc |
nav with aria-label="Table of contents" |
none | Table of contents (☰, group Advanced blocks) |
The block is void and shows no text of its own, like a divider. The command id is block.toc. Its role is in defaultSemantics (tocKinds). Its entries are buttons: they keep their events from the editor, so a click never moves the caret.
Clipboard
Copying exports an empty <nav data-edytor-toc></nav> (its list is drawn, never carried) and no plain text; pasting that HTML back imports a table of contents.
Styling
| Selector | Matches |
|---|---|
[data-edytor-type='toc'] |
The nav |
[data-edytor-toc-entry] |
One entry: data-level is its heading’s level, --edytor-toc-depth its steps in, [data-untitled] for an empty heading |
[data-edytor-toc-empty] |
The hint shown with no heading |
Without a theme the entries are the browser’s buttons, one after the other. The Notion theme draws them as Notion’s lines, indented by --edytor-toc-depth times --edytor-nest-indent, in its secondary text colour, with a hover background.