diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts
index 2c6d502..2015520 100644
--- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts
+++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts
@@ -10,6 +10,7 @@ import {
IconLetterY,
IconList,
IconListNumbers,
+ IconListTree,
IconMath,
IconMathFunction,
IconMovie,
@@ -96,6 +97,20 @@ const CommandGroups: SlashMenuGroupedItemsType = {
.run();
},
},
+ {
+ title: "Table of Contents",
+ description: "Create a table of contents.",
+ searchTerms: ["table", "contents", "list"],
+ icon: IconListTree,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .toggleTableOfContents()
+ .run();
+ },
+ },
{
title: "Bullet list",
description: "Create a simple bullet list.",
diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents-view.tsx b/apps/client/src/features/editor/components/table-of-contents/table-of-contents-view.tsx
new file mode 100644
index 0000000..d25d4e3
--- /dev/null
+++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents-view.tsx
@@ -0,0 +1,74 @@
+import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
+import {
+ IconAlertTriangleFilled,
+ IconCircleCheckFilled,
+ IconCircleXFilled,
+ IconInfoCircleFilled,
+} from "@tabler/icons-react";
+import { Alert, Divider, Group, Stack, Title, UnstyledButton, Text } from "@mantine/core";
+// import classes from "./callout.module.css";
+import { CalloutType } from "@docmost/editor-ext";
+import React, { useMemo } from "react";
+import { TextSelection } from "@tiptap/pm/state";
+import classes from "./table-of-contents.module.css";
+import clsx from "clsx";
+
+export default function TableOfContentsView(props: NodeViewProps) {
+ const { node, editor, selected } = props;
+ const { type } = node.attrs;
+
+ const headings = editor.getJSON().content?.filter((c) => c.type == "heading");
+
+ return (
+
+
+ {headings.map((value, index) => (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.button != 0) return;
+
+ if (editor) {
+ const headings = editor.view.dom.querySelectorAll("h1, h2, h3, h4, h5, h6");
+ const clickedHeading = headings[index];
+
+ // find selected heading position in DOM relative to editors view
+ const pos = editor.view.posAtDOM(clickedHeading, 0);
+
+ // start new state transaction on editors view state
+ const tr = editor.view.state.tr;
+ // move editor cursor to heading
+ tr.setSelection(new TextSelection(tr.doc.resolve(pos)));
+ editor.view.dispatch(tr);
+ editor.view.focus();
+
+ window.scrollTo({
+ top:
+ clickedHeading.getBoundingClientRect().top -
+ // subtract half of elements height to avoid viewport clipping
+ clickedHeading.getBoundingClientRect().height / 2 -
+ // substract headers height so that heading is visible after scroll.
+ // getComputedStyles is not evaluating "--app-shell-header-height" so have hardcoded pixels.
+ 45 * 2 +
+ window.scrollY,
+ behavior: "smooth",
+ });
+ }
+ }}
+ >
+
+ {value.content?.at(0).text}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.module.css b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.module.css
new file mode 100644
index 0000000..9ce2f70
--- /dev/null
+++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.module.css
@@ -0,0 +1,9 @@
+.divider {
+ flex: 1 0 0;
+}
+
+.heading {
+ :hover {
+ background-color: var(--mantine-color-default-hover);
+ }
+}
\ No newline at end of file
diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts
index 1123c30..f185c7c 100644
--- a/apps/client/src/features/editor/extensions/extensions.ts
+++ b/apps/client/src/features/editor/extensions/extensions.ts
@@ -35,6 +35,7 @@ import {
CustomCodeBlock,
Drawio,
Excalidraw,
+ TableofContents,
} from "@docmost/editor-ext";
import {
randomElement,
@@ -54,6 +55,7 @@ import CodeBlockView from "@/features/editor/components/code-block/code-block-vi
import DrawioView from "../components/drawio/drawio-view";
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
import plaintext from "highlight.js/lib/languages/plaintext";
+import TableOfContentsView from "../components/table-of-contents/table-of-contents-view";
const lowlight = createLowlight(common);
lowlight.register("mermaid", plaintext);
@@ -162,6 +164,9 @@ export const mainExtensions = [
Excalidraw.configure({
view: ExcalidrawView,
}),
+ TableofContents.configure({
+ view: TableOfContentsView
+ })
] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts
index 80e2035..64d7a04 100644
--- a/packages/editor-ext/src/index.ts
+++ b/packages/editor-ext/src/index.ts
@@ -14,4 +14,4 @@ export * from "./lib/attachment";
export * from "./lib/custom-code-block"
export * from "./lib/drawio";
export * from "./lib/excalidraw";
-
+export * from "./lib/table-of-contents";
diff --git a/packages/editor-ext/src/lib/table-of-contents.ts b/packages/editor-ext/src/lib/table-of-contents.ts
new file mode 100644
index 0000000..127ecac
--- /dev/null
+++ b/packages/editor-ext/src/lib/table-of-contents.ts
@@ -0,0 +1,89 @@
+import { Node, findChildren, findParentNode, mergeAttributes, wrappingInputRule } from "@tiptap/core";
+import { icon, setAttributes } from "./utils";
+import { ReactNodeViewRenderer } from "@tiptap/react";
+
+declare module "@tiptap/core" {
+ interface Commands {
+ tableOfContents: {
+ setTableOfContents: () => ReturnType;
+ unsetTableOfContents: () => ReturnType;
+ toggleTableOfContents: () => ReturnType;
+ };
+ }
+}
+
+export interface TableofContentsOptions {
+ HTMLAttributes: Record;
+ view: any;
+}
+
+export const TableofContents = Node.create({
+ name: "tableOfContents",
+
+ content: "block+",
+ inline: false,
+ group: "block",
+ isolating: true,
+ atom: true,
+ defining: true,
+
+ addOptions() {
+ return {
+ HTMLAttributes: {},
+ view: null,
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: `div[data-type="${this.name}"]`,
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ["div", mergeAttributes({ "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes), 0];
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(this.options.view);
+ },
+
+ addCommands() {
+ return {
+ setTableOfContents:
+ () =>
+ ({ commands }) => {
+ return commands.setNode(this.name);
+ },
+
+ unsetTableOfContents:
+ () =>
+ ({ commands }) => {
+ return commands.lift(this.name);
+ },
+
+ toggleTableOfContents:
+ () =>
+ ({ commands }) => {
+ return commands.toggleWrap(this.name);
+ },
+ };
+ },
+
+ // addInputRules() {
+ // return [
+ // wrappingInputRule({
+ // find: /^:::details\s$/,
+ // type: this.type,
+ // }),
+ // ];
+ // },
+
+ // addKeyboardShortcuts() {
+ // return {
+ // "Mod-Alt-d": () => this.editor.commands.toggleDetails(),
+ // };
+ // },
+});