diff --git a/apps/client/src/features/editor/components/heading/heading-view.tsx b/apps/client/src/features/editor/components/heading/heading-view.tsx index 5592bebf..5aaedd09 100644 --- a/apps/client/src/features/editor/components/heading/heading-view.tsx +++ b/apps/client/src/features/editor/components/heading/heading-view.tsx @@ -16,32 +16,36 @@ const generateSlug = (text: string) => export default function HeadingView({ node }: NodeViewProps) { const { t } = useTranslation(); - const [slug, setSlug] = useState(""); + const [combinedId, setCombinedId] = useState(""); const [url, setUrl] = useState(""); const [showAnchorButton, setShowAnchorButton] = useState(false); const tag: ElementType = `h${node.attrs.level}` as ElementType; + const uid = node.attrs.uid; useEffect(() => { - const text = node.textContent || ""; - const generatedSlug = generateSlug(text); - setSlug(generatedSlug); - - const baseUrl = window.location.href.split("#")[0]; - setUrl(`${baseUrl}#${generatedSlug}`); - }, [node.content]); + if (uid) { + const text = node.textContent || ""; + const textSlug = generateSlug(text); + const combined = textSlug ? `${textSlug}-${uid}` : uid; + setCombinedId(combined); + + const baseUrl = window.location.href.split("#")[0]; + setUrl(`${baseUrl}#${combined}`); + } + }, [uid, node.content]); return ( setShowAnchorButton(true)} onMouseLeave={() => setShowAnchorButton(false)} > - {showAnchorButton && node.textContent && ( + {showAnchorButton && uid && combinedId && node.textContent && ( {({ copied, copy }) => ( { - const el = document.getElementById(lastHash.current); + let el = document.getElementById(lastHash.current); + + if (!el) { + const hash = lastHash.current; + + if (hash.includes('-')) { + const parts = hash.split('-'); + const possibleUid = parts[parts.length - 1]; + + const elements = document.querySelectorAll('[id]'); + for (const element of elements) { + if (element.id.endsWith(`-${possibleUid}`)) { + el = element as HTMLElement; + break; + } + } + } + + if (!el) { + const elements = document.querySelectorAll('[id]'); + for (const element of elements) { + if (element.id.endsWith(`-${hash}`) || element.id === hash) { + el = element as HTMLElement; + break; + } + } + } + } + if (el) { const y = el.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top: y, behavior: "smooth" }); - window.history.replaceState(null, "", `#${lastHash.current}`); + window.history.replaceState(null, "", `#${el.id}`); } else if (retries > 0) { retries--; setTimeout(tryScroll, retryDelay); diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 9d427878..bd275a35 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -13,7 +13,7 @@ import { Color } from "@tiptap/extension-color"; import Table from "@tiptap/extension-table"; import TableHeader from "@tiptap/extension-table-header"; import SlashCommand from "@/features/editor/extensions/slash-command"; -import { Collaboration } from "@tiptap/extension-collaboration"; +import { Collaboration, isChangeOrigin } from "@tiptap/extension-collaboration"; import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor"; import { HocuspocusProvider } from "@hocuspocus/provider"; import { @@ -76,6 +76,8 @@ import { CharacterCount } from "@tiptap/extension-character-count"; import Heading from "@tiptap/extension-heading"; import HeadingView from "../components/heading/heading-view"; import { countWords } from "alfaaz"; +import UniqueID from '@tiptap/extension-unique-id'; +import { generateSlugId } from "../utils/nanoid"; const lowlight = createLowlight(common); lowlight.register("mermaid", plaintext); @@ -225,6 +227,12 @@ export const mainExtensions = [ CharacterCount.configure({ wordCounter: (text) => countWords(text), }), + UniqueID.configure({ + types: ['heading'], + attributeName: 'uid', + generateID: () => generateSlugId(), + filterTransaction: (transaction) => !isChangeOrigin(transaction), + }), ] as any; type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; diff --git a/apps/client/src/features/editor/utils/nanoid.ts b/apps/client/src/features/editor/utils/nanoid.ts new file mode 100644 index 00000000..c3aeca0a --- /dev/null +++ b/apps/client/src/features/editor/utils/nanoid.ts @@ -0,0 +1,5 @@ +import { customAlphabet } from "nanoid"; + +const slugIdAlphabet = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +export const generateSlugId = customAlphabet(slugIdAlphabet, 10); diff --git a/package.json b/package.json index f994f986..ad09f396 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@tiptap/extension-text-style": "^2.10.3", "@tiptap/extension-typography": "^2.10.3", "@tiptap/extension-underline": "^2.10.3", + "@tiptap/extension-unique-id": "^2.23.0", "@tiptap/extension-youtube": "^2.10.3", "@tiptap/html": "^2.10.3", "@tiptap/pm": "^2.10.3",