diff --git a/apps/web/src/components/pages/user/profile.tsx b/apps/web/src/components/pages/user/profile.tsx index 981b579b85..eab6a35202 100644 --- a/apps/web/src/components/pages/user/profile.tsx +++ b/apps/web/src/components/pages/user/profile.tsx @@ -86,7 +86,7 @@ export const Profile: NextPage = ({ userData }) => {
- {motivation && <>"{motivation}"} + {motivation && <>„{motivation}“} {isOwnProfile && !isNewlyRegisteredUser && renderEditMotivationLink()} diff --git a/apps/web/src/components/user-tools/share/share-modal.tsx b/apps/web/src/components/user-tools/share/share-modal.tsx index ef1d2295a3..7d37478a4a 100644 --- a/apps/web/src/components/user-tools/share/share-modal.tsx +++ b/apps/web/src/components/user-tools/share/share-modal.tsx @@ -8,6 +8,7 @@ import { faCopy, faDownload, faEnvelope, + faFileText, } from '@fortawesome/free-solid-svg-icons' import { QRCodeSVG } from 'qrcode.react' import { MouseEvent, useState, useEffect } from 'react' @@ -20,10 +21,12 @@ import { Instance } from '@/fetcher/graphql-types/operations' import { cn } from '@/helper/cn' import { colors } from '@/helper/colors' import { showToastNotice } from '@/helper/show-toast-notice' +import { serloDomain } from '@/helper/urls/serlo-domain' export interface ShareModalProps { isOpen: boolean setIsOpen: (open: boolean) => void + showCopyContent?: boolean showPdf?: boolean path?: string } @@ -36,9 +39,15 @@ interface EntryData { onClick?: (event: MouseEvent) => void } +const base = + process.env.NODE_ENV === 'development' + ? 'http://localhost:3000' + : 'https://' + serloDomain + export function ShareModal({ isOpen, setIsOpen, + showCopyContent, showPdf, path, }: ShareModalProps) { @@ -70,10 +79,41 @@ export function ShareModal({ } } + async function copyContentToClipboard() { + if (!pathOrId) return + try { + const url = `${base}/api/frontend/bildungsraum-share?href=${encodeURIComponent(pathOrId)}` + const res = await fetch(url) + const data = (await res.json()) as string + if (!res.ok) { + throw new Error( + 'injection-content API call failed with error: ' + data.toString() + ) + } + await navigator.clipboard.writeText(JSON.stringify(data)) + showToastNotice('👌 Erfolgreich kopiert', 'success') + } catch (e) { + // eslint-disable-next-line no-console + console.error(e) + showToastNotice( + '❌ Leider gab es ein Problem beim kopieren. Tut uns leid.', + 'warning' + ) + } + } + const shareUrl = `${window.location.protocol}//${window.location.host}/${pathOrId}` const urlEncoded = encodeURIComponent(shareUrl) const titleEncoded = encodeURIComponent(document.title) + const contentCopy = [ + { + title: 'Inhalt kopieren', + icon: faFileText, + onClick: () => copyContentToClipboard(), + }, + ] + const socialShare = [ { title: 'E-Mail', @@ -156,6 +196,24 @@ export function ShareModal({ {renderButtons(pdfData)} )} + + {showCopyContent ? ( // "de" only + <> +
+

Inhalt zum Bearbeiten kopieren

+

+ Du kannst diesen Inhalt in jedem Serlo Editor weiterbearbeiten: Hier + auf serlo.org und in LMS wie Moodle, Edu-sharing oder + itslearning, die den Serlo Editor eingebaut haben. +
+
+ Dazu einfach auf unten auf „Inhalt kopieren“ klicken, + einen Moment warten und dann Inhalt im Editor Textfeld Deines LMS + einfügen. +

+ {renderButtons(contentCopy)} + + ) : null} ) diff --git a/apps/web/src/components/user-tools/share/share.tsx b/apps/web/src/components/user-tools/share/share.tsx index dc873e2294..3a98873580 100644 --- a/apps/web/src/components/user-tools/share/share.tsx +++ b/apps/web/src/components/user-tools/share/share.tsx @@ -15,7 +15,7 @@ const ShareModal = dynamic(() => ) export function Share({ data, aboveContent }: MoreAuthorToolsProps) { - const { strings } = useInstanceData() + const { lang, strings } = useInstanceData() const [shareOpen, setShareOpen] = useState(false) const showPdf = @@ -28,6 +28,13 @@ export function Share({ data, aboveContent }: MoreAuthorToolsProps) { UuidType.Exercise, ].includes(data.typename as UuidType) + const showCopyContent = + lang === 'de' && + data && + [UuidType.Article, UuidType.ExerciseGroup, UuidType.Exercise].includes( + data.typename as UuidType + ) + return ( <> ) : null} diff --git a/apps/web/src/components/user/event.tsx b/apps/web/src/components/user/event.tsx index b313ad8d1a..e0cf688072 100644 --- a/apps/web/src/components/user/event.tsx +++ b/apps/web/src/components/user/event.tsx @@ -141,7 +141,7 @@ export function Event({ ), comment: (

- "{event.thread.thread.nodes[0].content}" + „{event.thread.thread.nodes[0].content}“

), }) diff --git a/apps/web/src/data/en/index.ts b/apps/web/src/data/en/index.ts index 69195e52f1..e3a22e868f 100644 --- a/apps/web/src/data/en/index.ts +++ b/apps/web/src/data/en/index.ts @@ -401,7 +401,7 @@ export const instanceData = { 'The provided authentication code is invalid, please try again.', code4000010: 'Have you already verified your email address?.%break% %verificationLinkText%', - code4000032: "You inserted less than 8 characters.", + code4000032: 'You inserted less than 8 characters.', code4060004: 'The recovery link is not valid or has already been used. Please try requesting an email again', code4070001: diff --git a/apps/web/src/fetcher/graphql-types/operations.ts b/apps/web/src/fetcher/graphql-types/operations.ts index a9ddc88b67..bc1a3b0011 100644 --- a/apps/web/src/fetcher/graphql-types/operations.ts +++ b/apps/web/src/fetcher/graphql-types/operations.ts @@ -2410,6 +2410,13 @@ type GetCommentsThreadsOldComments_VideoRevision_Fragment = { __typename?: 'Vide export type GetCommentsThreadsOldCommentsFragment = GetCommentsThreadsOldComments_Applet_Fragment | GetCommentsThreadsOldComments_AppletRevision_Fragment | GetCommentsThreadsOldComments_Article_Fragment | GetCommentsThreadsOldComments_ArticleRevision_Fragment | GetCommentsThreadsOldComments_Course_Fragment | GetCommentsThreadsOldComments_CoursePage_Fragment | GetCommentsThreadsOldComments_CoursePageRevision_Fragment | GetCommentsThreadsOldComments_CourseRevision_Fragment | GetCommentsThreadsOldComments_Event_Fragment | GetCommentsThreadsOldComments_EventRevision_Fragment | GetCommentsThreadsOldComments_Exercise_Fragment | GetCommentsThreadsOldComments_ExerciseGroup_Fragment | GetCommentsThreadsOldComments_ExerciseGroupRevision_Fragment | GetCommentsThreadsOldComments_ExerciseRevision_Fragment | GetCommentsThreadsOldComments_Page_Fragment | GetCommentsThreadsOldComments_PageRevision_Fragment | GetCommentsThreadsOldComments_TaxonomyTerm_Fragment | GetCommentsThreadsOldComments_User_Fragment | GetCommentsThreadsOldComments_Video_Fragment | GetCommentsThreadsOldComments_VideoRevision_Fragment; +export type ShareEditorContentQueryVariables = Exact<{ + path: Scalars['String']['input']; +}>; + + +export type ShareEditorContentQuery = { __typename?: 'Query', uuid?: { __typename: 'Applet', currentRevision?: { __typename?: 'AppletRevision', content: string } | null } | { __typename: 'AppletRevision' } | { __typename: 'Article', currentRevision?: { __typename?: 'ArticleRevision', content: string } | null } | { __typename: 'ArticleRevision' } | { __typename: 'Comment' } | { __typename: 'Course', currentRevision?: { __typename?: 'CourseRevision', content: string } | null } | { __typename: 'CoursePage', currentRevision?: { __typename?: 'CoursePageRevision', content: string } | null } | { __typename: 'CoursePageRevision' } | { __typename: 'CourseRevision' } | { __typename: 'Event', currentRevision?: { __typename?: 'EventRevision', content: string } | null } | { __typename: 'EventRevision' } | { __typename: 'Exercise', currentRevision?: { __typename?: 'ExerciseRevision', content: string } | null } | { __typename: 'ExerciseGroup', currentRevision?: { __typename?: 'ExerciseGroupRevision', content: string } | null } | { __typename: 'ExerciseGroupRevision' } | { __typename: 'ExerciseRevision' } | { __typename: 'Page', currentRevision?: { __typename?: 'PageRevision', content: string } | null } | { __typename: 'PageRevision' } | { __typename: 'TaxonomyTerm' } | { __typename: 'User' } | { __typename: 'Video', currentRevision?: { __typename?: 'VideoRevision', content: string } | null } | { __typename: 'VideoRevision' } | null }; + export type InjectionOnlyContentQueryVariables = Exact<{ path: Scalars['String']['input']; }>; diff --git a/apps/web/src/pages/api/frontend/bildungsraum-share.ts b/apps/web/src/pages/api/frontend/bildungsraum-share.ts new file mode 100644 index 0000000000..dde1e97f4b --- /dev/null +++ b/apps/web/src/pages/api/frontend/bildungsraum-share.ts @@ -0,0 +1,101 @@ +import { parseDocumentString } from '@editor/static-renderer/helper/parse-document-string' +import type { + EditorArticleDocument, + EditorExerciseDocument, + EditorExerciseGroupDocument, +} from '@editor/types/editor-plugins' +import { gql } from 'graphql-request' +import type { NextApiRequest, NextApiResponse } from 'next' + +import { endpoint } from '@/api/endpoint' +import { InjectionOnlyContentQuery } from '@/fetcher/graphql-types/operations' +import { isProduction } from '@/helper/is-production' + +/** + * Allows frontend to copy Serlo content to the clipboard. + * The content is unpacked for consistent pasting in the Editor. + */ +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const href = decodeURIComponent(String(req.query.href)) + + const [base] = href.split('#') + const path = base.startsWith('/') ? base : `/${base}` + + if (!path) { + return res.status(401).json('no path provided') + } + + try { + void fetch(endpoint, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ query, variables: { path } }), + }) + .then((res) => res.json()) + .then((data: { data: InjectionOnlyContentQuery }) => { + if (!data.data?.uuid) { + return res.status(404).json('not found') + } + + const uuid = data.data.uuid + + if (!Object.hasOwn(uuid, 'currentRevision') || !uuid.currentRevision) { + return res.status(404).json('no current revision') + } + + if (uuid.__typename === 'Article') { + const articleDocument = parseDocumentString( + uuid.currentRevision.content + ) as EditorArticleDocument + const articleContent = articleDocument.state.content + respondWithContent(articleContent) + return + } + + if ( + uuid.__typename === 'Exercise' || + uuid.__typename === 'ExerciseGroup' + ) { + const exercise = parseDocumentString(uuid.currentRevision.content) as + | EditorExerciseDocument + | EditorExerciseGroupDocument + respondWithContent({ plugin: 'rows', state: [exercise] }) + return + } + + return res.status(422).json('unsupported entity type') + }) + .catch((e) => { + return res.status(500).json(`${String(e)} at ${path}`) + }) + } catch (e) { + return res.status(500).json(`${String(e)} at ${path}`) + } + + function respondWithContent(content: any) { + const twoDaysInSeconds = 172800 + res.setHeader('Cache-Control', `maxage=${twoDaysInSeconds}`) + if (!isProduction) res.setHeader('Access-Control-Allow-Origin', '*') + res.status(200).json(content) + } +} + +const query = gql` + query shareEditorContent($path: String!) { + uuid(alias: { path: $path, instance: de }) { + __typename + + ... on AbstractEntity { + currentRevision { + content + } + } + } + } +` diff --git a/apps/web/src/serlo-editor-integration/h5p/index.tsx b/apps/web/src/serlo-editor-integration/h5p/index.tsx index 06019f3eaa..6796605ae6 100644 --- a/apps/web/src/serlo-editor-integration/h5p/index.tsx +++ b/apps/web/src/serlo-editor-integration/h5p/index.tsx @@ -130,7 +130,7 @@ function H5pEditor({ state }: H5pProps) { Registriere dich mit deiner E-Mail-Adresse und melde dich an.
  • - Klicke auf "Neuen Inhalt erstellen" und wähle eines + Klicke auf „Neuen Inhalt erstellen“ und wähle eines der folgenden Inhaltstypen:
      {Object.values(availableH5pExercises).map((exercise) => ( @@ -140,7 +140,7 @@ function H5pEditor({ state }: H5pProps) {
    • Erstelle deinen Inhalt, speichere ihn und klicke dann auf - "Inhalt bereitstellen". + „Inhalt bereitstellen“.
    • Füge die Verknüpfung zur Bereitstellung hier ein:
    diff --git a/packages/editor/src/i18n/strings/de/edit.ts b/packages/editor/src/i18n/strings/de/edit.ts index 7ac8584501..302c2e5c5f 100644 --- a/packages/editor/src/i18n/strings/de/edit.ts +++ b/packages/editor/src/i18n/strings/de/edit.ts @@ -321,6 +321,10 @@ export const editStrings = { 'Sorry, Elemente einfügen klappt nicht in Listen.', pastingPluginNotAllowedHere: 'Sorry, dieses Plugin kannst du hier nicht einfügen.', + unsupportedPluginsPasted: + 'Ein paar der Plugins die du eingefügt hast, werden hier nicht unterstützt.', + invalidDataPasted: + 'Sorry, mit den Daten die du einfügen willst stimmt etwas nicht. Das liegt wahrscheinlich an uns.', linkOverlay: { placeholder: 'Suchbegriff oder "/1234"', placeholderNonSerlo: 'Link', diff --git a/packages/editor/src/i18n/strings/en/edit.ts b/packages/editor/src/i18n/strings/en/edit.ts index 8f7c3a2326..28a1875a0c 100644 --- a/packages/editor/src/i18n/strings/en/edit.ts +++ b/packages/editor/src/i18n/strings/en/edit.ts @@ -310,6 +310,9 @@ export const editStrings = { 'Sorry, pasting elements inside of lists is not allowed.', pastingPluginNotAllowedHere: 'Sorry, pasting this plugin here is not allowed.', + unsupportedPluginsPasted: + 'There were unsupported plugins in the data you pasted.', + invalidDataPasted: 'Sorry, something is wrong with the data you pasted.', linkOverlay: { placeholder: 'https://… or /1234', placeholderNonSerlo: 'https://', diff --git a/packages/editor/src/plugin/helpers/editor-plugins.tsx b/packages/editor/src/plugin/helpers/editor-plugins.tsx index 1aca2d98af..f12231fa3a 100644 --- a/packages/editor/src/plugin/helpers/editor-plugins.tsx +++ b/packages/editor/src/plugin/helpers/editor-plugins.tsx @@ -1,4 +1,5 @@ import { EditorPlugin } from '../internal-plugin' +import { emitUnsupportedPluginsEvent } from './unsupported-plugin-event' export interface PluginWithData { type: string @@ -28,9 +29,12 @@ export const editorPlugins = (function () { function getByType(pluginType: string) { const plugins = getAllWithData() - const contextPlugin = - plugins.find((plugin) => plugin.type === pluginType) ?? - plugins.find((plugin) => plugin.type === 'unsupported') + let contextPlugin = plugins.find((plugin) => plugin.type === pluginType) + + if (contextPlugin === undefined) { + emitUnsupportedPluginsEvent() + contextPlugin = plugins.find((plugin) => plugin.type === 'unsupported') + } return (contextPlugin?.plugin as EditorPlugin) ?? null } diff --git a/packages/editor/src/plugin/helpers/unsupported-plugin-event.ts b/packages/editor/src/plugin/helpers/unsupported-plugin-event.ts new file mode 100644 index 0000000000..9c2b4ce192 --- /dev/null +++ b/packages/editor/src/plugin/helpers/unsupported-plugin-event.ts @@ -0,0 +1,15 @@ +const eventName = 'unsupportedPluginType' + +const event = new Event(eventName) + +export function emitUnsupportedPluginsEvent() { + document.dispatchEvent(event) +} + +export function listenForUnsupportedPlugins(callback: () => void) { + document.addEventListener(eventName, callback, { capture: true, once: true }) +} + +export function removeUnsupportedPluginsListener(callback: () => void) { + document.removeEventListener(eventName, callback, true) +} diff --git a/packages/editor/src/plugins/text/hooks/use-editable-key-down-handler.tsx b/packages/editor/src/plugins/text/hooks/use-editable-key-down-handler.tsx index 923046b7a0..fe623f297f 100644 --- a/packages/editor/src/plugins/text/hooks/use-editable-key-down-handler.tsx +++ b/packages/editor/src/plugins/text/hooks/use-editable-key-down-handler.tsx @@ -19,7 +19,7 @@ import { Editor as SlateEditor, Range, Node, Transforms } from 'slate' import { useTextConfig } from './use-text-config' import type { TextEditorProps } from '../components/text-editor' import { emptyDocumentFactory, mergePlugins } from '../utils/document' -import { insertPlugin } from '../utils/insert-plugin' +import { insertPlugins } from '../utils/insert-plugins' import { instanceStateStore } from '../utils/instance-state-store' import { isSelectionAtEnd, isSelectionAtStart } from '../utils/selection' @@ -74,13 +74,14 @@ export const useEditableKeydownHandler = ( payload: { insertIndex, insertCallback: (plugin) => { - insertPlugin({ - pluginType: plugin.plugin, + insertPlugins({ + plugins: [ + { pluginType: plugin.plugin, state: plugin.state }, + ], editor, id, - dispatch, - state: plugin.state, getStoreState: () => store.getState(), + dispatch, }) }, }, 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 fda7e3af89..e835e71b8b 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 @@ -2,6 +2,10 @@ import { isSelectionWithinList } from '@editor/editor-ui/plugin-toolbar/text-con import { showToastNotice } from '@editor/editor-ui/show-toast-notice' import { useEditStrings } from '@editor/i18n/edit-strings-provider' import { editorPlugins } from '@editor/plugin/helpers/editor-plugins' +import { + listenForUnsupportedPlugins, + removeUnsupportedPluginsListener, +} from '@editor/plugin/helpers/unsupported-plugin-event' import { captionPasteHandler } from '@editor/plugins/image/utils/caption-paste-handler' import { checkIsAllowedNesting } from '@editor/plugins/rows/utils/check-is-allowed-nesting' import { @@ -11,11 +15,15 @@ import { useStore, selectAncestorPluginTypes, } from '@editor/store' -import type { EditorRowsDocument } from '@editor/types/editor-plugins' +import { EditorPluginType } from '@editor/types/editor-plugin-type' +import type { AnyEditorDocument } from '@editor/types/editor-plugins' +import { fold } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/function' +import * as t from 'io-ts' import { useCallback } from 'react' import { Editor as SlateEditor } from 'slate' -import { insertPlugin } from '../utils/insert-plugin' +import { insertPlugins } from '../utils/insert-plugins' import { mathpixPasteHandler } from '../utils/mathpix-paste-handler' export interface UseEditablePasteHandlerArgs { @@ -60,44 +68,32 @@ export const useEditablePasteHandler = (args: UseEditablePasteHandlerArgs) => { mathpixPasteHandler({ event, editor, text }) - let media - // pasting editor document string and insert as plugins - if (!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) - ) { - event.preventDefault() // extra prevent for firefox to make it work 🤷 - media = { - pluginType: pluginDocument.plugin, - state: pluginDocument.state, - } - } else { - event.preventDefault() - showToastNotice(textStrings.pastingPluginNotAllowedHere, 'warning') - } - } - // Exit if not allowed to manipulate siblings if (!mayManipulateSiblings) return + let pluginsToAdd: Array<{ pluginType: string; state?: unknown }> = [] + + // Pasting editor document string and insert as plugins + if (!pluginsToAdd.length && text.startsWith('{"plugin":"rows"')) { + listenForUnsupportedPlugins(notifyUserOfUnsupportedPlugins) + const rowsDocument = decodeRowsPlugin(text) + if (!rowsDocument || !rowsDocument.state.length) return + rowsDocument.state.forEach(processPlugin) + } + // Iterate through all plugins and try to process clipboard data - for (const { plugin, type } of editorPlugins.getAllWithData()) { - const state = plugin.onFiles?.(files) ?? (await plugin.onText?.(text)) - if (state?.state) { - media = { state: state.state as unknown, pluginType: type } - break + if (!pluginsToAdd.length) { + for (const { plugin, type } of editorPlugins.getAllWithData()) { + const state = plugin.onFiles?.(files) ?? (await plugin.onText?.(text)) + if (state?.state) { + pluginsToAdd = [{ state: state.state as unknown, pluginType: type }] + break + } } } - // Exit if no media was processed from clipboard data - if (!media) return + // Exit if no plugin was processed from clipboard data + if (!pluginsToAdd.length) return // Prevent URL being pasted as text in the text plugin event.preventDefault() @@ -108,15 +104,57 @@ export const useEditablePasteHandler = (args: UseEditablePasteHandlerArgs) => { return } - // Insert the plugin with appropriate type and state - insertPlugin({ + // Insert the plugins with appropriate type and state + insertPlugins({ + plugins: pluginsToAdd, editor, id, - dispatch, getStoreState: () => store.getState(), - ...media, + dispatch, }) + + removeUnsupportedPluginsListener(notifyUserOfUnsupportedPlugins) + + function decodeRowsPlugin(input: string) { + return pipe( + StateDecoder.decode(JSON.parse(input)), + fold( + (errors) => throwError(errors) ?? null, + (decoded) => decoded + ) + ) + } + + function processPlugin({ plugin, state }: AnyEditorDocument) { + const typesOfAncestors = selectAncestorPluginTypes(store.getState(), id) + if (typesOfAncestors === null) return + if (checkIsAllowedNesting(plugin, typesOfAncestors)) { + pluginsToAdd.push({ pluginType: plugin, state }) + } else { + showToastNotice(textStrings.pastingPluginNotAllowedHere, 'warning') + } + } + + function notifyUserOfUnsupportedPlugins() { + showToastNotice(textStrings.unsupportedPluginsPasted, 'warning') + } + + function throwError(error: unknown) { + showToastNotice(textStrings.invalidDataPasted, 'warning') + // eslint-disable-next-line no-console + console.error('Pasted JSON data is not a valid editor-state: ', error) + } }, [dispatch, editor, id, textStrings, store] ) } + +const StateDecoder = t.strict({ + plugin: t.literal(EditorPluginType.Rows), + state: t.array( + t.strict({ + plugin: t.string, + state: t.unknown, + }) + ), +}) diff --git a/packages/editor/src/plugins/text/utils/insert-plugin.ts b/packages/editor/src/plugins/text/utils/insert-plugins.ts similarity index 54% rename from packages/editor/src/plugins/text/utils/insert-plugin.ts rename to packages/editor/src/plugins/text/utils/insert-plugins.ts index b48af43eda..79b8d9cbba 100644 --- a/packages/editor/src/plugins/text/utils/insert-plugin.ts +++ b/packages/editor/src/plugins/text/utils/insert-plugins.ts @@ -7,28 +7,28 @@ import { type RootState, } from '@editor/store' import { Action, ThunkDispatch } from '@reduxjs/toolkit' +import { reverse } from 'ramda' import { Editor as SlateEditor, Node } from 'slate' import { sliceNodesAfterSelection } from './document' -export interface insertPluginArgs { - pluginType: string +export interface insertPluginsArgs { + plugins: Array<{ pluginType: string; state?: unknown }> editor: SlateEditor id: string - dispatch: ThunkDispatch> - state?: unknown - getStoreState: () => RootState + dispatch: ThunkDispatch> } -export function insertPlugin({ - pluginType, +export function insertPlugins({ + plugins, editor, id, - dispatch, - state, getStoreState, -}: insertPluginArgs) { + dispatch, +}: insertPluginsArgs) { + if (!plugins.length) return + const storeState = getStoreState() const document = selectDocument(storeState, id) @@ -37,25 +37,41 @@ export function insertPlugin({ if (!document || !mayManipulateSiblings || !parent) return const parentPluginType = document.plugin + const reversedPlugins = reverse(plugins) const isEditorEmpty = Node.string(editor) === '' || Node.string(editor) === '/' - if (isEditorEmpty) { + if (isEditorEmpty) replaceCurrentTextPlugin() + else splitCurrentTextPlugin(parent.id) + + for (const { pluginType, state } of reversedPlugins) { + dispatch( + insertPluginChildAfter({ + parent: parent.id, + sibling: id, + document: { plugin: pluginType, state }, + }) + ) + } + + function replaceCurrentTextPlugin() { + const firstPlugin = reversedPlugins.pop() + const { pluginType, state } = firstPlugin! dispatch(runReplaceDocumentSaga({ id, pluginType, state })) - return } - const slicedNodes = sliceNodesAfterSelection(editor) + function splitCurrentTextPlugin(parentId: string) { + const slicedNodes = sliceNodesAfterSelection(editor) - if (slicedNodes) { - const cleanSlicedNodes = Node.string(slicedNodes[0]).length - ? slicedNodes - : slicedNodes.slice(1) - if (cleanSlicedNodes.length) { + if (slicedNodes) { + const cleanSlicedNodes = Node.string(slicedNodes[0]).length + ? slicedNodes + : slicedNodes.slice(1) + if (!cleanSlicedNodes.length) return dispatch( insertPluginChildAfter({ - parent: parent.id, + parent: parentId, sibling: id, document: { plugin: parentPluginType, @@ -65,12 +81,4 @@ export function insertPlugin({ ) } } - - dispatch( - insertPluginChildAfter({ - parent: parent.id, - sibling: id, - document: { plugin: pluginType, state }, - }) - ) }