Open galloppinggryphon opened 1 month ago
This is an interesting issue.
I think in the general case, where manually handling conversions between different Slate editors isn't feasible, the best solution would be to prevent Slate fragments from being copied between incompatible editors in the first place.
In addition to the application/x-slate-fragment
data type on the clipboard, Slate editors also encode the fragment in the text/html
data using a data-slate-fragment
HTML attribute on the first copied element.
withReact
already has a clipboardFragmentKey
option that can be used to customise application/x-slate-fragment
, but there's currently no way of customising data-slate-fragment
.
@galloppinggryphon Would you be able to make a PR on Slate such that the clipboardFragmentKey
option is used for generating the HTML attribute name? You'll need to modify with-react.ts
and dom.ts
to refactor all hardcoded occurrences of data-slate-fragment
.
To minimise the risk of breaking apps' custom logic, I think the defaults should remain the same. This might look like (simplified):
// Change the default from 'x-slate-fragment' to 'slate-fragment'
export const withReact = (editor: T, clipboardFormatKey = 'slate-fragment') => {
const dataType = `application/x-${clipboardFormatKey}` // default 'application/x-slate-fragment'
const attributeName = `data-${clipboardFormatKey}` // default 'data-slate-fragment'
// ...
}
Once this logic is in place, we can modify packages/core/src/client/plugins/react/createReactPlugin.ts
in Plate to make clipboardFormatKey
customisable, perhaps through a convenient prop on the <Plate>
component.
Description
The problem
Copy-pasting content between incompatible implementations of Slate-based editors creates an exception, or put another way, it seems Plate cannot handle elements with an unfamiliar/unrecognized
type
attribute value.Let's say you have a Plate editor and another Slate editor in another software package - in my case Keystone. These editors both use the Slate data format (
application/x-slate-fragment
), but element type is coded differently. For example:paragraph
instead ofp
heading
along withlevel: n
instead ofh1
,h2
, etccode
instead ofcode_block
And so on. While the pasting operation is successful (text appears unformatted), the
type
survived in the underlying AST. It's when you want to perform operations on these elements -- for example reformatting headers -- that you get a big nasty error:A screenshot of the error is attached. Evidently, the problem is in the
DropdownMenu
component (called byTurnIntoDropdownMenu
) that I installed via the CLI. I've included the code for both. The Plate installation is close to vanilla, as I'm just getting familiar with Plate.Code
Here's some incompatible markup (after pasting rich text into Plate) that will trigger an error:
dropdown-menu.tsx
```ts 'use client' import * as React from 'react' import { useCallback, useState } from 'react' import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' import { cn, createPrimitiveElement, withCn, withProps, withRef, withVariants, } from '@udecode/cn' import { cva } from 'class-variance-authority' import { Icons } from '../components/icons' export const DropdownMenu = DropdownMenuPrimitive.Root export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger export const DropdownMenuGroup = DropdownMenuPrimitive.Group export const DropdownMenuPortal = DropdownMenuPrimitive.Portal export const DropdownMenuSub = DropdownMenuPrimitive.Sub export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup export const DropdownMenuSubTrigger = withRef< typeof DropdownMenuPrimitive.SubTrigger, { inset?: boolean; } >( ( { children, className, inset, ...props }, ref ) => (turn-into-dropdown-menu.tsx
```ts import React from 'react' import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu' import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote' import { collapseSelection, focusEditor, getNodeEntries, isBlock, toggleNodeType, useEditorRef, useEditorSelector, } from '@udecode/plate-common' import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading' import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph' import { Icons } from '../components/icons' import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, useOpenState, } from './dropdown-menu' import { ToolbarButton } from './toolbar' import { KEY_LIST_STYLE_TYPE } from '@udecode/plate-indent-list' import { unwrapList } from '@udecode/plate-list' const items = [ { description: 'Paragraph', icon: Icons.paragraph, label: 'Paragraph', value: ELEMENT_PARAGRAPH, }, { description: 'Heading 1', icon: Icons.h1, label: 'Heading 1', value: ELEMENT_H1, }, { description: 'Heading 2', icon: Icons.h2, label: 'Heading 2', value: ELEMENT_H2, }, { description: 'Heading 3', icon: Icons.h3, label: 'Heading 3', value: ELEMENT_H3, }, { description: 'Quote (⌘+⇧+.)', icon: Icons.blockquote, label: 'Quote', value: ELEMENT_BLOCKQUOTE, }, // { // value: 'ul', // label: 'Bulleted list', // description: 'Bulleted list', // icon: Icons.ul, // }, // { // value: 'ol', // label: 'Numbered list', // description: 'Numbered list', // icon: Icons.ol, // }, ] const defaultItem = items.find( ( item ) => item.value === ELEMENT_PARAGRAPH )! export function TurnIntoDropdownMenu( props: DropdownMenuProps ) { const value: string = useEditorSelector( ( editor ) => { let initialNodeType: string = ELEMENT_PARAGRAPH let allNodesMatchInitialNodeType = false const codeBlockEntries = getNodeEntries( editor, { match: ( n ) => isBlock( editor, n ), mode: 'highest', } ) const nodes = Array.from( codeBlockEntries ) if ( nodes.length > 0 ) { initialNodeType = nodes[ 0 ][ 0 ].type as string allNodesMatchInitialNodeType = nodes.every( ( [ node ] ) => { const type: string = ( node?.type as string ) || ELEMENT_PARAGRAPH return type === initialNodeType } ) } return allNodesMatchInitialNodeType ? initialNodeType : ELEMENT_PARAGRAPH }, [] ) const editor = useEditorRef() const openState = useOpenState() const selectedItem = items.find( ( item ) => item.value === value ) ?? defaultItem const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem return (Workaround
For my own purposes, I created a small plugin to convert Keystone Slate JSON to Plate JSON. This fixes the problem and also retains formatting.
slate-deserializer-plugin.ts
```ts import { createPluginFactory, EElement, } from '@udecode/plate-common' export const KEY_DESERIALIZE_SLATE = 'slateDeserializer' function toPlateJson( data: EElementReproduction URL
No response
Reproduction steps
Plate version
36.2.1
Slate React version
0.107.1
Screenshots
Logs
No response
Browsers
Firefox
Funding