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 "~/components/dashboard/toolbars"
import { editorUpload } from "~/components/dashboard/toolbars/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,
isRendering,
setIsRendering,
}: {
initialContent: string
onChange: (value: string) => void
onCreateEditor: (view: EditorView) => void
isRendering: boolean
setIsRendering: (value: boolean) => void
}) {
const isMobileLayout = useIsMobileLayout()
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")
}}
/>
)}
)
}