mirror of
https://github.com/docmost/docmost.git
synced 2025-11-23 19:11:08 +10:00
table of contents node - WIP
This commit is contained in:
@ -20,6 +20,7 @@ import {
|
|||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconAppWindow,
|
IconAppWindow,
|
||||||
IconSitemap,
|
IconSitemap,
|
||||||
|
IconAlignLeft2,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
CommandProps,
|
CommandProps,
|
||||||
@ -153,6 +154,14 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
|||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
|
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",
|
title: "Image",
|
||||||
description: "Upload any image from your device.",
|
description: "Upload any image from your device.",
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
@ -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";
|
||||||
@ -11,6 +11,7 @@ import { Typography } from "@tiptap/extension-typography";
|
|||||||
import { TextStyle } from "@tiptap/extension-text-style";
|
import { TextStyle } from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
import SlashCommand from "@/features/editor/extensions/slash-command";
|
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 { Collaboration } from "@tiptap/extension-collaboration";
|
||||||
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
||||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
@ -40,6 +41,7 @@ import {
|
|||||||
Mention,
|
Mention,
|
||||||
Subpages,
|
Subpages,
|
||||||
TableDndExtension,
|
TableDndExtension,
|
||||||
|
TableOfContentsNode,
|
||||||
} from "@docmost/editor-ext";
|
} from "@docmost/editor-ext";
|
||||||
import {
|
import {
|
||||||
randomElement,
|
randomElement,
|
||||||
@ -78,6 +80,7 @@ import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboa
|
|||||||
import EmojiCommand from "./emoji-command";
|
import EmojiCommand from "./emoji-command";
|
||||||
import { CharacterCount } from "@tiptap/extension-character-count";
|
import { CharacterCount } from "@tiptap/extension-character-count";
|
||||||
import { countWords } from "alfaaz";
|
import { countWords } from "alfaaz";
|
||||||
|
import { TableOfContentsNodeview } from "@/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx";
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("mermaid", plaintext);
|
lowlight.register("mermaid", plaintext);
|
||||||
@ -228,19 +231,23 @@ export const mainExtensions = [
|
|||||||
SearchAndReplace.extend({
|
SearchAndReplace.extend({
|
||||||
addKeyboardShortcuts() {
|
addKeyboardShortcuts() {
|
||||||
return {
|
return {
|
||||||
'Mod-f': () => {
|
"Mod-f": () => {
|
||||||
const event = new CustomEvent("openFindDialogFromEditor", {});
|
const event = new CustomEvent("openFindDialogFromEditor", {});
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
'Escape': () => {
|
Escape: () => {
|
||||||
const event = new CustomEvent("closeFindDialogFromEditor", {});
|
const event = new CustomEvent("closeFindDialogFromEditor", {});
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
}).configure(),
|
}).configure(),
|
||||||
|
TiptapTableOfContents,
|
||||||
|
TableOfContentsNode.configure({
|
||||||
|
view: TableOfContentsNodeview,
|
||||||
|
}),
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
Embed,
|
Embed,
|
||||||
Mention,
|
Mention,
|
||||||
Subpages,
|
Subpages,
|
||||||
|
TableOfContentsNode,
|
||||||
} from '@docmost/editor-ext';
|
} from '@docmost/editor-ext';
|
||||||
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
||||||
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
||||||
@ -80,6 +81,7 @@ export const tiptapExtensions = [
|
|||||||
Embed,
|
Embed,
|
||||||
Mention,
|
Mention,
|
||||||
Subpages,
|
Subpages,
|
||||||
|
TableOfContentsNode,
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
export function jsonToHtml(tiptapJson: any) {
|
export function jsonToHtml(tiptapJson: any) {
|
||||||
|
|||||||
Submodule apps/server/src/ee updated: c92deeeac1...9957da11d6
@ -48,6 +48,7 @@
|
|||||||
"@tiptap/extension-table": "^2.10.3",
|
"@tiptap/extension-table": "^2.10.3",
|
||||||
"@tiptap/extension-table-cell": "^2.10.3",
|
"@tiptap/extension-table-cell": "^2.10.3",
|
||||||
"@tiptap/extension-table-header": "^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-table-row": "^2.10.3",
|
||||||
"@tiptap/extension-task-item": "^2.10.3",
|
"@tiptap/extension-task-item": "^2.10.3",
|
||||||
"@tiptap/extension-task-list": "^2.10.3",
|
"@tiptap/extension-task-list": "^2.10.3",
|
||||||
|
|||||||
@ -20,3 +20,4 @@ export * from "./lib/markdown";
|
|||||||
export * from "./lib/search-and-replace";
|
export * from "./lib/search-and-replace";
|
||||||
export * from "./lib/embed-provider";
|
export * from "./lib/embed-provider";
|
||||||
export * from "./lib/subpages";
|
export * from "./lib/subpages";
|
||||||
|
export * from "./lib/table-of-contents-node";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export { TableOfContentsNode } from "./table-of-contents";
|
||||||
@ -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
21
pnpm-lock.yaml
generated
@ -106,6 +106,9 @@ importers:
|
|||||||
'@tiptap/extension-table-header':
|
'@tiptap/extension-table-header':
|
||||||
specifier: ^2.10.3
|
specifier: ^2.10.3
|
||||||
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
|
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':
|
'@tiptap/extension-table-row':
|
||||||
specifier: ^2.10.3
|
specifier: ^2.10.3
|
||||||
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
|
version: 2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))
|
||||||
@ -4239,6 +4242,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@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':
|
'@tiptap/extension-table-row@2.14.0':
|
||||||
resolution: {integrity: sha512-a1GvCIju9xETIQu664lVQNftHqpPdRmwYp+1QzY82v3zHClso+tTLPeBSlbDdUscSmv3yZXgGML20IiOoR2l2Q==}
|
resolution: {integrity: sha512-a1GvCIju9xETIQu664lVQNftHqpPdRmwYp+1QzY82v3zHClso+tTLPeBSlbDdUscSmv3yZXgGML20IiOoR2l2Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -9621,6 +9630,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
uuid@10.0.0:
|
||||||
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
uuid@11.1.0:
|
uuid@11.1.0:
|
||||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -14305,6 +14318,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
|
'@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))':
|
'@tiptap/extension-table-row@2.14.0(@tiptap/core@2.14.0(@tiptap/pm@2.14.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
|
'@tiptap/core': 2.14.0(@tiptap/pm@2.14.0)
|
||||||
@ -20662,6 +20681,8 @@ snapshots:
|
|||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
uuid@10.0.0: {}
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
uuid@9.0.1: {}
|
uuid@9.0.1: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user