Rich text editor

This commit is contained in:
2025-11-13 22:15:59 +01:00
parent fb116d1b68
commit 07ebca03d4
3 changed files with 313 additions and 37 deletions
+247 -4
View File
@@ -11,6 +11,21 @@
"@blocknote/core": "^0.42.0",
"@blocknote/react": "^0.42.0",
"@blocknote/shadcn": "^0.42.0",
"@editorjs/checklist": "^1.6.0",
"@editorjs/code": "^2.9.3",
"@editorjs/delimiter": "^1.4.2",
"@editorjs/editorjs": "^2.31.0",
"@editorjs/embed": "^2.7.6",
"@editorjs/header": "^2.8.8",
"@editorjs/image": "^2.10.3",
"@editorjs/inline-code": "^1.5.2",
"@editorjs/link": "^2.6.2",
"@editorjs/list": "^2.0.9",
"@editorjs/marker": "^1.4.0",
"@editorjs/paragraph": "^2.11.7",
"@editorjs/quote": "^2.7.6",
"@editorjs/table": "^2.4.5",
"@editorjs/warning": "^1.4.1",
"@hello-pangea/dnd": "^18.0.1",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
@@ -32,7 +47,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20.19.24",
"@types/node": "^20.19.25",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
@@ -188,6 +203,222 @@
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/@codexteam/icons": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.3.3.tgz",
"integrity": "sha512-cp7mkZPgmBuSxigTm3Vb+DtVHYeX7qXfQd7o05vcLD8Ag5WvRlol2QSn5P10k0CDAJwmkH9nQGQLBycErS9lsQ==",
"license": "MIT"
},
"node_modules/@editorjs/caret": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@editorjs/caret/-/caret-1.0.3.tgz",
"integrity": "sha512-VmgwQJZgL/LQjk049JunzRV1YCa0vDi+BNEpbDmr5cp3lGZllq9QQFO1eI71ZPzvFVn3vvhb+eOif4sAEyGgbw==",
"license": "MIT",
"dependencies": {
"@editorjs/dom": "^1.0.1"
}
},
"node_modules/@editorjs/checklist": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@editorjs/checklist/-/checklist-1.6.0.tgz",
"integrity": "sha512-hRNP36DInr73mSK3noHBQeoQb7DA12ANfqTXufEkTgQzx+k4mRJ0HdeGukTIR4JbwjHJ9ecUBnnQqIEGnxCFEg==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.0"
}
},
"node_modules/@editorjs/code": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/@editorjs/code/-/code-2.9.3.tgz",
"integrity": "sha512-nXUrK3CjhpubvShYtcbkpZ9SU15IYwmJOsWZrlWYSzy9unZBRQthii6eABndsCtODzzV0yiSKmTp00RQkFow3Q==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.2"
}
},
"node_modules/@editorjs/delimiter": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@editorjs/delimiter/-/delimiter-1.4.2.tgz",
"integrity": "sha512-S8q2LpeYdYkVShLp7K8c4HLthDHBevLw+sT+iO0+SH0oMvFmld9SUon3DFzMQ2gG07EOdZGRZ958+sVxyvFjZw==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.2"
}
},
"node_modules/@editorjs/dom": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@editorjs/dom/-/dom-1.0.1.tgz",
"integrity": "sha512-yLO+86MYOIUr1Jl7SQw23SYT84ggv6aJW0EIRsI3NTHYgnQzmK7Bt2n5ZFupQlB0GJqmKqA5tCue3NKQb+o7Pw==",
"license": "MIT",
"dependencies": {
"@editorjs/helpers": "^1.0.1"
}
},
"node_modules/@editorjs/editorjs": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.31.0.tgz",
"integrity": "sha512-CBcIZXtPlg0dSlC5clO9OfTCmcxelj723jd4d67teFlaFJobjjxU1PmMxFJdhaRep5+nqdD0jr+fdJBqEDqt1g==",
"license": "Apache-2.0",
"dependencies": {
"@editorjs/caret": "^1.0.1",
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.5"
}
},
"node_modules/@editorjs/embed": {
"version": "2.7.6",
"resolved": "https://registry.npmjs.org/@editorjs/embed/-/embed-2.7.6.tgz",
"integrity": "sha512-L3agW/23mOI0L+oksUE9UOR5VSNCqapxLH5lma+5j+idjKCC31nxbx07x53MSJ4rlOTO1L7cFVhkqptEdOliJA==",
"license": "MIT",
"dependencies": {
"@editorjs/editorjs": "^2.29.1"
}
},
"node_modules/@editorjs/header": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.8.8.tgz",
"integrity": "sha512-bsMSs34u2hoi0UBuRoc5EGWXIFzJiwYgkFUYQGVm63y5FU+s8zPBmVx5Ip2sw1xgs0fqfDROqmteMvvmbCy62w==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.0.5",
"@editorjs/editorjs": "^2.29.1"
}
},
"node_modules/@editorjs/header/node_modules/@codexteam/icons": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
"integrity": "sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA==",
"license": "MIT"
},
"node_modules/@editorjs/helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@editorjs/helpers/-/helpers-1.0.1.tgz",
"integrity": "sha512-Lmr8ImoQvoROXtzhsIJsA1ZtXzH46DmE6O8hMjn9/AvQq62UfjREjn+Ewi6KxjIZMay2PsgDEbLlsVyNJGEaxw==",
"license": "MIT"
},
"node_modules/@editorjs/image": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@editorjs/image/-/image-2.10.3.tgz",
"integrity": "sha512-ekCsGICZOIdghF/U2T34H7CItqaWAoJDXbkRD+x8l/LIo/7Ozf7KovYm21qz+CluArgV4RurVFHqwlz+O0vfJA==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.0"
}
},
"node_modules/@editorjs/inline-code": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@editorjs/inline-code/-/inline-code-1.5.2.tgz",
"integrity": "sha512-IpEvwMTP3q89T/6vTi3ACoSMOcTdywtMsci1w19+Z/lxQn/NVuVah++ro93wQ/1ZVTyCr+TPUCchxwzY1/xaMg==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.2"
}
},
"node_modules/@editorjs/link": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@editorjs/link/-/link-2.6.2.tgz",
"integrity": "sha512-3cPx6M4ZvwDDvsi0E0fvMR3rvveAV/C0GRo1JLeZJ9cG9QgyoNolj4eu5Eqx3/r1XTC/he54qYEIZ/Dc4Lr4Ow==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.10.2",
"@codexteam/icons": "^0.0.4"
}
},
"node_modules/@editorjs/link/node_modules/@codexteam/icons": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.4.tgz",
"integrity": "sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q==",
"license": "MIT"
},
"node_modules/@editorjs/list": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@editorjs/list/-/list-2.0.9.tgz",
"integrity": "sha512-rUTgDSt5wygD3Dp24bNyp6vvye/Xf4UWju0ZuvWeP13Z4cu2z1Jb5JFSTEhCou72XUGuf4xVhtsd8cm/bwUS1g==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.2"
}
},
"node_modules/@editorjs/marker": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@editorjs/marker/-/marker-1.4.0.tgz",
"integrity": "sha512-5ipEXfL44jTTRzgNp/p/YjMO7jT08S5z4V8qA3FFJTfdhKgQyM3mvP1zpdYKw47ZZpVWMCncvk5Nto3BxihEtg==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.0.5"
}
},
"node_modules/@editorjs/marker/node_modules/@codexteam/icons": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
"integrity": "sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA==",
"license": "MIT"
},
"node_modules/@editorjs/paragraph": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@editorjs/paragraph/-/paragraph-2.11.7.tgz",
"integrity": "sha512-qD6bbWvRc4VvP0mXDOm+hOhzzhUYR9ZjcAvgCuKWcCbUMpCvhVF1s8NX40zdjekPi6JEnuHTamCncTrSzVsVhw==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.0.4"
}
},
"node_modules/@editorjs/paragraph/node_modules/@codexteam/icons": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.4.tgz",
"integrity": "sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q==",
"license": "MIT"
},
"node_modules/@editorjs/quote": {
"version": "2.7.6",
"resolved": "https://registry.npmjs.org/@editorjs/quote/-/quote-2.7.6.tgz",
"integrity": "sha512-D01KUMSDj2r+6Z+xjDkQqI+y6URpeHCvj0+P4pah+GtkG040lWjFb2H4pgHFXuol2cbfyAoraYSw85fuPheCvw==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.2",
"@editorjs/dom": "^0.0.5"
}
},
"node_modules/@editorjs/quote/node_modules/@editorjs/dom": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@editorjs/dom/-/dom-0.0.5.tgz",
"integrity": "sha512-SZ78Gwpkp3EUhjBIp0lSojeQ35V9acF8SubJsMeOH/vlOUE40GOnvvwWZnF05lO7bIB0dOHhhJy4N7IIAWxP2w==",
"license": "MIT",
"dependencies": {
"@editorjs/helpers": "^0.0.4"
}
},
"node_modules/@editorjs/quote/node_modules/@editorjs/helpers": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@editorjs/helpers/-/helpers-0.0.4.tgz",
"integrity": "sha512-ieg3dzo2m1/ELze/RMNADiAiC5amXxIlVXoJ5vvXITOu/p/dPsrF+Oi3h5gBYvtGk9vg5LJUSG5YWU0tBUO1tw==",
"license": "MIT"
},
"node_modules/@editorjs/table": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/@editorjs/table/-/table-2.4.5.tgz",
"integrity": "sha512-pF48R2wc5m0c+N+RjtCLXBGZd23Rl7EjfSFpmcSViwNsu5RwMgYGrEiQ8mzVh98mbvYQwXm/NYBi9DEUUs970A==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.0.6"
}
},
"node_modules/@editorjs/table/node_modules/@codexteam/icons": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.6.tgz",
"integrity": "sha512-L7Q5PET8PjKcBT5wp7VR+FCjwCi5PUp7rd/XjsgQ0CI5FJz0DphyHGRILMuDUdCW2MQT9NHbVr4QP31vwAkS/A==",
"license": "MIT"
},
"node_modules/@editorjs/warning": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@editorjs/warning/-/warning-1.4.1.tgz",
"integrity": "sha512-7A+irJ6gIh6li4DC7qw/NiWCThlBgtoPUeeIVdldVjoATAu+a1GjS+w/5L24ReacgQJNPVQPPCoMaS1nB83agw==",
"license": "MIT",
"dependencies": {
"@codexteam/icons": "^0.3.2",
"@editorjs/editorjs": "^2.30.7"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
@@ -2662,9 +2893,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
"version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2917,6 +3148,18 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/codex-notifier": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/codex-notifier/-/codex-notifier-1.1.2.tgz",
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==",
"license": "MIT"
},
"node_modules/codex-tooltip": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.5.tgz",
"integrity": "sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==",
"license": "MIT"
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+14 -4
View File
@@ -8,9 +8,19 @@
"start": "next start"
},
"dependencies": {
"@blocknote/core": "^0.42.0",
"@blocknote/react": "^0.42.0",
"@blocknote/shadcn": "^0.42.0",
"@editorjs/checklist": "^1.6.0",
"@editorjs/code": "^2.9.3",
"@editorjs/delimiter": "^1.4.2",
"@editorjs/editorjs": "^2.31.0",
"@editorjs/embed": "^2.7.6",
"@editorjs/header": "^2.8.8",
"@editorjs/inline-code": "^1.5.2",
"@editorjs/link": "^2.6.2",
"@editorjs/list": "^2.0.9",
"@editorjs/marker": "^1.4.0",
"@editorjs/quote": "^2.7.6",
"@editorjs/table": "^2.4.5",
"@editorjs/warning": "^1.4.1",
"@hello-pangea/dnd": "^18.0.1",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
@@ -32,7 +42,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20.19.24",
"@types/node": "^20.19.25",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
+52 -29
View File
@@ -1,37 +1,60 @@
import { SetState } from "@/lib/types";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/shadcn";
import { useEffect } from "react";
"use client";
export default function DocView({
doc,
setDoc,
}: {
doc: string;
setDoc: SetState<string>;
}) {
const editor = useCreateBlockNote();
import { useEffect, useRef, useState } from "react";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Quote from "@editorjs/quote";
import Code from "@editorjs/code";
import Table from "@editorjs/table";
import Delimiter from "@editorjs/delimiter";
import Warning from "@editorjs/warning";
import InlineCode from "@editorjs/inline-code";
export default function DocView() {
const holderRef = useRef<HTMLDivElement | null>(null);
const [doc, setDoc] = useState({ blocks: [] });
useEffect(() => {
if (editor && doc) {
editor.tryParseMarkdownToBlocks(doc);
}
}, [editor]);
let editor: any;
let isMounted = true;
(async () => {
const EditorJS = (await import("@editorjs/editorjs")).default;
if (!isMounted || !holderRef.current) return;
editor = new EditorJS({
holder: holderRef.current,
placeholder: "Start writing here...",
autofocus: true,
tools: {
list: { class: List, inlineToolbar: true },
quote: { class: Quote, inlineToolbar: true },
code: Code,
table: Table,
header: Header,
delimiter: Delimiter,
warning: Warning,
inlineCode: InlineCode,
},
data: doc,
async onChange() {
const savedData = await editor.save();
setDoc(savedData);
},
});
})();
return () => {
isMounted = false;
if (editor) editor.destroy();
};
}, []);
return (
<div className="h-full w-3xl mx-auto bg-accent/20 rounded-2xl px-4 py-2 docs-view" onClick={() => editor.focus()}>
<BlockNoteView
defaultValue={doc}
editor={editor}
shadCNComponents={{}}
className="w-full focus:outline-none focus:ring-0 appearance-none"
onChange={async () => {
if (editor) {
console.log(editor.blocksToMarkdownLossy(editor.document));
setDoc(editor.blocksToMarkdownLossy(editor.document));
}
}}
/>
<div className="h-full w-3xl mx-auto bg-accent/20 rounded-2xl px-4 py-2">
<div ref={holderRef} className="docs-view min-h-[400px]" />
</div>
);
}