From b79b693f509edc4790fda93aee7a7938db523d40 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:29:21 +0000 Subject: [PATCH] * wip * fix styling * use nanoid --- .../public/locales/en-US/translation.json | 1 + .../table-of-contents-nodeview.module.css | 76 ++++++++++--------- .../table-of-contents-nodeview.tsx | 52 +++++++++---- .../features/editor/extensions/extensions.ts | 5 +- .../src/collaboration/collaboration.util.ts | 5 ++ packages/editor-ext/src/lib/utils.ts | 8 +- 6 files changed, 97 insertions(+), 50 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 36985540..aafaebf9 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -380,6 +380,7 @@ "Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...", "Table of contents": "Table of contents", "Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents.", + "No table of contents yet": "No table of contents yet", "Share": "Share", "Public sharing": "Public sharing", "Shared by": "Shared by", diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.module.css b/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.module.css index acff0428..96290f66 100644 --- a/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.module.css +++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.module.css @@ -1,13 +1,18 @@ -.header { -} - .container { + counter-reset: h1 h2 h3 h4; + border-left: 1px solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); } .emptyState { + color: light-dark( + var(--mantine-color-dark-3), + var(--mantine-color-dark-2) + ) !important; } .link { + text-decoration: none; outline: none; cursor: pointer; display: block; @@ -19,9 +24,41 @@ 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; + border-bottom: none !important; + padding-left: calc(var(--level) * 1rem); + + &[style*="--level: 1"] { + counter-increment: h1; + counter-reset: h2 h3 h4; + padding-left: 6px; + &::before { + content: counter(h1) ". "; + } + } + + &[style*="--level: 2"] { + counter-increment: h2; + counter-reset: h3 h4; + &::before { + content: counter(h2) ". "; + } + } + + &[style*="--level: 3"] { + counter-increment: h3; + counter-reset: h4; + &::before { + content: counter(h3) ". "; + } + } + + &[style*="--level: 4"] { + counter-increment: h4; + &::before { + content: counter(h4) ". "; + } + } @mixin hover { background-color: light-dark( @@ -29,33 +66,4 @@ 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)); } diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx b/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx index 15772566..68542414 100644 --- a/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx +++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents-nodeview.tsx @@ -2,48 +2,74 @@ 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"; +import { useTranslation } from "react-i18next"; +import { TextSelection } from "@tiptap/pm/state"; export type TableOfContentsProps = { editor: CoreEditor; - onItemClick?: () => void; }; export const TableOfContentsNodeview = memo( - ({ editor, onItemClick }: TableOfContentsProps) => { + ({ editor }: TableOfContentsProps) => { const content = useEditorState({ editor, selector: (ctx) => (ctx.editor.storage.tableOfContents as TableOfContentsStorage)?.content, }); + const { t } = useTranslation(); + + const onTocItemClick = (e, id) => { + e.preventDefault(); + + if (editor) { + const element = editor.view.dom.querySelector(`[data-toc-id="${id}"`); + const pos = editor.view.posAtDOM(element, 0); + + // set focus + const tr = editor.view.state.tr; + + tr.setSelection(new TextSelection(tr.doc.resolve(pos))); + + editor.view.dispatch(tr); + + editor.view.focus(); + + if (history.pushState) { + history.pushState(null, null, `#${id}`); + } + + window.scrollTo({ + top: element.getBoundingClientRect().top + window.scrollY, + behavior: "smooth", + }); + } + }; return (
-
Table of contents
{content.length > 0 ? (
{content - .filter((item) => item.level <= 3) + .filter((item) => item.level <= 4) .map((item) => ( onTocItemClick(e, item.id)} + className={classes.link} + data-item-index={item.itemIndex} + draggable="false" > - {item.itemIndex}. {item.textContent} + {item.textContent} ))}
) : (
- Start adding headlines to your document … + {t("No table of contents yet")}
)}
diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 8c843be1..0ed70847 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -42,6 +42,7 @@ import { Subpages, TableDndExtension, TableOfContentsNode, + generateNodeId, } from "@docmost/editor-ext"; import { randomElement, @@ -244,7 +245,9 @@ export const mainExtensions = [ }; }, }).configure(), - TiptapTableOfContents, + TiptapTableOfContents.configure({ + getId: () => generateNodeId(), + }), TableOfContentsNode.configure({ view: TableOfContentsNodeview, }), diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index e1b9e164..c27c36f6 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -34,7 +34,9 @@ import { Mention, Subpages, TableOfContentsNode, + generateNodeId, } from '@docmost/editor-ext'; +import { TableOfContents as TiptapTableOfContents } from '@tiptap/extension-table-of-contents'; import { generateText, getSchema, JSONContent } from '@tiptap/core'; import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; // @tiptap/html library works best for generating prosemirror json state but not HTML @@ -82,6 +84,9 @@ export const tiptapExtensions = [ Mention, Subpages, TableOfContentsNode, + TiptapTableOfContents.configure({ + getId: () => generateNodeId(), + }), ] as any; export function jsonToHtml(tiptapJson: any) { diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts index 31cccc64..2f9cd579 100644 --- a/packages/editor-ext/src/lib/utils.ts +++ b/packages/editor-ext/src/lib/utils.ts @@ -5,6 +5,7 @@ import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { Node, ResolvedPos } from "@tiptap/pm/model"; import Table from "@tiptap/extension-table"; import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"; +import { customAlphabet } from "nanoid"; export const isRectSelected = (rect: any) => (selection: CellSelection) => { const map = TableMap.get(selection.$anchorCell.node(-1)); @@ -383,9 +384,12 @@ export function icon(name: string) { export function sanitizeUrl(url: string | undefined): string { if (!url) return ""; - + const sanitized = braintreeSanitizeUrl(url); - + // Return empty string instead of "about:blank" return sanitized === "about:blank" ? "" : sanitized; } + +const alphabet = "abcdefghijklmnopqrstuvwxyz"; +export const generateNodeId = customAlphabet(alphabet, 15);