From 6de86ce74d4f5d1b36ebac3b1ba786d3abc47ac2 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 18 Oct 2022 00:33:22 +0100 Subject: [PATCH] feat: use uiw react-codemirror --- package.json | 2 + pnpm-lock.yaml | 86 ++++++++++++- src/components/ui/Editor.tsx | 142 --------------------- src/components/ui/EditorToolbar.tsx | 2 +- src/pages/dashboard/[subdomain]/editor.tsx | 99 +++++++++----- 5 files changed, 155 insertions(+), 176 deletions(-) delete mode 100644 src/components/ui/Editor.tsx diff --git a/package.json b/package.json index 6b979428..11d1d5de 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@trpc/next": "^9.27.2", "@trpc/react": "^9.27.2", "@trpc/server": "^9.27.2", + "@uiw/react-codemirror": "^4.12.4", "@urql/core": "^3.0.3", "@web-std/form-data": "^3.0.2", "async-busboy": "^1.1.0", @@ -117,6 +118,7 @@ "@types/async-busboy": "^1.1.1", "@types/busboy": "^1.5.0", "@types/cookie": "^0.5.1", + "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", "@types/mjml": "^4.7.0", "@types/node": "^18.7.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d17ee467..1743e124 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,7 @@ specifiers: '@types/async-busboy': ^1.1.1 '@types/busboy': ^1.5.0 '@types/cookie': ^0.5.1 + '@types/hast': ^2.3.4 '@types/js-yaml': ^4.0.5 '@types/mjml': ^4.7.0 '@types/node': ^18.7.23 @@ -49,6 +50,7 @@ specifiers: '@types/sortablejs': ^1.15.0 '@types/ua-parser-js': ^0.7.36 '@types/uuid': ^8.3.4 + '@uiw/react-codemirror': ^4.12.4 '@unocss/cli': ^0.45.25 '@unocss/preset-icons': ^0.45.25 '@urql/core': ^3.0.3 @@ -154,6 +156,7 @@ dependencies: '@trpc/next': 9.27.2_dyodlyrl4jlccmeg4rab7q5slm '@trpc/react': 9.27.2_r3nohbkvzkycnbsdj5gzhfx2rm '@trpc/server': 9.27.2 + '@uiw/react-codemirror': 4.12.4_s3us5uspwddq2sobjfryl7muay '@urql/core': 3.0.3 '@web-std/form-data': 3.0.2 async-busboy: 1.1.0 @@ -230,6 +233,7 @@ devDependencies: '@types/async-busboy': 1.1.1 '@types/busboy': 1.5.0 '@types/cookie': 0.5.1 + '@types/hast': 2.3.4 '@types/js-yaml': 4.0.5 '@types/mjml': 4.7.0 '@types/node': 18.7.23 @@ -410,6 +414,19 @@ packages: to-fast-properties: 2.0.0 dev: false + /@codemirror/autocomplete/6.3.0_eqnojjybujbpyzalc7hq3ayyuy: + resolution: {integrity: sha512-4jEvh3AjJZTDKazd10J6ZsCIqaYxDMCeua5ouQxY8hlFIml+nr7le0SgBhT3SIytFBmdzPK3AUhXGuW3T79nVg==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + dependencies: + '@codemirror/language': 6.2.1 + '@codemirror/state': 6.1.2 + '@codemirror/view': 6.2.5 + '@lezer/common': 1.0.1 + dev: false + /@codemirror/autocomplete/6.3.0_vswjzb6pveu6y6iyyyir6hfjfy: resolution: {integrity: sha512-4jEvh3AjJZTDKazd10J6ZsCIqaYxDMCeua5ouQxY8hlFIml+nr7le0SgBhT3SIytFBmdzPK3AUhXGuW3T79nVg==} peerDependencies: @@ -500,10 +517,27 @@ packages: crelt: 1.0.5 dev: false + /@codemirror/search/6.2.1: + resolution: {integrity: sha512-Q1JgUSBjQZRPIddlXzad/AVDigdhriLxQNFyP0gfrDTq6LDHNhr95U/tW3bpVssGenkaLzujtu/7XoK4kyvL3g==} + dependencies: + '@codemirror/state': 6.1.2 + '@codemirror/view': 6.2.5 + crelt: 1.0.5 + dev: false + /@codemirror/state/6.1.2: resolution: {integrity: sha512-Mxff85Hp5va+zuj+H748KbubXjrinX/k28lj43H14T2D0+4kuvEFIEIO7hCEcvBT8ubZyIelt9yGOjj2MWOEQA==} dev: false + /@codemirror/theme-one-dark/6.1.0: + resolution: {integrity: sha512-AiTHtFRu8+vWT9wWUWDM+cog6ZwgivJogB1Tm/g40NIpLwph7AnmxrSzWfvJN5fBVufsuwBxecQCNmdcR5D7Aw==} + dependencies: + '@codemirror/language': 6.2.1 + '@codemirror/state': 6.1.2 + '@codemirror/view': 6.2.5 + '@lezer/highlight': 1.1.0 + dev: false + /@codemirror/view/6.2.5: resolution: {integrity: sha512-4kVS3RZi3nkPNfG2X/7+2L8IhoJv6dJ9yJz4KUlw4rwxI8OmVCKJzC/jnJqp+bVNOml+oA3gSF1kX2VvOiI72Q==} dependencies: @@ -2289,7 +2323,6 @@ packages: resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} dependencies: '@types/unist': 2.0.6 - dev: false /@types/js-yaml/4.0.5: resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} @@ -2434,7 +2467,6 @@ packages: /@types/unist/2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} - dev: false /@types/uuid/8.3.4: resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} @@ -2613,6 +2645,44 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /@uiw/codemirror-extensions-basic-setup/4.12.4_ofmrkt3gkrq4pi4vmtirqlyx44: + resolution: {integrity: sha512-owSCcRBtS2wYjxgBFkuIjfjWJHsR8AxgsQtqPpHB/6U0zCLuzKS/OM5ZRS2T3rdOizg0hCPztVvmshWeKjF+qw==} + peerDependencies: + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + dependencies: + '@codemirror/autocomplete': 6.3.0_eqnojjybujbpyzalc7hq3ayyuy + '@codemirror/commands': 6.1.0 + '@codemirror/language': 6.2.1 + '@codemirror/lint': 6.0.0 + '@codemirror/search': 6.2.1 + '@codemirror/state': 6.1.2 + '@codemirror/view': 6.2.5 + dev: false + + /@uiw/react-codemirror/4.12.4_s3us5uspwddq2sobjfryl7muay: + resolution: {integrity: sha512-92TAvN2z5snPjPtJDLmbqrqsXXYFYlBnWraXZuDc1XGaw80tB26ZkdEW79CD2QM4Y9LhFIt+sauwlmiAVDs/5A==} + peerDependencies: + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@babel/runtime': 7.19.0 + '@codemirror/commands': 6.1.0 + '@codemirror/state': 6.1.2 + '@codemirror/theme-one-dark': 6.1.0 + '@codemirror/view': 6.2.5 + '@uiw/codemirror-extensions-basic-setup': 4.12.4_ofmrkt3gkrq4pi4vmtirqlyx44 + codemirror: 6.0.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + transitivePeerDependencies: + - '@codemirror/language' + dev: false + /@unocss/astro/0.45.25: resolution: {integrity: sha512-TxFxESIvSZtaVIRP/cDyrwnYlHz78mG9ohhAhI0adLGi0v0yBwoGJ1krlqxAuTzhV191InTlwD/C5QevTsf86Q==} dependencies: @@ -4085,6 +4155,18 @@ packages: engines: {node: '>=0.10.0'} dev: false + /codemirror/6.0.1: + resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} + dependencies: + '@codemirror/autocomplete': 6.3.0_eqnojjybujbpyzalc7hq3ayyuy + '@codemirror/commands': 6.1.0 + '@codemirror/language': 6.2.1 + '@codemirror/lint': 6.0.0 + '@codemirror/search': 6.2.1 + '@codemirror/state': 6.1.2 + '@codemirror/view': 6.2.5 + dev: false + /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: diff --git a/src/components/ui/Editor.tsx b/src/components/ui/Editor.tsx deleted file mode 100644 index db83ea2a..00000000 --- a/src/components/ui/Editor.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react" -import { EditorState } from "@codemirror/state" -import { - EditorView, - keymap, - type ViewUpdate, - dropCursor, - drawSelection, - crosshairCursor, - placeholder as placeholderExtension, -} from "@codemirror/view" -import { - history, - defaultKeymap, - historyKeymap, - indentWithTab, -} from "@codemirror/commands" -import { markdown } from "@codemirror/lang-markdown" -import { - defaultHighlightStyle, - indentOnInput, - syntaxHighlighting, -} from "@codemirror/language" -import { IS_PROD } from "~/lib/constants" - -const theme = EditorView.theme({ - ".cm-scroller": { - fontFamily: "var(--font-sans)", - fontSize: "1rem", - overflow: "auto", - height: "100%", - }, - ".cm-content": { - height: "100%", - paddingBottom: "600px", - }, - "&.cm-editor.cm-focused": { - outline: "none", - }, -}) - -export const useEditor = ({ - value, - onChange, - placeholder, - onDropFile, -}: { - value: string - onChange: (value: string) => void - placeholder?: string - onDropFile?: (file: File, view: EditorView) => void -}) => { - const [node, setNode] = useState(null) - const [view, setView] = useState(null) - - const initEditor = useCallback( - (node: HTMLDivElement) => { - const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => { - if (vu.docChanged && typeof onChange === "function") { - const doc = vu.state.doc - const value = doc.toString() - onChange(value) - } - }) - - const view = new EditorView({ - state: EditorState.create({ - doc: "", - extensions: [ - keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), - history(), - dropCursor(), - drawSelection(), - indentOnInput(), - crosshairCursor(), - EditorState.allowMultipleSelections.of(true), - updateListener, - EditorView.lineWrapping, - markdown(), - placeholderExtension(placeholder || ""), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - theme, - EditorView.domEventHandlers({ - drop(e) { - const file = e.dataTransfer?.items[0]?.getAsFile() - if (!file) return - - onDropFile?.(file, view) - }, - paste(e) { - const file = e.clipboardData?.files[0] - if (!file) return - - onDropFile?.(file, view) - }, - }), - ], - }), - parent: node, - }) - - view.focus() - - return view - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [onChange, placeholder], - ) - - useEffect(() => { - const view = node && initEditor(node) - if (view) { - setView(view) - } - return () => { - view?.destroy() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [node]) - - // Update view state when `value` changed - useEffect(() => { - const currentValue = view ? view.state.doc.toString() : "" - if (view && value !== currentValue) { - if (!IS_PROD) { - console.log("updating editor value", value) - } - view.dispatch({ - changes: { from: 0, to: currentValue.length, insert: value || "" }, - }) - } - }, [value, view]) - - return { - view, - editorRef(newNode: HTMLDivElement) { - if (!newNode) return - if (node && newNode.isEqualNode(node)) return - setNode(newNode) - }, - } -} diff --git a/src/components/ui/EditorToolbar.tsx b/src/components/ui/EditorToolbar.tsx index 877238b7..c9017053 100644 --- a/src/components/ui/EditorToolbar.tsx +++ b/src/components/ui/EditorToolbar.tsx @@ -5,7 +5,7 @@ import { ICommand } from "~/editor" import { Tooltip } from "./Tooltip" export interface EditorToolbarProps { - view: EditorView | null + view?: EditorView toolbars: ICommand[] } diff --git a/src/pages/dashboard/[subdomain]/editor.tsx b/src/pages/dashboard/[subdomain]/editor.tsx index c11a23ed..c18aeb47 100644 --- a/src/pages/dashboard/[subdomain]/editor.tsx +++ b/src/pages/dashboard/[subdomain]/editor.tsx @@ -1,14 +1,12 @@ -import { type EditorView } from "@codemirror/view" import clsx from "clsx" import dayjs from "dayjs" import { useRouter } from "next/router" -import { ChangeEvent, useCallback, useEffect, useState } from "react" +import { ChangeEvent, useCallback, useEffect, useState, useRef } from "react" import toast from "react-hot-toast" import { toolbars } from "~/editor" import { DashboardLayout } from "~/components/dashboard/DashboardLayout" import { DashboardMain } from "~/components/dashboard/DashboardMain" import { PublishButton } from "~/components/dashboard/PublishButton" -import { useEditor } from "~/components/ui/Editor" import { EditorToolbar } from "~/components/ui/EditorToolbar" import { Input } from "~/components/ui/Input" import { UniLink } from "~/components/ui/UniLink" @@ -25,6 +23,10 @@ import { useQueryClient } from "@tanstack/react-query" import { PageContent } from "~/components/common/PageContent" import pinyin from "pinyin" import { GITHUB_LINK } from "~/lib/env" +import type { Root } from "hast" +import CodeMirror from "@uiw/react-codemirror" +import { EditorView } from "@codemirror/view" +import { markdown } from "@codemirror/lang-markdown" const getInputDatetimeValue = (date: Date | string) => { const str = dayjs(date).format() @@ -76,7 +78,6 @@ export default function SubdomainEditor() { tags: "", content: "", }) - const [content, setContent] = useState("") const [defaultSlug, setDefaultSlug] = useState("") type Values = typeof values @@ -151,7 +152,7 @@ export default function SubdomainEditor() { }, [createOrUpdatePage.isSuccess]) const handleDropFile = useCallback( - async (file: File, view: EditorView) => { + async (file: File, view?: EditorView | null) => { const toastId = toast.loading("Uploading...") try { if (!file.type.startsWith("image/")) { @@ -162,7 +163,7 @@ export default function SubdomainEditor() { toast.success("Uploaded!", { id: toastId, }) - view.dispatch( + view?.dispatch( view.state.replaceSelection( `\n![${file.name.replace(/\.\w+$/, "")}](${key})\n`, ), @@ -176,23 +177,6 @@ export default function SubdomainEditor() { [uploadFile], ) - const handleEditorChange = (newValue: string) => { - setContent(newValue) - } - - useEffect(() => { - if (content !== values.content) { - updateValue("content", content) - } - }, [content, updateValue, values.content]) - - const { editorRef, view } = useEditor({ - value: content, - onChange: handleEditorChange, - onDropFile: handleDropFile, - placeholder: `Start writing...`, - }) - useEffect(() => { if (!page.data || !draftKey) return @@ -201,7 +185,6 @@ export default function SubdomainEditor() { local?.date && +new Date(local?.date) > +new Date(page.data.date_updated) if (useLocal && local?.values) { setValues(local?.values) - setContent(local?.values.content || "") } else { setValues({ title: page.data.title || "", @@ -215,10 +198,46 @@ export default function SubdomainEditor() { ?.join(", ") || "", content: page.data.body?.content || "", }) - setContent(page.data.body?.content || "") } }, [page.data, subdomain, draftKey]) + const [view, setView] = useState() + + const extensions = [ + markdown(), + EditorView.theme({ + ".cm-scroller": { + fontFamily: "var(--font-sans)", + fontSize: "1rem", + overflow: "auto", + height: "100%", + }, + ".cm-content": { + paddingBottom: "600px", + }, + "&.cm-editor.cm-focused": { + outline: "none", + }, + "&.cm-editor": { + height: "100%", + }, + }), + EditorView.domEventHandlers({ + drop(e) { + const file = e.dataTransfer?.items[0]?.getAsFile() + if (!file) return + + handleDropFile(file, view) + }, + paste(e) { + const file = e.clipboardData?.files[0] + if (!file) return + + handleDropFile(file, view) + }, + }), + ] + return ( <> @@ -273,17 +292,35 @@ export default function SubdomainEditor() { }} onChange={(e) => updateValue("title", e.target.value)} className="h-12 ml-1 inline-flex items-center border-none text-3xl font-bold w-full focus:outline-none" - placeholder="Title goes here.." />
-
+ setView(view)} + basicSetup={{ + lineNumbers: false, + foldGutter: false, + highlightActiveLine: false, + history: true, + defaultKeymap: true, + historyKeymap: true, + dropCursor: true, + drawSelection: true, + indentOnInput: true, + crosshairCursor: true, + allowMultipleSelections: true, + syntaxHighlighting: true, + }} + indentWithTab={true} + onChange={(value) => updateValue("content", value)} + placeholder="Start writing..." + />