UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

615 lines (581 loc) 35.4 kB
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead import uuid from 'uuid'; import { ACTION, INPUT_METHOD, PasteTypes } from '@atlaskit/editor-common/analytics'; import { addLinkMetadata } from '@atlaskit/editor-common/card'; import { insideTable } from '@atlaskit/editor-common/core-utils'; import { getExtensionAutoConvertersFromProvider } from '@atlaskit/editor-common/extensions'; import { isNestedTablesSupported } from '@atlaskit/editor-common/nesting'; import { isPastedFile as isPastedFileFromEvent, md } from '@atlaskit/editor-common/paste'; import { measureRender } from '@atlaskit/editor-common/performance/measure-render'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { SyncBlockRendererDataAttributeName } from '@atlaskit/editor-common/sync-block'; import { removeBreakoutFromRendererSyncBlockHTML, transformSingleColumnLayout, transformSingleLineCodeBlockToCodeMark, transformSliceEnsureListItemParagraphFirst, transformSliceNestedExpandToExpand, transformSliceToDecisionList, transformSliceToJoinAdjacentCodeBlocks, transformSliceToRemoveLegacyContentMacro, transformSliceToRemoveMacroId } from '@atlaskit/editor-common/transforms'; import { containsAnyAnnotations, extractSliceFromStep, linkifyContent, mapChildren } from '@atlaskit/editor-common/utils'; import { MarkdownTransformer } from '@atlaskit/editor-markdown-transformer'; import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model'; import { contains, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { handlePaste as handlePasteTable } from '@atlaskit/editor-tables/utils'; import { insm } from '@atlaskit/insm'; import { extractClientIdsFromHtml } from '@atlaskit/media-common'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { PastePluginActionTypes } from '../editor-actions/actions'; import { splitParagraphs, upgradeTextToLists } from '../editor-commands/commands'; import { transformSliceForMedia, transformSliceToCorrectMediaWrapper, transformSliceToMediaSingleWithNewExperience, unwrapNestedMediaElements } from '../pm-plugins/media'; import { createPasteMeasurePayload, getContentNodeTypes, handleCodeBlockWithAnalytics, handleExpandWithAnalytics, handleMarkdownWithAnalytics, handleMediaSingleWithAnalytics, handleNestedTablePasteWithAnalytics, handlePasteAsPlainTextWithAnalytics, handlePasteIntoCaptionWithAnalytics, handlePasteIntoTaskAndDecisionWithAnalytics, handlePasteLinkOnSelectedTextWithAnalytics, handlePasteNonNestableBlockNodesIntoListWithAnalytics, handlePastePanelOrDecisionIntoListWithAnalytics, handlePastePreservingMarksWithAnalytics, handleRichTextWithAnalytics, handleSelectedTableWithAnalytics, sendPasteAnalyticsEvent } from './analytics'; import { createClipboardTextSerializer } from './create-clipboard-text-serializer'; import { createPluginState, pluginKey as stateKey } from './plugin-factory'; import { escapeBackslashAndLinksExceptCodeBlock, getPasteSource, htmlContainsSingleFile, htmlHasInvalidLinkTags, isPastedFromExcel, isPastedFromWord, removeDuplicateInvalidLinks, transformUnsupportedBlockCardToInline } from './util'; import { handleVSCodeBlock } from './util/edge-cases/handleVSCodeBlock'; import { handleMacroAutoConvert, handleMention, handleParagraphBlockMarks, handlePasteExpand, handleTableContentPasteInBodiedExtension } from './util/handlers'; import { handleSyncBlocksPaste } from './util/sync-block'; import { htmlHasIncompleteTable, isPastedFromTinyMCEConfluence, tryRebuildCompleteTableHtml } from './util/tinyMCE'; export const isInsideBlockQuote = state => { const { blockquote } = state.schema.nodes; return hasParentNodeOfType(blockquote)(state.selection); }; const enableNewDomainCheckToImproveSmartLinkResolveRate = hostname => { return expValEquals('improve_3p_smart_link_resolve_rate', 'isEnabled', true) && ( // OneDrive Shortlinks hostname.endsWith('1drv.ms') || // MS Teams links hostname.endsWith('teams.live.com') || hostname.endsWith('teams.cloud.microsoft') || hostname.endsWith('teams.microsoft.com')); }; const PASTE = 'Editor Paste Plugin Paste Duration'; export function isSharePointUrl(url) { if (!url) { return false; } try { const urlObj = new URL(url); const hostname = urlObj.hostname.toLowerCase(); const protocol = urlObj.protocol.toLowerCase(); // Only accept HTTPS URLs for security if (protocol !== 'https:') { return false; } // Check if hostname ends with the trusted domains (not just contains them) return hostname.endsWith('sharepoint.com') || hostname.endsWith('onedrive.com') || hostname.endsWith('onedrive.live.com') || enableNewDomainCheckToImproveSmartLinkResolveRate(hostname); } catch { // If URL parsing fails, return false for safety return false; } } export function createPlugin(schema, dispatchAnalyticsEvent, dispatch, featureFlags, pluginInjectionApi, getIntl, cardOptions, sanitizePrivateContent, providerFactory, pasteWarningOptions) { var _pluginInjectionApi$a; const editorAnalyticsAPI = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions; const atlassianMarkDownParser = new MarkdownTransformer(schema, md); function getMarkdownSlice(text, openStart, openEnd) { const escapedTextInput = escapeBackslashAndLinksExceptCodeBlock(text); const doc = atlassianMarkDownParser.parse(escapedTextInput); if (doc && doc.content) { return new Slice(doc.content, openStart, openEnd); } return; } let extensionAutoConverter; async function setExtensionAutoConverter(name, extensionProviderPromise) { if (name !== 'extensionProvider' || !extensionProviderPromise) { return; } try { extensionAutoConverter = await getExtensionAutoConvertersFromProvider(extensionProviderPromise); } catch (e) { // eslint-disable-next-line no-console console.error(e); } } if (providerFactory) { providerFactory.subscribe('extensionProvider', setExtensionAutoConverter); } let mostRecentPasteEvent; let pastedFromBitBucket = false; return new SafePlugin({ key: stateKey, state: createPluginState(dispatch, { activeFlag: null, pastedMacroPositions: {}, lastContentPasted: null }), props: { // For serialising to plain text clipboardTextSerializer: createClipboardTextSerializer(getIntl()), handleDOMEvents: { // note paste: (view, event) => { mostRecentPasteEvent = event; if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true) && event.clipboardData) { insm.startHeavyTask('paste'); } return false; } }, // note handlePaste(view, rawEvent, slice) { var _text, _schema$nodes, _schema$nodes2, _schema$nodes3, _pluginInjectionApi$m, _schema$nodes$table; const event = rawEvent; if (!event.clipboardData) { return false; } let text = event.clipboardData.getData('text/plain'); const html = event.clipboardData.getData('text/html'); const uriList = event.clipboardData.getData('text/uri-list'); // Extract clientId values from pasted HTML for media cross-product copy/paste // This must be done before ProseMirror parses the HTML, as clientId is not stored in ADF if (fg('platform_media_cross_client_copy_with_auth')) { extractClientIdsFromHtml(html); } // Links copied from iOS Safari share button only have the text/uri-list data type // ProseMirror don't do anything with this type so we want to make our own open slice // with url as text content so link is pasted inline if (uriList && !text && !html) { text = uriList; slice = new Slice(Fragment.from(schema.text(text)), 1, 1); } if ((_text = text) !== null && _text !== void 0 && _text.includes('\r')) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp text = text.replace(/\r/g, ''); } // Strip Legacy Content Macro (LCM) extensions on paste if (!fg('platform_editor_legacy_content_macro_insert')) { slice = transformSliceToRemoveLegacyContentMacro(slice, schema); } const isPastedFile = isPastedFileFromEvent(event); const isPlainText = text && !html; const isRichText = !!html; // Bail if copied content has files if (isPastedFile) { if (!html) { if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { insm.endHeavyTask('paste'); } /** * Microsoft Office, Number, Pages, etc. adds an image to clipboard * with other mime-types so we don't let the event reach media. * The detection ration here is that if the payload has both `html` and * `files`, then it could be one of above or an image copied from web. * Here, we don't have html, so we return true to allow default event behaviour */ return true; } /** * We want to return false for external copied image to allow * it to be uploaded by the client. * * Scenario where we are pasting an external image inside a block quote * is skipped and handled in handleRichText */ if (htmlContainsSingleFile(html) && !isInsideBlockQuote(view.state)) { if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { insm.endHeavyTask('paste'); } return true; } /** * https://product-fabric.atlassian.net/browse/ED-21993 * stopImmediatePropagation will run the first event attached to the same element * Which chould have race condition issue */ event.stopPropagation(); } const { state } = view; const content = getContentNodeTypes(slice.content); // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead const pasteId = uuid(); const measureName = `${PASTE}_${pasteId}`; measureRender(measureName, ({ duration, distortedDuration }) => { const payload = createPasteMeasurePayload({ view, duration, content, distortedDuration }); if (payload) { dispatchAnalyticsEvent(payload); } if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { insm.endHeavyTask('paste'); } }); const getLastPastedSlice = tr => { let slice; for (const step of tr.steps) { const stepSlice = extractSliceFromStep(step); if (stepSlice) { slice = stepSlice; } } return slice; }; // creating a custom dispatch because we want to add a meta whenever we do a paste. const dispatch = tr => { var _state$doc$resolve$no; // https://product-fabric.atlassian.net/browse/ED-12633 // don't add closeHistory call if we're pasting a text inside placeholder text as we want the whole action // to be atomic const { placeholder } = state.schema.nodes; const isPastingTextInsidePlaceholderText = ((_state$doc$resolve$no = state.doc.resolve(state.selection.$anchor.pos).nodeAfter) === null || _state$doc$resolve$no === void 0 ? void 0 : _state$doc$resolve$no.type) === placeholder; // Don't add closeHistory if we're pasting over layout columns, as we will appendTransaction // to cleanup the layout's structure and we want to keep the paste and re-structuring as // one event. const isPastingOverLayoutColumns = hasParentNodeOfType(state.schema.nodes.layoutColumn)(state.selection); // don't add closeHistory call if we're pasting a table, as some tables may involve additional // appendedTransactions to repair them (if they're partial or incomplete) and we don't want // to split those repairing transactions in prosemirror-history when they're being added to the // "done" stack const isPastingTable = tr.steps.some(step => { var _slice$content; const slice = extractSliceFromStep(step); let tableExists = false; slice === null || slice === void 0 ? void 0 : (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.forEach(node => { if (node.type === state.schema.nodes.table) { tableExists = true; } }); return tableExists; }); // Don't flag as a paste event (add closeHistory) when the paste affects a list and // the list plugin's appendTransaction will normalise the structure, and we want the // paste + normalisation to be a single undo step. // Pasting into an existing list — selection is inside a list node. let isPastingIntoList = false; // Pasting list content from an external source — slice top-level contains a list node. let isPastingListContent = false; if (expValEqualsNoExposure('platform_editor_flexible_list_schema', 'isEnabled', true)) { const listNodeTypes = [state.schema.nodes.bulletList, state.schema.nodes.orderedList, state.schema.nodes.taskList].filter(n => Boolean(n)); isPastingIntoList = hasParentNodeOfType(listNodeTypes)(state.selection); for (let i = 0; i < slice.content.childCount; i++) { if (listNodeTypes.includes(slice.content.child(i).type)) { isPastingListContent = true; break; } } } if (!isPastingTextInsidePlaceholderText && !isPastingTable && !isPastingOverLayoutColumns && !isPastingIntoList && !isPastingListContent && pluginInjectionApi !== null && pluginInjectionApi !== void 0 && pluginInjectionApi.betterTypeHistory) { var _pluginInjectionApi$b; tr = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$b = pluginInjectionApi.betterTypeHistory) === null || _pluginInjectionApi$b === void 0 ? void 0 : _pluginInjectionApi$b.actions.flagPasteEvent(tr); } const isDocChanged = tr.docChanged; addLinkMetadata(view.state.selection, tr, { action: isPlainText ? ACTION.PASTED_AS_PLAIN : ACTION.PASTED, inputMethod: INPUT_METHOD.CLIPBOARD }); // handleMacroAutoConvert dispatches twice // we make sure to call paste options toolbar // only for a valid paste action if (isDocChanged) { const pastedSlice = getLastPastedSlice(tr); if (pastedSlice) { var _input; const pasteStartPos = state.selection.from; const pasteEndPos = tr.selection.to; const contentPasted = { pasteStartPos, pasteEndPos, text, isShiftPressed: Boolean( // eslint-disable-next-line @typescript-eslint/no-explicit-any view.shiftKey || ((_input = view.input) === null || _input === void 0 ? void 0 : _input.shiftKey)), isPlainText: Boolean(isPlainText), pastedSlice, pastedAt: Date.now(), pasteSource: getPasteSource(event) }; tr.setMeta(stateKey, { type: PastePluginActionTypes.ON_PASTE, contentPasted }); } } // the handlePaste definition overrides the generic prosemirror behaviour which would previously // include a uiEvent meta of paste. To align with the docs (https://prosemirror.net/docs/ref/#state.Transaction) // This will re-add the uiEvent meta. view.dispatch(tr.setMeta('uiEvent', 'paste')); }; slice = handleParagraphBlockMarks(state, slice); slice = handleVSCodeBlock({ state, slice, event, text }); if (editorExperiment('platform_synced_block', true)) { slice = handleSyncBlocksPaste(slice, schema, getPasteSource(event), html, pasteWarningOptions, pluginInjectionApi); } const plainTextPasteSlice = linkifyContent(state.schema)(slice); if (handlePasteAsPlainTextWithAnalytics(editorAnalyticsAPI)(view, event, plainTextPasteSlice)(state, dispatch, view)) { return true; } if (handlePasteIntoCaptionWithAnalytics(editorAnalyticsAPI)(view, event, slice, PasteTypes.richText)(state, dispatch)) { // Create a custom handler to avoid handling with handleRichText method // As SafeInsert is used inside handleRichText which caused some bad UX like this: // https://product-fabric.atlassian.net/browse/MEX-1520 // Converting caption to plain text needs to be handled before transformSliceForMedia // as createChecked will fail when trying to create a mediaSingle node with a caption // that is not plain text. return true; } // transform slices based on destination slice = transformSliceForMedia(slice, schema, pluginInjectionApi)(state.selection); let markdownSlice; if (isPlainText) { var _markdownSlice; markdownSlice = getMarkdownSlice(text, slice.openStart, slice.openEnd); // https://product-fabric.atlassian.net/browse/ED-15134 // Lists are not allowed within Blockquotes at this time. Attempting to // paste a markdown list ie. ">- foo" will yeild a markdownSlice of size 0. // Rather then blocking the paste action with no UI feedback, this will instead // force a "paste as plain text" action by clearing the markdownSlice. markdownSlice = !((_markdownSlice = markdownSlice) !== null && _markdownSlice !== void 0 && _markdownSlice.size) ? undefined : markdownSlice; if (markdownSlice) { var _pluginInjectionApi$c, _pluginInjectionApi$c2, _pluginInjectionApi$e, _pluginInjectionApi$e2; // linkify text prior to converting to macro if (handlePasteLinkOnSelectedTextWithAnalytics(editorAnalyticsAPI)(view, event, markdownSlice, PasteTypes.markdown)(state, dispatch)) { return true; } // run macro autoconvert prior to other conversions if (handleMacroAutoConvert(text, markdownSlice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.card) === null || _pluginInjectionApi$c === void 0 ? void 0 : (_pluginInjectionApi$c2 = _pluginInjectionApi$c.actions) === null || _pluginInjectionApi$c2 === void 0 ? void 0 : _pluginInjectionApi$c2.queueCardsFromChangedTr, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e = pluginInjectionApi.extension) === null || _pluginInjectionApi$e === void 0 ? void 0 : (_pluginInjectionApi$e2 = _pluginInjectionApi$e.actions) === null || _pluginInjectionApi$e2 === void 0 ? void 0 : _pluginInjectionApi$e2.runMacroAutoConvert, cardOptions, extensionAutoConverter)(state, dispatch, view)) { // TODO: ED-26959 - handleMacroAutoConvert dispatch twice, so we can't use the helper sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, markdownSlice, { type: PasteTypes.markdown }); return true; } } } slice = transformUnsupportedBlockCardToInline(slice, state, cardOptions); // Handles edge case so that when copying text from the top level of the document // it can be pasted into nodes like panels/actions/decisions without removing them. // Overriding openStart to be 1 when only pasting a paragraph makes the preferred // depth favour the text, rather than the paragraph node. // https://github.com/ProseMirror/prosemirror-transform/blob/master/src/replace.js#:~:text=Transform.prototype.-,replaceRange,-%3D%20function(from%2C%20to const selectionDepth = state.selection.$head.depth; const selectionParentNode = state.selection.$head.node(selectionDepth - 1); const selectionParentType = selectionParentNode === null || selectionParentNode === void 0 ? void 0 : selectionParentNode.type; const edgeCaseNodeTypes = [(_schema$nodes = schema.nodes) === null || _schema$nodes === void 0 ? void 0 : _schema$nodes.panel, (_schema$nodes2 = schema.nodes) === null || _schema$nodes2 === void 0 ? void 0 : _schema$nodes2.taskList, (_schema$nodes3 = schema.nodes) === null || _schema$nodes3 === void 0 ? void 0 : _schema$nodes3.decisionList]; if (slice.openStart === 0 && slice.openEnd !== 1 && selectionParentNode && edgeCaseNodeTypes.includes(selectionParentType)) { // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop slice.openStart = 1; } // If we're in a code block, append the text contents of clipboard inside it if (handleCodeBlockWithAnalytics(editorAnalyticsAPI)(view, event, slice, text)(state, dispatch)) { return true; } if (handleMediaSingleWithAnalytics(editorAnalyticsAPI)(view, event, slice, isPastedFile ? PasteTypes.binary : PasteTypes.richText, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$m = pluginInjectionApi.media) === null || _pluginInjectionApi$m === void 0 ? void 0 : _pluginInjectionApi$m.actions.insertMediaAsMediaSingle)(state, dispatch, view)) { return true; } if (handleSelectedTableWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) { return true; } let isNestedMarkdownTable = false; // if paste a markdown table inside a table cell, we should treat it as a table slice const isParentNodeTdOrTh = selectionParentType === schema.nodes.tableCell || selectionParentType === schema.nodes.tableHeader; isNestedMarkdownTable = !!(markdownSlice && isPlainText && isParentNodeTdOrTh && getContentNodeTypes(markdownSlice.content).includes((_schema$nodes$table = schema.nodes.table) === null || _schema$nodes$table === void 0 ? void 0 : _schema$nodes$table.name)); slice = isNestedMarkdownTable ? markdownSlice : slice; // get editor-tables to handle pasting tables if it can // otherwise, just the replace the selection with the content if (handlePasteTable(view, event, slice, { pasteSource: getPasteSource(event) })) { sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, slice, { type: PasteTypes.richText }); return true; } // handle paste of nested tables to ensure nesting limits are respected if (handleNestedTablePasteWithAnalytics(editorAnalyticsAPI, isNestedTablesSupported(state.schema))(view, event, slice)(state, dispatch)) { return true; } if (handlePasteIntoTaskAndDecisionWithAnalytics(view, event, slice, isPlainText ? PasteTypes.plain : PasteTypes.richText, pluginInjectionApi)(state, dispatch)) { return true; } // If the clipboard only contains plain text, attempt to parse it as Markdown if (isPlainText && markdownSlice && !isNestedMarkdownTable) { if (handlePastePreservingMarksWithAnalytics(view, event, markdownSlice, PasteTypes.markdown, pluginInjectionApi)(state, dispatch)) { return true; } return handleMarkdownWithAnalytics(view, event, markdownSlice, pluginInjectionApi)(state, dispatch); } if (isRichText && isInsideBlockQuote(state)) { //If pasting inside blockquote //Skip the blockquote node and keep remaining nodes as they are //prevent doing this if there is list inside blockquote as the list is pasted incorrectly inside blockquote due to wrong openStart and openEnd const { blockquote } = schema.nodes; const children = []; mapChildren(slice.content, node => { if (node.type === blockquote && !contains(node, state.schema.nodes.listItem)) { for (let i = 0; i < node.childCount; i++) { children.push(node.child(i)); } } else { children.push(node); } }); slice = new Slice(Fragment.fromArray(children), slice.openStart, slice.openEnd); } // finally, handle rich-text copy-paste if (isRichText || isNestedMarkdownTable) { var _pluginInjectionApi$c3, _pluginInjectionApi$c4, _pluginInjectionApi$e3, _pluginInjectionApi$e4, _pluginInjectionApi$l; // linkify the text where possible slice = linkifyContent(state.schema)(slice); if (handlePasteLinkOnSelectedTextWithAnalytics(editorAnalyticsAPI)(view, event, slice, PasteTypes.richText)(state, dispatch)) { return true; } // run macro autoconvert prior to other conversions if (handleMacroAutoConvert(text, slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c3 = pluginInjectionApi.card) === null || _pluginInjectionApi$c3 === void 0 ? void 0 : (_pluginInjectionApi$c4 = _pluginInjectionApi$c3.actions) === null || _pluginInjectionApi$c4 === void 0 ? void 0 : _pluginInjectionApi$c4.queueCardsFromChangedTr, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e3 = pluginInjectionApi.extension) === null || _pluginInjectionApi$e3 === void 0 ? void 0 : (_pluginInjectionApi$e4 = _pluginInjectionApi$e3.actions) === null || _pluginInjectionApi$e4 === void 0 ? void 0 : _pluginInjectionApi$e4.runMacroAutoConvert, cardOptions, extensionAutoConverter)(state, dispatch, view)) { // TODO: ED-26959 - handleMacroAutoConvert dispatch twice, so we can't use the helper sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, slice, { type: PasteTypes.richText }); return true; } // Special handling for SharePoint URLs generated from Share button // eslint-disable-next-line @atlaskit/platform/no-preconditioning if (isSharePointUrl(text) && (fg('platform_editor_sharepoint_url_smart_card_fallback') || fg('platform_editor_sharepoint_url_smart_card_jira'))) { // Create an inline card directly for SharePoint URLs to show the "Connect" button const inlineCardNode = schema.nodes.inlineCard.create({ url: text }); const cardSlice = new Slice(Fragment.from(inlineCardNode), 0, 0); if (dispatch) { dispatch(state.tr.replaceSelection(cardSlice)); } return true; } // handle the case when copy content from a table cell inside bodied extension if (handleTableContentPasteInBodiedExtension(slice)(state, dispatch)) { return true; } // remove annotation marks from the pasted data if they are not present in the document // for the cases when they are pasted from external pages if (slice.content.size && containsAnyAnnotations(slice, state)) { var _pluginInjectionApi$a2; pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a2 = pluginInjectionApi.annotation) === null || _pluginInjectionApi$a2 === void 0 ? void 0 : _pluginInjectionApi$a2.actions.stripNonExistingAnnotations(slice, state); } if (handlePastePreservingMarksWithAnalytics(view, event, slice, PasteTypes.richText, pluginInjectionApi)(state, dispatch)) { return true; } // Check that we are pasting in a location that does not accept // breakout marks, if so we strip the mark and paste. Note that // breakout marks are only valid in the root document. if (selectionParentType !== state.schema.nodes.doc) { const sliceCopy = Slice.fromJSON(state.schema, slice.toJSON() || {}); sliceCopy.content.descendants(node => { // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop node.marks = node.marks.filter(mark => mark.type.name !== 'breakout'); // as breakout marks should only be on top level nodes, // we don't traverse the entire document return false; }); slice = sliceCopy; } if (handleExpandWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) { return true; } if (!insideTable(state)) { slice = transformSliceNestedExpandToExpand(slice, state.schema); } if (handlePastePanelOrDecisionIntoListWithAnalytics(editorAnalyticsAPI)(view, event, slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$l = pluginInjectionApi.list) === null || _pluginInjectionApi$l === void 0 ? void 0 : _pluginInjectionApi$l.actions.findRootParentListNode)(state, dispatch)) { return true; } if (handlePasteNonNestableBlockNodesIntoListWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) { return true; } return handleRichTextWithAnalytics(view, event, slice, pluginInjectionApi)(state, dispatch); } return false; }, transformPasted(slice) { var _pluginInjectionApi$e5, _pluginInjectionApi$e6, _pluginInjectionApi$e7; if (sanitizePrivateContent) { slice = handleMention(slice, schema); } /* Bitbucket copies diffs as multiple adjacent code blocks * so we merge ALL adjacent code blocks to support paste here */ if (pastedFromBitBucket) { slice = transformSliceToJoinAdjacentCodeBlocks(slice); } // Filter out expand nodes if allowExpand is false if (!(pluginInjectionApi !== null && pluginInjectionApi !== void 0 && (_pluginInjectionApi$e5 = pluginInjectionApi.expand) !== null && _pluginInjectionApi$e5 !== void 0 && (_pluginInjectionApi$e6 = _pluginInjectionApi$e5.sharedState) !== null && _pluginInjectionApi$e6 !== void 0 && (_pluginInjectionApi$e7 = _pluginInjectionApi$e6.currentState()) !== null && _pluginInjectionApi$e7 !== void 0 && _pluginInjectionApi$e7.allowInsertion) && expValEquals('platform_editor_expand_paste_in_comment_editor', 'isEnabled', true)) { slice = handlePasteExpand(slice); } slice = transformSingleLineCodeBlockToCodeMark(slice, schema); slice = transformSliceToCorrectMediaWrapper(slice, schema); slice = transformSliceToMediaSingleWithNewExperience(slice, schema, pluginInjectionApi); slice = transformSliceToDecisionList(slice, schema); // splitting linebreaks into paragraphs must happen before upgrading text to lists slice = splitParagraphs(slice, schema); slice = upgradeTextToLists(slice, schema); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (slice.content.childCount && slice.content.lastChild.type === schema.nodes.codeBlock) { slice = new Slice(slice.content, 0, 0); } if (!editorExperiment('single_column_layouts', true)) { slice = transformSingleColumnLayout(slice, schema); } slice = transformSliceToRemoveMacroId(slice, schema); if (expValEquals('platform_editor_flexible_list_schema', 'isEnabled', true) && !expValEquals('platform_editor_flexible_list_indentation', 'isEnabled', true)) { // Prevent pasted externally-authored flexible list HTML from producing flexible list structures // Only when schema support is enabled but indentation behaviour is not, meaning editor gracefully // handles the structure, but ideally does not produce it slice = transformSliceEnsureListItemParagraphFirst(slice, schema); } return slice; }, transformPastedHTML(html) { // Fix for issue ED-4438 // text from google docs should not be pasted as inline code if (html.indexOf('id="docs-internal-guid-') >= 0) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp html = html.replace(/white-space:pre/g, ''); // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp html = html.replace(/white-space:pre-wrap/g, ''); } // Partial fix for ED-7331: During a copy/paste from the legacy tinyMCE // confluence editor, if we encounter an incomplete table (e.g. table elements // not wrapped in <table>), we try to rebuild a complete, valid table if possible. if (mostRecentPasteEvent && isPastedFromTinyMCEConfluence(mostRecentPasteEvent, html) && htmlHasIncompleteTable(html)) { const completeTableHtml = tryRebuildCompleteTableHtml(html); if (completeTableHtml) { html = completeTableHtml; } } if (!isPastedFromWord(html) && !isPastedFromExcel(html) && html.indexOf('<img ') >= 0) { html = unwrapNestedMediaElements(html); } // https://product-fabric.atlassian.net/browse/ED-11714 // Checking for edge case when copying a list item containing links from Notion // The html from this case is invalid with duplicate nested links if (htmlHasInvalidLinkTags(html)) { html = removeDuplicateInvalidLinks(html); } // Fix for ED-13568: Code blocks being copied/pasted when next to each other get merged pastedFromBitBucket = html.indexOf('data-qa="code-line"') >= 0; // Remove breakout marks HTML around sync block renderer nodes // so the breakout mark doesn't get applied to the wrong nodes if (html.indexOf(SyncBlockRendererDataAttributeName) >= 0 && editorExperiment('platform_synced_block', true)) { html = removeBreakoutFromRendererSyncBlockHTML(html); } mostRecentPasteEvent = null; return html; } } }); }