feat: use uiw react-codemirror

This commit is contained in:
DIYgod 2022-10-18 00:33:22 +01:00
parent 66d09c21b0
commit 6de86ce74d
No known key found for this signature in database
GPG Key ID: D159328F47A80DCA
5 changed files with 155 additions and 176 deletions

View File

@ -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",

View File

@ -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:

View File

@ -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<HTMLDivElement | null>(null)
const [view, setView] = useState<EditorView | null>(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)
},
}
}

View File

@ -5,7 +5,7 @@ import { ICommand } from "~/editor"
import { Tooltip } from "./Tooltip"
export interface EditorToolbarProps {
view: EditorView | null
view?: EditorView
toolbars: ICommand[]
}

View File

@ -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<EditorView>()
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 (
<>
<DashboardLayout title="Editor">
@ -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.."
/>
</div>
<div className="mt-5 flex-1 flex overflow-hidden">
<div
className="px-5 h-full border-r w-1/2 overflow-scroll"
ref={editorRef}
></div>
<CodeMirror
className="px-5 h-full border-r w-1/2"
value={values.content}
extensions={extensions}
onCreateEditor={(view) => 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..."
/>
<PageContent
className="px-5 w-1/2 overflow-scroll pb-[200px]"
content={content}
content={values.content}
></PageContent>
</div>
</div>