UNPKG

@atlaskit/editor-plugin-paste

Version:

Paste plugin for @atlaskit/editor-core

142 lines (135 loc) 7.07 kB
import { uuid } from '@atlaskit/adf-schema'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { mapSlice } from '@atlaskit/editor-common/utils'; import { PastePluginActionTypes } from '../../editor-actions/actions'; import { FLAG_TYPE } from '../../pastePluginType'; import { pluginKey } from '../../pm-plugins/plugin-factory'; // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp const RESOURCE_ID_REGEX = /data-resource-id="(confluence-page|jira-work-item)\//; var FLAG_ID = /*#__PURE__*/function (FLAG_ID) { FLAG_ID["CANNOT_PASTE_CONTENT"] = "cannot-paste-content"; return FLAG_ID; }(FLAG_ID || {}); const transformSyncBlockNode = (node, schema, isFromEditor) => { // if copying from renderer, flatten out the content and remove the sync block if (!isFromEditor) { return node.content; } // sync blocks need a unique localId to function correctly const newAttrs = { ...node.attrs, localId: uuid.generate() }; return schema.nodes.syncBlock.create(newAttrs, null, [...node.marks]); }; const transformBodiedSyncBlockNode = (node, isFromEditor) => { // if copying from renderer, flatten out the content and remove the bodied sync block if (!isFromEditor) { return node.content; } // this is not possible as all bodiedSyncBlocks have already been converted into a syncBlock by now. return node; }; const showWarningFlag = ({ api, title, description, urlText, urlHref }) => { // Use setTimeout to dispatch transaction in next tick and avoid re-entrant dispatch setTimeout(() => { api === null || api === void 0 ? void 0 : api.core.actions.execute(({ tr }) => { const flag = { id: FLAG_ID.CANNOT_PASTE_CONTENT, description, title, urlText, urlHref, type: FLAG_TYPE.WARNING }; return tr.setMeta(pluginKey, { type: PastePluginActionTypes.SET_ACTIVE_FLAG, activeFlag: flag }); }); }, 0); }; // Check if rawHtml contains a synced block // example: "<meta charset='utf-8'><html><head></head><body><div data-sync-block=\"\" data-local-id=\"\" data-resource-id=\"d64883c8-1270-431d-a1d3-51d36a1ed5f4\" data-prosemirror-content-type=\"node\" data-prosemirror-node-name=\"syncBlock\" data-prosemirror-node-block=\"true\" data-pm-slice=\"0 0 []\"></div></body></html>" const hasSyncedBlockInRawHtml = rawHtml => { return Boolean(rawHtml === null || rawHtml === void 0 ? void 0 : rawHtml.includes('data-sync-block="')); }; /** * Extracts the source product of a synced block reference from the pasted raw HTML. * * A reference block's `data-resource-id` is prefixed with the source product, e.g. * `confluence-page/5769323474/cdf6a1bc-...`. We only ever emit a controlled, * enumerated value (`confluence-page` | `jira-work-item`) so the resulting * analytics attribute can never carry user-generated content. Returns `undefined` * when the marker is missing or the prefix is unrecognised. * * Note: this intentionally mirrors `getSourceProductFromResourceIdSafe` from * `@atlaskit/editor-synced-block-provider/utils` but is duplicated locally to * avoid adding a cross-package dependency from the paste plugin onto the * synced-block provider. */ const getSourceProductFromRawHtml = rawHtml => { // The capture group already constrains the match to exactly these two literals, // so the assertion is safe. This mirrors `getContentIdAndProductFromResourceId` // in `@atlaskit/editor-synced-block-provider`. const match = rawHtml.match(RESOURCE_ID_REGEX); return match === null || match === void 0 ? void 0 : match[1]; }; /** * If we are copying from editor, transform the copied source or reference sync block to a new reference sync block * Otherwise, (e.g. if copying from renderer), flatten out the content and remove the sync block * Also, show a warning flag if the pasted content contains a synced block and the paste warning options are configured. */ export const handleSyncBlocksPaste = (slice, schema, pasteSource, rawHtml, pasteWarningOptions, api) => { const isFromEditor = pasteSource === 'fabric-editor'; const isSyncedBlockInRawHtml = hasSyncedBlockInRawHtml(rawHtml); let hasSyncedBlockInSlice = false; slice = mapSlice(slice, node => { if (node.type === schema.nodes.syncBlock) { hasSyncedBlockInSlice = true; return transformSyncBlockNode(node, schema, isFromEditor); } else if (node.type === schema.nodes.bodiedSyncBlock) { hasSyncedBlockInSlice = true; return transformBodiedSyncBlockNode(node, isFromEditor); } return node; }); // A synced block reference was on the clipboard (`isSyncedBlockInRawHtml`) but the // destination schema dropped it (`!hasSyncedBlockInSlice`) — i.e. the user attempted // to insert a synced block into a surface that does not support synced blocks (e.g. // Bitbucket). Emit a track event so this can be measured directly instead of relying // on the "copied-but-never-landed" proxy. See EDITOR-7749. if (!hasSyncedBlockInSlice && isSyncedBlockInRawHtml) { var _api$analytics, _api$analytics$action; api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent({ eventType: EVENT_TYPE.TRACK, action: ACTION.INSERT_ATTEMPTED, actionSubject: ACTION_SUBJECT.SYNCED_BLOCK, actionSubjectId: ACTION_SUBJECT_ID.UNSUPPORTED_SURFACE, attributes: { sourceProduct: getSourceProductFromRawHtml(rawHtml), pasteSource } }); if (pasteWarningOptions !== null && pasteWarningOptions !== void 0 && pasteWarningOptions.cannotPasteSyncedBlock) { var _pasteWarningOptions$, _pasteWarningOptions$2, _pasteWarningOptions$3, _pasteWarningOptions$4; showWarningFlag({ api, title: pasteWarningOptions === null || pasteWarningOptions === void 0 ? void 0 : (_pasteWarningOptions$ = pasteWarningOptions.cannotPasteSyncedBlock) === null || _pasteWarningOptions$ === void 0 ? void 0 : _pasteWarningOptions$.title, description: pasteWarningOptions === null || pasteWarningOptions === void 0 ? void 0 : (_pasteWarningOptions$2 = pasteWarningOptions.cannotPasteSyncedBlock) === null || _pasteWarningOptions$2 === void 0 ? void 0 : _pasteWarningOptions$2.description, urlText: pasteWarningOptions === null || pasteWarningOptions === void 0 ? void 0 : (_pasteWarningOptions$3 = pasteWarningOptions.cannotPasteSyncedBlock) === null || _pasteWarningOptions$3 === void 0 ? void 0 : _pasteWarningOptions$3.urlText, urlHref: pasteWarningOptions === null || pasteWarningOptions === void 0 ? void 0 : (_pasteWarningOptions$4 = pasteWarningOptions.cannotPasteSyncedBlock) === null || _pasteWarningOptions$4 === void 0 ? void 0 : _pasteWarningOptions$4.urlHref }); } } return slice; };