Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 heading and toggle-heading of 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:

PropType
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.

Type(block: { type: string; data: Record<string, unknown> }) => number | null
entry?Snippet<[TocEntryPayload]>

One entry's markup, replacing the default button (see [Your entries](#your-entries)).

TypeSnippet<[TocEntryPayload]>
empty?Snippet<[TocEmptyPayload]>

What shows while the document has no heading, replacing the default paragraph: `{ label }`, the view's `toc.empty`.

TypeSnippet<[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.

Was this page helpful?