feat: support comment cm
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
55df59877e
commit
38ecf47f8f
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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...")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue