table of contents node - WIP

This commit is contained in:
Philipinho
2025-10-15 12:20:08 +01:00
parent 042836cb6d
commit 2b6ac81c61
11 changed files with 212 additions and 4 deletions

View File

@ -20,6 +20,7 @@ import {
IconCalendar,
IconAppWindow,
IconSitemap,
IconAlignLeft2,
} from "@tabler/icons-react";
import {
CommandProps,
@ -153,6 +154,14 @@ const CommandGroups: SlashMenuGroupedItemsType = {
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
},
{
title: "Table of contents",
description: "Insert table of contents",
searchTerms: ["toc"],
icon: IconAlignLeft2,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).insertTableOfContents().run(),
},
{
title: "Image",
description: "Upload any image from your device.",

View File

@ -0,0 +1,61 @@
.header {
}
.container {
}
.emptyState {
}
.link {
outline: none;
cursor: pointer;
display: block;
width: 100%;
text-align: start;
word-wrap: break-word;
background-color: transparent;
color: var(--mantine-color-text);
font-size: var(--mantine-font-size-sm);
line-height: var(--mantine-line-height-sm);
padding: 6px;
border-top-right-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
border: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-2),
var(--mantine-color-dark-6)
);
}
@media (max-width: $mantine-breakpoint-sm) {
& {
border: none !important;
padding-left: 0px;
}
}
}
.linkActive {
font-weight: 500;
border-left-color: light-dark(
var(--mantine-color-grey-5),
var(--mantine-color-grey-3)
);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
&,
&:hover {
background-color: light-dark(
var(--mantine-color-gray-3),
var(--mantine-color-dark-5)
) !important;
}
}
.leftBorder {
border-left: 1px solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}

View File

@ -0,0 +1,53 @@
import { Editor as CoreEditor } from "@tiptap/core";
import { TableOfContentsStorage } from "@tiptap/extension-table-of-contents";
import { NodeViewWrapper, useEditorState } from "@tiptap/react";
import { memo } from "react";
import { clsx } from "clsx";
import classes from "./table-of-contents-nodeview.module.css";
export type TableOfContentsProps = {
editor: CoreEditor;
onItemClick?: () => void;
};
export const TableOfContentsNodeview = memo(
({ editor, onItemClick }: TableOfContentsProps) => {
const content = useEditorState({
editor,
selector: (ctx) =>
(ctx.editor.storage.tableOfContents as TableOfContentsStorage)?.content,
});
return (
<NodeViewWrapper>
<div contentEditable={false}>
<div className={classes.header}>Table of contents</div>
{content.length > 0 ? (
<div className={classes.container}>
{content.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
style={{ marginLeft: `${1 * item.level - 1}rem` }}
onClick={onItemClick}
className={clsx(
classes.link,
item.isActive && classes.linkActive,
)}
>
{item.itemIndex}. {item.textContent}
</a>
))}
</div>
) : (
<div className={classes.emptyState}>
Start adding headlines to your document
</div>
)}
</div>
</NodeViewWrapper>
);
},
);
TableOfContentsNodeview.displayName = "TableOfContentsNodeview";

View File

@ -11,6 +11,7 @@ import { Typography } from "@tiptap/extension-typography";
import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import SlashCommand from "@/features/editor/extensions/slash-command";
import { TableOfContents as TiptapTableOfContents } from "@tiptap/extension-table-of-contents";
import { Collaboration } from "@tiptap/extension-collaboration";
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
import { HocuspocusProvider } from "@hocuspocus/provider";
@ -40,6 +41,7 @@ import {
Mention,
Subpages,
TableDndExtension,
TableOfContentsNode,
} from "@docmost/editor-ext";
import {
randomElement,
@ -78,6 +80,7 @@ import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboa
import EmojiCommand from "./emoji-command";
import { CharacterCount } from "@tiptap/extension-character-count";
import { countWords } from "alfaaz";
import { TableOfContentsNodeview } from "@/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx";
const lowlight = createLowlight(common);
lowlight.register("mermaid", plaintext);
@ -228,19 +231,23 @@ export const mainExtensions = [
SearchAndReplace.extend({
addKeyboardShortcuts() {
return {
'Mod-f': () => {
"Mod-f": () => {
const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event);
return true;
},
'Escape': () => {
Escape: () => {
const event = new CustomEvent("closeFindDialogFromEditor", {});
document.dispatchEvent(event);
return true;
},
}
};
},
}).configure(),
TiptapTableOfContents,
TableOfContentsNode.configure({
view: TableOfContentsNodeview,
}),
] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];

View File

@ -33,6 +33,7 @@ import {
Embed,
Mention,
Subpages,
TableOfContentsNode,
} from '@docmost/editor-ext';
import { generateText, getSchema, JSONContent } from '@tiptap/core';
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
@ -80,6 +81,7 @@ export const tiptapExtensions = [
Embed,
Mention,
Subpages,
TableOfContentsNode,
] as any;
export function jsonToHtml(tiptapJson: any) {