Skip to content

set x-slate-fragment in PlateView #4431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/platejs-core-minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@platejs/core": patch
---

Added copy functionality to **PlateStatic** component. When users copy content from a static Plate editor, the selection is now properly serialized with the `x-slate-fragment` data format, enabling seamless paste operations into other Slate editors while preserving the rich structure of the content.

- Added `onCopy` handler to PlateStatic that sets clipboard data in three formats: Slate fragment, HTML, and plain text
- Added `setFragmentDataStatic` utility to handle copy operations for static editors
- Added `getSelectedDomBlocks` utility to extract selected DOM elements with Slate metadata
- Added `getPlainText` utility to recursively extract plain text from DOM nodes
7 changes: 7 additions & 0 deletions apps/www/src/app/dev/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function DevLayout(props: { children: React.ReactNode }) {
return (
<>
<main>{props.children}</main>
</>
);
}
41 changes: 41 additions & 0 deletions apps/www/src/app/dev/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useEffect, useState } from 'react';

import { usePlateViewEditor } from '@platejs/core/react';

import { BaseEditorKit } from '@/registry/components/editor/editor-base-kit';
import { playgroundValue } from '@/registry/examples/values/playground-value';
import { EditorView } from '@/registry/ui/editor';
import { EditorStatic } from '@/registry/ui/editor-static';

export default function DevPage() {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

// const editor = createStaticEditor({ plugins: BaseEditorKit });
const editor = usePlateViewEditor(
{
plugins: BaseEditorKit,
value: playgroundValue,
},
[]
);

if (!isClient) {
return <main>Loading...</main>;
}

return (
<main>
<EditorView editor={editor} />

<h1>123</h1>

<EditorStatic editor={editor} />
</main>
);
}
14 changes: 12 additions & 2 deletions apps/www/src/registry/ui/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import * as React from 'react';

import type { VariantProps } from 'class-variance-authority';
import type { PlateContentProps } from 'platejs/react';
import type { PlateContentProps, PlateViewProps } from 'platejs/react';

import { cva } from 'class-variance-authority';
import { PlateContainer, PlateContent } from 'platejs/react';
import { PlateContainer, PlateContent, PlateView } from 'platejs/react';

import { cn } from '@/lib/utils';

Expand Down Expand Up @@ -112,3 +112,13 @@ export const Editor = React.forwardRef<HTMLDivElement, EditorProps>(
);

Editor.displayName = 'Editor';

export function EditorView({
className,
variant,
...props
}: PlateViewProps & VariantProps<typeof editorVariants>) {
return <PlateView {...props} />;
}

EditorView.displayName = 'EditorView';
54 changes: 54 additions & 0 deletions packages/core/src/lib/static/editor/createStaticEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type Editor, type Value, createEditor } from '@platejs/slate';

import type { AnyPluginConfig } from '../../plugin';
import type { CorePlugin } from '../../plugins';

import {
type CreateSlateEditorOptions,
type WithSlateOptions,
withSlate,
} from '../../editor';
import { getStaticPlugins } from '../plugins/getStaticPlugins';

type CreateStaticEditorOptions<
V extends Value = Value,
P extends AnyPluginConfig = CorePlugin,
> = CreateSlateEditorOptions<V, P> & {
/** Enable copy plugin. */
copyPlugin?: boolean;
};

type WithStaticOptions<
V extends Value = Value,
P extends AnyPluginConfig = CorePlugin,
> = WithSlateOptions<V, P> & {
copyPlugin?: boolean;
};

const withStatic = <
V extends Value = Value,
P extends AnyPluginConfig = CorePlugin,
>(
editor: Editor,
options: WithStaticOptions<V, P> = {}
) => {
const { plugins = [], ...rest } = options;

const staticPlugins = getStaticPlugins({
copyPlugin: options.copyPlugin,
}) as any;

options.plugins = [...staticPlugins, ...plugins];

return withSlate<V, P>(editor, options);
};

export const createStaticEditor = <
V extends Value = Value,
P extends AnyPluginConfig = CorePlugin,
>({
editor = createEditor(),
...options
}: CreateStaticEditorOptions<V, P> = {}) => {
return withStatic<V, P>(editor, options);
};
5 changes: 5 additions & 0 deletions packages/core/src/lib/static/editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* @file Automatically generated by barrelsby.
*/

export * from './createStaticEditor';
2 changes: 2 additions & 0 deletions packages/core/src/lib/static/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export * from './serializeHtml';
export * from './types';
export * from './components/index';
export * from './deserialize/index';
export * from './editor/index';
export * from './plugins/index';
export * from './utils/index';
45 changes: 45 additions & 0 deletions packages/core/src/lib/static/internal/getPlainText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { DOMElement, DOMNode, DOMText } from '@platejs/slate';

const getDefaultView = (value: any): Window | null => {
return value?.ownerDocument?.defaultView || null;
};

/** Check if a DOM node is an element node. */

const isDOMElement = (value: any): value is DOMElement => {
return isDOMNode(value) && value.nodeType === 1;
};

/** Check if a value is a DOM node. */

const isDOMNode = (value: any): value is DOMNode => {
const window = getDefaultView(value);
return !!window && value instanceof window.Node;
};

/** Check if a DOM node is an element node. */
const isDOMText = (value: any): value is DOMText => {
return isDOMNode(value) && value.nodeType === 3;
};

