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 { 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user