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) {

View File

@ -48,6 +48,7 @@
"@tiptap/extension-table": "^2.10.3",
"@tiptap/extension-table-cell": "^2.10.3",
"@tiptap/extension-table-header": "^2.10.3",
"@tiptap/extension-table-of-contents": "2.26.3",
"@tiptap/extension-table-row": "^2.10.3",
"@tiptap/extension-task-item": "^2.10.3",
"@tiptap/extension-task-list": "^2.10.3",

View File

@ -20,3 +20,4 @@ export * from "./lib/markdown";
export * from "./lib/search-and-replace";
export * from "./lib/embed-provider";
export * from "./lib/subpages";
export * from "./lib/table-of-contents-node";

View File

@ -0,0 +1 @@
export { TableOfContentsNode } from "./table-of-contents";

View File

@ -0,0 +1,52 @@
import { Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
export interface TableOfContentsNodeOptions {
HTMLAttributes: Record<string, any>;
view: any;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
tableOfContentsNode: {
insertTableOfContents: () => ReturnType;
};
}
}
export const TableOfContentsNode = Node.create<TableOfContentsNodeOptions>({
name: "tableOfContentsNode",
group: "block",
atom: true,
selectable: true,
draggable: true,
inline: false,
parseHTML() {
return [
{
tag: 'div[data-type="table-of-content"]',
},
];
},
renderHTML({ HTMLAttributes }) {
return ["div", { ...HTMLAttributes, "data-type": "table-of-content" }];
},
addNodeView() {
return ReactNodeViewRenderer(this.options.view);
},
addCommands() {
return {
insertTableOfContents:
() =>
({ commands }) => {
return commands.insertContent({
type: this.name,
});
},
};
},
});

21
pnpm-lock.yaml generated
View File

@ -106,6 +106,9 @@ importers:
'@tiptap/extension-table-header':
specifier: ^2.10.3
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
'@tiptap/extension-table-of-contents':
specifier: 2.26.3
version: 2.26.3(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)
'@tiptap/extension-table-row':
specifier: ^2.10.3
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
@ -4239,6 +4242,12 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-table-of-contents@2.26.3':
resolution: {integrity: sha512-uARSWaxIIx/oWqP8aDP4t4znC7dmbfpjUNFi4t9D2XS1GmQVbmbKgpinLt+K4QcXkLuRGLYqGVv4kpN3WVm1Bg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-table-row@2.14.0':
resolution: {integrity: sha512-a1GvCIju9xETIQu664lVQNftHqpPdRmwYp+1QzY82v3zHClso+tTLPeBSlbDdUscSmv3yZXgGML20IiOoR2l2Q==}
peerDependencies:
@ -9621,6 +9630,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
@ -14305,6 +14318,12 @@ snapshots:
dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
'@tiptap/extension-table-of-contents@2.26.3(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))(@tiptap/pm@2.14.0)':
dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
'@tiptap/pm': 2.14.0
uuid: 10.0.0
'@tiptap/extension-table-row@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))':
dependencies:
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
@ -20662,6 +20681,8 @@ snapshots:
utils-merge@1.0.1: {}
uuid@10.0.0: {}
uuid@11.1.0: {}
uuid@9.0.1: {}