@atlaskit/editor-plugin-clipboard
Version:
Clipboard plugin for @atlaskit/editor-core
139 lines (132 loc) • 6.84 kB
JavaScript
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;