diff --git a/src/app/dashboard/[subdomain]/editor/post-editor.tsx b/src/app/dashboard/[subdomain]/editor/post-editor.tsx
index 5d4d7c03..35787d97 100644
--- a/src/app/dashboard/[subdomain]/editor/post-editor.tsx
+++ b/src/app/dashboard/[subdomain]/editor/post-editor.tsx
@@ -1,20 +1,8 @@
"use client"
-import { useDebounceEffect } from "ahooks"
-import type { Root } from "mdast"
import { nanoid } from "nanoid"
-import dynamic from "next/dynamic"
import { useParams, useRouter, useSearchParams } from "next/navigation"
-import {
- ChangeEvent,
- FC,
- memo,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react"
+import { ChangeEvent, FC, memo, useCallback, useEffect, useState } from "react"
import toast from "react-hot-toast"
import { shallow } from "zustand/shallow"
@@ -23,6 +11,7 @@ import { DateInput } from "@mantine/dates"
import { useQueryClient } from "@tanstack/react-query"
import { DashboardMain } from "~/components/dashboard/DashboardMain"
+import DualColumnEditor from "~/components/dashboard/DualColumnEditor"
import { OptionsButton } from "~/components/dashboard/OptionsButton"
import { PublishButton } from "~/components/dashboard/PublishButton"
import PublishedModal from "~/components/dashboard/PublishedModal"
@@ -35,8 +24,7 @@ import { useModalStack } from "~/components/ui/ModalStack"
import { Switch } from "~/components/ui/Switch"
import { TagInput } from "~/components/ui/TagInput"
import { UniLink } from "~/components/ui/UniLink"
-import { toolbarShortcuts, toolbars } from "~/editor"
-import { editorUpload } from "~/editor/Multimedia"
+import { toolbars } from "~/editor"
import {
Values,
initialEditorState,
@@ -45,7 +33,6 @@ import {
import { useGetState } from "~/hooks/useGetState"
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
import { useBeforeMounted } from "~/hooks/useSyncOnce"
-import { useUploadFile } from "~/hooks/useUploadFile"
import { showConfetti } from "~/lib/confetti"
import { RESERVED_TAGS } from "~/lib/constants"
import { getDefaultSlug } from "~/lib/default-slug"
@@ -56,7 +43,6 @@ import { getPageVisibility } from "~/lib/page-helpers"
import { delStorage, setStorage } from "~/lib/storage"
import { ExpandedNote, NoteType, PageVisibilityEnum } from "~/lib/types"
import { cn, pick } from "~/lib/utils"
-import { Rendered, renderPageContent } from "~/markdown"
import { checkPageSlug } from "~/models/page.model"
import {
useCreatePage,
@@ -67,30 +53,6 @@ import {
} from "~/queries/page"
import { useGetSite } from "~/queries/site"
-const DynamicCodeMirrorEditor = dynamic(
- () => import("~/components/ui/CodeMirror"),
- {
- ssr: false,
- loading: () => (
-
- Loading...
-
- ),
- },
-)
-
-const DynamicPageContent = dynamic(
- () => import("~/components/common/PageContent"),
- {
- ssr: false,
- loading: () => (
-
- Loading...
-
- ),
- },
-)
-
export default function PostEditor() {
const router = useRouter()
const queryClient = useQueryClient()
@@ -160,8 +122,6 @@ export default function PostEditor() {
}
}, [page.isSuccess, page.data])
- const uploadFile = useUploadFile()
-
// reset editor state when page changes
useBeforeMounted(() => {
useEditorState.setState({
@@ -392,27 +352,7 @@ export default function PostEditor() {
)
}, [page.data, subdomain, draftKey, site.data?.characterId])
- const [currentScrollArea, setCurrentScrollArea] = useState("")
const [view, setView] = useState()
- const [tree, setTree] = useState()
-
- // preview
-
- const [parsedContent, setParsedContent] = useState()
-
- useDebounceEffect(
- () => {
- const result = renderPageContent(values.content)
- setTree(result.tree)
- setParsedContent(result)
- },
- [values.content],
- {
- wait: 500,
- },
- )
-
- const previewRef = useRef(null)
// editor
const onCreateEditor = useCallback(
@@ -429,115 +369,6 @@ export default function PostEditor() {
[updateValue],
)
- const handleDropFile = useCallback(
- async (file: File) => {
- if (view) {
- editorUpload(file, view)
- }
- },
- [uploadFile, view],
- )
-
- const computedPosition = useCallback(() => {
- let previewChildNodes = previewRef.current?.childNodes[0]?.childNodes
- const editorElementList: number[] = []
- const previewElementList: number[] = []
- if (view?.state && previewChildNodes) {
- tree?.children.forEach((child, index) => {
- if (
- child.position &&
- previewChildNodes?.[index] &&
- (child as any).tagName !== "style"
- ) {
- if (child.position.start.line > view.state.doc.lines) return
- const line = view.state?.doc.line(child.position.start.line)
- const block = view.lineBlockAt(line.from)
- if (block) {
- editorElementList.push(block.top)
- previewElementList.push(
- (previewChildNodes[index] as HTMLElement).offsetTop,
- )
- }
- }
- })
- }
- return {
- editorElementList,
- previewElementList,
- }
- }, [view, tree])
-
- const onScroll = useCallback(
- (scrollTop: number, area: string) => {
- if (
- currentScrollArea === area &&
- previewRef.current?.parentElement &&
- view
- ) {
- const position = computedPosition()
-
- let selfElement
- let selfPosition
- let targetElement
- let targetPosition
- if (area === "preview") {
- selfElement = previewRef.current.parentElement
- selfPosition = position.previewElementList
- targetElement = view.scrollDOM
- targetPosition = position.editorElementList
- } else {
- selfElement = view.scrollDOM
- selfPosition = position.editorElementList
- targetElement = previewRef.current.parentElement
- targetPosition = position.previewElementList
- }
-
- let scrollElementIndex = 0
- for (let i = 0; i < selfPosition.length; i++) {
- if (scrollTop < selfPosition[i]) {
- scrollElementIndex = i - 1
- break
- }
- }
-
- // scroll to bottom
- if (scrollTop >= selfElement.scrollHeight - selfElement.clientHeight) {
- targetElement.scrollTop =
- targetElement.scrollHeight - targetElement.clientHeight
- return
- }
-
- // scroll to position
- if (scrollElementIndex >= 0) {
- let ratio =
- (scrollTop - selfPosition[scrollElementIndex]) /
- (selfPosition[scrollElementIndex + 1] -
- selfPosition[scrollElementIndex])
- targetElement.scrollTop =
- ratio *
- (targetPosition[scrollElementIndex + 1] -
- targetPosition[scrollElementIndex]) +
- targetPosition[scrollElementIndex]
- }
- }
- },
- [view, computedPosition, currentScrollArea],
- )
-
- const onEditorScroll = useCallback(
- (scrollTop: number) => {
- onScroll(scrollTop, "editor")
- },
- [onScroll],
- )
-
- const onPreviewScroll = useCallback(
- (scrollTop: number) => {
- onScroll(scrollTop, "preview")
- },
- [onScroll],
- )
-
const onPreviewButtonClick = useCallback(() => {
window.open(
`/site/${subdomain}/preview/${draftKey.replace(
@@ -568,18 +399,6 @@ export default function PostEditor() {
}
}, [draftKey, site.data?.characterId])
- const cmStyle = useMemo(
- () => ({
- ".cm-scroller": {
- padding: "0 1.25rem",
- },
- ".cm-content": {
- paddingBottom: "600px",
- },
- }),
- [],
- )
-
return (
<>
@@ -686,52 +505,12 @@ export default function PostEditor() {
placeholder={t("Title goes here...") || ""}
/>
-
- {!(isMobileLayout && isRendering) && (
-
{
- setCurrentScrollArea("editor")
- }}
- className={cn(
- "h-full flex-1",
- isRendering ? "border-r" : "",
- )}
- shortcuts={toolbarShortcuts}
- />
- )}
- {!isMobileLayout && (
-
-
setIsRendering(!isRendering)}
- >
- {isRendering ? (
-
- ) : (
-
- )}
-
-
- )}
- {isRendering && (
- {
- setCurrentScrollArea("preview")
- }}
- />
- )}
+
+
{!isMobileLayout && (
diff --git a/src/components/dashboard/DualColumnEditor.tsx b/src/components/dashboard/DualColumnEditor.tsx
new file mode 100644
index 00000000..af39273a
--- /dev/null
+++ b/src/components/dashboard/DualColumnEditor.tsx
@@ -0,0 +1,264 @@
+import { useDebounceEffect } from "ahooks"
+import type { Root } from "mdast"
+import dynamic from "next/dynamic"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+
+import { EditorView } from "@codemirror/view"
+
+import { toolbarShortcuts } from "~/editor"
+import { editorUpload } from "~/editor/Multimedia"
+import { useIsMobileLayout } from "~/hooks/useMobileLayout"
+import { useUploadFile } from "~/hooks/useUploadFile"
+import { useTranslation } from "~/lib/i18n/client"
+import { cn } from "~/lib/utils"
+import { Rendered, renderPageContent } from "~/markdown"
+
+const DynamicCodeMirrorEditor = dynamic(
+ () => import("~/components/ui/CodeMirror"),
+ {
+ ssr: false,
+ loading: () => (
+
+ Loading...
+
+ ),
+ },
+)
+
+const DynamicPageContent = dynamic(
+ () => import("~/components/common/PageContent"),
+ {
+ ssr: false,
+ loading: () => (
+
+ Loading...
+
+ ),
+ },
+)
+
+export default function DualColumnEditor({
+ initialContent,
+ onChange,
+ onCreateEditor,
+}: {
+ initialContent: string
+ onChange: (value: string) => void
+ onCreateEditor: (view: EditorView) => void
+}) {
+ const isMobileLayout = useIsMobileLayout()
+ const [isRendering, setIsRendering] = useState(!isMobileLayout)
+ const { t } = useTranslation("dashboard")
+ const uploadFile = useUploadFile()
+
+ const [currentScrollArea, setCurrentScrollArea] = useState("")
+ const [view, setView] = useState()
+ const [tree, setTree] = useState()
+ const [values, setValues] = useState("")
+
+ // preview
+ const [parsedContent, setParsedContent] = useState()
+
+ useEffect(() => {
+ setValues(initialContent)
+ }, [initialContent])
+
+ useDebounceEffect(
+ () => {
+ const result = renderPageContent(values)
+ setTree(result.tree)
+ setParsedContent(result)
+ },
+ [values],
+ {
+ wait: 500,
+ },
+ )
+
+ const previewRef = useRef(null)
+
+ // editor
+ const onCreateEditorInside = useCallback(
+ (view: EditorView) => {
+ setView?.(view)
+ onCreateEditor?.(view)
+ },
+ [setView],
+ )
+
+ const handleDropFile = useCallback(
+ async (file: File) => {
+ if (view) {
+ editorUpload(file, view)
+ }
+ },
+ [uploadFile, view],
+ )
+
+ const computedPosition = useCallback(() => {
+ let previewChildNodes = previewRef.current?.childNodes[0]?.childNodes
+ const editorElementList: number[] = []
+ const previewElementList: number[] = []
+ if (view?.state && previewChildNodes) {
+ tree?.children.forEach((child, index) => {
+ if (
+ child.position &&
+ previewChildNodes?.[index] &&
+ (child as any).tagName !== "style"
+ ) {
+ if (child.position.start.line > view.state.doc.lines) return
+ const line = view.state?.doc.line(child.position.start.line)
+ const block = view.lineBlockAt(line.from)
+ if (block) {
+ editorElementList.push(block.top)
+ previewElementList.push(
+ (previewChildNodes[index] as HTMLElement).offsetTop,
+ )
+ }
+ }
+ })
+ }
+ return {
+ editorElementList,
+ previewElementList,
+ }
+ }, [view, tree])
+
+ const onScroll = useCallback(
+ (scrollTop: number, area: string) => {
+ if (
+ currentScrollArea === area &&
+ previewRef.current?.parentElement &&
+ view
+ ) {
+ const position = computedPosition()
+
+ let selfElement
+ let selfPosition
+ let targetElement
+ let targetPosition
+ if (area === "preview") {
+ selfElement = previewRef.current.parentElement
+ selfPosition = position.previewElementList
+ targetElement = view.scrollDOM
+ targetPosition = position.editorElementList
+ } else {
+ selfElement = view.scrollDOM
+ selfPosition = position.editorElementList
+ targetElement = previewRef.current.parentElement
+ targetPosition = position.previewElementList
+ }
+
+ let scrollElementIndex = 0
+ for (let i = 0; i < selfPosition.length; i++) {
+ if (scrollTop < selfPosition[i]) {
+ scrollElementIndex = i - 1
+ break
+ }
+ }
+
+ // scroll to bottom
+ if (scrollTop >= selfElement.scrollHeight - selfElement.clientHeight) {
+ targetElement.scrollTop =
+ targetElement.scrollHeight - targetElement.clientHeight
+ return
+ }
+
+ // scroll to position
+ if (scrollElementIndex >= 0) {
+ let ratio =
+ (scrollTop - selfPosition[scrollElementIndex]) /
+ (selfPosition[scrollElementIndex + 1] -
+ selfPosition[scrollElementIndex])
+ targetElement.scrollTop =
+ ratio *
+ (targetPosition[scrollElementIndex + 1] -
+ targetPosition[scrollElementIndex]) +
+ targetPosition[scrollElementIndex]
+ }
+ }
+ },
+ [view, computedPosition, currentScrollArea],
+ )
+
+ const onEditorScroll = useCallback(
+ (scrollTop: number) => {
+ onScroll(scrollTop, "editor")
+ },
+ [onScroll],
+ )
+
+ const onPreviewScroll = useCallback(
+ (scrollTop: number) => {
+ onScroll(scrollTop, "preview")
+ },
+ [onScroll],
+ )
+
+ const cmStyle = useMemo(
+ () => ({
+ ".cm-scroller": {
+ padding: "0 1.25rem",
+ },
+ ".cm-content": {
+ paddingBottom: "600px",
+ },
+ }),
+ [],
+ )
+
+ const onChangeInside = useCallback(
+ (value: string) => {
+ setValues(value)
+ onChange(value)
+ },
+ [setValues, onChange],
+ )
+
+ return (
+
+ {!(isMobileLayout && isRendering) && (
+
{
+ setCurrentScrollArea("editor")
+ }}
+ className={cn("h-full flex-1", isRendering ? "border-r" : "")}
+ shortcuts={toolbarShortcuts}
+ />
+ )}
+ {!isMobileLayout && (
+
+
setIsRendering(!isRendering)}
+ >
+ {isRendering ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+ {isRendering && (
+ {
+ setCurrentScrollArea("preview")
+ }}
+ />
+ )}
+
+ )
+}