feat: use uiw react-codemirror
This commit is contained in:
parent
66d09c21b0
commit
6de86ce74d
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { ICommand } from "~/editor"
|
|||
import { Tooltip } from "./Tooltip"
|
||||
|
||||
export interface EditorToolbarProps {
|
||||
view: EditorView | null
|
||||
view?: EditorView
|
||||
toolbars: ICommand[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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\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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue