diff --git a/src/app/dashboard/[subdomain]/editor/page.tsx b/src/app/dashboard/[subdomain]/editor/page.tsx index 0d5ab838..41dd39e0 100644 --- a/src/app/dashboard/[subdomain]/editor/page.tsx +++ b/src/app/dashboard/[subdomain]/editor/page.tsx @@ -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 ( <> @@ -579,11 +591,15 @@ export default function SubdomainEditor() { {isMobileLayout ? ( !isRendering ? ( { setCurrentScrollArea("editor") diff --git a/src/components/common/CommentInput.tsx b/src/components/common/CommentInput.tsx index 69a467b6..43cf9f05 100644 --- a/src/components/common/CommentInput.tsx +++ b/src/components/common/CommentInput.tsx @@ -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() + 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( + () => ( + + ), + [account?.character, t, form], + ) return (
- { + 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} />
@@ -107,12 +144,26 @@ export const CommentInput: React.FC<{ - 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()) + }) + }} /> diff --git a/src/components/ui/CodeMirror.tsx b/src/components/ui/CodeMirror.tsx index 5c3c1e7b..7d140380 100644 --- a/src/components/ui/CodeMirror.tsx +++ b/src/components/ui/CodeMirror.tsx @@ -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 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 ( - }> + + ) + } + > ) @@ -64,13 +79,11 @@ const LazyCodeMirrorEditor = forwardRef< const [loading, setLoading] = useState(true) const editorElementRef = useRef(null) const isUnmounted = useIsUnmounted() - const { t } = useTranslation("dashboard") - const isMobileLayout = useIsMobileLayout() const [cmEditor, setCmEditor] = useState(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<
- {loading && } + {loading && + (props.LoadingComponent ? ( + createElement(props.LoadingComponent) + ) : ( + + ))} ) }) diff --git a/src/editor/Multimedia.tsx b/src/editor/Multimedia.tsx index cced7908..d006a00b 100644 --- a/src/editor/Multimedia.tsx +++ b/src/editor/Multimedia.tsx @@ -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...") diff --git a/src/editor/helper.ts b/src/editor/helper.ts new file mode 100644 index 00000000..150744fb --- /dev/null +++ b/src/editor/helper.ts @@ -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() +} diff --git a/src/editor/index.tsx b/src/editor/index.tsx index dec5955a..5da42bf8 100644 --- a/src/editor/index.tsx +++ b/src/editor/index.tsx @@ -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" diff --git a/src/hooks/useCodemirrorTheme.ts b/src/hooks/useCodemirrorTheme.ts index da7ab439..67ca2eee 100644 --- a/src/hooks/useCodemirrorTheme.ts +++ b/src/hooks/useCodemirrorTheme.ts @@ -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 + +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 }