UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

208 lines (201 loc) • 10.5 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import { FLAG_ID } from '../../types'; import { syncedBlockPluginKey } from '../main'; import { matchesRequestedBlock, recomputeDeleteTransaction, recomputeUnsyncTransaction } from './recompute-delete-transaction'; var onRetry = function onRetry(api, syncBlockStore) { return function () { var _api$core; api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref) { var tr = _ref.tr; return tr.setMeta(syncedBlockPluginKey, { bodiedSyncBlockDeletionStatus: 'processing', activeFlag: false }); }); syncBlockStore.sourceManager.retryDeletion(); }; }; var onDismissed = function onDismissed(syncBlockStore) { return function (tr) { syncBlockStore.sourceManager.clearPendingDeletion(); return tr.setMeta(syncedBlockPluginKey, { bodiedSyncBlockDeletionStatus: 'none' }); }; }; var topLevelSourceBlocks = function topLevelSourceBlocks(doc, isSourceBlock) { var blocks = []; doc.forEach(function (node) { if (isSourceBlock(node)) { blocks.push(node); } }); return blocks; }; /** * Whether `tr` removes every block in `wanted` from the document. Identity follows * `matchesRequestedBlock` (localId, falling back to resourceId) so this agrees with the * recompute helpers. */ var removesRequestedBlocks = function removesRequestedBlocks(tr, isSourceBlock, wanted) { var before = topLevelSourceBlocks(tr.before, isSourceBlock); var after = topLevelSourceBlocks(tr.doc, isSourceBlock); return wanted.every(function (block) { return before.some(function (node) { return matchesRequestedBlock(node, block); }) && !after.some(function (node) { return matchesRequestedBlock(node, block); }); }); }; /** * Replay the undo that removed `wanted` once the source deletion has been confirmed and * persisted (EDITOR-8863). * * The original undo transaction was rejected by `filterTransaction` so the confirmation could * run, which leaves the history event on top of the undo stack. Building the undo again from * the live state (via the history plugin) restores whatever the block was converted from * (e.g. a table), unlike a recomputed whole-node delete which drops that content. * * Guard: only dispatch if that undo still removes the same source block(s). If the user * edited in the meantime the top of the stack is no longer that event, so return `false` and * let the caller fall back to the recomputed unwrap. */ var replayConfirmedUndo = function replayConfirmedUndo(api, isSourceBlock, wanted, onTransaction) { var _api$history; var undo = api === null || api === void 0 || (_api$history = api.history) === null || _api$history === void 0 ? void 0 : _api$history.commands.undo; if (!undo || !(api !== null && api !== void 0 && api.core)) { return false; } var applied = false; api.core.actions.execute(function (_ref2) { var tr = _ref2.tr; var undoTr = undo({ tr: tr }); if (!undoTr || !removesRequestedBlocks(undoTr, isSourceBlock, wanted)) { return null; } undoTr.setMeta('isConfirmedSyncBlockDeletion', true); onTransaction(undoTr); applied = true; return undoTr; }); return applied; }; export var handleBodiedSyncBlockRemoval = function handleBodiedSyncBlockRemoval(_ref3) { var api = _ref3.api, confirmationTransactionRef = _ref3.confirmationTransactionRef, deletionReason = _ref3.deletionReason, feedbackCallbacks = _ref3.feedbackCallbacks, mechanism = _ref3.mechanism, removed = _ref3.removed, syncBlockStore = _ref3.syncBlockStore; var removedAttrs = removed.map(function (node) { return node.attrs; }); var isSourceBlock = syncBlockStore.sourceManager.isSourceBlock; // Clear potential old pending deletion to retreat the deletion as first attempt syncBlockStore.sourceManager.clearPendingDeletion(); // If there are source sync blocks being removed, and we need to confirm with user before deleting, // we block the transaction here, and wait for user confirmation to proceed with deletion. // See editor-common/src/sync-block/sync-block-store-manager.ts for how we handle user confirmation and // proceed with deletion. syncBlockStore.sourceManager.deleteSyncBlocksWithConfirmation(removedAttrs, deletionReason, function () { var _api$core2; var closeBlockMenu = function closeBlockMenu(confirmedTr) { if (isExperimentEnabled('platform_editor_blocks_exp_patch_10')) { var _api$blockControls; // The original transaction's closeMenu meta was rejected along with it above, // so re-apply it here to close the block menu once the change actually lands. api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 || (_api$blockControls = _api$blockControls.commands) === null || _api$blockControls === void 0 || _api$blockControls.toggleBlockMenu({ closeMenu: true })({ tr: confirmedTr }); } }; // Any undo that removes a source block (creation, paste, ...) must restore what was // there before. A recomputed whole-node delete drops the converted content and leaves // the event on the undo stack, so replay the undo instead (EDITOR-8863). var shouldReplayUndo = mechanism === 'undo' && isExperimentEnabled('platform_editor_blocks_exp_patch_10'); if (shouldReplayUndo && replayConfirmedUndo(api, isSourceBlock, removedAttrs, function (undoTr) { closeBlockMenu(undoTr); feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(undoTr); })) { return; } // Recompute the delete fresh from the live document instead of // replaying a stashed-and-rebased transaction. The stash/rebase // approach produced stale, schema-invalid transactions when the // document changed shape (local edits or remote collab) while the // confirmation modal was open — the dominant signature being // "Invalid content for node bodiedSyncBlock: <>". See EDITOR-7889. api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref4) { var tr = _ref4.tr; // EDITOR-8230: unsync must remove only the sync wrapper and keep the block's // content inline, whereas delete removes the whole block. Before this branch, // the confirm path always recomputed a full delete, so unsyncing a source block // silently deleted its content. When the deletion was actually triggered by // unsync, recompute an unwrap (replaceWith content) instead of a delete. // The same applies when the undo could not be replayed (the undo stack changed // while the backend delete was in flight): the backend delete has already // succeeded, so drop the wrapper but keep the content (EDITOR-8863). var recomputedTr = deletionReason === 'source-block-unsynced' || shouldReplayUndo ? recomputeUnsyncTransaction(tr, isSourceBlock, removedAttrs) : recomputeDeleteTransaction(tr, isSourceBlock, removedAttrs); // The target node(s) no longer exist in the live document (e.g. // a remote collaborator already removed them). There is nothing // to delete locally, so return `null` to skip the dispatch // entirely — dispatching the untouched transaction would be a // no-op that still runs the plugin's filter/append hooks. The // backend deletion has already been issued by the store manager. if (!recomputedTr) { return null; } closeBlockMenu(recomputedTr); recomputedTr.setMeta('isConfirmedSyncBlockDeletion', true); feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteTransaction(recomputedTr); if (!recomputedTr.getMeta(pmHistoryPluginKey)) { // bodiedSyncBlock deletion is expected to be permanent (cannot undo) // For a normal deletion (not triggered by undo), remove it from history so that it cannot be undone recomputedTr.setMeta('addToHistory', false); } return recomputedTr; }); }, function (success) { var _api$core3; feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDeleteCompleted(success); api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref5) { var tr = _ref5.tr; var newState; if (!success) { newState = { activeFlag: { id: FLAG_ID.FAIL_TO_DELETE, onRetry: onRetry(api, syncBlockStore), onDismissed: onDismissed(syncBlockStore) } }; } else { newState = { activeFlag: false }; } newState = _objectSpread(_objectSpread({}, newState), {}, { bodiedSyncBlockDeletionStatus: syncBlockStore.sourceManager.isRetryingDeletion() ? // For retry, reset to none directly to clean up the status 'none' : // For the first attempt, set to completed for deletion modal can close the modal 'completed' }); return tr.setMeta(syncedBlockPluginKey, newState); }); }, function () { confirmationTransactionRef.current = undefined; feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDestroy(); }, mechanism); return false; };