refactor(content-editor): enhance mode handling and integrate allowed modes across components
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user