@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
215 lines (207 loc) • 10.9 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.handleBodiedSyncBlockRemoval = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _utils = require("@atlaskit/editor-common/utils");
var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
var _types = require("../../types");
var _main = require("../main");
var _recomputeDeleteTransaction = require("./recompute-delete-transaction");
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) { (0, _defineProperty2.default)(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; }
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(_main.syncedBlockPluginKey, {
bodiedSyncBlockDeletionStatus: 'processing',
activeFlag: false
});
});
syncBlockStore.sourceManager.retryDeletion();
};
};
var onDismissed = function onDismissed(syncBlockStore) {
return function (tr) {
syncBlockStore.sourceManager.clearPendingDeletion();
return tr.setMeta(_main.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 (0, _recomputeDeleteTransaction.matchesRequestedBlock)(node, block);
}) && !after.some(function (node) {
return (0, _recomputeDeleteTransaction.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;
};
var handleBodiedSyncBlockRemoval = exports.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 ((0, _isExperimentEnabled.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' && (0, _isExperimentEnabled.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 ? (0, _recomputeDeleteTransaction.recomputeUnsyncTransaction)(tr, isSourceBlock, removedAttrs) : (0, _recomputeDeleteTransaction.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(_utils.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: _types.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(_main.syncedBlockPluginKey, newState);
});
}, function () {
confirmationTransactionRef.current = undefined;
feedbackCallbacks === null || feedbackCallbacks === void 0 || feedbackCallbacks.onDestroy();
}, mechanism);
return false;
};