update web to dashboard
This commit is contained in:
@@ -1,171 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
import {
|
||||
BoldIcon,
|
||||
CodeIcon,
|
||||
Heading1Icon,
|
||||
ItalicIcon,
|
||||
LinkIcon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
QuoteIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
import { EditorToolbar } from "./toolbar"
|
||||
import type { BaseEditorProps } from "./types"
|
||||
|
||||
export type MarkdownEditorProps = BaseEditorProps & {
|
||||
value: string
|
||||
onChange: (nextValue: string) => void
|
||||
rows?: number
|
||||
}
|
||||
|
||||
export function MarkdownEditor({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = ".",
|
||||
disabled = false,
|
||||
rows = 16,
|
||||
className,
|
||||
}: MarkdownEditorProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const handleWrapSelection = (prefix: string, suffix = prefix) => {
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea || disabled) {
|
||||
return
|
||||
}
|
||||
const start = textarea.selectionStart ?? 0
|
||||
const end = textarea.selectionEnd ?? 0
|
||||
const selected = value.slice(start, end)
|
||||
const next = `${value.slice(0, start)}${prefix}${selected}${suffix}${value.slice(end)}`
|
||||
onChange(next)
|
||||
requestAnimationFrame(() => {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(start + prefix.length, end + prefix.length)
|
||||
})
|
||||
}
|
||||
|
||||
const handleInsertLinePrefix = (prefix: string) => {
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea || disabled) {
|
||||
return
|
||||
}
|
||||
const start = textarea.selectionStart ?? 0
|
||||
const end = textarea.selectionEnd ?? 0
|
||||
const lineStart = value.lastIndexOf("\n", start - 1) + 1
|
||||
const lineEndRaw = value.indexOf("\n", end)
|
||||
const lineEnd = lineEndRaw === -1 ? value.length : lineEndRaw
|
||||
const selectedLines = value.slice(lineStart, lineEnd)
|
||||
const nextLines = selectedLines
|
||||
.split("\n")
|
||||
.map((line) => `${prefix}${line}`)
|
||||
.join("\n")
|
||||
const next = `${value.slice(0, lineStart)}${nextLines}${value.slice(lineEnd)}`
|
||||
onChange(next)
|
||||
requestAnimationFrame(() => {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(lineStart, lineStart + nextLines.length)
|
||||
})
|
||||
}
|
||||
|
||||
const handleInsertLink = () => {
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea || disabled) {
|
||||
return
|
||||
}
|
||||
const start = textarea.selectionStart ?? 0
|
||||
const end = textarea.selectionEnd ?? 0
|
||||
const selected = value.slice(start, end) || "链接文本"
|
||||
const markdown = `[${selected}](https://)`
|
||||
const next = `${value.slice(0, start)}${markdown}${value.slice(end)}`
|
||||
onChange(next)
|
||||
requestAnimationFrame(() => {
|
||||
textarea.focus()
|
||||
const urlStart = start + markdown.lastIndexOf("https://")
|
||||
textarea.setSelectionRange(urlStart, urlStart + "https://".length)
|
||||
})
|
||||
}
|
||||
|
||||
const toolbarActions = [
|
||||
{
|
||||
key: "heading1",
|
||||
label: "一级标题",
|
||||
icon: Heading1Icon,
|
||||
disabled,
|
||||
onClick: () => handleInsertLinePrefix("# "),
|
||||
},
|
||||
{ key: "separator-1", type: "separator" as const },
|
||||
{
|
||||
key: "bold",
|
||||
label: "粗体",
|
||||
icon: BoldIcon,
|
||||
disabled,
|
||||
onClick: () => handleWrapSelection("**"),
|
||||
},
|
||||
{
|
||||
key: "italic",
|
||||
label: "斜体",
|
||||
icon: ItalicIcon,
|
||||
disabled,
|
||||
onClick: () => handleWrapSelection("*"),
|
||||
},
|
||||
{
|
||||
key: "code",
|
||||
label: "行内代码",
|
||||
icon: CodeIcon,
|
||||
disabled,
|
||||
onClick: () => handleWrapSelection("`"),
|
||||
},
|
||||
{ key: "separator-2", type: "separator" as const },
|
||||
{
|
||||
key: "bulletList",
|
||||
label: "无序列表",
|
||||
icon: ListIcon,
|
||||
disabled,
|
||||
onClick: () => handleInsertLinePrefix("- "),
|
||||
},
|
||||
{
|
||||
key: "orderedList",
|
||||
label: "有序列表",
|
||||
icon: ListOrderedIcon,
|
||||
disabled,
|
||||
onClick: () => handleInsertLinePrefix("1. "),
|
||||
},
|
||||
{
|
||||
key: "blockquote",
|
||||
label: "引用",
|
||||
icon: QuoteIcon,
|
||||
disabled,
|
||||
onClick: () => handleInsertLinePrefix("> "),
|
||||
},
|
||||
{
|
||||
key: "link",
|
||||
label: "链接",
|
||||
icon: LinkIcon,
|
||||
disabled,
|
||||
onClick: handleInsertLink,
|
||||
},
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-background">
|
||||
<EditorToolbar actions={toolbarActions} />
|
||||
<div className="p-2">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
className={`min-h-64 max-h-96 resize-y border-0 px-2 py-2 text-sm leading-7 shadow-none focus-visible:ring-0 ${className ?? ""}`}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user