export const getPlainText = (domNode: DOMNode) => {
let text = '';

if (isDOMText(domNode) && domNode.nodeValue) {
return domNode.nodeValue;
}

if (isDOMElement(domNode)) {
for (const childNode of Array.from(domNode.childNodes)) {
text += getPlainText(childNode);
}

const display = getComputedStyle(domNode).getPropertyValue('display');

if (display === 'block' || display === 'list' || domNode.tagName === 'BR') {
text += '\n';
}
}

return text;
};
56 changes: 56 additions & 0 deletions packages/core/src/lib/static/plugins/CopyPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Descendant } from '@platejs/slate';

import type { SlateEditor } from '../..';

import { createTSlatePlugin } from '../../plugin';
import { getPlainText } from '../internal/getPlainText';
import { getSelectedDomBlocks } from '../utils/getSelectedDomBlocks';
import { getSelectedDomNode } from '../utils/getSelectedDomNode';
import { isSelectOutside } from '../utils/isSelectOutside';

export const setFragmentDataStatic = (
editor: SlateEditor,
data: Pick<DataTransfer, 'getData' | 'setData'>
) => {
const domBlocks = getSelectedDomBlocks();
const html = getSelectedDomNode();

if (!html || !domBlocks) return;

const selectOutside = isSelectOutside(html);

if (selectOutside) return;

// only crossing multiple blocks
if (domBlocks.length > 0) {
const fragment: Descendant[] = [];

Array.from(domBlocks).forEach((node: any) => {
const blockId = node.dataset.slateId;
const block = editor.api.node({ id: blockId, at: [] });

// prevent inline elements like link and table cells.
if (block && block[1].length === 1) {
fragment.push(block[0]);
}
});

const string = JSON.stringify(fragment);
const encoded = window.btoa(encodeURIComponent(string));

data.setData('application/x-slate-fragment', encoded);
data.setData('text/html', html.innerHTML);
data.setData('text/plain', getPlainText(html));
}
};

export const CopyPlugin = createTSlatePlugin({
key: 'copy',
options: {},
}).overrideEditor(({ editor }) => ({
transforms: {
setFragmentData(fragment) {
return setFragmentDataStatic(editor, fragment);
},
},
}));
14 changes: 14 additions & 0 deletions packages/core/src/lib/static/plugins/getStaticPlugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CopyPlugin } from './CopyPlugin';

export type GetStaticPluginsOptions = {
/** Enable copy plugin. */
copyPlugin?: boolean;
};

export const getStaticPlugins = ({
copyPlugin = true,
}: GetStaticPluginsOptions) => {
const staticPlugins = [CopyPlugin.configure({ enabled: copyPlugin })];

return [...staticPlugins];
};
6 changes: 6 additions & 0 deletions packages/core/src/lib/static/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @file Automatically generated by barrelsby.
*/

export * from './CopyPlugin';
export * from './getStaticPlugins';
15 changes: 15 additions & 0 deletions packages/core/src/lib/static/utils/getSelectedDomBlocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** Get the slate nodes from the DOM selection */
export const getSelectedDomBlocks = () => {
const selection = window.getSelection();

if (!selection || selection.rangeCount === 0) return;

const range = selection.getRangeAt(0);
const fragment = range.cloneContents();

const domBlocks = fragment.querySelectorAll(
'[data-slate-node="element"][data-slate-id]'
);

return Array.from(domBlocks);
};
13 changes: 13 additions & 0 deletions packages/core/src/lib/static/utils/getSelectedDomNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** Get the DOM node from the DOM selection */
export const getSelectedDomNode = () => {
const selection = window.getSelection();

if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);

const htmlFragment = range.cloneContents();
const div = document.createElement('div');
div.append(htmlFragment);

return div;
};
3 changes: 3 additions & 0 deletions packages/core/src/lib/static/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
export * from './createStaticString';
export * from './getNodeDataAttributes';
export * from './getRenderNodeStaticProps';
export * from './getSelectedDomBlocks';
export * from './getSelectedDomNode';
export * from './isSelectOutside';
export * from './pipeDecorate';
export * from './stripHtmlClassNames';
export * from './stripSlateDataAttributes';
12 changes: 12 additions & 0 deletions packages/core/src/lib/static/utils/isSelectOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getSelectedDomNode } from './getSelectedDomNode';

/** Check if the DOM selection is outside the editor */
export const isSelectOutside = (html?: HTMLElement): boolean => {
const domNodes = html ?? getSelectedDomNode();

if (!domNodes) return false;

const selectOutside = !!domNodes?.querySelector('[data-slate-editor="true"');

return selectOutside;
};
23 changes: 23 additions & 0 deletions packages/core/src/react/components/PlateView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { useCallback } from 'react';

import { type PlateStaticProps, PlateStatic } from '../../lib';

export type PlateViewProps = PlateStaticProps & {};

export const PlateView = (props: PlateViewProps) => {
return (
<PlateStatic
onCopy={useCallback(
(e: React.ClipboardEvent<HTMLDivElement>) => {
props.editor.tf.setFragmentData(e.clipboardData);

if (e.clipboardData.getData('application/x-slate-fragment')) {
e.preventDefault();
}
},
[props.editor.tf]
)}
{...props}
/>
);
};
1 change: 1 addition & 0 deletions packages/core/src/react/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export * from './PlateContent';
export * from './PlateControllerEffect';
export * from './PlateSlate';
export * from './PlateTest';
export * from './PlateView';
export * from './plate-nodes';
export * from './withHOC';
1 change: 1 addition & 0 deletions packages/core/src/react/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
export * from './PlateEditor';
export * from './getPlateCorePlugins';
export * from './usePlateEditor';
export * from './usePlateViewEditor';
export * from './withPlate';
Loading