UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

379 lines (376 loc) 18.2 kB
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD, PasteContents, PasteTypes } from '@atlaskit/editor-common/analytics'; import { getLinkDomain, mapSlice } from '@atlaskit/editor-common/utils'; import { findParentNode } from '@atlaskit/editor-prosemirror/utils'; import { getPasteSource } from './util'; import { handleCodeBlock, handleExpandPaste, handleMarkdown, handleMediaSingle, handlePasteAsPlainText, handlePasteIntoCaption, handlePasteIntoTaskOrDecisionOrPanel, handlePasteLinkOnSelectedText, handlePasteNonNestableBlockNodesIntoList, handlePastePanelOrDecisionContentIntoList, handlePastePreservingMarks, handleRichText, handleSelectedTable, handleNestedTablePaste } from './util/handlers'; const contentToPasteContent = { url: PasteContents.url, paragraph: PasteContents.text, bulletList: PasteContents.bulletList, orderedList: PasteContents.orderedList, heading: PasteContents.heading, blockquote: PasteContents.blockquote, codeBlock: PasteContents.codeBlock, panel: PasteContents.panel, rule: PasteContents.rule, mediaSingle: PasteContents.mediaSingle, mediaCard: PasteContents.mediaCard, mediaGroup: PasteContents.mediaGroup, table: PasteContents.table, tableCells: PasteContents.tableCells, tableHeader: PasteContents.tableHeader, tableRow: PasteContents.tableRow, decisionList: PasteContents.decisionList, decisionItem: PasteContents.decisionItem, taskList: PasteContents.taskItem, extension: PasteContents.extension, bodiedExtension: PasteContents.bodiedExtension, blockCard: PasteContents.blockCard, layoutSection: PasteContents.layoutSection }; const nodeToActionSubjectId = { blockquote: ACTION_SUBJECT_ID.PASTE_BLOCKQUOTE, blockCard: ACTION_SUBJECT_ID.PASTE_BLOCK_CARD, bodiedExtension: ACTION_SUBJECT_ID.PASTE_BODIED_EXTENSION, bulletList: ACTION_SUBJECT_ID.PASTE_BULLET_LIST, codeBlock: ACTION_SUBJECT_ID.PASTE_CODE_BLOCK, decisionList: ACTION_SUBJECT_ID.PASTE_DECISION_LIST, extension: ACTION_SUBJECT_ID.PASTE_EXTENSION, heading: ACTION_SUBJECT_ID.PASTE_HEADING, mediaGroup: ACTION_SUBJECT_ID.PASTE_MEDIA_GROUP, mediaSingle: ACTION_SUBJECT_ID.PASTE_MEDIA_SINGLE, orderedList: ACTION_SUBJECT_ID.PASTE_ORDERED_LIST, panel: ACTION_SUBJECT_ID.PASTE_PANEL, rule: ACTION_SUBJECT_ID.PASTE_RULE, table: ACTION_SUBJECT_ID.PASTE_TABLE, tableCell: ACTION_SUBJECT_ID.PASTE_TABLE_CELL, tableHeader: ACTION_SUBJECT_ID.PASTE_TABLE_HEADER, tableRow: ACTION_SUBJECT_ID.PASTE_TABLE_ROW, taskList: ACTION_SUBJECT_ID.PASTE_TASK_LIST }; export function getContent({ schema, slice }) { const { nodes: { paragraph }, marks: { link } } = schema; const nodeOrMarkName = new Set(); slice.content.forEach(node => { if (node.type === paragraph && node.content.size === 0) { // Skip empty paragraph return; } if (node.type.name === 'text' && link.isInSet(node.marks)) { nodeOrMarkName.add('url'); return; } // Check node contain link if (node.type === paragraph && node.rangeHasMark(0, node.nodeSize - 2, link)) { nodeOrMarkName.add('url'); return; } nodeOrMarkName.add(node.type.name); }); if (nodeOrMarkName.size > 1) { return PasteContents.mixed; } if (nodeOrMarkName.size === 0) { return PasteContents.uncategorized; } const type = nodeOrMarkName.values().next().value; // @ts-ignore - TS2538 TypeScript 5.9.2 upgrade const pasteContent = contentToPasteContent[type]; return pasteContent ? pasteContent : PasteContents.uncategorized; } export function getMediaTraceId(slice) { let traceId; mapSlice(slice, node => { if (node.type.name === 'media' || node.type.name === 'mediaInline') { traceId = node.attrs.__mediaTraceId; } return node; }); return traceId; } function getActionSubjectId({ selection, schema }) { const { nodes: { paragraph, listItem, taskItem, decisionItem } } = schema; const parent = findParentNode(node => { if (node.type !== paragraph && node.type !== listItem && node.type !== taskItem && node.type !== decisionItem) { return true; } return false; })(selection); if (!parent) { return ACTION_SUBJECT_ID.PASTE_PARAGRAPH; } const parentType = parent.node.type; const actionSubjectId = nodeToActionSubjectId[parentType.name]; return actionSubjectId ? actionSubjectId : ACTION_SUBJECT_ID.PASTE_PARAGRAPH; } function createPasteAsPlainPayload(actionSubjectId, text, linksInPasteCount) { return { action: ACTION.PASTED_AS_PLAIN, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId, eventType: EVENT_TYPE.TRACK, attributes: { inputMethod: INPUT_METHOD.KEYBOARD, pasteSize: text.length, linksInPasteCount } }; } function createPastePayload(actionSubjectId, attributes, linkDomain) { return { action: ACTION.PASTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId, eventType: EVENT_TYPE.TRACK, attributes: { inputMethod: INPUT_METHOD.KEYBOARD, ...attributes }, ...(linkDomain && linkDomain.length > 0 ? { nonPrivacySafeAttributes: { linkDomain } } : {}) }; } function createPasteAnalyticsPayloadBySelection(event, slice, pasteContext, pluginInjectionApi) { return selection => { var _pluginInjectionApi$m, _pluginInjectionApi$m2; const text = event.clipboardData ? event.clipboardData.getData('text/plain') || event.clipboardData.getData('text/uri-list') : ''; const actionSubjectId = getActionSubjectId({ selection: selection, schema: selection.$from.doc.type.schema }); const pasteSize = slice.size; const content = getContent({ schema: selection.$from.doc.type.schema, slice }); const linkUrls = []; const mediaTraceId = getMediaTraceId(slice); // If we have a link among the pasted content, grab the // domain and send it up with the analytics event if (content === PasteContents.url || content === PasteContents.mixed) { mapSlice(slice, node => { const linkMark = node.marks.find(mark => mark.type.name === 'link'); if (linkMark) { linkUrls.push(linkMark.attrs.href); } return node; }); } if (pasteContext.asPlain) { return createPasteAsPlainPayload(actionSubjectId, text, linkUrls.length); } const source = getPasteSource(event); const mentionIds = []; const mentionLocalIds = []; slice.content.descendants(node => { if (node.type.name === 'mention') { mentionIds.push(node.attrs.id); mentionLocalIds.push(node.attrs.localId); } }); if (pluginInjectionApi !== null && pluginInjectionApi !== void 0 && (_pluginInjectionApi$m = pluginInjectionApi.mention) !== null && _pluginInjectionApi$m !== void 0 && (_pluginInjectionApi$m2 = _pluginInjectionApi$m.actions) !== null && _pluginInjectionApi$m2 !== void 0 && _pluginInjectionApi$m2.announceMentionsInsertion) { var _pluginInjectionApi$m3, _pluginInjectionApi$m4; const mentionsInserted = []; slice.content.descendants(node => { if (node.type.name === 'mention') { mentionsInserted.push({ type: 'added', id: node.attrs.id, localId: node.attrs.localId, method: 'pasted' }); } if (node.type.name === 'taskItem') { node.content.forEach(nodeContent => { if (nodeContent.type.name === 'mention') { mentionsInserted.push({ type: 'added', localId: nodeContent.attrs.localId, id: nodeContent.attrs.id, taskLocalId: node.attrs.localId, method: 'pasted' }); } }); return false; } }); pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$m3 = pluginInjectionApi.mention) === null || _pluginInjectionApi$m3 === void 0 ? void 0 : (_pluginInjectionApi$m4 = _pluginInjectionApi$m3.actions) === null || _pluginInjectionApi$m4 === void 0 ? void 0 : _pluginInjectionApi$m4.announceMentionsInsertion(mentionsInserted); } if (pasteContext.type === PasteTypes.plain) { return createPastePayload(actionSubjectId, { pasteSize: text.length, type: pasteContext.type, content: PasteContents.text, source, hyperlinkPasteOnText: false, linksInPasteCount: linkUrls.length, mentionIds, mentionLocalIds, pasteSplitList: pasteContext.pasteSplitList }); } const linkDomains = linkUrls.map(getLinkDomain); return createPastePayload(actionSubjectId, { type: pasteContext.type, pasteSize, content, source, hyperlinkPasteOnText: !!pasteContext.hyperlinkPasteOnText, linksInPasteCount: linkUrls.length, mediaTraceId, mentionIds, mentionLocalIds, pasteSplitList: pasteContext.pasteSplitList }, linkDomains); }; } export function createPasteAnalyticsPayload(view, event, slice, pasteContext) { return createPasteAnalyticsPayloadBySelection(event, slice, pasteContext)(view.state.selection); } // TODO: ED-6612 - We should not dispatch only analytics, it's preferred to wrap each command with his own analytics. // However, handlers like handleMacroAutoConvert dispatch multiple time, // so pasteCommandWithAnalytics is useless in this case. export const sendPasteAnalyticsEvent = editorAnalyticsAPI => (view, event, slice, pasteContext) => { const tr = view.state.tr; const payload = createPasteAnalyticsPayload(view, event, slice, pasteContext); editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(tr); view.dispatch(tr); }; export const handlePasteAsPlainTextWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.plain, asPlain: true }))(handlePasteAsPlainText(slice, event, editorAnalyticsAPI)); export const handlePasteIntoTaskAndDecisionWithAnalytics = (view, event, slice, type, pluginInjectionApi) => { var _pluginInjectionApi$a, _pluginInjectionApi$c, _pluginInjectionApi$c2; return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions)(createPasteAnalyticsPayloadBySelection(event, slice, { type }))(handlePasteIntoTaskOrDecisionOrPanel(slice, 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)); }; export const handlePasteIntoCaptionWithAnalytics = editorAnalyticsAPI => (view, event, slice, type) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type }))(handlePasteIntoCaption(slice)); export const handleCodeBlockWithAnalytics = editorAnalyticsAPI => (view, event, slice, text) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.plain }))(handleCodeBlock(text)); export const handleMediaSingleWithAnalytics = editorAnalyticsAPI => (view, event, slice, type, insertMediaAsMediaSingle) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type }))(handleMediaSingle(INPUT_METHOD.CLIPBOARD, insertMediaAsMediaSingle)(slice)); export const handlePastePreservingMarksWithAnalytics = (view, event, slice, type, pluginInjectionApi) => { var _pluginInjectionApi$a2, _pluginInjectionApi$c3, _pluginInjectionApi$c4; return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a2 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a2 === void 0 ? void 0 : _pluginInjectionApi$a2.actions)(createPasteAnalyticsPayloadBySelection(event, slice, { type }))(handlePastePreservingMarks(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)); }; export const handleMarkdownWithAnalytics = (view, event, slice, pluginInjectionApi) => { var _pluginInjectionApi$a3, _pluginInjectionApi$c5, _pluginInjectionApi$c6; return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a3 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : _pluginInjectionApi$a3.actions)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.markdown }))(handleMarkdown(slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c5 = pluginInjectionApi.card) === null || _pluginInjectionApi$c5 === void 0 ? void 0 : (_pluginInjectionApi$c6 = _pluginInjectionApi$c5.actions) === null || _pluginInjectionApi$c6 === void 0 ? void 0 : _pluginInjectionApi$c6.queueCardsFromChangedTr)); }; export const handleRichTextWithAnalytics = (view, event, slice, pluginInjectionApi) => { var _pluginInjectionApi$a4, _pluginInjectionApi$c7, _pluginInjectionApi$c8; return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a4 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.actions)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.richText }, pluginInjectionApi))(handleRichText(slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c7 = pluginInjectionApi.card) === null || _pluginInjectionApi$c7 === void 0 ? void 0 : (_pluginInjectionApi$c8 = _pluginInjectionApi$c7.actions) === null || _pluginInjectionApi$c8 === void 0 ? void 0 : _pluginInjectionApi$c8.queueCardsFromChangedTr)); }; const injectAnalyticsPayloadBeforeCommand = editorAnalyticsAPI => createPayloadByTransaction => { return mainCommand => { return (state, dispatch, view) => { let originalTransaction = state.tr; const fakeDispatch = tr => { originalTransaction = tr; }; const result = mainCommand(state, fakeDispatch, view); if (!result) { return false; } if (dispatch && originalTransaction.docChanged) { // it needs to know the selection before the changes const payload = createPayloadByTransaction(state.selection); editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(originalTransaction); dispatch(originalTransaction); } return true; }; }; }; export const handlePastePanelOrDecisionIntoListWithAnalytics = editorAnalyticsAPI => (view, event, slice, findRootParentListNode) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.richText }))(handlePastePanelOrDecisionContentIntoList(slice, findRootParentListNode)); export const handlePasteNonNestableBlockNodesIntoListWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.richText, pasteSplitList: true }))(handlePasteNonNestableBlockNodesIntoList(slice)); export const handleExpandWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.richText, pasteSplitList: true }))(handleExpandPaste(slice)); export const handleNestedTablePasteWithAnalytics = (editorAnalyticsAPI, isNestingTablesSupported) => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.richText, pasteSplitList: true }))(handleNestedTablePaste(slice, isNestingTablesSupported)); export const handleSelectedTableWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type: PasteTypes.richText }))(handleSelectedTable(editorAnalyticsAPI)(slice)); export const handlePasteLinkOnSelectedTextWithAnalytics = editorAnalyticsAPI => (view, event, slice, type) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, { type, hyperlinkPasteOnText: true }))(handlePasteLinkOnSelectedText(slice)); export const createPasteMeasurePayload = ({ view, duration, content, distortedDuration }) => { const pasteIntoNode = getActionSubjectId({ selection: view.state.selection, schema: view.state.schema }); return { action: ACTION.PASTED_TIMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL, attributes: { pasteIntoNode, content, time: duration, distortedDuration } }; }; export const getContentNodeTypes = content => { let nodeTypes = new Set(); if (content.size) { content.forEach(node => { if (node.content && node.content.size) { nodeTypes = new Set([...nodeTypes, ...getContentNodeTypes(node.content)]); } nodeTypes.add(node.type.name); }); } return Array.from(nodeTypes); };