我用 npx @udecode/plate-ui@latest add image-element
添加了Image Element 组件
这添加了标题、媒体弹出框和可调整大小的组件。
在 Platejs 文档中,当我添加图像时一切正常
图1
图2
图3
视频
https://github.com/udecode/plate/assets/163764831/51b85949-253a-4663-b239-0062928e186e
但是在我的编辑器上,当我通过单击固定工具栏中的按钮添加图像(与 Platejs 文档中相同)时,它不会出现,所有内容都会消失并留下空白页面
图1
图2
图3
视频
https://github.com/udecode/plate/assets/163764831/fddeb502-0afa-42af-81c7-55e9a2c3fb96
我收到错误
固定工具栏按钮.tsx
import {
MARK_BOLD,
MARK_CODE,
MARK_ITALIC,
MARK_STRIKETHROUGH,
MARK_UNDERLINE,
} from '@udecode/plate-basic-marks';
import { useEditorReadOnly } from '@udecode/plate-common';
import { Icons } from '@/components/plate-ui/icons';
import { InsertDropdownMenu } from './insert-dropdown-menu';
import { MarkToolbarButton } from './mark-toolbar-button';
import { ModeDropdownMenu } from './mode-dropdown-menu';
import { ToolbarGroup } from './toolbar';
import { TurnIntoDropdownMenu } from './turn-into-dropdown-menu';
import { AlignDropdownMenu } from '@/components/plate-ui/align-dropdown-menu';
import { MediaToolbarButton } from '@/components/plate-ui/media-toolbar-button';
import { ELEMENT_IMAGE } from '@udecode/plate-media';
export function FixedToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<div className="w-full overflow-hidden">
<div
className="flex flex-wrap"
style={{
transform: 'translateX(calc(-1px))',
}}
>
{!readOnly && (
<>
<ToolbarGroup noSeparator>
<InsertDropdownMenu />
<TurnIntoDropdownMenu />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton tooltip="Bold (⌘+B)" nodeType={MARK_BOLD}>
<Icons.bold />
</MarkToolbarButton>
<MarkToolbarButton tooltip="Italic (⌘+I)" nodeType={MARK_ITALIC}>
<Icons.italic />
</MarkToolbarButton>
<MarkToolbarButton
tooltip="Underline (⌘+U)"
nodeType={MARK_UNDERLINE}
>
<Icons.underline />
</MarkToolbarButton>
<MarkToolbarButton
tooltip="Strikethrough (⌘+⇧+M)"
nodeType={MARK_STRIKETHROUGH}
>
<Icons.strikethrough />
</MarkToolbarButton>
<MarkToolbarButton tooltip="Code (⌘+E)" nodeType={MARK_CODE}>
<Icons.code />
</MarkToolbarButton>
<AlignDropdownMenu />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton nodeType={ELEMENT_IMAGE} />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup noSeparator>
<ModeDropdownMenu />
</ToolbarGroup>
</div>
</div>
);
}
标题.tsx
import { cn, withCn, withVariants } from '@udecode/cn';
import {
Caption as CaptionPrimitive,
CaptionTextarea as CaptionTextareaPrimitive,
} from '@udecode/plate-caption';
import { cva } from 'class-variance-authority';
const captionVariants = cva('max-w-full', {
variants: {
align: {
left: 'mr-auto',
center: 'mx-auto',
right: 'ml-auto',
},
},
defaultVariants: {
align: 'center',
},
});
export const Caption = withVariants(CaptionPrimitive, captionVariants, [
'align',
]);
export const CaptionTextarea = withCn(
CaptionTextareaPrimitive,
cn(
'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',
'focus:outline-none focus:[&::placeholder]:opacity-0',
'text-center print:placeholder:text-transparent'
)
);
media-popover.tsx
import React, { useEffect } from 'react';
import {
isSelectionExpanded,
useEditorSelector,
useElement,
useRemoveNodeButton,
} from '@udecode/plate-common';
import {
floatingMediaActions,
FloatingMedia as FloatingMediaPrimitive,
useFloatingMediaSelectors,
} from '@udecode/plate-media';
import { useReadOnly, useSelected } from 'slate-react';
import { Icons } from '@/components/plate-ui/icons';
import { Button, buttonVariants } from './button';
import { inputVariants } from './input';
import { Popover, PopoverAnchor, PopoverContent } from './popover';
import { Separator } from './separator';
export interface MediaPopoverProps {
pluginKey?: string;
children: React.ReactNode;
}
export function MediaPopover({ pluginKey, children }: MediaPopoverProps) {
const readOnly = useReadOnly();
const selected = useSelected();
const selectionCollapsed = useEditorSelector(
(editor) => !isSelectionExpanded(editor),
[]
);
const isOpen = !readOnly && selected && selectionCollapsed;
const isEditing = useFloatingMediaSelectors().isEditing();
useEffect(() => {
if (!isOpen && isEditing) {
floatingMediaActions.isEditing(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
const element = useElement();
const { props: buttonProps } = useRemoveNodeButton({ element });
if (readOnly) return <>{children}</>;
return (
<Popover open={isOpen} modal={false}>
<PopoverAnchor>{children}</PopoverAnchor>
<PopoverContent
className="w-auto p-1"
onOpenAutoFocus={(e) => e.preventDefault()}
>
{isEditing ? (
<div className="flex w-[330px] flex-col">
<div className="flex items-center">
<div className="flex items-center pl-3 text-muted-foreground">
<Icons.link className="size-4" />
</div>
<FloatingMediaPrimitive.UrlInput
className={inputVariants({ variant: 'ghost', h: 'sm' })}
placeholder="Paste the embed link..."
options={{
pluginKey,
}}
/>
</div>
</div>
) : (
<div className="box-content flex h-9 items-center gap-1">
<FloatingMediaPrimitive.EditButton
className={buttonVariants({ variant: 'ghost', size: 'sm' })}
>
Edit link
</FloatingMediaPrimitive.EditButton>
<Separator orientation="vertical" className="my-1" />
<Button variant="ghost" size="sms" {...buttonProps}>
<Icons.delete className="size-4" />
</Button>
</div>
)}
</PopoverContent>
</Popover>
);
}
可调整大小.tsx
import { cn, withRef, withVariants } from '@udecode/cn';
import {
Resizable as ResizablePrimitive,
ResizeHandle as ResizeHandlePrimitive,
} from '@udecode/plate-resizable';
import { cva } from 'class-variance-authority';
export const mediaResizeHandleVariants = cva(
cn(
'top-0 flex w-6 select-none flex-col justify-center',
"after:flex after:h-16 after:w-[3px] after:rounded-[6px] after:bg-ring after:opacity-0 after:content-['_'] group-hover:after:opacity-100"
),
{
variants: {
direction: {
left: '-left-3 -ml-3 pl-3',
right: '-right-3 -mr-3 items-end pr-3',
},
},
}
);
const resizeHandleVariants = cva(cn('absolute z-40'), {
variants: {
direction: {
left: 'h-full cursor-col-resize',
right: 'h-full cursor-col-resize',
top: 'w-full cursor-row-resize',
bottom: 'w-full cursor-row-resize',
},
},
});
const ResizeHandleVariants = withVariants(
ResizeHandlePrimitive,
resizeHandleVariants,
['direction']
);
export const ResizeHandle = withRef<typeof ResizeHandlePrimitive>(
(props, ref) => (
<ResizeHandleVariants
ref={ref}
direction={props.options?.direction}
{...props}
/>
)
);
const resizableVariants = cva('', {
variants: {
align: {
left: 'mr-auto',
center: 'mx-auto',
right: 'ml-auto',
},
},
});
export const Resizable = withVariants(ResizablePrimitive, resizableVariants, [
'align',
]);
插件.ts
import { createPlugins } from '@udecode/plate-common';
import { withDraggables } from '@/components/plate-ui/with-draggables';
import { withPlaceholders } from '@/components/plate-ui/placeholder';
import {
createImagePlugin,
createMediaEmbedPlugin,
ELEMENT_IMAGE,
} from '@udecode/plate-media';
import { ImageElement } from '@/components/plate-ui/image-element';
export const plugins = createPlugins(
[
createImagePlugin(),
createMediaEmbedPlugin(),
],
{
components: withDraggables(
withPlaceholders({
[ELEMENT_IMAGE]: ImageElement,
})
),
}
);
我的编辑
import { Plate, Value } from '@udecode/plate-common';
import { Editor } from '@/components/plate-ui/editor';
import { useRef, useState } from 'react';
import { cn } from '@udecode/cn';
import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { plugins } from '@/lib/plate/plugins';
import { CursorOverlay } from '@/components/plate-ui/cursor-overlay';
import { FixedToolbar } from '@/components/plate-ui/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/plate-ui/fixed-toolbar-buttons';
import { FloatingToolbar } from '@/components/plate-ui/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/plate-ui/floating-toolbar-buttons';
import { TooltipProvider } from '@radix-ui/react-tooltip';
const initialValue = [
{
id: '1',
type: ELEMENT_PARAGRAPH,
children: [{ text: 'Hello, World!' }],
},
{
id: '2',
type: ELEMENT_PARAGRAPH,
children: [{ text: 'Hello, test!' }],
},
];
export function CreateProject(){
const [contentJson, setContentJson] = useState<Value>(initialValue);
const containerRef = useRef(null);
return (
<div className='h-[60vh] w-[60%] mx-auto mt-20'>
<TooltipProvider>
<DndProvider backend={HTML5Backend}>
<div className="relative">
<Plate
plugins={plugins}
initialValue={initialValue}
onChange={(newValue) => {
setContentJson(newValue);
}}
>
<div
ref={containerRef}
className={cn(
'relative',
// Block selection
'[&_.slate-start-area-left]:!w-[64px] [&_.slate-start-area-right]:!w-[64px] [&_.slate-start-area-top]:!h-4'
)}
>
{/* <TooltipProvider> */}
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
{/* </TooltipProvider> */}
<Editor
// className="px-[96px] py-16"
className="px-[50px] py-16"
autoFocus
focusRing={false}
variant="ghost"
size="md"
/>
{/* <TooltipProvider> */}
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
{/* </TooltipProvider> */}
<CursorOverlay containerRef={containerRef} />
</div>
</Plate>
</div>
</DndProvider>
</TooltipProvider>
</div>
)
}
我有一个预览部分,我将 json 转换为 html
useEffect(() => {
const element = createElement("div", null,
contentJson.map((element) => {
let className:string="";
switch (element.type){
case "p":
className += " text-lg"
break;
case "h1":
className += " text-4xl font-bold mt-0 mb-1"
break;
case "h2":
className += " text-2xl font-semibold mb-px mt-[1.4em]"
break;
case "h3":
className += " text-xl font-semibold mb-px mt-[1em]"
break;
}
if (element.align) {
switch (element.align){
case "center":
className += " text-center"
break;
case "left":
className += " text-left"
break;
case "right":
className += " text-right"
break;
}
}
return (
createElement(element.type, { className }, element.children.map((text) => {
let className:string="";
if (text.bold){ className += " font-bold" }
if (text.italic){ className += " italic" }
if (text.underline){ className += " underline" }
if (text.strikethrough){ className += " line-through" }
return (
createElement("span", { className }, text.text as string)
)
}))
)
})
)
setContent(element)
}, [contentJson])
我使用CreateElement创建html标签,因此当它是图像(element.type)时,它会创建2个img标签,这会导致问题。
我变了
return (
createElement(element.type, { className }, element.children.map((text) => {
let className:string="";
if (text.bold){ className += " font-bold" }
if (text.italic){ className += " italic" }
if (text.underline){ className += " underline" }
if (text.strikethrough){ className += " line-through" }
return (
createElement("span", { className }, text.text as string)
)
}))
)
到
return (
element.type === "img" ?
createElement("img", { className, src: element.url }, null)
:
createElement(element.type, { className }, element.children.map((text) => {
let className:string="";
if (text.bold){ className += " font-bold" }
if (text.italic){ className += " italic" }
if (text.underline){ className += " underline" }
if (text.strikethrough){ className += " line-through" }
return (
createElement("span", { className }, text.text as string)
)
}))
)
只需要为图像添加类