From e50e8d6ac3c70a4950d94fbdccbed10f4fd49ca8 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 14:39:52 +0200 Subject: [PATCH 01/17] Commit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a705a35f8..386bbd45f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pem + .idea/ .vercel test-results/ From ea6d6bcc6577c79d9e35082d1c5b13a1bd2630b2 Mon Sep 17 00:00:00 2001 From: Mathilde Lannes Date: Mon, 2 Jun 2025 14:59:59 +0200 Subject: [PATCH 02/17] Display basic reference --- examples/01-basic/01-minimal/App.tsx | 121 ++++++++++++++++++++- examples/01-basic/01-minimal/Reference.tsx | 71 ++++++++++++ examples/01-basic/01-minimal/package.json | 3 +- examples/01-basic/01-minimal/styles.css | 5 + pnpm-lock.yaml | 29 +++++ 5 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 examples/01-basic/01-minimal/Reference.tsx create mode 100644 examples/01-basic/01-minimal/styles.css diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index a3b92bafd..aa5f884ad 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -1,12 +1,125 @@ +import { + BlockNoteSchema, + defaultInlineContentSpecs, + filterSuggestionItems, +} from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; -import { useCreateBlockNote } from "@blocknote/react"; +import { + DefaultReactSuggestionItem, + SuggestionMenuController, + useCreateBlockNote, +} from "@blocknote/react"; +import { Reference } from "./Reference"; export default function App() { - // Creates a new editor instance. - const editor = useCreateBlockNote(); + const schema = BlockNoteSchema.create({ + inlineContentSpecs: { + ...defaultInlineContentSpecs, + reference: Reference, + }, + }); + + const getReferenceMenuItems = ( + editor: typeof schema.BlockNoteEditor, + ): DefaultReactSuggestionItem[] => { + const citations = [ + { + key: 1, + doi: "10.1093/ajae/aaq063", + author: "Steve Smith", + title: "Understanding BlockNote", + journal: "BlockNote Journal", + year: 2023, + }, + { + key: 2, + doi: "10.1234/example.doi", + author: "Jane Doe", + title: "Exploring BlockNote Features", + journal: "BlockNote Features Journal", + year: 2022, + }, + { + key: 3, + doi: "10.5678/another.example", + author: "John Doe", + title: "Advanced BlockNote Techniques", + journal: "BlockNote Techniques Journal", + year: 2021, + }, + ]; + + return citations.map((citation) => ({ + title: citation.title, + onItemClick: () => { + editor.insertInlineContent([ + { + type: "reference", + props: { + ...citation, + }, + }, + " ", + ]); + }, + })); + }; + + + const editor = useCreateBlockNote({ + schema, + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Woah, you can add a reference like this: ", + styles: {}, + }, + { + type: "reference", + props: { + key: 1, + doi: "10.1093/ajae/aaq063", + author: "Steve Smith", + title: "Understanding BlockNote", + journal: "BlockNote Journal", + year: 2023, + }, + }, + { + type: "text", + text: " <- This is an example reference", + styles: {}, + }, + ], + }, + { + type: "paragraph", + content: "Press the '@' key to open the references menu and add another", + }, + { + type: "paragraph", + }, + ], + }); // Renders the editor instance using a React component. - return ; + return ( + + + filterSuggestionItems(getReferenceMenuItems(editor), query) + } + /> + + ); } diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx new file mode 100644 index 000000000..6ab942141 --- /dev/null +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -0,0 +1,71 @@ +import { createReactInlineContentSpec } from "@blocknote/react"; +import { useFloating, useHover, useInteractions } from "@floating-ui/react"; +import { useState } from "react"; +import "./styles.css"; + +export const Reference = createReactInlineContentSpec( + { + type: "reference", + propSchema: { + key: { + type: "number", + default: 1, + description: "The key for the reference.", + }, + doi: { + default: "Unknown", + }, + author: { + type: "string", + default: "Unknown Author", + }, + title: { + type: "string", + default: "Unknown Title", + }, + journal: { + type: "string", + default: "Unknown Journal", + }, + year: { + type: "number", + default: 2023, + }, + }, + content: "none", + }, + { + render: (props) => { + const [isOpen, setIsOpen] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + }); + + const hover = useHover(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + + const citation = props.inlineContent.props; + + return ( + + + [{citation.key}] + + {isOpen && ( +
+ {citation.author}, {citation.title}, {citation.year} +
+ )} +
+ ); + }, + }, +); diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json index 5179e1057..1f7cc8ecd 100644 --- a/examples/01-basic/01-minimal/package.json +++ b/examples/01-basic/01-minimal/package.json @@ -15,6 +15,7 @@ "@blocknote/ariakit": "latest", "@blocknote/mantine": "latest", "@blocknote/shadcn": "latest", + "@floating-ui/react": "0.27.11", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -24,4 +25,4 @@ "@vitejs/plugin-react": "^4.3.1", "vite": "^5.3.4" } -} \ No newline at end of file +} diff --git a/examples/01-basic/01-minimal/styles.css b/examples/01-basic/01-minimal/styles.css new file mode 100644 index 000000000..f2457adea --- /dev/null +++ b/examples/01-basic/01-minimal/styles.css @@ -0,0 +1,5 @@ +.floating { + background-color: black; + padding: 5px 10px; + border-radius: 5px; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38fa08518..4a1829487 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,9 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn + '@floating-ui/react': + specifier: 0.27.11 + version: 0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -6106,12 +6109,24 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.3': + resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/react@0.26.28': resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.27.11': + resolution: {integrity: sha512-ZVtJxk4gQceaAOm1p5TlNeUcSxvzEwbAkKPgLYfV2b3aavC9Up9OZ5qPWQrMCASzmXUtNK1VuKdrqZkhAYhaeQ==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} @@ -16812,6 +16827,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.13 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16820,6 +16841,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tabbable: 6.2.0 + '@floating-ui/react@0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.9 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + '@floating-ui/utils@0.2.9': {} '@hapi/hoek@9.3.0': {} From 6c70fcdcba06fefb66813670f90524ab3988ff7a Mon Sep 17 00:00:00 2001 From: Mathilde Lannes Date: Mon, 2 Jun 2025 15:04:48 +0200 Subject: [PATCH 03/17] Display basic reference (#1733) Co-authored-by: Mathilde Lannes --- examples/01-basic/01-minimal/App.tsx | 121 ++++++++++++++++++++- examples/01-basic/01-minimal/Reference.tsx | 71 ++++++++++++ examples/01-basic/01-minimal/package.json | 3 +- examples/01-basic/01-minimal/styles.css | 5 + pnpm-lock.yaml | 29 +++++ 5 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 examples/01-basic/01-minimal/Reference.tsx create mode 100644 examples/01-basic/01-minimal/styles.css diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index a3b92bafd..aa5f884ad 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -1,12 +1,125 @@ +import { + BlockNoteSchema, + defaultInlineContentSpecs, + filterSuggestionItems, +} from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; -import { useCreateBlockNote } from "@blocknote/react"; +import { + DefaultReactSuggestionItem, + SuggestionMenuController, + useCreateBlockNote, +} from "@blocknote/react"; +import { Reference } from "./Reference"; export default function App() { - // Creates a new editor instance. - const editor = useCreateBlockNote(); + const schema = BlockNoteSchema.create({ + inlineContentSpecs: { + ...defaultInlineContentSpecs, + reference: Reference, + }, + }); + + const getReferenceMenuItems = ( + editor: typeof schema.BlockNoteEditor, + ): DefaultReactSuggestionItem[] => { + const citations = [ + { + key: 1, + doi: "10.1093/ajae/aaq063", + author: "Steve Smith", + title: "Understanding BlockNote", + journal: "BlockNote Journal", + year: 2023, + }, + { + key: 2, + doi: "10.1234/example.doi", + author: "Jane Doe", + title: "Exploring BlockNote Features", + journal: "BlockNote Features Journal", + year: 2022, + }, + { + key: 3, + doi: "10.5678/another.example", + author: "John Doe", + title: "Advanced BlockNote Techniques", + journal: "BlockNote Techniques Journal", + year: 2021, + }, + ]; + + return citations.map((citation) => ({ + title: citation.title, + onItemClick: () => { + editor.insertInlineContent([ + { + type: "reference", + props: { + ...citation, + }, + }, + " ", + ]); + }, + })); + }; + + + const editor = useCreateBlockNote({ + schema, + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Woah, you can add a reference like this: ", + styles: {}, + }, + { + type: "reference", + props: { + key: 1, + doi: "10.1093/ajae/aaq063", + author: "Steve Smith", + title: "Understanding BlockNote", + journal: "BlockNote Journal", + year: 2023, + }, + }, + { + type: "text", + text: " <- This is an example reference", + styles: {}, + }, + ], + }, + { + type: "paragraph", + content: "Press the '@' key to open the references menu and add another", + }, + { + type: "paragraph", + }, + ], + }); // Renders the editor instance using a React component. - return ; + return ( + + + filterSuggestionItems(getReferenceMenuItems(editor), query) + } + /> + + ); } diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx new file mode 100644 index 000000000..6ab942141 --- /dev/null +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -0,0 +1,71 @@ +import { createReactInlineContentSpec } from "@blocknote/react"; +import { useFloating, useHover, useInteractions } from "@floating-ui/react"; +import { useState } from "react"; +import "./styles.css"; + +export const Reference = createReactInlineContentSpec( + { + type: "reference", + propSchema: { + key: { + type: "number", + default: 1, + description: "The key for the reference.", + }, + doi: { + default: "Unknown", + }, + author: { + type: "string", + default: "Unknown Author", + }, + title: { + type: "string", + default: "Unknown Title", + }, + journal: { + type: "string", + default: "Unknown Journal", + }, + year: { + type: "number", + default: 2023, + }, + }, + content: "none", + }, + { + render: (props) => { + const [isOpen, setIsOpen] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + }); + + const hover = useHover(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + + const citation = props.inlineContent.props; + + return ( + + + [{citation.key}] + + {isOpen && ( +
+ {citation.author}, {citation.title}, {citation.year} +
+ )} +
+ ); + }, + }, +); diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json index 5179e1057..1f7cc8ecd 100644 --- a/examples/01-basic/01-minimal/package.json +++ b/examples/01-basic/01-minimal/package.json @@ -15,6 +15,7 @@ "@blocknote/ariakit": "latest", "@blocknote/mantine": "latest", "@blocknote/shadcn": "latest", + "@floating-ui/react": "0.27.11", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -24,4 +25,4 @@ "@vitejs/plugin-react": "^4.3.1", "vite": "^5.3.4" } -} \ No newline at end of file +} diff --git a/examples/01-basic/01-minimal/styles.css b/examples/01-basic/01-minimal/styles.css new file mode 100644 index 000000000..f2457adea --- /dev/null +++ b/examples/01-basic/01-minimal/styles.css @@ -0,0 +1,5 @@ +.floating { + background-color: black; + padding: 5px 10px; + border-radius: 5px; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38fa08518..4a1829487 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,9 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn + '@floating-ui/react': + specifier: 0.27.11 + version: 0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -6106,12 +6109,24 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.3': + resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/react@0.26.28': resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.27.11': + resolution: {integrity: sha512-ZVtJxk4gQceaAOm1p5TlNeUcSxvzEwbAkKPgLYfV2b3aavC9Up9OZ5qPWQrMCASzmXUtNK1VuKdrqZkhAYhaeQ==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} @@ -16812,6 +16827,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.13 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16820,6 +16841,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tabbable: 6.2.0 + '@floating-ui/react@0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.9 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + '@floating-ui/utils@0.2.9': {} '@hapi/hoek@9.3.0': {} From aa271885701760ebe8628eb8cbe19e30424a0056 Mon Sep 17 00:00:00 2001 From: Mathilde Lannes Date: Mon, 2 Jun 2025 16:14:25 +0200 Subject: [PATCH 04/17] Use citation.js to display reference --- examples/01-basic/01-minimal/App.tsx | 8 +- examples/01-basic/01-minimal/Reference.tsx | 122 +++++++++--------- .../01-minimal/ReferenceInlineBlock.tsx | 39 ++++++ examples/01-basic/01-minimal/main.tsx | 2 +- examples/01-basic/01-minimal/package.json | 7 +- pnpm-lock.yaml | 97 ++++++++++++++ 6 files changed, 204 insertions(+), 71 deletions(-) create mode 100644 examples/01-basic/01-minimal/ReferenceInlineBlock.tsx diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index aa5f884ad..45fd47064 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -11,13 +11,13 @@ import { SuggestionMenuController, useCreateBlockNote, } from "@blocknote/react"; -import { Reference } from "./Reference"; +import { ReferenceInlineBlock } from "./ReferenceInlineBlock"; export default function App() { const schema = BlockNoteSchema.create({ inlineContentSpecs: { ...defaultInlineContentSpecs, - reference: Reference, + reference: ReferenceInlineBlock, }, }); @@ -67,7 +67,6 @@ export default function App() { })); }; - const editor = useCreateBlockNote({ schema, initialContent: [ @@ -103,7 +102,8 @@ export default function App() { }, { type: "paragraph", - content: "Press the '@' key to open the references menu and add another", + content: + "Press the '@' key to open the references menu and add another", }, { type: "paragraph", diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index 6ab942141..d896079d9 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -1,71 +1,65 @@ -import { createReactInlineContentSpec } from "@blocknote/react"; +import { Cite } from "@citation-js/core"; +import "@citation-js/plugin-csl"; +import "@citation-js/plugin-doi"; import { useFloating, useHover, useInteractions } from "@floating-ui/react"; -import { useState } from "react"; -import "./styles.css"; +import { useEffect, useState } from "react"; -export const Reference = createReactInlineContentSpec( - { - type: "reference", - propSchema: { - key: { - type: "number", - default: 1, - description: "The key for the reference.", - }, - doi: { - default: "Unknown", - }, - author: { - type: "string", - default: "Unknown Author", - }, - title: { - type: "string", - default: "Unknown Title", - }, - journal: { - type: "string", - default: "Unknown Journal", - }, - year: { - type: "number", - default: 2023, - }, - }, - content: "none", - }, - { - render: (props) => { - const [isOpen, setIsOpen] = useState(false); +type Props = { + inlineContent: { + props: { + key: number; + doi: string; + author: string; + title: string; + journal: string; + year: number; + }; + }; +}; - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - onOpenChange: setIsOpen, - }); +export const Reference = (props: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [bibliography, setBibliography] = useState(""); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + }); - const hover = useHover(context); + const hover = useHover(context); - const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + + useEffect(() => { + Cite.async(props.inlineContent.props.doi).then((data) => { + console.log("Cite data:", data); + // Format output + const bibliography = data.format("bibliography", { + format: "html", + template: "apa", + lang: "en-US", + }); + setBibliography(bibliography); + }); + }, [props.inlineContent.props]); - const citation = props.inlineContent.props; + const citation = props.inlineContent.props; - return ( - - - [{citation.key}] - - {isOpen && ( -
- {citation.author}, {citation.title}, {citation.year} -
- )} -
- ); - }, - }, -); + return ( + + + [{citation.key}] + + {isOpen && ( +
+
+
+ )} + + ); +}; diff --git a/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx b/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx new file mode 100644 index 000000000..c35e33641 --- /dev/null +++ b/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx @@ -0,0 +1,39 @@ +import { createReactInlineContentSpec } from "@blocknote/react"; +import { Reference } from "./Reference"; +import "./styles.css"; + +export const ReferenceInlineBlock = createReactInlineContentSpec( + { + type: "reference", + propSchema: { + key: { + type: "number", + default: 1, + description: "The key for the reference.", + }, + doi: { + default: "Unknown", + }, + author: { + type: "string", + default: "Unknown Author", + }, + title: { + type: "string", + default: "Unknown Title", + }, + journal: { + type: "string", + default: "Unknown Journal", + }, + year: { + type: "number", + default: 2023, + }, + }, + content: "none", + }, + { + render: Reference, + }, +); diff --git a/examples/01-basic/01-minimal/main.tsx b/examples/01-basic/01-minimal/main.tsx index 6284417d6..eb7c9b1b0 100644 --- a/examples/01-basic/01-minimal/main.tsx +++ b/examples/01-basic/01-minimal/main.tsx @@ -7,5 +7,5 @@ const root = createRoot(document.getElementById("root")!); root.render( - + , ); diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json index 1f7cc8ecd..fdeb89930 100644 --- a/examples/01-basic/01-minimal/package.json +++ b/examples/01-basic/01-minimal/package.json @@ -10,11 +10,14 @@ "preview": "vite preview" }, "dependencies": { - "@blocknote/core": "latest", - "@blocknote/react": "latest", "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", "@blocknote/mantine": "latest", + "@blocknote/react": "latest", "@blocknote/shadcn": "latest", + "@citation-js/core": "^0.7.18", + "@citation-js/plugin-csl": "^0.7.18", + "@citation-js/plugin-doi": "^0.7.18", "@floating-ui/react": "0.27.11", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a1829487..731bce914 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,15 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn + '@citation-js/core': + specifier: ^0.7.18 + version: 0.7.18(encoding@0.1.13) + '@citation-js/plugin-csl': + specifier: ^0.7.18 + version: 0.7.18(@citation-js/core@0.7.18(encoding@0.1.13)) + '@citation-js/plugin-doi': + specifier: ^0.7.18 + version: 0.7.18(@citation-js/core@0.7.18(encoding@0.1.13)) '@floating-ui/react': specifier: 0.27.11 version: 0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -5352,6 +5361,30 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@citation-js/core@0.7.18': + resolution: {integrity: sha512-EjLuZWA5156dIFGdF7OnyPyWFBW43B8Ckje6Sn/W2RFxHDu0oACvW4/6TNgWT80jhEA4bVFm7ahrZe9MJ2B2UQ==} + engines: {node: '>=16.0.0'} + + '@citation-js/date@0.5.1': + resolution: {integrity: sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==} + engines: {node: '>=10.0.0'} + + '@citation-js/name@0.4.2': + resolution: {integrity: sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==} + engines: {node: '>=6'} + + '@citation-js/plugin-csl@0.7.18': + resolution: {integrity: sha512-cJcOdEZurmtIxNj0d4cOERHpVQJB/mN3YPSDNqfI/xTFRN3bWDpFAsaqubPtMO2ZPpoDS+ZGIP1kggbwCfMmlA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@citation-js/core': ^0.7.0 + + '@citation-js/plugin-doi@0.7.18': + resolution: {integrity: sha512-7ccmhfJJSDUhUqpWxesLAp3m1P5dhnZ4QNMctwJnU41T9vKGF7MXPKqMONSvL5JDZ7o7iWQTj2BFhSmh0euQxw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@citation-js/core': ^0.7.0 + '@cloudflare/workerd-darwin-64@1.20240129.0': resolution: {integrity: sha512-DfVVB5IsQLVcWPJwV019vY3nEtU88c2Qu2ST5SQxqcGivZ52imagLRK0RHCIP8PK4piSiq90qUC6ybppUsw8eg==} engines: {node: '>=16'} @@ -9606,6 +9639,9 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + citeproc@2.4.63: + resolution: {integrity: sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -10672,6 +10708,9 @@ packages: picomatch: optional: true + fetch-ponyfill@7.1.0: + resolution: {integrity: sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==} + fflate@0.7.4: resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} @@ -12387,6 +12426,15 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch@2.6.13: + resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -13938,6 +13986,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + sync-fetch@0.4.5: + resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} + engines: {node: '>=14'} + system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -16343,6 +16395,30 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 + '@citation-js/core@0.7.18(encoding@0.1.13)': + dependencies: + '@citation-js/date': 0.5.1 + '@citation-js/name': 0.4.2 + fetch-ponyfill: 7.1.0(encoding@0.1.13) + sync-fetch: 0.4.5(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@citation-js/date@0.5.1': {} + + '@citation-js/name@0.4.2': {} + + '@citation-js/plugin-csl@0.7.18(@citation-js/core@0.7.18(encoding@0.1.13))': + dependencies: + '@citation-js/core': 0.7.18(encoding@0.1.13) + '@citation-js/date': 0.5.1 + citeproc: 2.4.63 + + '@citation-js/plugin-doi@0.7.18(@citation-js/core@0.7.18(encoding@0.1.13))': + dependencies: + '@citation-js/core': 0.7.18(encoding@0.1.13) + '@citation-js/date': 0.5.1 + '@cloudflare/workerd-darwin-64@1.20240129.0': optional: true @@ -20862,6 +20938,8 @@ snapshots: ci-info@3.9.0: {} + citeproc@2.4.63: {} + cjs-module-lexer@1.4.3: {} class-variance-authority@0.7.1: @@ -22172,6 +22250,12 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fetch-ponyfill@7.1.0(encoding@0.1.13): + dependencies: + node-fetch: 2.6.13(encoding@0.1.13) + transitivePeerDependencies: + - encoding + fflate@0.7.4: {} fflate@0.8.2: {} @@ -24551,6 +24635,12 @@ snapshots: node-addon-api@7.1.1: {} + node-fetch@2.6.13(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -26367,6 +26457,13 @@ snapshots: symbol-tree@3.2.4: {} + sync-fetch@0.4.5(encoding@0.1.13): + dependencies: + buffer: 5.7.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + system-architecture@0.1.0: {} tabbable@6.2.0: {} From 26d75efe2e45728a79f9834616f249fb1cce60cc Mon Sep 17 00:00:00 2001 From: Mathilde Lannes Date: Mon, 2 Jun 2025 16:23:51 +0200 Subject: [PATCH 05/17] Add FIXME message regarding the call of `dangerouslySetInnerHTML` --- examples/01-basic/01-minimal/Reference.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index d896079d9..760a53279 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -57,6 +57,7 @@ export const Reference = (props: Props) => { style={floatingStyles} {...getFloatingProps()} > + {/* FIXME do not use `dangerouslySetInnerHTML` to embed citation */}
)} From cf5a776231d1820927c4f67f1b02ad50df7d9a65 Mon Sep 17 00:00:00 2001 From: Mathilde Lannes Date: Mon, 2 Jun 2025 16:33:35 +0200 Subject: [PATCH 06/17] Display short citation instead of citation index --- examples/01-basic/01-minimal/Reference.tsx | 25 ++++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index 760a53279..c3f8ef115 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -19,7 +19,7 @@ type Props = { export const Reference = (props: Props) => { const [isOpen, setIsOpen] = useState(false); - const [bibliography, setBibliography] = useState(""); + const [bibliography, setBibliography] = useState(null); const { refs, floatingStyles, context } = useFloating({ open: isOpen, @@ -31,24 +31,17 @@ export const Reference = (props: Props) => { const { getReferenceProps, getFloatingProps } = useInteractions([hover]); useEffect(() => { - Cite.async(props.inlineContent.props.doi).then((data) => { - console.log("Cite data:", data); - // Format output - const bibliography = data.format("bibliography", { - format: "html", - template: "apa", - lang: "en-US", - }); - setBibliography(bibliography); - }); + Cite.async(props.inlineContent.props.doi).then(setBibliography); }, [props.inlineContent.props]); - const citation = props.inlineContent.props; + if (!bibliography) { + return Loading...; + } return ( - [{citation.key}] + {bibliography.format("citation")} {isOpen && (
{ {...getFloatingProps()} > {/* FIXME do not use `dangerouslySetInnerHTML` to embed citation */} -
+
)} From d5997ba42e85757818da855d2d5826616ed3111a Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 16:36:13 +0200 Subject: [PATCH 07/17] - Added placeholder bibliography block - Added reference edit panel - Added empty reference button - Cleaned up code --- examples/01-basic/01-minimal/App.tsx | 80 ++----- examples/01-basic/01-minimal/Bibliography.tsx | 59 +++++ examples/01-basic/01-minimal/Reference.tsx | 225 ++++++++++++++++-- examples/01-basic/01-minimal/styles.css | 4 + 4 files changed, 292 insertions(+), 76 deletions(-) create mode 100644 examples/01-basic/01-minimal/Bibliography.tsx diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index aa5f884ad..d8a616656 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -1,5 +1,6 @@ import { BlockNoteSchema, + defaultBlockSpecs, defaultInlineContentSpecs, filterSuggestionItems, } from "@blocknote/core"; @@ -7,67 +8,29 @@ import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { - DefaultReactSuggestionItem, + getDefaultReactSlashMenuItems, SuggestionMenuController, useCreateBlockNote, } from "@blocknote/react"; -import { Reference } from "./Reference"; + +import { + BibliographyBlockContent, + getInsertBibliographyBlockSlashMenuItem, +} from "./Bibliography.js"; +import { getInsertReferenceSlashMenuItem, Reference } from "./Reference.js"; export default function App() { const schema = BlockNoteSchema.create({ + blockSpecs: { + ...defaultBlockSpecs, + bibliography: BibliographyBlockContent, + }, inlineContentSpecs: { ...defaultInlineContentSpecs, reference: Reference, }, }); - const getReferenceMenuItems = ( - editor: typeof schema.BlockNoteEditor, - ): DefaultReactSuggestionItem[] => { - const citations = [ - { - key: 1, - doi: "10.1093/ajae/aaq063", - author: "Steve Smith", - title: "Understanding BlockNote", - journal: "BlockNote Journal", - year: 2023, - }, - { - key: 2, - doi: "10.1234/example.doi", - author: "Jane Doe", - title: "Exploring BlockNote Features", - journal: "BlockNote Features Journal", - year: 2022, - }, - { - key: 3, - doi: "10.5678/another.example", - author: "John Doe", - title: "Advanced BlockNote Techniques", - journal: "BlockNote Techniques Journal", - year: 2021, - }, - ]; - - return citations.map((citation) => ({ - title: citation.title, - onItemClick: () => { - editor.insertInlineContent([ - { - type: "reference", - props: { - ...citation, - }, - }, - " ", - ]); - }, - })); - }; - - const editor = useCreateBlockNote({ schema, initialContent: [ @@ -87,7 +50,8 @@ export default function App() { type: "reference", props: { key: 1, - doi: "10.1093/ajae/aaq063", + // doi: "10.1093/ajae/aaq063", + doi: "", author: "Steve Smith", title: "Understanding BlockNote", journal: "BlockNote Journal", @@ -103,7 +67,8 @@ export default function App() { }, { type: "paragraph", - content: "Press the '@' key to open the references menu and add another", + content: + "Press the '@' key to open the references menu and add another", }, { type: "paragraph", @@ -113,11 +78,18 @@ export default function App() { // Renders the editor instance using a React component. return ( - + - filterSuggestionItems(getReferenceMenuItems(editor), query) + filterSuggestionItems( + [ + ...getDefaultReactSlashMenuItems(editor), + getInsertReferenceSlashMenuItem(editor), + getInsertBibliographyBlockSlashMenuItem(editor), + ], + query, + ) } /> diff --git a/examples/01-basic/01-minimal/Bibliography.tsx b/examples/01-basic/01-minimal/Bibliography.tsx new file mode 100644 index 000000000..bf128023c --- /dev/null +++ b/examples/01-basic/01-minimal/Bibliography.tsx @@ -0,0 +1,59 @@ +import { + BlockConfig, + BlockNoteEditor, + BlockSchema, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { createReactBlockSpec } from "@blocknote/react"; +import { RiFileListFill } from "react-icons/ri"; + +export const bibliographyBlockConfig = { + type: "bibliography", + propSchema: { + bibTexJSON: { + default: "[]", + }, + }, + content: "none", + isSelectable: false, +} as const satisfies BlockConfig; + +export type BibliographyBlockConfig = typeof bibliographyBlockConfig; + +export const BibliographyBlockContent = createReactBlockSpec( + bibliographyBlockConfig, + { + render: () => { + return ( +
+

Bibliography

+

This is where your bibliography will be displayed.

+
+ ); + }, + }, +); + +export const getInsertBibliographyBlockSlashMenuItem = < + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + editor: BlockNoteEditor, +) => ({ + title: "Bibliography", + subtext: "Insert a bibliography block", + icon: , + onItemClick: () => { + editor.insertBlocks( + [ + { + type: "bibliography", + }, + ], + editor.document[editor.document.length - 1], + "after", + ); + }, +}); diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index 6ab942141..1eb53407b 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -1,7 +1,83 @@ -import { createReactInlineContentSpec } from "@blocknote/react"; -import { useFloating, useHover, useInteractions } from "@floating-ui/react"; +import { + createReactInlineContentSpec, + useComponentsContext, +} from "@blocknote/react"; +import { + useClick, + // useDismiss, + useFloating, + useHover, + useInteractions, +} from "@floating-ui/react"; import { useState } from "react"; import "./styles.css"; +import { + Block, + BlockNoteEditor, + BlockSchema, + BlockSchemaWithBlock, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { RiLink } from "react-icons/ri"; + +import { BibliographyBlockConfig } from "./Bibliography"; + +const useFloatingHover = () => { + const [isHovered, setIsHovered] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isHovered, + onOpenChange: setIsHovered, + }); + + const hover = useHover(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + + return { + isHovered, + referenceElementProps: { + ref: refs.setReference, + ...getReferenceProps(), + }, + floatingElementProps: { + ref: refs.setFloating, + style: floatingStyles, + ...getFloatingProps(), + }, + }; +}; + +const useFloatingClick = () => { + const [isOpen, setIsOpen] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + }); + + const open = useClick(context); + // const dismiss = useDismiss(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + open, + // dismiss, + ]); + + return { + isOpen, + referenceElementProps: { + ref: refs.setReference, + ...getReferenceProps(), + }, + floatingElementProps: { + ref: refs.setFloating, + style: floatingStyles, + ...getFloatingProps(), + }, + }; +}; export const Reference = createReactInlineContentSpec( { @@ -9,57 +85,111 @@ export const Reference = createReactInlineContentSpec( propSchema: { key: { type: "number", - default: 1, - description: "The key for the reference.", + default: 0, }, doi: { - default: "Unknown", + default: "", }, author: { type: "string", - default: "Unknown Author", + default: "", }, title: { type: "string", - default: "Unknown Title", + default: "", }, journal: { type: "string", - default: "Unknown Journal", + default: "", }, year: { type: "number", - default: 2023, + default: 0, }, }, content: "none", }, { render: (props) => { - const [isOpen, setIsOpen] = useState(false); + const Components = useComponentsContext()!; - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - onOpenChange: setIsOpen, - }); + const referenceDetailsFloating = useFloatingHover(); + const referenceEditFloating = useFloatingClick(); - const hover = useHover(context); + const citation = props.inlineContent.props; - const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + const [newDOI, setNewDOI] = useState(citation.doi); - const citation = props.inlineContent.props; + if (!citation.doi) { + return ( + + + {referenceEditFloating.isOpen && ( + {}} + tabs={[ + { + name: "DOI", + tabPanel: ( + + setNewDOI(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + props.updateInlineContent({ + type: "reference", + props: { + ...citation, + doi: newDOI, + }, + }); + } + }} + data-test={"embed-input"} + /> + { + props.updateInlineContent({ + type: "reference", + props: { + ...citation, + doi: newDOI, + }, + }); + }} + data-test="embed-input-button" + > + Update Reference + + + ), + }, + ]} + loading={false} + /> + )} + + ); + } return ( - + [{citation.key}] - {isOpen && ( + {referenceDetailsFloating.isHovered && (
{citation.author}, {citation.title}, {citation.year}
@@ -69,3 +199,54 @@ export const Reference = createReactInlineContentSpec( }, }, ); + +export const getInsertReferenceSlashMenuItem = < + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + editor: BlockNoteEditor, +) => ({ + title: "Reference", + subtext: "Reference to a bibliography block source", + icon: , + onItemClick: () => { + editor.insertInlineContent([ + { + type: "reference", + } as any, + ]); + + let bibliographyBlock: + | Block< + BlockSchemaWithBlock<"bibliography", BibliographyBlockConfig>, + I, + S + > + | undefined = undefined; + + editor.forEachBlock((block) => { + if (block.type === "bibliography") { + bibliographyBlock = block as any; + } + + if (bibliographyBlock) { + return false; + } + + return true; + }); + + if (!bibliographyBlock) { + editor.insertBlocks( + [ + { + type: "bibliography", + }, + ], + editor.document[editor.document.length - 1], + "after", + ); + } + }, +}); diff --git a/examples/01-basic/01-minimal/styles.css b/examples/01-basic/01-minimal/styles.css index f2457adea..32757d586 100644 --- a/examples/01-basic/01-minimal/styles.css +++ b/examples/01-basic/01-minimal/styles.css @@ -3,3 +3,7 @@ padding: 5px 10px; border-radius: 5px; } + +.reference-panel { + position: absolute; +} From 62a8df6016c69017fb842fba207c02f51a1cccc7 Mon Sep 17 00:00:00 2001 From: Mathilde Lannes Date: Mon, 2 Jun 2025 16:39:49 +0200 Subject: [PATCH 08/17] Remove unused block properties --- examples/01-basic/01-minimal/App.tsx | 22 +------------------ examples/01-basic/01-minimal/Reference.tsx | 5 ----- .../01-minimal/ReferenceInlineBlock.tsx | 21 ------------------ 3 files changed, 1 insertion(+), 47 deletions(-) diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index 45fd47064..0f7be9037 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -26,33 +26,18 @@ export default function App() { ): DefaultReactSuggestionItem[] => { const citations = [ { - key: 1, doi: "10.1093/ajae/aaq063", - author: "Steve Smith", - title: "Understanding BlockNote", - journal: "BlockNote Journal", - year: 2023, }, { - key: 2, doi: "10.1234/example.doi", - author: "Jane Doe", - title: "Exploring BlockNote Features", - journal: "BlockNote Features Journal", - year: 2022, }, { - key: 3, doi: "10.5678/another.example", - author: "John Doe", - title: "Advanced BlockNote Techniques", - journal: "BlockNote Techniques Journal", - year: 2021, }, ]; return citations.map((citation) => ({ - title: citation.title, + title: `Add reference: ${citation.doi}`, onItemClick: () => { editor.insertInlineContent([ { @@ -85,12 +70,7 @@ export default function App() { { type: "reference", props: { - key: 1, doi: "10.1093/ajae/aaq063", - author: "Steve Smith", - title: "Understanding BlockNote", - journal: "BlockNote Journal", - year: 2023, }, }, { diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index c3f8ef115..54c90862b 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -7,12 +7,7 @@ import { useEffect, useState } from "react"; type Props = { inlineContent: { props: { - key: number; doi: string; - author: string; - title: string; - journal: string; - year: number; }; }; }; diff --git a/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx b/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx index c35e33641..c0d87df5a 100644 --- a/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx +++ b/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx @@ -6,30 +6,9 @@ export const ReferenceInlineBlock = createReactInlineContentSpec( { type: "reference", propSchema: { - key: { - type: "number", - default: 1, - description: "The key for the reference.", - }, doi: { default: "Unknown", }, - author: { - type: "string", - default: "Unknown Author", - }, - title: { - type: "string", - default: "Unknown Title", - }, - journal: { - type: "string", - default: "Unknown Journal", - }, - year: { - type: "number", - default: 2023, - }, }, content: "none", }, From 23fa1f225a85aa79ccb719af2b88bdc3c876d1ed Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 16:55:06 +0200 Subject: [PATCH 09/17] Fixed small merge errors --- examples/01-basic/01-minimal/App.tsx | 2 + examples/01-basic/01-minimal/Reference.tsx | 10 +++-- .../01-minimal/ReferenceInlineBlock.tsx | 39 ------------------- pnpm-lock.yaml | 14 ++++++- 4 files changed, 21 insertions(+), 44 deletions(-) delete mode 100644 examples/01-basic/01-minimal/ReferenceInlineBlock.tsx diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index d8a616656..ce82ad565 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -19,6 +19,8 @@ import { } from "./Bibliography.js"; import { getInsertReferenceSlashMenuItem, Reference } from "./Reference.js"; +import "./styles.css"; + export default function App() { const schema = BlockNoteSchema.create({ blockSpecs: { diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index 793518efb..da7d6107c 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -20,11 +20,10 @@ import { useHover, useInteractions, } from "@floating-ui/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { RiLink } from "react-icons/ri"; -import { BibliographyBlockConfig } from "./Bibliography"; -import "./styles.css"; +import { BibliographyBlockConfig } from "./Bibliography.js"; const useFloatingHover = () => { const [isHovered, setIsHovered] = useState(false); @@ -205,7 +204,10 @@ export const Reference = createReactInlineContentSpec( [{citation.key}]
{referenceDetailsFloating.isHovered && ( -
+
{/* FIXME do not use `dangerouslySetInnerHTML` to embed citation */}
diff --git a/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx b/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx deleted file mode 100644 index c35e33641..000000000 --- a/examples/01-basic/01-minimal/ReferenceInlineBlock.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { createReactInlineContentSpec } from "@blocknote/react"; -import { Reference } from "./Reference"; -import "./styles.css"; - -export const ReferenceInlineBlock = createReactInlineContentSpec( - { - type: "reference", - propSchema: { - key: { - type: "number", - default: 1, - description: "The key for the reference.", - }, - doi: { - default: "Unknown", - }, - author: { - type: "string", - default: "Unknown Author", - }, - title: { - type: "string", - default: "Unknown Title", - }, - journal: { - type: "string", - default: "Unknown Journal", - }, - year: { - type: "number", - default: 2023, - }, - }, - content: "none", - }, - { - render: Reference, - }, -); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b7fe68cc..22f6c7505 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,18 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn + '@citation-js/core': + specifier: ^0.7.18 + version: 0.7.18(encoding@0.1.13) + '@citation-js/plugin-csl': + specifier: ^0.7.18 + version: 0.7.18(@citation-js/core@0.7.18(encoding@0.1.13)) + '@citation-js/plugin-doi': + specifier: ^0.7.18 + version: 0.7.18(@citation-js/core@0.7.18(encoding@0.1.13)) + '@floating-ui/react': + specifier: ^0.27.11 + version: 0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -18306,7 +18318,7 @@ snapshots: '@radix-ui/react-popper@1.2.2(@types/react-dom@18.3.5(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-arrow': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.20)(react@18.3.1) '@radix-ui/react-context': 1.1.1(@types/react@18.3.20)(react@18.3.1) From a9b198fe192a0b208da2f06273e41e35768424cc Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 17:09:25 +0200 Subject: [PATCH 10/17] Fixed small merge issues --- examples/01-basic/01-minimal/Reference.tsx | 51 +++++++--------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx index da7d6107c..049de898d 100644 --- a/examples/01-basic/01-minimal/Reference.tsx +++ b/examples/01-basic/01-minimal/Reference.tsx @@ -85,29 +85,9 @@ export const Reference = createReactInlineContentSpec( { type: "reference", propSchema: { - key: { - type: "number", - default: 0, - }, doi: { default: "", }, - author: { - type: "string", - default: "", - }, - title: { - type: "string", - default: "", - }, - journal: { - type: "string", - default: "", - }, - year: { - type: "number", - default: 0, - }, }, content: "none", }, @@ -118,24 +98,19 @@ export const Reference = createReactInlineContentSpec( const referenceDetailsFloating = useFloatingHover(); const referenceEditFloating = useFloatingClick(); - const [bibliography, setBibliography] = useState(""); + const citation = props.inlineContent.props; + + const [newDOI, setNewDOI] = useState(citation.doi); + + const [bibliography, setBibliography] = useState(null); useEffect(() => { - Cite.async(props.inlineContent.props.doi).then((data) => { - console.log("Cite data:", data); - // Format output - const bibliography = data.format("bibliography", { - format: "html", - template: "apa", - lang: "en-US", - }); - setBibliography(bibliography); - }); + Cite.async(props.inlineContent.props.doi).then(setBibliography); }, [props.inlineContent.props]); - const citation = props.inlineContent.props; - - const [newDOI, setNewDOI] = useState(citation.doi); + if (!bibliography) { + return Loading...; + } if (!citation.doi) { return ( @@ -201,7 +176,7 @@ export const Reference = createReactInlineContentSpec( return ( - [{citation.key}] + {bibliography.format("citation")} {referenceDetailsFloating.isHovered && (
{/* FIXME do not use `dangerouslySetInnerHTML` to embed citation */} -
+
)} From c252f1bcd307f48191e4b8a2b1562950178303a2 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 19:12:47 +0200 Subject: [PATCH 11/17] Stupid commit --- packages/core/src/editor/BlockNoteEditor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 8008312c3..bd48ba33d 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -540,6 +540,7 @@ export class BlockNoteEditor< protected constructor( protected readonly options: Partial>, ) { + console.log("test"); super(); const anyOpts = options as any; if (anyOpts.onEditorContentChange) { From 289c785c5f5d37da6c2568b50f917f07974c32f4 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 19:17:22 +0200 Subject: [PATCH 12/17] Another stupid commit --- packages/core/src/editor/BlockNoteEditor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index bd48ba33d..897793ced 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -540,6 +540,7 @@ export class BlockNoteEditor< protected constructor( protected readonly options: Partial>, ) { + // eslint-disable-next-line no-console console.log("test"); super(); const anyOpts = options as any; From 0c3c9e7ae5809dd5b5971c549122350056724dc1 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 2 Jun 2025 22:21:21 +0200 Subject: [PATCH 13/17] Refactored custom blocks/inline content to be in react package --- examples/01-basic/01-minimal/App.tsx | 31 ++- examples/01-basic/01-minimal/Bibliography.tsx | 59 ----- examples/01-basic/01-minimal/Reference.tsx | 249 ------------------ examples/01-basic/01-minimal/package.json | 4 - .../core/src/schema/inlineContent/types.ts | 7 + packages/react/package.json | 3 + .../BibliographyBlockContent.tsx | 52 ++++ .../getBibliographyReactSlashMenuItems.tsx | 44 ++++ .../ReferenceInlineContent.tsx | 236 +++++++++++++++++ .../src/schema/ReactInlineContentSpec.tsx | 27 +- pnpm-lock.yaml | 9 + 11 files changed, 390 insertions(+), 331 deletions(-) delete mode 100644 examples/01-basic/01-minimal/Bibliography.tsx delete mode 100644 examples/01-basic/01-minimal/Reference.tsx create mode 100644 packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx create mode 100644 packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx create mode 100644 packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index f4df6d8af..46b7cba79 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -8,28 +8,24 @@ import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { + ReactBibliographyBlockContent, + getBibliographyReactSlashMenuItems, getDefaultReactSlashMenuItems, SuggestionMenuController, useCreateBlockNote, } from "@blocknote/react"; -import { - BibliographyBlockContent, - getInsertBibliographyBlockSlashMenuItem, -} from "./Bibliography.js"; -import { getInsertReferenceSlashMenuItem, Reference } from "./Reference.js"; - import "./styles.css"; export default function App() { const schema = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, - bibliography: BibliographyBlockContent, + bibliography: ReactBibliographyBlockContent, }, inlineContentSpecs: { ...defaultInlineContentSpecs, - reference: Reference, + reference: ReactReferenceInlineContent, }, }); @@ -51,8 +47,8 @@ export default function App() { { type: "reference", props: { - // doi: "10.1093/ajae/aaq063", - doi: "", + doi: "10.1093/ajae/aaq063", + // doi: "", }, }, { @@ -67,6 +63,18 @@ export default function App() { content: "Press the '@' key to open the references menu and add another", }, + { + type: "bibliography", + props: { + bibTexJSON: JSON.stringify([ + "10.1093/ajae/aaq063", // Example DOI + "https://doi.org/10.48550/arXiv.2505.23896", // Another example DOI + "https://doi.org/10.48550/arXiv.2505.23900", + "https://doi.org/10.48550/arXiv.2505.23904", + "https://doi.org/10.48550/arXiv.2505.24234", + ]), + }, + }, { type: "paragraph", }, @@ -82,8 +90,7 @@ export default function App() { filterSuggestionItems( [ ...getDefaultReactSlashMenuItems(editor), - getInsertReferenceSlashMenuItem(editor), - getInsertBibliographyBlockSlashMenuItem(editor), + ...getBibliographyReactSlashMenuItems(editor), ], query, ) diff --git a/examples/01-basic/01-minimal/Bibliography.tsx b/examples/01-basic/01-minimal/Bibliography.tsx deleted file mode 100644 index bf128023c..000000000 --- a/examples/01-basic/01-minimal/Bibliography.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { - BlockConfig, - BlockNoteEditor, - BlockSchema, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { createReactBlockSpec } from "@blocknote/react"; -import { RiFileListFill } from "react-icons/ri"; - -export const bibliographyBlockConfig = { - type: "bibliography", - propSchema: { - bibTexJSON: { - default: "[]", - }, - }, - content: "none", - isSelectable: false, -} as const satisfies BlockConfig; - -export type BibliographyBlockConfig = typeof bibliographyBlockConfig; - -export const BibliographyBlockContent = createReactBlockSpec( - bibliographyBlockConfig, - { - render: () => { - return ( -
-

Bibliography

-

This is where your bibliography will be displayed.

-
- ); - }, - }, -); - -export const getInsertBibliographyBlockSlashMenuItem = < - B extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - editor: BlockNoteEditor, -) => ({ - title: "Bibliography", - subtext: "Insert a bibliography block", - icon: , - onItemClick: () => { - editor.insertBlocks( - [ - { - type: "bibliography", - }, - ], - editor.document[editor.document.length - 1], - "after", - ); - }, -}); diff --git a/examples/01-basic/01-minimal/Reference.tsx b/examples/01-basic/01-minimal/Reference.tsx deleted file mode 100644 index 049de898d..000000000 --- a/examples/01-basic/01-minimal/Reference.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { - Block, - BlockNoteEditor, - BlockSchema, - BlockSchemaWithBlock, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - createReactInlineContentSpec, - useComponentsContext, -} from "@blocknote/react"; -import { Cite } from "@citation-js/core"; -import "@citation-js/plugin-csl"; -import "@citation-js/plugin-doi"; -import { - useClick, - // useDismiss, - useFloating, - useHover, - useInteractions, -} from "@floating-ui/react"; -import { useEffect, useState } from "react"; -import { RiLink } from "react-icons/ri"; - -import { BibliographyBlockConfig } from "./Bibliography.js"; - -const useFloatingHover = () => { - const [isHovered, setIsHovered] = useState(false); - - const { refs, floatingStyles, context } = useFloating({ - open: isHovered, - onOpenChange: setIsHovered, - }); - - const hover = useHover(context); - - const { getReferenceProps, getFloatingProps } = useInteractions([hover]); - - return { - isHovered, - referenceElementProps: { - ref: refs.setReference, - ...getReferenceProps(), - }, - floatingElementProps: { - ref: refs.setFloating, - style: floatingStyles, - ...getFloatingProps(), - }, - }; -}; - -const useFloatingClick = () => { - const [isOpen, setIsOpen] = useState(false); - - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - onOpenChange: setIsOpen, - }); - - const open = useClick(context); - // const dismiss = useDismiss(context); - - const { getReferenceProps, getFloatingProps } = useInteractions([ - open, - // dismiss, - ]); - - return { - isOpen, - referenceElementProps: { - ref: refs.setReference, - ...getReferenceProps(), - }, - floatingElementProps: { - ref: refs.setFloating, - style: floatingStyles, - ...getFloatingProps(), - }, - }; -}; - -export const Reference = createReactInlineContentSpec( - { - type: "reference", - propSchema: { - doi: { - default: "", - }, - }, - content: "none", - }, - { - render: (props) => { - const Components = useComponentsContext()!; - - const referenceDetailsFloating = useFloatingHover(); - const referenceEditFloating = useFloatingClick(); - - const citation = props.inlineContent.props; - - const [newDOI, setNewDOI] = useState(citation.doi); - - const [bibliography, setBibliography] = useState(null); - - useEffect(() => { - Cite.async(props.inlineContent.props.doi).then(setBibliography); - }, [props.inlineContent.props]); - - if (!bibliography) { - return Loading...; - } - - if (!citation.doi) { - return ( - - - {referenceEditFloating.isOpen && ( - {}} - tabs={[ - { - name: "DOI", - tabPanel: ( - - setNewDOI(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - props.updateInlineContent({ - type: "reference", - props: { - ...citation, - doi: newDOI, - }, - }); - } - }} - data-test={"embed-input"} - /> - { - props.updateInlineContent({ - type: "reference", - props: { - ...citation, - doi: newDOI, - }, - }); - }} - data-test="embed-input-button" - > - Update Reference - - - ), - }, - ]} - loading={false} - /> - )} - - ); - } - - return ( - - - {bibliography.format("citation")} - - {referenceDetailsFloating.isHovered && ( -
- {/* FIXME do not use `dangerouslySetInnerHTML` to embed citation */} -
-
- )} - - ); - }, - }, -); - -export const getInsertReferenceSlashMenuItem = < - B extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - editor: BlockNoteEditor, -) => ({ - title: "Reference", - subtext: "Reference to a bibliography block source", - icon: , - onItemClick: () => { - editor.insertInlineContent([ - { - type: "reference", - } as any, - ]); - - let bibliographyBlock: - | Block< - BlockSchemaWithBlock<"bibliography", BibliographyBlockConfig>, - I, - S - > - | undefined = undefined; - - editor.forEachBlock((block) => { - if (block.type === "bibliography") { - bibliographyBlock = block as any; - } - - if (bibliographyBlock) { - return false; - } - - return true; - }); - - if (!bibliographyBlock) { - editor.insertBlocks( - [ - { - type: "bibliography", - }, - ], - editor.document[editor.document.length - 1], - "after", - ); - } - }, -}); diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json index 3c67efde6..dc94a9305 100644 --- a/examples/01-basic/01-minimal/package.json +++ b/examples/01-basic/01-minimal/package.json @@ -15,10 +15,6 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@citation-js/core": "^0.7.18", - "@citation-js/plugin-csl": "^0.7.18", - "@citation-js/plugin-doi": "^0.7.18", - "@floating-ui/react": "^0.27.11", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts index 6ec87055d..6415a86f6 100644 --- a/packages/core/src/schema/inlineContent/types.ts +++ b/packages/core/src/schema/inlineContent/types.ts @@ -22,6 +22,13 @@ export type InlineContentImplementation = node: Node; }; +export type InlineContentSchemaWithInlineContent< + IType extends string, + C extends InlineContentConfig, +> = { + [k in IType]: C; +}; + // Container for both the config and implementation of InlineContent, // and the type of `implementation` is based on that of the config export type InlineContentSpec = { diff --git a/packages/react/package.json b/packages/react/package.json index 57eda00c1..df3e8ae78 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,6 +59,9 @@ }, "dependencies": { "@blocknote/core": "0.31.1", + "@citation-js/core": "^0.7.18", + "@citation-js/plugin-csl": "^0.7.18", + "@citation-js/plugin-doi": "^0.7.18", "@emoji-mart/data": "^1.2.1", "@floating-ui/react": "^0.26.4", "@tiptap/core": "^2.12.0", diff --git a/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx b/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx new file mode 100644 index 000000000..0e25c6333 --- /dev/null +++ b/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx @@ -0,0 +1,52 @@ +import { BlockConfig } from "@blocknote/core"; +import { Cite } from "@citation-js/core"; +import "@citation-js/plugin-csl"; +import "@citation-js/plugin-doi"; +import { useState, useEffect } from "react"; + +import { + createReactBlockSpec, + ReactCustomBlockRenderProps, +} from "../../schema/ReactBlockSpec.js"; + +export const bibliographyBlockConfig = { + type: "bibliography", + propSchema: { + bibTexJSON: { + default: "[]", + }, + }, + content: "none", + isSelectable: false, +} as const satisfies BlockConfig; + +export const Bibliography = ( + props: ReactCustomBlockRenderProps, +) => { + const [bibliography, setBibliography] = useState([]); + + useEffect(() => { + async function fetchBibliography() { + const dois: string[] = JSON.parse(props.block.props.bibTexJSON); + const cites = await Promise.all(dois.map((doi) => Cite.async(doi))); + + setBibliography(cites); + } + + fetchBibliography(); + }, [props.block.props.bibTexJSON]); + + return ( +
+

Bibliography

+ {bibliography.map((cite: any) => ( +
{cite.format("bibliography")}
+ ))} +
+ ); +}; + +export const ReactBibliographyBlockContent = createReactBlockSpec( + bibliographyBlockConfig, + { render: Bibliography }, +); diff --git a/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx new file mode 100644 index 000000000..502e8c618 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx @@ -0,0 +1,44 @@ +import { + BlockSchema, + InlineContentSchema, + StyleSchema, + BlockNoteEditor, +} from "@blocknote/core"; +import { RiFileListFill, RiLink } from "react-icons/ri"; + +export const getBibliographyReactSlashMenuItems = < + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + editor: BlockNoteEditor, +) => [ + { + title: "Reference", + subtext: "Reference to a bibliography block source", + icon: , + onItemClick: () => { + editor.insertInlineContent([ + { + type: "reference", + } as any, + ]); + }, + }, + { + title: "Bibliography", + subtext: "Insert a bibliography block", + icon: , + onItemClick: () => { + editor.insertBlocks( + [ + { + type: "bibliography", + }, + ], + editor.document[editor.document.length - 1], + "after", + ); + }, + }, +]; diff --git a/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx b/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx new file mode 100644 index 000000000..7f371a25b --- /dev/null +++ b/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx @@ -0,0 +1,236 @@ +import { + Block, + BlockSchemaWithBlock, + InlineContentConfig, + InlineContentSchemaWithInlineContent, +} from "@blocknote/core"; +import { + createReactInlineContentSpec, + ReactCustomInlineContentRenderProps, + useComponentsContext, +} from "@blocknote/react"; +import { Cite } from "@citation-js/core"; +import "@citation-js/plugin-csl"; +import "@citation-js/plugin-doi"; +import { + useClick, + // useDismiss, + useFloating, + useHover, + useInteractions, +} from "@floating-ui/react"; +import { useCallback, useEffect, useState } from "react"; + +import { bibliographyBlockConfig } from "../../blocks/BibliographyBlockContent/BibliographyBlockContent.js"; + +export const referenceInlineContentConfig = { + type: "reference", + propSchema: { + doi: { + default: "", + }, + }, + content: "none", +} satisfies InlineContentConfig; + +const useFloatingHover = () => { + const [isHovered, setIsHovered] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isHovered, + onOpenChange: setIsHovered, + }); + + const hover = useHover(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([hover]); + + return { + isHovered, + referenceElementProps: { + ref: refs.setReference, + ...getReferenceProps(), + }, + floatingElementProps: { + ref: refs.setFloating, + style: floatingStyles, + ...getFloatingProps(), + }, + }; +}; + +const useFloatingClick = () => { + const [isOpen, setIsOpen] = useState(false); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + }); + + const open = useClick(context); + // const dismiss = useDismiss(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + open, + // dismiss, + ]); + + return { + isOpen, + referenceElementProps: { + ref: refs.setReference, + ...getReferenceProps(), + }, + floatingElementProps: { + ref: refs.setFloating, + style: floatingStyles, + ...getFloatingProps(), + }, + }; +}; + +export const Reference = ( + props: ReactCustomInlineContentRenderProps< + typeof referenceInlineContentConfig, + any + >, +) => { + const Components = useComponentsContext()!; + + const referenceDetailsFloating = useFloatingHover(); + const referenceEditFloating = useFloatingClick(); + + const citation = props.inlineContent.props; + + const [newDOI, setNewDOI] = useState(citation.doi); + + const [bibliography, setBibliography] = useState(null); + + useEffect(() => { + Cite.async(props.inlineContent.props.doi).then(setBibliography); + }, [props.inlineContent.props]); + + const applyNewDOI = useCallback(() => { + props.updateInlineContent({ + type: "reference", + props: { + ...citation, + doi: newDOI, + }, + }); + + let bibliographyBlock: + | Block< + BlockSchemaWithBlock<"bibliography", typeof bibliographyBlockConfig>, + InlineContentSchemaWithInlineContent< + "reference", + typeof referenceInlineContentConfig + >, + any + > + | undefined = undefined; + + props.editor.forEachBlock((block) => { + if (block.type === "bibliography") { + bibliographyBlock = block as any; + } + + if (bibliographyBlock) { + return false; + } + + return true; + }); + + if (!bibliographyBlock) { + props.editor.insertBlocks( + [ + { + type: "bibliography", + }, + ], + props.editor.document[props.editor.document.length - 1], + "after", + ); + } + }, [citation, newDOI, props]); + + if (!bibliography) { + return Loading...; + } + + if (!citation.doi) { + return ( + + + {referenceEditFloating.isOpen && ( + { + // Do nothing until we have more tabs + }} + tabs={[ + { + name: "DOI", + tabPanel: ( + + setNewDOI(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + applyNewDOI(); + } + }} + data-test={"embed-input"} + /> + applyNewDOI()} + data-test="embed-input-button" + > + Update Reference + + + ), + }, + ]} + loading={false} + /> + )} + + ); + } + + return ( + + + {bibliography.format("citation")} + + {referenceDetailsFloating.isHovered && ( +
+ {/* FIXME do not use `dangerouslySetInnerHTML` to embed citation */} +
+
+ )} + + ); +}; + +export const ReactReferenceInlineContent = createReactInlineContentSpec( + referenceInlineContentConfig, + { render: Reference }, +); diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx index 63d81c014..67033ac2d 100644 --- a/packages/react/src/schema/ReactInlineContentSpec.tsx +++ b/packages/react/src/schema/ReactInlineContentSpec.tsx @@ -15,6 +15,7 @@ import { propsToAttributes, StyleSchema, BlockNoteEditor, + InlineContentSchemaWithInlineContent, } from "@blocknote/core"; import { NodeViewProps, @@ -27,19 +28,29 @@ import { FC } from "react"; import { renderToDOMSpec } from "./@util/ReactRenderUtil.js"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks +export type ReactCustomInlineContentRenderProps< + T extends CustomInlineContentConfig, + S extends StyleSchema, +> = { + inlineContent: InlineContentFromConfig; + updateInlineContent: ( + update: PartialCustomInlineContentFromConfig, + ) => void; + editor: BlockNoteEditor< + any, + InlineContentSchemaWithInlineContent, + S + >; + contentRef: (node: HTMLElement | null) => void; +}; + // extend BlockConfig but use a React render function export type ReactInlineContentImplementation< T extends CustomInlineContentConfig, // I extends InlineContentSchema, S extends StyleSchema, > = { - render: FC<{ - inlineContent: InlineContentFromConfig; - updateInlineContent: ( - update: PartialCustomInlineContentFromConfig, - ) => void; - contentRef: (node: HTMLElement | null) => void; - }>; + render: FC>; // TODO? // toExternalHTML?: FC<{ // block: BlockFromConfig; @@ -133,6 +144,7 @@ export function createReactInlineContentSpec< updateInlineContent={() => { // No-op }} + editor={editor} contentRef={refCB} /> ), @@ -168,6 +180,7 @@ export function createReactInlineContentSpec< > Date: Tue, 3 Jun 2025 09:40:22 +0200 Subject: [PATCH 14/17] Fixed exports --- examples/01-basic/01-minimal/App.tsx | 3 ++- packages/react/src/index.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index 46b7cba79..9ff51e5f2 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -8,9 +8,10 @@ import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; import { - ReactBibliographyBlockContent, getBibliographyReactSlashMenuItems, getDefaultReactSlashMenuItems, + ReactBibliographyBlockContent, + ReactReferenceInlineContent, SuggestionMenuController, useCreateBlockNote, } from "@blocknote/react"; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a506cdfbe..7630099af 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,6 +6,7 @@ export * from "./editor/ComponentsContext.js"; export * from "./i18n/dictionary.js"; export * from "./blocks/AudioBlockContent/AudioBlockContent.js"; +export * from "./blocks/BibliographyBlockContent/BibliographyBlockContent.js"; export * from "./blocks/FileBlockContent/FileBlockContent.js"; export * from "./blocks/FileBlockContent/helpers/render/AddFileButton.js"; export * from "./blocks/FileBlockContent/helpers/render/FileBlockWrapper.js"; @@ -18,6 +19,8 @@ export * from "./blocks/ImageBlockContent/ImageBlockContent.js"; export * from "./blocks/PageBreakBlockContent/getPageBreakReactSlashMenuItems.js"; export * from "./blocks/VideoBlockContent/VideoBlockContent.js"; +export * from "./inlineContent/ReferenceInlineContent/ReferenceInlineContent.js"; + export * from "./components/FormattingToolbar/DefaultButtons/AddCommentButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/AddTiptapCommentButton.js"; export * from "./components/FormattingToolbar/DefaultButtons/BasicTextStyleButton.js"; @@ -61,6 +64,7 @@ export * from "./components/SideMenu/DragHandleMenu/DragHandleMenuProps.js"; export * from "./components/SuggestionMenu/SuggestionMenuController.js"; export * from "./components/SuggestionMenu/SuggestionMenuWrapper.js"; export * from "./components/SuggestionMenu/getDefaultReactSlashMenuItems.js"; +export * from "./components/SuggestionMenu/getBibliographyReactSlashMenuItems.js"; export * from "./components/SuggestionMenu/hooks/useCloseSuggestionMenuNoItems.js"; export * from "./components/SuggestionMenu/hooks/useLoadSuggestionMenuItems.js"; export * from "./components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.js"; From 2ead96b8527e40d94ad30eee1ee8dee18c16d924 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 3 Jun 2025 10:35:53 +0200 Subject: [PATCH 15/17] Added `useSingleBibliographyBlock` hook and cleaned up slash menu items --- examples/01-basic/01-minimal/App.tsx | 3 + .../core/src/blocks/defaultBlockTypeGuards.ts | 30 ++++++ .../hooks/useSingleBibliographyBlock.tsx | 31 +++++++ .../getBibliographyReactSlashMenuItems.tsx | 91 +++++++++++++------ packages/react/src/index.ts | 1 + 5 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 packages/react/src/blocks/BibliographyBlockContent/hooks/useSingleBibliographyBlock.tsx diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index 9ff51e5f2..ac4d8909b 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -14,6 +14,7 @@ import { ReactReferenceInlineContent, SuggestionMenuController, useCreateBlockNote, + useSingleBibliographyBlock, } from "@blocknote/react"; import "./styles.css"; @@ -82,6 +83,8 @@ export default function App() { ], }); + useSingleBibliographyBlock(editor); + // Renders the editor instance using a React component. return ( diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 5db988da9..b66fa5a81 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -1,9 +1,11 @@ import { CellSelection } from "prosemirror-tables"; import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; import { + BlockConfig, BlockFromConfig, BlockSchema, FileBlockConfig, + InlineContentConfig, InlineContentSchema, StyleSchema, } from "../schema/index.js"; @@ -31,6 +33,20 @@ export function checkDefaultBlockTypeInSchema< ); } +export function checkBlockTypeInSchema< + BlockType extends string, + Config extends BlockConfig, +>( + blockType: BlockType, + blockConfig: Config, + editor: BlockNoteEditor, +): editor is BlockNoteEditor<{ [T in BlockType]: Config }, any, any> { + return ( + blockType in editor.schema.blockSchema && + editor.schema.blockSchema[blockType] === blockConfig + ); +} + export function checkDefaultInlineContentTypeInSchema< InlineContentType extends keyof DefaultInlineContentSchema, B extends BlockSchema, @@ -50,6 +66,20 @@ export function checkDefaultInlineContentTypeInSchema< ); } +export function checkInlineContentTypeInSchema< + InlineContentType extends string, + Config extends InlineContentConfig, +>( + inlineContentType: InlineContentType, + inlineContentConfig: Config, + editor: BlockNoteEditor, +): editor is BlockNoteEditor { + return ( + inlineContentType in editor.schema.inlineContentSchema && + editor.schema.inlineContentSchema[inlineContentType] === inlineContentConfig + ); +} + export function checkBlockIsDefaultType< BlockType extends keyof DefaultBlockSchema, I extends InlineContentSchema, diff --git a/packages/react/src/blocks/BibliographyBlockContent/hooks/useSingleBibliographyBlock.tsx b/packages/react/src/blocks/BibliographyBlockContent/hooks/useSingleBibliographyBlock.tsx new file mode 100644 index 000000000..65c4a59b1 --- /dev/null +++ b/packages/react/src/blocks/BibliographyBlockContent/hooks/useSingleBibliographyBlock.tsx @@ -0,0 +1,31 @@ +import { + BlockSchema, + InlineContentSchema, + StyleSchema, + BlockNoteEditor, +} from "@blocknote/core"; +import { useEditorChange } from "../../../hooks/useEditorChange.js"; + +export const useSingleBibliographyBlock = < + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + editor?: BlockNoteEditor, +) => { + useEditorChange((editor) => { + const bibliographyBlockIds: string[] = []; + + editor.forEachBlock((block) => { + if (block.type === "bibliography") { + bibliographyBlockIds.push(block.id); + } + + return true; + }, true); + + if (bibliographyBlockIds.length > 1) { + editor.removeBlocks(bibliographyBlockIds.slice(1)); + } + }, editor); +}; diff --git a/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx index 502e8c618..fcf8e7a33 100644 --- a/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getBibliographyReactSlashMenuItems.tsx @@ -3,8 +3,13 @@ import { InlineContentSchema, StyleSchema, BlockNoteEditor, + checkBlockTypeInSchema, + checkInlineContentTypeInSchema, } from "@blocknote/core"; import { RiFileListFill, RiLink } from "react-icons/ri"; +import { DefaultReactSuggestionItem } from "./types.js"; +import { referenceInlineContentConfig } from "../../inlineContent/ReferenceInlineContent/ReferenceInlineContent.js"; +import { bibliographyBlockConfig } from "../../blocks/BibliographyBlockContent/BibliographyBlockContent.js"; export const getBibliographyReactSlashMenuItems = < B extends BlockSchema, @@ -12,33 +17,63 @@ export const getBibliographyReactSlashMenuItems = < S extends StyleSchema, >( editor: BlockNoteEditor, -) => [ - { - title: "Reference", - subtext: "Reference to a bibliography block source", - icon: , - onItemClick: () => { - editor.insertInlineContent([ - { - type: "reference", - } as any, - ]); - }, - }, - { - title: "Bibliography", - subtext: "Insert a bibliography block", - icon: , - onItemClick: () => { - editor.insertBlocks( - [ +) => { + const items: DefaultReactSuggestionItem[] = []; + + if ( + checkInlineContentTypeInSchema( + "reference", + referenceInlineContentConfig, + editor, + ) + ) { + items.push({ + title: "Reference", + subtext: "Reference to a bibliography block source", + icon: , + aliases: ["ciataion", "cite", "bib"], + onItemClick: () => { + editor.insertInlineContent([ { - type: "bibliography", + type: "reference", }, - ], - editor.document[editor.document.length - 1], - "after", - ); - }, - }, -]; + ]); + }, + }); + } + + const bibliographyBlockInSchema = checkBlockTypeInSchema( + "bibliography", + bibliographyBlockConfig, + editor, + ); + let bibliographyBlockAlreadyExists = false; + editor.forEachBlock((block) => { + if (block.type === "bibliography") { + bibliographyBlockAlreadyExists = true; + return false; + } + return true; + }); + + if (bibliographyBlockInSchema && !bibliographyBlockAlreadyExists) { + items.push({ + title: "Bibliography", + subtext: "Insert a bibliography block", + icon: , + onItemClick: () => { + editor.insertBlocks( + [ + { + type: "bibliography", + }, + ], + editor.document[editor.document.length - 1], + "after", + ); + }, + }); + } + + return items; +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7630099af..25ecd58eb 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -7,6 +7,7 @@ export * from "./i18n/dictionary.js"; export * from "./blocks/AudioBlockContent/AudioBlockContent.js"; export * from "./blocks/BibliographyBlockContent/BibliographyBlockContent.js"; +export * from "./blocks/BibliographyBlockContent/hooks/useSingleBibliographyBlock.js"; export * from "./blocks/FileBlockContent/FileBlockContent.js"; export * from "./blocks/FileBlockContent/helpers/render/AddFileButton.js"; export * from "./blocks/FileBlockContent/helpers/render/FileBlockWrapper.js"; From e7c23113f9fc654d0c5a90d809d47e8027101494 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 3 Jun 2025 10:40:41 +0200 Subject: [PATCH 16/17] Added `@ts-ignore` flags to fix build --- .../blocks/BibliographyBlockContent/BibliographyBlockContent.tsx | 1 + .../ReferenceInlineContent/ReferenceInlineContent.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx b/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx index 0e25c6333..58f905e85 100644 --- a/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx +++ b/packages/react/src/blocks/BibliographyBlockContent/BibliographyBlockContent.tsx @@ -1,4 +1,5 @@ import { BlockConfig } from "@blocknote/core"; +// @ts-ignore import { Cite } from "@citation-js/core"; import "@citation-js/plugin-csl"; import "@citation-js/plugin-doi"; diff --git a/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx b/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx index 7f371a25b..791a9f534 100644 --- a/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx +++ b/packages/react/src/inlineContent/ReferenceInlineContent/ReferenceInlineContent.tsx @@ -9,6 +9,7 @@ import { ReactCustomInlineContentRenderProps, useComponentsContext, } from "@blocknote/react"; +// @ts-ignore import { Cite } from "@citation-js/core"; import "@citation-js/plugin-csl"; import "@citation-js/plugin-doi"; From 35498af5b7e09d4597e236df830363c9bf8373fc Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 3 Jun 2025 10:43:41 +0200 Subject: [PATCH 17/17] Updated package lock --- pnpm-lock.yaml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cca77a72..d61e64f56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,18 +234,6 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn - '@citation-js/core': - specifier: ^0.7.18 - version: 0.7.18(encoding@0.1.13) - '@citation-js/plugin-csl': - specifier: ^0.7.18 - version: 0.7.18(@citation-js/core@0.7.18(encoding@0.1.13)) - '@citation-js/plugin-doi': - specifier: ^0.7.18 - version: 0.7.18(@citation-js/core@0.7.18(encoding@0.1.13)) - '@floating-ui/react': - specifier: ^0.27.11 - version: 0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -6163,12 +6151,6 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.27.11': - resolution: {integrity: sha512-ZVtJxk4gQceaAOm1p5TlNeUcSxvzEwbAkKPgLYfV2b3aavC9Up9OZ5qPWQrMCASzmXUtNK1VuKdrqZkhAYhaeQ==} - peerDependencies: - react: '>=17.0.0' - react-dom: '>=17.0.0' - '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} @@ -16926,14 +16908,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tabbable: 6.2.0 - '@floating-ui/react@0.27.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@floating-ui/utils': 0.2.9 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tabbable: 6.2.0 - '@floating-ui/utils@0.2.9': {} '@hapi/hoek@9.3.0': {}