diff --git a/apps/web/src/components/user/profile-experimental.tsx b/apps/web/src/components/user/profile-experimental.tsx
index 275885d0ee..fac14a5742 100644
--- a/apps/web/src/components/user/profile-experimental.tsx
+++ b/apps/web/src/components/user/profile-experimental.tsx
@@ -11,6 +11,12 @@ export const features = {
activeInDev: true,
hideInProduction: true,
},
+ editorPluginCopyTool: {
+ cookieName: 'useEditorPluginCopyTool',
+ isActive: false,
+ activeInDev: true,
+ hideInProduction: true,
+ },
editorAnchorLinkCopyTool: {
cookieName: 'useEditorAnchorLinkCopyTool',
isActive: false,
diff --git a/apps/web/src/data/en/index.ts b/apps/web/src/data/en/index.ts
index 8266b558f6..1c12f21be7 100644
--- a/apps/web/src/data/en/index.ts
+++ b/apps/web/src/data/en/index.ts
@@ -836,6 +836,8 @@ export const loggedInData = {
link: 'Link (%ctrlOrCmd% + K)',
noElementPasteInLists:
'Sorry, pasting elements inside of lists is not allowed.',
+ pastingPluginNotAllowedHere:
+ 'Sorry, pasting this plugin here is not allowed.',
linkOverlay: {
placeholder: 'https://… or /1234',
inputLabel: 'Paste or type a link',
@@ -1084,6 +1086,8 @@ export const loggedInData = {
ready: 'Ready to save?',
anchorLinkWarning:
'This link will only work in the frontend and for content that has a somewhat new revision.',
+ pluginCopyInfo: 'You can now paste this plugin into text plugins',
+ pluginCopyButtonLabel: 'Copy plugin to clipboard',
},
taxonomy: {
title: 'Title',
diff --git a/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-copy-tool.tsx b/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-copy-tool.tsx
new file mode 100644
index 0000000000..6b6cfafb86
--- /dev/null
+++ b/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-copy-tool.tsx
@@ -0,0 +1,47 @@
+import { selectStaticDocumentWithoutIds, store } from '@editor/store'
+import { faCopy } from '@fortawesome/free-solid-svg-icons'
+import { shouldUseFeature } from '@serlo/frontend/src/components/user/profile-experimental'
+import { useInstanceData } from '@serlo/frontend/src/contexts/instance-context'
+import { useEditorStrings } from '@serlo/frontend/src/contexts/logged-in-data-context'
+import { showToastNotice } from '@serlo/frontend/src/helper/show-toast-notice'
+import { useCallback } from 'react'
+
+import { DropdownButton } from './dropdown-button'
+
+interface PluginCopyToolProps {
+ pluginId: string
+}
+/**
+ * experimental plugin to copy plugin's editor state to the clipboard
+ */
+export function PluginCopyTool({ pluginId }: PluginCopyToolProps) {
+ const editorStrings = useEditorStrings()
+ const { strings } = useInstanceData()
+
+ const handleOnClick = useCallback(() => {
+ const document = selectStaticDocumentWithoutIds(store.getState(), pluginId)
+ const rowsDocument = { plugin: 'rows', state: [document] }
+
+ void navigator.clipboard.writeText(JSON.stringify(rowsDocument))
+ showToastNotice(strings.share.copySuccess, 'success', 2000)
+ showToastNotice(
+ '👉 ' + editorStrings.edtrIo.pluginCopyInfo,
+ undefined,
+ 4000
+ )
+ }, [pluginId, strings, editorStrings])
+
+ if (!navigator.clipboard || !shouldUseFeature('editorPluginCopyTool')) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx b/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx
index d22d156aad..89c96c5836 100644
--- a/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx
+++ b/packages/editor/src/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools.tsx
@@ -14,6 +14,7 @@ import { useCallback, useContext } from 'react'
import { AnchorLinkCopyTool } from './anchor-link-copy-tool'
import { DropdownButton } from './dropdown-button'
+import { PluginCopyTool } from './plugin-copy-tool'
interface PluginDefaultToolsProps {
pluginId: string
@@ -82,6 +83,7 @@ export function PluginDefaultTools({ pluginId }: PluginDefaultToolsProps) {
icon={faTrashAlt}
dataQa="remove-plugin-button"
/>
+
{serloEntityId ? (
) : null}
diff --git a/packages/editor/src/plugins/text/hooks/use-editable-paste-handler.tsx b/packages/editor/src/plugins/text/hooks/use-editable-paste-handler.tsx
index 6b2d6e5b5c..60a236a1ea 100644
--- a/packages/editor/src/plugins/text/hooks/use-editable-paste-handler.tsx
+++ b/packages/editor/src/plugins/text/hooks/use-editable-paste-handler.tsx
@@ -1,17 +1,21 @@
import { isSelectionWithinList } from '@editor/editor-ui/plugin-toolbar/text-controls/utils/list'
import { editorPlugins } from '@editor/plugin/helpers/editor-plugins'
+import { checkIsAllowedNesting } from '@editor/plugins/rows/utils/check-is-allowed-nesting'
import {
selectDocument,
selectMayManipulateSiblings,
useAppDispatch,
store,
+ selectAncestorPluginTypes,
} from '@editor/store'
+import type { EditorRowsDocument } from '@editor/types/editor-plugins'
import { useEditorStrings } from '@serlo/frontend/src/contexts/logged-in-data-context'
import { showToastNotice } from '@serlo/frontend/src/helper/show-toast-notice'
import { useCallback } from 'react'
import { Editor as SlateEditor } from 'slate'
import { insertPlugin } from '../utils/insert-plugin'
+import { shouldUseFeature } from '@/components/user/profile-experimental'
interface UseEditablePasteHandlerArgs {
editor: SlateEditor
@@ -31,14 +35,43 @@ export const useEditablePasteHandler = (args: UseEditablePasteHandlerArgs) => {
const text = event.clipboardData.getData('text')
if (!files.length && !text) return
- // Exit if unable to select document data or if not allowed to manipulate siblings
+ // Exit if unable to select document data
const storeState = store.getState()
const document = selectDocument(storeState, id)
const mayManipulateSiblings = selectMayManipulateSiblings(storeState, id)
- if (!document || !mayManipulateSiblings) return
+ if (!document) return
+ let media
+
+ // pasting editor document string and insert as plugins
+ if (
+ shouldUseFeature('editorPluginCopyTool') &&
+ !media &&
+ text.startsWith('{"plugin":"rows"')
+ ) {
+ const rowsDocument = JSON.parse(text) as EditorRowsDocument
+ if (rowsDocument.state.length !== 1) return
+ const pluginDocument = rowsDocument.state.at(0)
+ const typesOfAncestors = selectAncestorPluginTypes(store.getState(), id)
+ if (!pluginDocument || typesOfAncestors === null) return
+
+ if (
+ mayManipulateSiblings &&
+ checkIsAllowedNesting(pluginDocument.plugin, typesOfAncestors)
+ ) {
+ media = {
+ pluginType: pluginDocument.plugin,
+ state: pluginDocument.state,
+ }
+ } else {
+ event.preventDefault()
+ showToastNotice(textStrings.pastingPluginNotAllowedHere, 'warning')
+ }
+ }
+
+ // Exit if not allowed to manipulate siblings
+ if (!mayManipulateSiblings) return
// Iterate through all plugins and try to process clipboard data
- let media
for (const { plugin, type } of editorPlugins.getAllWithData()) {
const state = plugin.onFiles?.(files) ?? plugin.onText?.(text)
if (state?.state) {