refactor(content-editor): enhance mode handling and integrate allowed modes across components

This commit is contained in:
mlogclub
2026-04-18 13:05:39 +08:00
parent 797bee65f9
commit d9c4b669b2
5 changed files with 57 additions and 11 deletions
@@ -2,10 +2,14 @@
import { cn } from "@/lib/utils"
import type { ContentMode } from "./types"
import {
CONTENT_MODE_OPTIONS,
type ContentMode,
} from "./types"
type EditorModeSwitchProps = {
value: ContentMode
allowedModes?: ReadonlyArray<ContentMode>
disabled?: boolean
onChange: (nextMode: ContentMode) => void
}
@@ -17,13 +21,20 @@ const MODE_OPTIONS: Array<{ value: ContentMode; label: string }> = [
export function EditorModeSwitch({
value,
allowedModes = CONTENT_MODE_OPTIONS,
disabled = false,
onChange,
}: EditorModeSwitchProps) {
const options = MODE_OPTIONS.filter((option) => allowedModes.includes(option.value))
if (options.length <= 1) {
return null
}
return (
<div className="mx-0.5 rounded-[3px] border border-border/80 bg-transparent p-0">
<div className="flex items-center">
{MODE_OPTIONS.map((option) => {
{options.map((option) => {
const active = option.value === value
return (
<button
@@ -46,6 +46,7 @@ type HtmlEditorProps = {
value: string
onChange: (nextValue: string) => void
mode: ContentMode
allowedModes: ReadonlyArray<ContentMode>
onModeChange: (nextMode: ContentMode) => void
fullscreen: boolean
onToggleFullscreen: () => void
@@ -61,6 +62,7 @@ export const HtmlEditor = forwardRef<HtmlEditorRef, HtmlEditorProps>(
value,
onChange,
mode,
allowedModes,
onModeChange,
fullscreen,
onToggleFullscreen,
@@ -180,12 +182,12 @@ export const HtmlEditor = forwardRef<HtmlEditorRef, HtmlEditorProps>(
content: (
<EditorModeSwitch
value={mode}
allowedModes={allowedModes}
disabled={disabled}
onChange={onModeChange}
/>
),
},
{ key: "separator-mode", type: "separator" },
{
key: "bold",
label: "粗体",
@@ -327,6 +329,10 @@ export const HtmlEditor = forwardRef<HtmlEditorRef, HtmlEditorProps>(
},
]
if (allowedModes.length > 1) {
actions.splice(1, 0, { key: "separator-mode", type: "separator" })
}
if (!editor) {
return null
}
+31 -7
View File
@@ -8,7 +8,12 @@ import { cn } from "@/lib/utils"
import { htmlToMarkdown, markdownToHtml } from "./convert"
import { HtmlEditor } from "./html-editor"
import { MarkdownEditor } from "./markdown-editor"
import type { ContentMode, ContentValue, UploadImageHandler } from "./types"
import {
CONTENT_MODE_OPTIONS,
type ContentMode,
type ContentValue,
type UploadImageHandler,
} from "./types"
type ContentEditorProps = {
value: ContentValue
@@ -17,6 +22,7 @@ type ContentEditorProps = {
disabled?: boolean
onUploadImage?: UploadImageHandler
height?: number | string
allowedModes?: ReadonlyArray<ContentMode>
}
function normalizeHeight(height?: number | string) {
@@ -47,10 +53,15 @@ export function ContentEditor({
disabled = false,
onUploadImage,
height,
allowedModes = CONTENT_MODE_OPTIONS,
}: ContentEditorProps) {
const editorHeight = normalizeHeight(height)
const [fullscreen, setFullscreen] = useState(false)
const [mounted, setMounted] = useState(false)
const normalizedAllowedModes = allowedModes.length > 0 ? allowedModes : CONTENT_MODE_OPTIONS
const activeMode = normalizedAllowedModes.includes(value.mode)
? value.mode
: normalizedAllowedModes[0]
useEffect(() => {
setMounted(true)
@@ -79,7 +90,12 @@ export function ContentEditor({
const handleModeChange = useCallback(
(nextMode: ContentMode) => {
if (disabled || nextMode === value.mode) {
if (
disabled ||
normalizedAllowedModes.length <= 1 ||
nextMode === activeMode ||
!normalizedAllowedModes.includes(nextMode)
) {
return
}
const currentText = value.raw.trim()
@@ -97,12 +113,18 @@ export function ContentEditor({
onChange({
mode: nextMode,
raw: convertContent(value.mode, value.raw),
raw: convertContent(activeMode, value.raw),
})
},
[disabled, onChange, value.mode, value.raw]
[activeMode, disabled, normalizedAllowedModes, onChange, value.raw]
)
useEffect(() => {
if (value.mode !== activeMode) {
onChange({ mode: activeMode, raw: value.raw })
}
}, [activeMode, onChange, value.mode, value.raw])
const content = (
<div
className={cn(
@@ -110,11 +132,12 @@ export function ContentEditor({
fullscreen && "fixed inset-0 z-[10000] overflow-hidden bg-background p-4"
)}
>
{value.mode === "markdown" ? (
{activeMode === "markdown" ? (
<MarkdownEditor
value={value.raw}
onChange={(nextRaw) => onChange({ mode: "markdown", raw: nextRaw })}
mode={value.mode}
mode={activeMode}
allowedModes={normalizedAllowedModes}
onModeChange={handleModeChange}
fullscreen={fullscreen}
onToggleFullscreen={() => setFullscreen((current) => !current)}
@@ -127,7 +150,8 @@ export function ContentEditor({
<HtmlEditor
value={value.raw}
onChange={(nextRaw) => onChange({ mode: "html", raw: nextRaw })}
mode={value.mode}
mode={activeMode}
allowedModes={normalizedAllowedModes}
onModeChange={handleModeChange}
fullscreen={fullscreen}
onToggleFullscreen={() => setFullscreen((current) => !current)}
@@ -24,6 +24,7 @@ type MarkdownEditorProps = {
value: string
onChange: (nextValue: string) => void
mode: ContentMode
allowedModes: ReadonlyArray<ContentMode>
onModeChange: (nextMode: ContentMode) => void
fullscreen: boolean
onToggleFullscreen: () => void
@@ -39,6 +40,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
value,
onChange,
mode,
allowedModes,
onModeChange,
fullscreen,
onToggleFullscreen,
@@ -57,6 +59,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
<EditorModeSwitch
key="mode-switch"
value={mode}
allowedModes={allowedModes}
disabled={disabled}
onChange={onModeChange}
/>,
@@ -73,7 +76,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
)}
</NormalToolbar>,
],
[disabled, fullscreen, mode, onModeChange, onToggleFullscreen]
[allowedModes, disabled, fullscreen, mode, onModeChange, onToggleFullscreen]
)
useImperativeHandle(ref, () => ({
@@ -2,6 +2,8 @@ import type { ComponentType, ReactNode } from "react"
export type ContentMode = "markdown" | "html"
export const CONTENT_MODE_OPTIONS = ["markdown", "html"] as const
export type ContentValue = {
mode: ContentMode
raw: string