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