mirror of
https://github.com/docmost/docmost.git
synced 2025-11-23 08:51:09 +10:00
table of contents node - WIP
This commit is contained in:
@ -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.",
|
||||
|
||||
@ -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 { 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[];
|
||||
|
||||
@ -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) {
|
||||
|
||||
Submodule apps/server/src/ee updated: c92deeeac1...9957da11d6
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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':
|
||||
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: {}
|
||||
|
||||
Reference in New Issue
Block a user