feat: support comment cm

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2023-05-18 11:27:50 +08:00
parent 55df59877e
commit 38ecf47f8f
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
7 changed files with 207 additions and 109 deletions

View File

@ -474,6 +474,18 @@ export default function SubdomainEditor() {
}
}, [draftKey, site.data?.characterId])
const cmStyle = useMemo(
() => ({
".cm-scroller": {
padding: isMobileLayout ? "0 1.25rem" : "unset",
},
".cm-content": {
paddingBottom: "600px",
},
}),
[isMobileLayout],
)
return (
<>
<DashboardMain fullWidth>
@ -579,11 +591,15 @@ export default function SubdomainEditor() {
{isMobileLayout ? (
!isRendering ? (
<CodeMirrorEditor
cmStyle={cmStyle}
value={initialContent}
placeholder={t("Start writing...") as string}
onChange={onChange}
handleDropFile={handleDropFile}
onScroll={onEditorScroll}
// onUpdate={onUpdate}
className={`h-full ${
isMobileLayout ? "w-full" : "border-r w-1/2 px-5"
}`}
onCreateEditor={onCreateEditor}
onMouseEnter={() => {
setCurrentScrollArea("editor")

View File

@ -1,16 +1,20 @@
import { CharacterEntity, NoteEntity } from "crossbell.js"
import { useEffect } from "react"
import { useCallback, useEffect, useRef } from "react"
import { useForm } from "react-hook-form"
import { EditorView } from "@codemirror/view"
import { useAccountState } from "@crossbell/connect-kit"
import { Popover } from "@headlessui/react"
import { Avatar } from "~/components/ui/Avatar"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { editorUpload } from "~/editor/Multimedia"
import { useUploadFile } from "~/hooks/useUploadFile"
import { useTranslation } from "~/lib/i18n/client"
import { useCommentPage, useUpdateComment } from "~/queries/page"
import { CodeMirrorEditor } from "../ui/CodeMirror"
import { Input } from "../ui/Input"
import { EmojiPicker } from "./EmojiPicker"
export const CommentInput: React.FC<{
@ -76,6 +80,38 @@ export const CommentInput: React.FC<{
}
}, [commentPage.isSuccess, updateComment.isSuccess, form, onSubmitted])
const cmViewRef = useRef<EditorView>()
const onCreateEditor = useCallback((view: EditorView) => {
cmViewRef.current = view
}, [])
const uploadFile = useUploadFile()
const handleDropFile = useCallback(
async (file: File) => {
const view = cmViewRef.current
if (view) {
editorUpload(file, view)
}
},
[uploadFile],
)
const InputHolder = useCallback(
() => (
<Input
id="content"
isBlock
required={!!account?.character}
disabled={!account?.character}
multiline
maxLength={600}
className="mb-2"
placeholder={t("Write a comment on the blockchain") || ""}
{...form.register("content", {})}
/>
),
[account?.character, t, form],
)
return (
<div className="xlog-comment-input flex">
<Avatar
@ -86,16 +122,17 @@ export const CommentInput: React.FC<{
/>
<form className="w-full" onSubmit={handleSubmit}>
<div>
<Input
id="content"
isBlock
required={!!account?.character}
disabled={!account?.character}
multiline
maxLength={600}
className="mb-2"
placeholder={t("Write a comment on the blockchain") || ""}
<CodeMirrorEditor
{...form.register("content", {})}
onChange={(val) => {
form.setValue("content", val)
}}
handleDropFile={handleDropFile}
className="mb-2 p-3 h-[74px] border focus-within:border-accent border-[var(--border-color)] rounded-lg outline-2 outline-transparent"
placeholder={t("Write a comment on the blockchain") || ""}
maxLength={600}
onCreateEditor={onCreateEditor}
LoadingComponent={InputHolder}
/>
</div>
<div className="flex justify-between">
@ -107,12 +144,26 @@ export const CommentInput: React.FC<{
</Popover.Button>
<Popover.Panel className="absolute left-0 top-full z-10">
<EmojiPicker
onEmojiSelect={(e: any) =>
form.setValue(
"content",
form.getValues("content") + e.native,
)
}
onEmojiSelect={(e: any) => {
const emojiValue = e.native
const view = cmViewRef.current
if (!view) return
const state = view.state
const range = state.selection.ranges[0]
view.dispatch({
changes: {
from: range.from,
to: range.to,
insert: `${emojiValue}`,
},
selection: { anchor: range.from },
})
requestAnimationFrame(() => {
// console.log(view.state.doc.toString(), "statevalue")
form.setValue("content", view.state.doc.toString())
})
}}
/>
</Popover.Panel>
</>

View File

@ -1,5 +1,8 @@
import {
CSSProperties,
FC,
Suspense,
createElement,
forwardRef,
useEffect,
useImperativeHandle,
@ -22,7 +25,6 @@ import {
import { useIsDark } from "~/hooks/useDarkMode"
import { useGetState } from "~/hooks/useGetState"
import { useIsUnmounted } from "~/hooks/useLifecycle"
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
const LoadingHolder = () => {
const { t } = useTranslation("common")
@ -34,13 +36,18 @@ const LoadingHolder = () => {
}
interface XLogCodeMirrorEditorProps {
value: string
value?: string
maxLength?: number
placeholder?: string
className?: string
cmStyle?: Record<string, CSSProperties>
onChange?: (value: string, viewUpdate: ViewUpdate) => void
handleDropFile?: (file: File) => void
onScroll?: (scrollTop: number) => void
onUpdate?: (update: ViewUpdate) => void
onCreateEditor?: (view: EditorView, state: EditorState) => void
onMouseEnter?: () => void
LoadingComponent?: FC
}
export const CodeMirrorEditor = forwardRef<
@ -48,7 +55,15 @@ export const CodeMirrorEditor = forwardRef<
XLogCodeMirrorEditorProps
>((props, ref) => {
return (
<Suspense fallback={<LoadingHolder />}>
<Suspense
fallback={
props.LoadingComponent ? (
createElement(props.LoadingComponent)
) : (
<LoadingHolder />
)
}
>
<LazyCodeMirrorEditor {...props} ref={ref} />
</Suspense>
)
@ -64,13 +79,11 @@ const LazyCodeMirrorEditor = forwardRef<
const [loading, setLoading] = useState(true)
const editorElementRef = useRef<HTMLDivElement>(null)
const isUnmounted = useIsUnmounted()
const { t } = useTranslation("dashboard")
const isMobileLayout = useIsMobileLayout()
const [cmEditor, setCmEditor] = useState<EditorView | null>(null)
const isDark = useIsDark()
useCodeMirrorStyle(cmEditor)
useCodeMirrorStyle(cmEditor, props.cmStyle)
useCodeMirrorAutoToggleTheme(cmEditor, isDark)
useImperativeHandle(ref, () => cmEditor!)
@ -79,7 +92,6 @@ const LazyCodeMirrorEditor = forwardRef<
const getHandleDropFile = useGetState(handleDropFile)
const getOnScroll = useGetState(onScroll)
const getValue = useGetState(value)
const getProps = useGetState(props)
useEffect(() => {
@ -134,13 +146,13 @@ const LazyCodeMirrorEditor = forwardRef<
{ languages },
] = modules
const props = getProps()
const editorState = EditorState.create({
doc: getValue(),
doc: props.value || "",
extensions: [
placeholder(t("Start writing...") || ""),
placeholder(props.placeholder || ""),
EditorView.updateListener.of((vu) => {
const props = getProps()
const { onUpdate, onChange } = props
onUpdate?.(vu)
@ -153,6 +165,10 @@ const LazyCodeMirrorEditor = forwardRef<
) {
const doc = vu.state.doc
const value = doc.toString()
if (props.maxLength && value.length > props.maxLength) {
return
}
onChange(value, vu)
}
}),
@ -235,13 +251,14 @@ const LazyCodeMirrorEditor = forwardRef<
<div
ref={editorElementRef}
onMouseEnter={onMouseEnter}
className={
loading
? ""
: `h-full ${isMobileLayout ? "w-full" : "border-r w-1/2 px-5"}`
}
className={loading ? "" : props.className}
/>
{loading && <LoadingHolder />}
{loading &&
(props.LoadingComponent ? (
createElement(props.LoadingComponent)
) : (
<LoadingHolder />
))}
</>
)
})

View File

@ -6,7 +6,8 @@ import { type EditorView } from "@codemirror/view"
import { MAXIMUM_FILE_SIZE } from "~/lib/constants"
import { UploadFile } from "~/lib/upload-file"
import { ICommand, wrapExecute } from "."
import type { ICommand } from "."
import { wrapExecute } from "./helper"
export async function editorUpload(file: File, view: EditorView) {
const toastId = toast.loading("Uploading...")

47
src/editor/helper.ts Normal file
View File

@ -0,0 +1,47 @@
import { EditorSelection } from "@codemirror/state"
import { EditorView } from "@codemirror/view"
export type IWrapExecute = {
view: EditorView
prepend: string
append: string
}
export const wrapExecute = ({ view, prepend, append }: IWrapExecute) => {
const range = view.state.selection.ranges[0]
const selection = view.state.sliceDoc(
range.from - prepend.length,
range.to + append.length,
)
if (selection.startsWith(prepend) && selection.endsWith(append)) {
view.dispatch(
view.state.changeByRange((range) => ({
changes: [
{
from: range.from - prepend.length,
to: range.to + append.length,
insert: view.state.sliceDoc(range.from, range.to),
},
],
range: EditorSelection.range(
range.from - prepend.length,
range.to - prepend.length,
),
})),
)
view.focus()
return
}
view.dispatch(
view.state.changeByRange((range) => ({
changes: [
{ from: range.from, insert: prepend },
{ from: range.to, insert: append },
],
range: EditorSelection.range(
range.from + prepend.length,
range.to + prepend.length,
),
})),
)
view.focus()
}

View File

@ -49,12 +49,6 @@ export type IPrependExecute = {
prepend: string
}
export type IWrapExecute = {
view: EditorView
prepend: string
append: string
}
export const prependExecute = ({ view, prepend }: IPrependExecute) => {
const range = view.state.selection.ranges[0]
const selection = view.state.sliceDoc(range.from - prepend.length, range.to)
@ -90,47 +84,6 @@ export const prependExecute = ({ view, prepend }: IPrependExecute) => {
)
view.focus()
}
export const wrapExecute = ({ view, prepend, append }: IWrapExecute) => {
const range = view.state.selection.ranges[0]
const selection = view.state.sliceDoc(
range.from - prepend.length,
range.to + append.length,
)
if (selection.startsWith(prepend) && selection.endsWith(append)) {
view.dispatch(
view.state.changeByRange((range) => ({
changes: [
{
from: range.from - prepend.length,
to: range.to + append.length,
insert: view.state.sliceDoc(range.from, range.to),
},
],
range: EditorSelection.range(
range.from - prepend.length,
range.to - prepend.length,
),
})),
)
view.focus()
return
}
view.dispatch(
view.state.changeByRange((range) => ({
changes: [
{ from: range.from, insert: prepend },
{ from: range.to, insert: append },
],
range: EditorSelection.range(
range.from + prepend.length,
range.to + prepend.length,
),
})),
)
view.focus()
}
export const toolbars: ICommand[] = [
Heading,
Bold,
@ -152,3 +105,6 @@ export const toolbars: ICommand[] = [
Cloud,
Help,
]
export { wrapExecute } from "./helper"
export type { IWrapExecute } from "./helper"

View File

@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"
import { CSSProperties, useEffect, useMemo, useRef } from "react"
import type { Extension } from "@codemirror/state"
import { Compartment } from "@codemirror/state"
@ -7,7 +7,6 @@ import { EditorView } from "@codemirror/view"
import { githubLight } from "@ddietr/codemirror-themes/theme/github-light"
import { useIsUnmounted } from "./useLifecycle"
import { useIsMobileLayout } from "./useMobileLayout"
export const monospaceFonts = `"OperatorMonoSSmLig Nerd Font","Cascadia Code PL","FantasqueSansMono Nerd Font","operator mono","Fira code Retina","Fira code","Consolas", Monaco, "Hannotate SC", monospace, -apple-system`
@ -39,44 +38,55 @@ export const useCodeMirrorAutoToggleTheme = (
}, [view, isDark])
}
export const useCodeMirrorStyle = (view: EditorView | null) => {
const isMobileLayout = useIsMobileLayout()
const baseCmStyle = {
".cm-scroller": {
fontFamily: monospaceFonts,
fontSize: "1rem",
overflow: "auto",
height: "100%",
},
"&.cm-editor.cm-focused": {
outline: "none",
},
"&.cm-editor": {
height: "100%",
backgroundColor: "transparent",
},
} as Record<string, CSSProperties>
export const useCodeMirrorStyle = (view: EditorView | null, cmStyle?: any) => {
const isUnmounted = useIsUnmounted()
const once = useRef(false)
const getStyle = () => {
return {
".cm-scroller": {
fontFamily: monospaceFonts,
fontSize: "1rem",
overflow: "auto",
height: "100%",
padding: isMobileLayout ? "0 1.25rem" : "unset",
},
".cm-content": {
paddingBottom: "600px",
},
"&.cm-editor.cm-focused": {
outline: "none",
},
"&.cm-editor": {
height: "100%",
backgroundColor: "transparent",
},
const mergedCmStyle = useMemo(() => {
const nextStyle = {} as any
for (const key in baseCmStyle) {
nextStyle[key] = {
...baseCmStyle[key],
...cmStyle?.[key],
}
}
}
return nextStyle
}, [cmStyle])
useEffect(() => {
if (!view) return
view.dispatch({
effects: [extensionMap.style.reconfigure(EditorView.theme(getStyle()))],
effects: [
extensionMap.style.reconfigure(EditorView.theme(mergedCmStyle)),
],
})
}, [view, isMobileLayout])
}, [view, mergedCmStyle])
if (isUnmounted()) return
if (!once.current) {
if (!view) return
view.dispatch({
effects: [extensionMap.style.reconfigure(EditorView.theme(getStyle()))],
effects: [
extensionMap.style.reconfigure(EditorView.theme(mergedCmStyle)),
],
})
once.current = true
}