UNPKG

@atlaskit/editor-plugin-clipboard

Version:

Clipboard plugin for @atlaskit/editor-core

139 lines (132 loc) 6.84 kB
import { ACTION } from '@atlaskit/editor-common/analytics'; import { getAnalyticsPayload } from '@atlaskit/editor-common/clipboard'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { DOMSerializer, Fragment } from '@atlaskit/editor-prosemirror/model'; import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { clipboardPluginKey } from './plugin-key'; export let ClipboardEventType = /*#__PURE__*/function (ClipboardEventType) { ClipboardEventType["CUT"] = "CUT"; ClipboardEventType["COPY"] = "COPY"; return ClipboardEventType; }({}); let lastEventType = null; export const createPlugin = ({ dispatchAnalyticsEvent, schema }) => { let editorView; const getEditorView = () => editorView; return new SafePlugin({ key: clipboardPluginKey, view: view => { editorView = view; return { update: view => { editorView = view; } }; }, props: { handleDOMEvents: { cut: view => { setLastEventType(ClipboardEventType.CUT); return sendClipboardAnalytics(view, dispatchAnalyticsEvent, ACTION.CUT); }, copy: view => { setLastEventType(ClipboardEventType.COPY); return sendClipboardAnalytics(view, dispatchAnalyticsEvent, ACTION.COPIED); } }, clipboardSerializer: createClipboardSerializer(schema, getEditorView) } }); }; /** * Overrides Prosemirror's default clipboardSerializer, in order to fix table row copy/paste bug raised in ED-13003. * This allows us to store the original table’s attributes on the new table that the row is wrapped with when it is being copied. * e.g. keeping the layout on a row that is copied. * We store the default serializer in order to call it after we handle the table row case. */ export const createClipboardSerializer = (schema, getEditorView) => { const oldSerializer = DOMSerializer.fromSchema(schema); const newSerializer = new DOMSerializer(oldSerializer.nodes, oldSerializer.marks); const originalSerializeFragment = newSerializer.serializeFragment.bind(newSerializer); newSerializer.serializeFragment = (content, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any options = {}, target) => { var _content$firstChild, _content$firstChild2; const editorView = getEditorView(); const selection = editorView.state.selection; // We do not need to handle when a user copies a tableRow + other content. // In that scenario it already wraps the Row with correct Table and attributes. if (!options.tableWrapperExists) { let i = 0; while (i < content.childCount) { var _content$maybeChild; if (((_content$maybeChild = content.maybeChild(i)) === null || _content$maybeChild === void 0 ? void 0 : _content$maybeChild.type.name) === 'table') { options.tableWrapperExists = true; break; } i++; } } // When the content being copied includes a tableRow that is not already wrapped with a table, // We will wrap it with one ourselves, while preserving the parent table's attributes. if (((_content$firstChild = content.firstChild) === null || _content$firstChild === void 0 ? void 0 : _content$firstChild.type.name) === 'tableRow' && !options.tableWrapperExists) { // We only want 1 table wrapping the rows. // tableWrapperExist is a custom prop added solely for the purposes of this recursive algorithm. // The function is recursively called for each node in the tree captured in the fragment. // For recursive logic see the bind call above and the prosemirror-model (https://github.com/ProseMirror/prosemirror-model/blob/master/src/to_dom.js#L44 // and https://github.com/ProseMirror/prosemirror-model/blob/master/src/to_dom.js#L87) options.tableWrapperExists = true; const parentTable = findParentNodeOfType(schema.nodes.table)(selection); const attributes = parentTable === null || parentTable === void 0 ? void 0 : parentTable.node.attrs; const newTable = schema.nodes.table; // Explicitly remove local id since we are creating a new table and it should have a unique local id which will be generated. const newTableNode = newTable.createChecked({ ...attributes, localId: undefined }, content); const newContent = Fragment.from(newTableNode); // Pass updated content into original ProseMirror serializeFragment function. // Currently incorrectly typed in @Types. See this GitHub thread: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/57668 //@ts-ignore return originalSerializeFragment(newContent, options, target); } // Remove annotations from media nodes when copying to clipboard, only do this for copy operations // and keep existing content nodes from the parent. if (lastEventType === ClipboardEventType.COPY && ((_content$firstChild2 = content.firstChild) === null || _content$firstChild2 === void 0 ? void 0 : _content$firstChild2.type.name) === 'media') { var _mediaNode$marks; const mediaNode = content.firstChild; const strippedMediaNode = schema.nodes.media.createChecked(mediaNode.attrs, mediaNode.content, (_mediaNode$marks = mediaNode.marks) === null || _mediaNode$marks === void 0 ? void 0 : _mediaNode$marks.filter(mark => mark.type.name !== 'annotation')); // Content for media parents can include multiple content nodes (media and captions). We now take that // into consideration when we are stripping annotations. let contentArray = [strippedMediaNode]; content.forEach(node => { if (node.type.name !== 'media') { contentArray = [...contentArray, node]; } }); const newContent = Fragment.fromArray(contentArray); // Currently incorrectly typed, see comment above // @ts-ignore return originalSerializeFragment(newContent, options, target); } // If we're not copying any rows or media nodes, just run default serializeFragment function. // Currently incorrectly typed in @Types. See this GitHub thread: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/57668 //@ts-ignore return originalSerializeFragment(content, options, target); }; return newSerializer; }; export const sendClipboardAnalytics = (view, dispatchAnalyticsEvent, action) => { const clipboardAnalyticsPayload = getAnalyticsPayload(view.state, action); if (clipboardAnalyticsPayload) { dispatchAnalyticsEvent(clipboardAnalyticsPayload); } // return false so we don't block any other plugins' cut or copy handlers // from running just because we are sending an analytics event return false; }; export const setLastEventType = eventType => lastEventType = eventType;