@atlaskit/editor-plugin-paste
Version:
Paste plugin for @atlaskit/editor-core
379 lines (376 loc) • 18.2 kB
JavaScript
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);
};