UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

1,245 lines 67.2 kB
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.syncedBlockPluginKey = exports.getPromptedFeedbackEntryPoint = exports.getDeleteMechanism = exports.deleteMechanismMetaKey = exports.creationMetaKey = exports.createPlugin = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _analytics = require("@atlaskit/editor-common/analytics");
var _collab = require("@atlaskit/editor-common/collab");
var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
var _selection = require("@atlaskit/editor-common/selection");
var _syncBlock = require("@atlaskit/editor-common/sync-block");
var _utils = require("@atlaskit/editor-common/utils");
var _editorPluginConnectivity = require("@atlaskit/editor-plugin-connectivity");
var _state = require("@atlaskit/editor-prosemirror/state");
var _transform = require("@atlaskit/editor-prosemirror/transform");
var _view2 = require("@atlaskit/editor-prosemirror/view");
var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
var _utils2 = require("@atlaskit/editor-synced-block-provider/utils");
var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
var _bodiedSyncedBlock = require("../nodeviews/bodiedSyncedBlock");
var _syncedBlock = require("../nodeviews/syncedBlock");
var _types = require("../types");
var _handleBodiedSyncBlockCreation = require("./utils/handle-bodied-sync-block-creation");
var _handleBodiedSyncBlockRemoval = require("./utils/handle-bodied-sync-block-removal");
var _hasSyncedBlocks = require("./utils/has-synced-blocks");
var _ignoreDomEvent = require("./utils/ignore-dom-event");
var _selectionDecorations = require("./utils/selection-decorations");
var _trackSyncBlocks7 = require("./utils/track-sync-blocks");
var _transactionInsertsSyncedBlock = require("./utils/transaction-inserts-synced-block");
var _utils3 = require("./utils/utils");
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; }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
var syncedBlockPluginKey = exports.syncedBlockPluginKey = new _state.PluginKey('syncedBlockPlugin');

/**
 * Dedicated transaction-meta {@link PluginKey} the synced-block removal command
 * sets so {@link getDeleteMechanism} can report a toolbar Delete as
 * `deleteButton` (otherwise indistinguishable from a keyboard delete — both are
 * plain ReplaceSteps). A separate key (rather than `syncedBlockPluginKey`, which
 * carries plugin-state transitions read in `apply()`, or a bare string) keeps
 * this transient signal uniquely namespaced and isolated.
 */
var deleteMechanismMetaKey = exports.deleteMechanismMetaKey = new _state.PluginKey('syncedBlockDeleteMechanism');
var syncedBlockPromptedFeedbackMetaKey = new _state.PluginKey('syncedBlockPromptedFeedback');

/**
 * Creation-type signals set by {@link createSyncedBlock} on the creating
 * transaction. The async creation handler forwards them to the store manager,
 * which attaches them to the `syncedBlockCreate` event. A dedicated key keeps
 * this transient signal isolated from plugin state.
 */

var creationMetaKey = exports.creationMetaKey = new _state.PluginKey('syncedBlockCreationMeta');
var mapRetryCreationPosMap = function mapRetryCreationPosMap(oldMap, newRetryCreationPos, mapPos) {
  var resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;

  // Fast path: no new entry and nothing to remap — return the same reference.
  // This is critical for PR-E (EDITOR-6929) which relies on reference equality
  // to short-circuit SharedStateAPI deep-equality checks.
  if (!resourceId && oldMap.size === 0) {
    return oldMap;
  }
  var newMap = new Map(oldMap);
  if (resourceId) {
    var pos = newRetryCreationPos.pos;
    if (!pos) {
      newMap.delete(resourceId);
    } else {
      newMap.set(resourceId, pos);
    }
  }
  if (newMap.size === 0) {
    return newMap;
  }
  var _iterator = _createForOfIteratorHelper(newMap.entries()),
    _step;
  try {
    for (_iterator.s(); !(_step = _iterator.n()).done;) {
      var _step$value = (0, _slicedToArray2.default)(_step.value, 2),
        id = _step$value[0],
        _pos = _step$value[1];
      newMap.set(id, {
        from: mapPos(_pos.from),
        to: mapPos(_pos.to)
      });
    }
  } catch (err) {
    _iterator.e(err);
  } finally {
    _iterator.f();
  }
  return newMap;
};
var showCopiedFlag = function showCopiedFlag(api, isLivePage, isSourceContentUnpublished, sourceProduct) {
  (0, _utils3.deferDispatch)(function () {
    api === null || api === void 0 || api.core.actions.execute(function (_ref) {
      var tr = _ref.tr;
      return tr.setMeta(syncedBlockPluginKey, {
        activeFlag: {
          id: _types.FLAG_ID.SYNC_BLOCK_COPIED,
          isLivePage: isLivePage,
          isSourceContentUnpublished: isSourceContentUnpublished,
          sourceProduct: sourceProduct
        }
      });
    });
  });
};
var showExtensionInSyncBlockWarningIfNeeded = function showExtensionInSyncBlockWarningIfNeeded(tr, state, api, extensionFlagShown) {
  var _api$connectivity;
  if (!tr.docChanged || tr.getMeta('isRemote') || Boolean(tr.getMeta(_utils.pmHistoryPluginKey)) || (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity = api.connectivity) === null || _api$connectivity === void 0 || (_api$connectivity = _api$connectivity.sharedState.currentState()) === null || _api$connectivity === void 0 ? void 0 : _api$connectivity.mode)) {
    return;
  }
  var resourceId = (0, _utils3.wasExtensionInsertedInBodiedSyncBlock)(tr, state);
  // Only show the flag on the first instance per sync block (same as UNPUBLISHED_SYNC_BLOCK_PASTED)
  if (resourceId && !extensionFlagShown.has(resourceId)) {
    extensionFlagShown.add(resourceId);
    (0, _utils3.deferDispatch)(function () {
      api === null || api === void 0 || api.core.actions.execute(function (_ref2) {
        var tr = _ref2.tr;
        return tr.setMeta(syncedBlockPluginKey, {
          activeFlag: {
            id: _types.FLAG_ID.EXTENSION_IN_SYNC_BLOCK
          }
        });
      });
    });
  }
};
var getDeleteReason = function getDeleteReason(tr) {
  var reason = tr.getMeta('deletionReason');
  if (!reason) {
    return 'source-block-deleted';
  }
  return reason;
};

/** Narrows the history plugin meta, matching the editor-plugin-card pattern. */
var isHistoryMeta = function isHistoryMeta(meta) {
  return (0, _typeof2.default)(meta) === 'object' && meta !== null && 'redo' in meta && typeof meta.redo === 'boolean';
};

/**
 * Derive how a source bodiedSyncBlock removal was performed, for the `mechanism`
 * analytics dimension. Each value names the user action; most-specific wins:
 *  - `undo` / `redo` — history transaction.
 *  - `deleteButton` — the explicit "Delete" control in the synced-block toolbar
 *    (the removal command tags its transaction with {@link deleteMechanismMetaKey}).
 *  - `selectionReplaced` — ReplaceStep on a node-selected block (the
 *    accidental-overwrite path: block selected, then typed/pasted over).
 *  - `keyboardDelete` — ReplaceStep removal at a caret/range selection
 *    (Backspace/Delete key).
 *  - `other` — anything else (unsync, conversion, code-dispatched, or a
 *    structural `ReplaceAroundStep` such as wrap/lift/unwrap).
 *
 * Only a plain `ReplaceStep` counts as a direct user deletion. `ReplaceAroundStep`
 * is used for wrapping/lifting/unwrapping content, not direct removal, so a
 * removal carried by one is classified as `other` rather than misreported as a
 * keyboard delete.
 *
 * `state` is the pre-transaction state, so its selection reflects what was
 * selected when the edit was made.
 */
var getDeleteMechanism = exports.getDeleteMechanism = function getDeleteMechanism(tr, state) {
  var _selection$node, _selection$node2;
  // Keep the legacy analytics classification unchanged while activation is off.
  // The stricter history, structural-step, and reference-selection handling is
  // only needed by the prompted feedback rollout.
  var isSyncBlockActivationEnabled = (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true);
  var historyMeta = tr.getMeta(_utils.pmHistoryPluginKey);
  if (historyMeta) {
    if (isSyncBlockActivationEnabled && !isHistoryMeta(historyMeta)) {
      return 'other';
    }
    return isHistoryMeta(historyMeta) && historyMeta.redo ? 'redo' : 'undo';
  }

  // The toolbar Delete control tags its transaction so we can report it
  // distinctly from a keyboard delete (both produce a plain ReplaceStep).
  if (tr.getMeta(deleteMechanismMetaKey) === 'deleteButton') {
    return 'deleteButton';
  }
  var hasStructuralStep = isSyncBlockActivationEnabled && tr.steps.some(function (step) {
    return step instanceof _transform.ReplaceAroundStep;
  });
  var hasReplaceStep = tr.steps.some(function (step) {
    return step instanceof _transform.ReplaceStep;
  });
  if (hasStructuralStep || !hasReplaceStep) {
    return 'other';
  }
  var selection = state.selection;
  var isNodeSelected = selection instanceof _state.NodeSelection && (((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type.name) === 'bodiedSyncBlock' || isSyncBlockActivationEnabled && ((_selection$node2 = selection.node) === null || _selection$node2 === void 0 ? void 0 : _selection$node2.type.name) === 'syncBlock');
  return isNodeSelected ? 'selectionReplaced' : 'keyboardDelete';
};
var getPromptedFeedbackEntryPoint = exports.getPromptedFeedbackEntryPoint = function getPromptedFeedbackEntryPoint(mechanism) {
  if (mechanism === 'undo') {
    return 'prompted-undo';
  }
  if (mechanism === 'deleteButton' || mechanism === 'keyboardDelete' || mechanism === 'selectionReplaced') {
    return 'prompted-delete';
  }
  return undefined;
};
/**
 * True when a source `bodiedSyncBlock` holds user content — i.e. not just the
 * single empty paragraph it is created with (used to detect empty→content).
 */
var bodiedSyncBlockHasContent = function bodiedSyncBlockHasContent(node) {
  if (node.childCount === 0) {
    return false;
  }
  if (node.childCount === 1) {
    var child = node.firstChild;
    // A freshly-created block is a single empty paragraph; anything else
    // (non-empty paragraph, or a different/first block) counts as content.
    return !((child === null || child === void 0 ? void 0 : child.type.name) === 'paragraph' && child.content.size === 0);
  }
  return true;
};

/**
 * Ask the store manager to fire the one-shot first-content-added event for every
 * populated source block. The manager only emits for blocks it created empty
 * this session and dedupes per block, so iterating all populated blocks is safe
 * and self-limiting. Called only when `hasEditInSyncBlock` flagged an in-block
 * edit, so this walk runs rarely.
 */
var emitFirstContentAddedForEditedBlocks = function emitFirstContentAddedForEditedBlocks(tr, state, syncBlockStore) {
  var bodiedSyncBlock = state.schema.nodes.bodiedSyncBlock;
  tr.doc.descendants(function (node) {
    if (node.type === bodiedSyncBlock) {
      if (bodiedSyncBlockHasContent(node)) {
        syncBlockStore.sourceManager.maybeEmitFirstContentAdded(node.attrs.resourceId, node.attrs.localId);
      }
      // bodiedSyncBlocks are top-level and not nested, so no need to descend.
      return false;
    }
    return true;
  });
};
var filterTransactionOnline = function filterTransactionOnline(_ref3) {
  var _tr$getMeta;
  var tr = _ref3.tr,
    state = _ref3.state,
    syncBlockStore = _ref3.syncBlockStore,
    api = _ref3.api,
    confirmationTransactionRef = _ref3.confirmationTransactionRef,
    ctx = _ref3.ctx,
    bodiedSyncBlockRemoved = _ref3.bodiedSyncBlockRemoved,
    bodiedSyncBlockAdded = _ref3.bodiedSyncBlockAdded,
    extensionFlagShown = _ref3.extensionFlagShown;
  var _trackSyncBlocks = (0, _trackSyncBlocks7.trackSyncBlocks)(syncBlockStore.referenceManager.isReferenceBlock, tr, state),
    syncBlockRemoved = _trackSyncBlocks.removed,
    syncBlockAdded = _trackSyncBlocks.added;
  syncBlockRemoved.forEach(function (syncBlock) {
    var _api$analytics;
    api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 || _api$analytics.fireAnalyticsEvent({
      action: _analytics.ACTION.DELETED,
      actionSubject: _analytics.ACTION_SUBJECT.SYNCED_BLOCK,
      actionSubjectId: _analytics.ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_DELETE,
      attributes: {
        resourceId: syncBlock.attrs.resourceId,
        blockInstanceId: syncBlock.attrs.localId
      },
      eventType: _analytics.EVENT_TYPE.OPERATIONAL
    });
  });

  // Annotate every reference-block insertion with `sourceProduct`
  // (derived from the resourceId) and `isPaste` (when the originating transaction was a
  // paste). Together with the host product reported by the embedding product's pageview
  // events this lets us triangulate cross-product paste behaviour without standing up a
  // dedicated `cross_product_paste` event subject.
  var isPaste = Boolean((_tr$getMeta = tr.getMeta('paste')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : tr.getMeta('uiEvent') === 'paste');
  var isPasteOrDrop = isPaste || tr.getMeta('uiEvent') === 'drop';
  var hasRemovedSyncBlock = syncBlockRemoved.length > 0 || bodiedSyncBlockRemoved.length > 0;
  var isCutRemoval = hasRemovedSyncBlock && ctx.consumeCutRemoval();
  var canPromptForRemoval = Boolean(ctx.onGiveFeedback) && hasRemovedSyncBlock && !(0, _collab.isDirtyTransaction)(tr) && !isCutRemoval && !isPasteOrDrop;
  var promptedFeedbackEntryPoint = canPromptForRemoval ? getPromptedFeedbackEntryPoint(getDeleteMechanism(tr, state)) : undefined;
  var entryPoint = promptedFeedbackEntryPoint && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true) ? promptedFeedbackEntryPoint : undefined;
  if (entryPoint && syncBlockRemoved.length > 0 && bodiedSyncBlockRemoved.length === 0) {
    tr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
      context: {
        blockType: 'reference',
        entryPoint: entryPoint
      }
    });
  }
  syncBlockAdded.forEach(function (syncBlock) {
    var _api$analytics2;
    api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.actions) === null || _api$analytics2 === void 0 || _api$analytics2.fireAnalyticsEvent({
      action: _analytics.ACTION.INSERTED,
      actionSubject: _analytics.ACTION_SUBJECT.DOCUMENT,
      actionSubjectId: _analytics.ACTION_SUBJECT_ID.SYNCED_BLOCK,
      attributes: {
        resourceId: syncBlock.attrs.resourceId,
        blockInstanceId: syncBlock.attrs.localId,
        sourceProduct: (0, _utils2.getSourceProductFromResourceIdSafe)(syncBlock.attrs.resourceId),
        isPaste: isPaste
      },
      eventType: _analytics.EVENT_TYPE.TRACK
    });
  });
  if (bodiedSyncBlockRemoved.length > 0) {
    // Recompute the delete fresh from the live document on confirm
    // (see handleBodiedSyncBlockRemoval). See EDITOR-7889.
    var mechanism = getDeleteMechanism(tr, state);
    var feedbackContext = entryPoint && getDeleteReason(tr) !== 'source-block-unsynced' ? {
      blockType: 'source',
      entryPoint: entryPoint
    } : undefined;
    if (feedbackContext) {
      var sourceAttempt = ctx.queueSourceFeedback(feedbackContext);
      return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, {
        onDeleteCompleted: function onDeleteCompleted(success) {
          return ctx.completeSourceDeletion(sourceAttempt, success);
        },
        onDeleteTransaction: function onDeleteTransaction(deleteTr) {
          return deleteTr.setMeta(syncedBlockPromptedFeedbackMetaKey, {
            context: feedbackContext,
            sourceAttempt: sourceAttempt
          });
        },
        onDestroy: function onDestroy() {
          return ctx.clearSourceFeedback(sourceAttempt);
        }
      });
    }
    return (0, _handleBodiedSyncBlockRemoval.handleBodiedSyncBlockRemoval)(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr), mechanism, undefined);
  }
  if (bodiedSyncBlockAdded.length > 0) {
    if (tr.getMeta(_utils.pmHistoryPluginKey)) {
      // We don't allow bodiedSyncBlock creation via redo, however, we need to return true here to let transaction through so history can be updated properly.
      // If we simply returns false, creation from redo is blocked as desired, but this results in editor showing redo as possible even though it's not.
      // After true is returned here and the node is created, we delete the node in the filterTransaction immediately, which cancels out the creation
      return true;
    }

    // Defense-in-depth: if a bodiedSyncBlock arrives via paste or drag-and-drop,
    // it may have bypassed transformCopied (e.g. cut, browser clipboard).
    // Do not call createBlock — it would use this page's parentId which may
    // be wrong. The bodiedSyncBlock content will still be inserted but will
    // not be registered as a source block in Block Service, which is safer
    // than creating a zombie block under the wrong ARI.
    if (isPasteOrDrop) {
      return true;
    }

    // Forward creation-type signals captured by createSyncedBlock onto the async
    // `syncedBlockCreate` success event.
    var creationMeta = tr.getMeta(creationMetaKey);
    (0, _handleBodiedSyncBlockCreation.handleBodiedSyncBlockCreation)(bodiedSyncBlockAdded, state, api, creationMeta);
    return true;
  }

  // On an in-block edit, let the store manager fire the one-shot first-content
  // event (dedupe + empty→content scoping live there). After the add/remove
  // branches so it never runs for the creation transaction itself.
  if ((0, _trackSyncBlocks7.hasEditInSyncBlock)(tr, state)) {
    emitFirstContentAddedForEditedBlocks(tr, state, syncBlockStore);
  }
  showExtensionInSyncBlockWarningIfNeeded(tr, state, api, extensionFlagShown);
  return true;
};
var filterTransactionOffline = function filterTransactionOffline(_ref4) {
  var tr = _ref4.tr,
    state = _ref4.state,
    syncBlockStore = _ref4.syncBlockStore,
    api = _ref4.api,
    isConfirmedSyncBlockDeletion = _ref4.isConfirmedSyncBlockDeletion,
    bodiedSyncBlockRemoved = _ref4.bodiedSyncBlockRemoved,
    bodiedSyncBlockAdded = _ref4.bodiedSyncBlockAdded;
  var _trackSyncBlocks2 = (0, _trackSyncBlocks7.trackSyncBlocks)(syncBlockStore.referenceManager.isReferenceBlock, tr, state),
    syncBlockRemoved = _trackSyncBlocks2.removed,
    syncBlockAdded = _trackSyncBlocks2.added;
  var errorFlag = false;
  if (isConfirmedSyncBlockDeletion || bodiedSyncBlockRemoved.length > 0 || syncBlockRemoved.length > 0) {
    errorFlag = _types.FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE;
  } else if (bodiedSyncBlockAdded.length > 0 || syncBlockAdded.length > 0) {
    errorFlag = _types.FLAG_ID.CANNOT_CREATE_WHEN_OFFLINE;
  } else if ((0, _trackSyncBlocks7.hasEditInSyncBlock)(tr, state)) {
    errorFlag = _types.FLAG_ID.CANNOT_EDIT_WHEN_OFFLINE;
  }
  if (errorFlag) {
    (0, _utils3.deferDispatch)(function () {
      api === null || api === void 0 || api.core.actions.execute(function (_ref5) {
        var tr = _ref5.tr;
        return tr.setMeta(syncedBlockPluginKey, {
          activeFlag: {
            id: errorFlag
          }
        });
      });
    });
    return false;
  }
  return true;
};

/**
 * Build the status decoration set for sync-block nodes. This performs a full
 * `doc.descendants()` walk so it must only be called when a status signal
 * actually changes (offline, view-mode, dragging, pending-creation). Between
 * status changes the caller should use `decorationSet.map(tr.mapping, tr.doc)`
 * instead (see EDITOR-6930).
 */
var buildStatusDecorations = function buildStatusDecorations(doc, syncBlockStore, isOffline, isViewMode, isDragging) {
  // Fast path: when all status flags are off and no creations are in flight,
  // no node can produce a decoration — skip the full doc traversal.
  if (!isOffline && !isViewMode && !isDragging && !syncBlockStore.sourceManager.hasPendingCreations()) {
    return _view2.DecorationSet.empty;
  }
  var offlineDecorations = [];
  var viewModeDecorations = [];
  var loadingDecorations = [];
  var dragDecorations = [];
  doc.descendants(function (node, pos) {
    if (node.type.name === 'bodiedSyncBlock' && isOffline) {
      offlineDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
        class: _syncBlock.SyncBlockStateCssClassName.disabledClassName
      }));
    }
    if (syncBlockStore.isSyncBlock(node) && isViewMode) {
      viewModeDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
        class: _syncBlock.SyncBlockStateCssClassName.viewModeClassName
      }));
    }
    if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
      loadingDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
        class: _syncBlock.SyncBlockStateCssClassName.creationLoadingClassName
      }));
    }
    if (isDragging && syncBlockStore.isSyncBlock(node)) {
      dragDecorations.push(_view2.Decoration.node(pos, pos + node.nodeSize, {
        class: _syncBlock.SyncBlockStateCssClassName.draggingClassName
      }));
    }

    // only traverse the top-level node of the document, as syncBlock and bodiedSyncBlock are top-level nodes
    return false;
  });
  return _view2.DecorationSet.create(doc, [].concat(offlineDecorations, viewModeDecorations, loadingDecorations, dragDecorations));
};

/**
 * Encapsulates mutable state that persists across transactions in the
 * synced block plugin. Replaces module-level closure variables so state
 * is explicitly scoped to a single plugin instance.
 */
var SyncedBlockPluginContext = /*#__PURE__*/function () {
  function SyncedBlockPluginContext(onGiveFeedback, shouldShowFeedbackPrompt, api) {
    (0, _classCallCheck2.default)(this, SyncedBlockPluginContext);
    (0, _defineProperty2.default)(this, "confirmationTransactionRef", {
      current: undefined
    });
    (0, _defineProperty2.default)(this, "_cutRemovalPending", false);
    (0, _defineProperty2.default)(this, "cutRemovalGeneration", 0);
    (0, _defineProperty2.default)(this, "_isCopyEvent", false);
    (0, _defineProperty2.default)(this, "_isCutEvent", false);
    (0, _defineProperty2.default)(this, "feedbackPromptConsumed", false);
    (0, _defineProperty2.default)(this, "unpublishedFlagShown", new Set());
    (0, _defineProperty2.default)(this, "extensionFlagShown", new Set());
    this.onGiveFeedback = onGiveFeedback;
    this.shouldShowFeedbackPrompt = shouldShowFeedbackPrompt;
    this.api = api;
  }
  return (0, _createClass2.default)(SyncedBlockPluginContext, [{
    key: "isCopyEvent",
    get: function get() {
      return this._isCopyEvent;
    }
  }, {
    key: "markCopyEvent",
    value: function markCopyEvent() {
      this._isCopyEvent = true;
    }
  }, {
    key: "markCutEvent",
    value: function markCutEvent() {
      this._isCutEvent = true;
    }
  }, {
    key: "consumeCopyEvent",
    value: function consumeCopyEvent() {
      var was = this._isCopyEvent;
      this._isCopyEvent = false;
      return was;
    }
  }, {
    key: "consumeCutEvent",
    value: function consumeCutEvent() {
      var was = this._isCutEvent;
      this._isCutEvent = false;
      return was;
    }
  }, {
    key: "markCutRemovalPending",
    value: function markCutRemovalPending() {
      var _this = this;
      this._cutRemovalPending = true;
      var generation = ++this.cutRemovalGeneration;
      queueMicrotask(function () {
        if (_this.cutRemovalGeneration === generation) {
          _this._cutRemovalPending = false;
        }
      });
    }
  }, {
    key: "consumeCutRemoval",
    value: function consumeCutRemoval() {
      var wasPending = this._cutRemovalPending;
      this._cutRemovalPending = false;
      this.cutRemovalGeneration += 1;
      return wasPending;
    }
  }, {
    key: "queueSourceFeedback",
    value: function queueSourceFeedback(context) {
      var sourceAttempt = Symbol('syncedBlockSourceFeedbackAttempt');
      this.pendingSourceFeedback = {
        context: context,
        deletionSucceeded: false,
        sourceAttempt: sourceAttempt,
        transactionApplied: false
      };
      return sourceAttempt;
    }
  }, {
    key: "completeSourceDeletion",
    value: function completeSourceDeletion(sourceAttempt, success) {
      var _this$pendingSourceFe;
      if (((_this$pendingSourceFe = this.pendingSourceFeedback) === null || _this$pendingSourceFe === void 0 ? void 0 : _this$pendingSourceFe.sourceAttempt) !== sourceAttempt) {
        return;
      }
      if (!success) {
        // Keep the attempt pending because retryDeletion reuses these callbacks.
        // Dismissal or cancellation clears it through the destroy callback.
        return;
      }
      this.pendingSourceFeedback.deletionSucceeded = true;
      this.flushSourceFeedback(sourceAttempt);
    }
  }, {
    key: "clearSourceFeedback",
    value: function clearSourceFeedback(sourceAttempt) {
      var _this$pendingSourceFe2;
      if (((_this$pendingSourceFe2 = this.pendingSourceFeedback) === null || _this$pendingSourceFe2 === void 0 ? void 0 : _this$pendingSourceFe2.sourceAttempt) === sourceAttempt) {
        this.pendingSourceFeedback = undefined;
      }
    }
  }, {
    key: "handleAppliedFeedback",
    value: function handleAppliedFeedback(_ref6) {
      var _this$pendingSourceFe3;
      var context = _ref6.context,
        sourceAttempt = _ref6.sourceAttempt;
      if (context.blockType === 'source' && sourceAttempt && ((_this$pendingSourceFe3 = this.pendingSourceFeedback) === null || _this$pendingSourceFe3 === void 0 ? void 0 : _this$pendingSourceFe3.sourceAttempt) === sourceAttempt) {
        this.pendingSourceFeedback.transactionApplied = true;
        this.flushSourceFeedback(sourceAttempt);
        return;
      }
      if (context.blockType === 'reference') {
        this.requestFeedbackPrompt(context);
      }
    }
  }, {
    key: "flushSourceFeedback",
    value: function flushSourceFeedback(sourceAttempt) {
      var _this$pendingSourceFe4, _this$pendingSourceFe5;
      if (((_this$pendingSourceFe4 = this.pendingSourceFeedback) === null || _this$pendingSourceFe4 === void 0 ? void 0 : _this$pendingSourceFe4.sourceAttempt) === sourceAttempt && (_this$pendingSourceFe5 = this.pendingSourceFeedback) !== null && _this$pendingSourceFe5 !== void 0 && _this$pendingSourceFe5.deletionSucceeded && this.pendingSourceFeedback.transactionApplied) {
        var context = this.pendingSourceFeedback.context;
        this.pendingSourceFeedback = undefined;
        this.requestFeedbackPrompt(context);
      }
    }
  }, {
    key: "requestFeedbackPrompt",
    value: function requestFeedbackPrompt(context) {
      var _this2 = this;
      if (this.feedbackPromptConsumed) {
        return;
      }
      this.feedbackPromptConsumed = true;
      (0, _utils3.deferDispatch)(function () {
        try {
          var _this2$shouldShowFeed, _this2$shouldShowFeed2;
          var shouldShow = (_this2$shouldShowFeed = (_this2$shouldShowFeed2 = _this2.shouldShowFeedbackPrompt) === null || _this2$shouldShowFeed2 === void 0 ? void 0 : _this2$shouldShowFeed2.call(_this2)) !== null && _this2$shouldShowFeed !== void 0 ? _this2$shouldShowFeed : true;
          void Promise.resolve(shouldShow).then(function (isEligible) {
            var _this2$api;
            if (!isEligible) {
              return;
            }
            (_this2$api = _this2.api) === null || _this2$api === void 0 || _this2$api.core.actions.execute(function (_ref7) {
              var tr = _ref7.tr;
              return tr.setMeta(syncedBlockPluginKey, {
                activeFlag: {
                  id: _types.FLAG_ID.SYNC_BLOCK_FEEDBACK_PROMPT,
                  feedbackContext: context
                }
              });
            });
          }).catch(function () {});
        } catch (_unused) {
          // Persisted feedback eligibility is optional product UI.
        }
      });
    }
  }]);
}();
var createPlugin = exports.createPlugin = function createPlugin(options, pmPluginFactoryParams, syncBlockStore, api) {
  var _ref8 = options || {},
    _ref8$useLongPressSel = _ref8.useLongPressSelection,
    useLongPressSelection = _ref8$useLongPressSel === void 0 ? false : _ref8$useLongPressSel;
  var ctx = new SyncedBlockPluginContext(options === null || options === void 0 ? void 0 : options.onGiveFeedback, options === null || options === void 0 ? void 0 : options.shouldShowFeedbackPrompt, api);
  var confirmationTransactionRef = ctx.confirmationTransactionRef;
  var unpublishedFlagShown = ctx.unpublishedFlagShown;
  var extensionFlagShown = ctx.extensionFlagShown;

  // Update plugin state post-flush to sync hasUnsavedBodiedSyncBlockChanges.
  // It prevents false "Changes may not be saved" warnings when publishing
  // Classic pages with sync blocks.
  syncBlockStore.sourceManager.registerFlushCompletionCallback(function () {
    (0, _utils3.deferDispatch)(function () {
      api === null || api === void 0 || api.core.actions.execute(function (_ref9) {
        var tr = _ref9.tr;
        return tr;
      });
    });
  });

  // Trigger a deferred flush when block creation completes after the initial
  // flush timed out waiting for it. This ensures content created from
  // existing text is eventually persisted even if no further edits are made.
  // See EDITOR-7112.
  syncBlockStore.sourceManager.registerPostCreationFlushCallback(function () {
    (0, _utils3.deferDispatch)(function () {
      syncBlockStore.sourceManager.flush();
    });
  });

  // Set up callback to detect unpublished sync blocks when they're fetched
  syncBlockStore.referenceManager.setOnUnpublishedSyncBlockDetected(function (resourceId) {
    // Only show the flag once per sync block
    if (!unpublishedFlagShown.has(resourceId)) {
      unpublishedFlagShown.add(resourceId);
      // Surface the source product so the flag's copy can be tailored — Jira work
      // items get "Pasted from unsaved item" / "...when the item's description is
      // saved" rather than the Confluence page-flavoured default. Falls back to
      // undefined when the resourceId can't be parsed (legacy shapes), in which
      // case the Confluence default is used.
      var sourceProduct = (0, _utils2.getSourceProductFromResourceIdSafe)(resourceId);
      (0, _utils3.deferDispatch)(function () {
        api === null || api === void 0 || api.core.actions.execute(function (_ref0) {
          var tr = _ref0.tr;
          return tr.setMeta(syncedBlockPluginKey, {
            activeFlag: {
              id: _types.FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED,
              sourceProduct: sourceProduct
            }
          });
        });
      });
    }
  });
  return new _safePlugin.SafePlugin({
    key: syncedBlockPluginKey,
    state: {
      init: function init(_, instance) {
        var _api$connectivity2, _api$editorViewMode, _api$userIntent;
        // When the document has no synced blocks, we skip the eager fetch +
        // cache walks. They will be re-run lazily by `apply` the first time a
        // synced block enters the document (paste, collab insert, or
        // programmatic insert).
        var docHasSyncedBlocks = (0, _hasSyncedBlocks.hasSyncedBlocks)(instance.doc);
        if (docHasSyncedBlocks) {
          var syncBlockNodes = instance.doc.children.filter(syncBlockStore.referenceManager.isReferenceBlock);
          syncBlockStore.referenceManager.fetchSyncBlocksData((0, _editorSyncedBlockProvider.convertPMNodesToSyncBlockNodes)(syncBlockNodes));

          // Populate source sync block cache from initial document.
          // This replaces the constructor call in the nodeview.
          instance.doc.forEach(function (node) {
            if (syncBlockStore.sourceManager.isSourceBlock(node)) {
              syncBlockStore.sourceManager.updateSyncBlockData(node, false);
            }
          });

          // Fetch statuses from the backend so we can identify unpublished blocks on cancel
          syncBlockStore.sourceManager.fetchAndCacheStatuses();
        }

        // Read initial shared-state signals for status decorations
        var initIsOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 || (_api$connectivity2 = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2 === void 0 ? void 0 : _api$connectivity2.mode);
        var initIsViewMode = (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view';
        var initIsDragging = (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'dragging';

        // Build initial status decoration set (EDITOR-6930).
        // When the perf gate is ON and the doc has synced blocks we do a
        // single traversal here; afterwards `apply()` will map or rebuild
        // only when a status signal changes.
        var initStatusDecorationSet = docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view2.DecorationSet.empty;
        return {
          selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
          activeFlag: false,
          syncBlockStore: syncBlockStore,
          retryCreationPosMap: new Map(),
          hasSyncedBlocks: docHasSyncedBlocks,
          hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
          statusDecorationSet: initStatusDecorationSet,
          prevIsOffline: initIsOffline,
          prevIsViewMode: initIsViewMode,
          prevIsDragging: initIsDragging
        };
      },
      apply: function apply(tr, currentPluginState, oldEditorState) {
        var _api$userIntent2, _api$connectivity3, _api$editorViewMode2, _meta$activeFlag, _meta$bodiedSyncBlock;
        var meta = tr.getMeta(syncedBlockPluginKey);
        var activeFlag = currentPluginState.activeFlag,
          selectionDecorationSet = currentPluginState.selectionDecorationSet,
          bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
          retryCreationPosMap = currentPluginState.retryCreationPosMap,
          prevHasSyncedBlocks = currentPluginState.hasSyncedBlocks,
          prevStatusDecorationSet = currentPluginState.statusDecorationSet,
          prevOffline = currentPluginState.prevIsOffline,
          prevViewMode = currentPluginState.prevIsViewMode,
          prevDragging = currentPluginState.prevIsDragging;

        // Pre-compute once so the fast-path probe and the bottom ref-equality
        // short-circuit share the same source-manager snapshot. Track C will make this O(1).
        var nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();

        // --- Fast path (EDITOR-6934 follow-up, see Confluence 7050540723 §9.3 / §9.9):
        // when no plugin meta is set, and neither the document nor selection changed,
        // most synced-block plugin state cannot change. The source manager is mutable
        // outside ProseMirror transactions, though, so preserve existing empty-transaction
        // sync semantics by checking that snapshot first.
        //
        // Note: selection-only transactions intentionally fall through to the normal
        // apply path because calculateDecorations depends on drag/view-mode state
        // that is not captured here.
        //
        // We also need to ensure shared-state signals (drag, offline, view-mode)
        // haven't changed — these are propagated via empty transactions and drive
        // status decoration rebuilds (e.g. drag border). Without this check the
        // fast path would swallow drag-start transactions and the synced block
        // border would never appear (see VR test: synced-block-drag-selection).
        if (!meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && (api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 || (_api$userIntent2 = _api$userIntent2.sharedState.currentState()) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.currentUserIntent) === 'dragging' === prevDragging && (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 || (_api$connectivity3 = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3 === void 0 ? void 0 : _api$connectivity3.mode) === prevOffline && (api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 || (_api$editorViewMode2 = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.mode) === 'view' === prevViewMode) {
          return currentPluginState;
        }

        // Lazy-init bookkeeping: once a synced block enters the document we
        // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
        var nextHasSyncedBlocks = prevHasSyncedBlocks;
        if (!prevHasSyncedBlocks && tr.docChanged) {
          if ((0, _transactionInsertsSyncedBlock.transactionInsertsSyncedBlock)(tr)) {
            nextHasSyncedBlocks = true;
          }
        }
        var newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
        : selectionDecorationSet;
        if (!tr.selection.eq(oldEditorState.selection)) {
          newDecorationSet = (0, _selectionDecorations.calculateDecorations)(tr.doc, tr.selection, tr.doc.type.schema);
        } else if (tr.docChanged) {
          var existingDecorationsLength = selectionDecorationSet.find().length;
          var newDecorationsLength = newDecorationSet.find().length;

          // Edge case: When document nodes are replaced, the mapping can lose decorations
          // We rebuild decorations when the document changes but the selection hasn't.
          // We can do this check because we only expect 1 decoration for the selection
          if (existingDecorationsLength !== newDecorationsLength) {
            newDecorationSet = (0, _selectionDecorations.calculateDecorations)(tr.doc, tr.selection, tr.doc.type.schema);
          }
        }

        // --- Status decoration set (EDITOR-6930) ---
        // When the perf gate is ON we maintain `statusDecorationSet` in
        // plugin state so the `decorations` prop becomes an O(1) lookup.
        var nextStatusDecorationSet = prevStatusDecorationSet;
        var nextIsOffline = prevOffline;
        var nextIsViewMode = prevViewMode;
        var nextIsDragging = prevDragging;
        if (!nextHasSyncedBlocks) {
          // No synced blocks → keep empty status decorations
          nextStatusDecorationSet = _view2.DecorationSet.empty;
        } else {
          var _api$connectivity4, _api$editorViewMode3, _api$userIntent3;
          // Read current shared-state signals
          nextIsOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity4 = api.connectivity) === null || _api$connectivity4 === void 0 || (_api$connectivity4 = _api$connectivity4.sharedState.currentState()) === null || _api$connectivity4 === void 0 ? void 0 : _api$connectivity4.mode);
          nextIsViewMode = (api === null || api === void 0 || (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 || (_api$editorViewMode3 = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3 === void 0 ? void 0 : _api$editorViewMode3.mode) === 'view';
          nextIsDragging = (api === null || api === void 0 || (_api$userIntent3 = api.userIntent) === null || _api$userIntent3 === void 0 || (_api$userIntent3 = _api$userIntent3.sharedState.currentState()) === null || _api$userIntent3 === void 0 ? void 0 : _api$userIntent3.currentUserIntent) === 'dragging';

          // Determine whether we need a full rebuild or a cheap map
          var hasSyncedBlocksJustFlipped = nextHasSyncedBlocks && !prevHasSyncedBlocks;
          var statusSignalChanged = nextIsOffline !== prevOffline || nextIsViewMode !== prevViewMode || nextIsDragging !== prevDragging;
          // Meta-driven status changes (e.g. pending creation
          // completed, retry creation pos updated)
          var hasMetaStatusChange = !!(meta !== null && meta !== void 0 && meta.retryCreationPos) || !!(meta !== null && meta !== void 0 && meta.activeFlag);
          if (hasSyncedBlocksJustFlipped || statusSignalChanged || hasMetaStatusChange) {
            // Full rebuild — a status signal changed
            nextStatusDecorationSet = buildStatusDecorations(tr.doc, syncBlockStore, nextIsOffline, nextIsViewMode, nextIsDragging);
          } else if (tr.docChanged) {
            // Cheap map — positions shifted but status unchanged
            nextStatusDecorationSet = prevStatusDecorationSet.map(tr.mapping, tr.doc);
          }
          // else: nothing changed, keep same reference
        }
        var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
        var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
        var nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
        var nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;

        // --- Reference equality (EDITOR-6929): return the same object
        // when ALL fields are reference-equal to avoid SharedStateAPI
        // notifying subscribers and triggering React re-renders. ---
        if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging) {
          return currentPluginState;
        }
        return {
          activeFlag: nextActiveFlag,
          selectionDecorationSet: newDecorationSet,
          syncBlockStore: syncBlockStore,
          retryCreationPosMap: newRetryCreationPosMap,
          hasSyncedBlocks: nextHasSyncedBlocks,
          bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
          hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
          statusDecorationSet: nextStatusDecorationSet,
          prevIsOffline: nextIsOffline,
          prevIsViewMode: nextIsViewMode,
          prevIsDragging: nextIsDragging
        };
      }
    },
    props: {
      nodeViews: {
        syncBlock: function syncBlock(node, view, getPos, _decorations) {
          return (
            // To support SSR, pass `syncBlockStore` here
            // and do not use lazy loading.
            // We cannot start rendering and then load `syncBlockStore` asynchronously,
            // because obtaining it is asynchronous (sharedPluginState.currentState() is delayed).
            new _syncedBlock.SyncBlock({
              api: api,
              options: options,
              node: node,
              view: view,
              getPos: getPos,
              portalProviderAPI: pmPluginFactoryParams.portalProviderAPI,
              eventDispatcher: pmPluginFactoryParams.eventDispatcher,
              syncBlockStore: syncBlockStore,
              intl: pmPluginFactoryParams.getIntl()
            }).init()
          );
        },
        bodiedSyncBlock: (0, _bodiedSyncedBlock.bodiedSyncBlockNodeView)({
          pluginOptions: options,
          pmPluginFactoryParams: pmPluginFactoryParams,
          api: api,
          syncBlockStore: syncBlockStore
        })
      },
      decorations: function decorations(state) {
        var _api$focus$sharedStat, _api$focus;
        var currentPluginState = syncedBlockPluginKey.getState(state);
        if (!currentPluginState) {
          return _view2.DecorationSet.empty;
        }
        var selectionDecorationSet = currentPluginState.selectionDecorationSet,
          statusDecorationSet = currentPluginState.statusDecorationSet,
          docHasSyncedBlocks = currentPluginState.hasSyncedBlocks;

        // When the perf gate is ON, both `selectionDecorationSet` and
        // `statusDecorationSet` are maintained in plugin state by
        // `apply()`. The `decorations` prop is now an O(1) merge of
        // the two cached sets — no `doc.descendants()` walk, no
        // shared-state reads.
        if (!docHasSyncedBlocks) {
          return selectionDecorationSet;
        }

        // Focus state is read live here (single cheap read) because
        // it only gates whether selection decorations are included —
        // it does not affect the status decoration set and can change
        // within the same transaction cycle.
        var hasFocus = (_api$focus$sharedStat = api === null || api === void 0 || (_api$focus = api.focus) === null || _api$focus === void 0 || (_api$focus = _api$focus.sharedState) === null || _api$focus === void 0 || (_api$focus = _api$focus.currentState()) === null || _api$focus === void 0 ? void 0 : _api$focus.hasFocus) !== null && _api$focus$sharedStat !== void 0 ? _api$focus$sharedStat : true;

        // Merge selection + status decorations.
        // When the editor is unfocused,
        // omit selection decorations (matches old behaviour).
        var statusDecorations = statusDecorationSet.find();
        if (statusDecorations.length === 0) {
          return hasFocus ? selectionDecorationSet : _view2.DecorationSet.empty;
        } else {
          return hasFocus ? selectionDecorationSet.add(state.doc, statusDecorations) : statusDecorationSet;
        }
      },
      handleClickOn: (0, _selection.createSelectionClickHandler)(['bodiedSyncBlock'], function (target) {
        return !!target.closest(".".concat(_syncBlock.BodiedSyncBlockSharedCssClassName.prefix));
      }, {
        useLongPressSelection: useLongPressSelection
      }),
      handleDOMEvents: {
        mouseover: function mouseover(view, event) {
          return (0, _ignoreDomEvent.shouldIgnoreDomEvent)(view, event, api);
        },
        mousedown: function mousedown(view, event) {
          return (0, _ignoreDomEvent.shouldIgnoreDomEvent)(view, event, api);
        },
        copy: function copy() {
          ctx.markCopyEvent();
          return false;
        },
        cut: function cut() {
          ctx.consumeCutEvent();
          ctx.markCutEvent();
          return false;
        }
      },
      transformPasted: function transformPasted(slice, _view) {
        // Defense against bodiedSyncBlock nodes arriving via paste
        // (e.g. drag-and-drop, cut-paste, or browser-level clipboard
        // operations that bypass the transformCopied handler).
        // We cannot convert to a syncBlock reference here because we don't
        // know the source page's parentId (generateResourceIdForReference
        // would use the current page's parentId which is wrong).
        // Instead, strip the bodiedSyncBlock wrapper and keep only the
        // inner content to prevent createBlock being called with the
        // wrong parentId.
        return (0, _utils.mapSlice)(slice, function (node) {
          if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
            // Return the inner content without the sync block wrapper.
            // This is the same behavior as when a partial selection of
            // a bodiedSyncBlock is copied (see transformCopied line 937).
            return node.content;
          }
          return node;
        });
      },
      transformCopied: function transformCopied(slice, _ref1) {
        var state = _ref1.state;
        var pluginState = syncedBlockPluginKey.getState(state);
        var syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
        var schema = state.schema;
        var isCopy = ctx.consumeCopyEvent();
        var isCut = ctx.consumeCutEvent();
        // A prompted feedback cut is always a cut, so the two never need combining.
        var isPromptedFeedbackCut = Boolean(syncBlockStore && (options === null || options === void 0 ? void 0 : options.onGiveFeedback) && isCut) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true);
        if (!syncBlockStore || !isCopy && !isCut) {
          return slice;
        }
        var containsCutSyncBlock = false;
        var transformedSlice = (0, _utils.mapSlice)(slice, function (node) {
          if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
            var _syncBlockStore$refer;
            if (isCut) {
              containsCutSyncBlock = isPromptedFeedbackCut;
              return node;
            }
            showCopiedFlag(api, options === null || options === void 0 ? void 0 : options.__livePage, ((_syncBlockStore$refer = syncBlockStore.referenceManager.getFromCache(node.attrs.resourceId)) === null || _syncBlockStore$refer === void 0 || (_syncBlockStore$refer = _syncBlockStore$refer.data) === null || _syncBlockStore$refer === void 0 ? void 0 : _syncBlockStore$refer.status) === 'unpublished', (0, _utils2.getSourceProductFromResourceIdSafe)(node.attrs.resourceId));
            return node;
          }
          if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
            if (isPromptedFeedbackCut && (0, _utils3.sliceFullyContainsNode)(slice, node)) {
              containsCutSyncBlock = true;
            }
            // if we only selected part of the bodied sync block content,
            // remove the sync block node and only keep the content
            if (!(0, _utils3.sliceFullyContainsNode)(slice, node)) {
              return node.content;
            }
            if (isCut) {
              return node;
            }
            var newResourceId = syncBlockStore.referenceManager.generateResourceIdForReference(node.attrs.resourceId);
            showCopiedFlag(api, options === null || options === void 0 ? void 0 : options.__livePage, syncBlockStore.sourceManager.getStatus(node.attrs.resourceId) !== 'active', (0, _utils2.getSourceProductFromResourceIdSafe)(newResourceId));
            // Convert bodiedSyncBlock to syncBlock
            // The paste transformation will regenrate the localId
            var newAttrs = _objectSpread(_objectSpread({}, node.attrs), {}, {
              resourceId: newResourceId
            });
            var newMarks = schema.nodes.syncBlock.markSet ? node.marks.filter(function (mark) {
              var _schema$nodes$syncBlo;
              return (_schema$nodes$syncBlo = schema.nodes.syncBlock.markSet) === null || _schema$nodes$syncBlo === void 0 ? void 0 : _schema$nodes$syncBlo.includes(mark.type);
            }) : node.marks;
            return schema.nodes.syncBlock.create(newAttrs, null, newMarks);
          }
          return node;
        });
        if (containsCutSyncBlock) {
          ctx.markCutRemovalPending();
        }
        return transformedSlice;
      }
    },
    filterTransaction: function filterTransaction(tr, state) {
      var _api$editorViewMode4, _api$connectivity5;
      // Lazy-init: when no synced block currently exists in the doc and the
      // transaction does not insert one, all downstream filter logic is a
      // no-op. Avoid both the shared-state reads and the `trackSyncBlocks`
      // walks for the ~99.97% of pages that have no synced blocks.
      var pluginState = syncedBlockPluginKey.getState(state);
      if (pluginState && !pluginState.hasSyncedBlocks && !(0, _transactionInsertsSyncedBlock.transactionInsertsSyncedBlock)(tr)) {
        return true;
      }
      var viewMode = api === null || api === void 0 || (_api$editorViewMode4 = api.editorViewMode) === null || _api$editorViewMode4 === void 0 || (_api$editorViewMode4 = _api$editorViewMode4.sharedState.currentState()) === null || _api$editorViewMode4 === void 0 ? void 0 : _api$editorViewMode4.mode;
      if (viewMode === 'view') {
        return true;
      }
      var isOffline = (0, _editorPluginConnectivity.isOfflineMode)(api === null || api === void 0 || (_api$connectivity5 = api.connectivity) === null || _api$connectivity5 === void 0 || (_api$connectivity5 = _api$connectivity5.sharedState.currentState()) === null || _api$connectivity5 === void 0 ? void 0 : _api$connectivity5.mode);
      var isConfirmedSyncBlockDeletion = Boolean(tr.getMeta('isConfirmedSyncBlockDeletion'));

      // Track newly added reference sync blocks before processing the transaction
      if (tr.docChanged && !tr.getMeta('isRemote')) {
        var _trackSyncBlocks3 = (0, _trackSyncBlocks7.trackSyncBlocks)(syncBlockStore.referenceManager.isReferenceBlock, tr, state),
          added = _trackSyncBlocks3.added;
        added.forEach(function (nodeInfo) {
          var _nodeInfo$attrs;
          if ((_nodeInfo$attrs = nodeInfo.attrs) !== null && _nodeInfo$attrs !== void 0 && _nodeInfo$attrs.resourceId) {
            syncBlockStore.referenceManager.markAsNewlyAdded(nodeInfo.attrs.resourceId);
          }
        });
      }

      // if doc changed and it's a remote transaction, check if any synced block were added,
      // and if so, for source synced blocks, ensure we update the cache with them
      // and for reference synced blocks, ensure we fetch the data from the server
      if (tr.docChanged && tr.getMeta('isRemote')) {
        var _trackSyncBlocks4 = (0, _trackSyncBlocks7.trackSyncBlocks)(function (node) {
            return syncBlockStore.isSyncBlock(node);
          }, tr, state),
          _added = _trackSyncBlocks4.added;
        var sourceSyncBlockNodes = _added.filter(function (nodeInfo) {
          return nodeInfo.node && syncBlockStore.sourceManager.isSourceBlock(nodeInfo.node);
        });
        var referenceSyncBlockNodes = _added.filter(function (nodeInfo) {
          return nodeInfo.node && syncBlockStore.referenceManager.isReferenceBlock(nodeInfo.node);
        });
        sourceSyncBlockNodes.forEach(function (nodeInfo) {
          var _nodeInfo$attrs2;
          if ((_nodeInfo$attrs2 = nodeInfo.attrs) !== null && _nodeInfo$attrs2 !== void 0 && _nodeInfo$attrs2.resourceId && nodeInfo.node) {
            syncBlockStore.sourceManager.updateSyncBlockData(nodeInfo.node, tr.getMeta('isRemote'));
          }
        });

        // Fetch statuses for remotely-added source sync blocks
        // so we can identify unpublished blocks on cancel
        if (sourceSyncBlockNodes.length > 0) {
          syncBlockStore.sourceManager.fetchAndCacheStatuses();
        }
        var syncBlockNodes = referenceSyncBlockNodes.map(function (nodeInfo) {
          return nodeInfo.node;
        }).filter(function (node) {
          return node !== undefined;
        });
        syncBlockStore.referenceManager.fetchSyncBlocksData((0, _editorSyncedBlockProvider.convertPMNodesToSyncBlockNodes)(syncBlockNodes));
      }
      if (!tr.docChanged || Boolean(tr.getMeta('isRemote')) || !isOffline && isConfirmedSyncBlockDeletion) {
        return true;
      }
      var _trackSyncBlocks5 = (0, _trackSyncBlocks7.trackSyncBlocks)(syncBlockStore.sourceManager.isSourceBlock, tr, state),
        bodiedSyncBlockRemoved = _trackSyncBlocks5.removed,
        bodiedSyncBlockAdded = _trackSyncBlocks5.added;
      return isOffline ? filterTransactionOffline({
        tr: tr,
        state: state,
        syncBlockStore: syncBlockStore,
        api: api,
        isConfirmedSyncBlockDeletion: isConfirmedSyncBlockDeletion,
        bodiedSyncBlockRemoved: bodiedSyncBlockRemoved,
        bodiedSyncBlockAdded: bodiedSyncBlockAdded
      }) : filterTransactionOnline({
        tr: tr,
        state: state,
        syncBlockStore: syncBlockStore,
        api: api,
        confirmationTransactionRef: confirmationTransactionRef,
        ctx: ctx,
        bodiedSyncBlockRemoved: bodiedSyncBlockRemoved,
        bodiedSyncBlockAdded: bodiedSyncBlockAdded,
        extensionFlagShown: extensionFlagShown
      });
    },
    appendTransaction: function appendTransaction(trs, oldState, newState) {
      var _api$editorViewMode5;
      // Lazy-init: when neither the previous nor the new state contains a
      // synced block (and none of the dispatched transactions inserts one),
      // skip all downstream work. This is the hot path on the ~99.97% of
      // pages that don't use synced blocks (see EDITOR-6586).
      var oldPluginState = syncedBlockPluginKey.getState(oldState);
      var newPluginState = syncedBlockPluginKey.getState(newState);
      var hadOrHasSyncedBlocks = !!(oldPluginState !== null && oldPluginState !== void 0 && oldPluginState.hasSyncedBlocks) || !!(newPluginState !== null && newPluginState !== void 0 && newPluginState.hasSyncedBlocks);
      if (!hadOrHasSyncedBlocks) {
        return null;
      }
      var viewMode = api === null || api === void 0 || (_api$editorViewMode5 = api.editorViewMode) === null || _api$editorViewMode5 === void 0 || (_api$editorViewMode5 = _api$editorViewMode5.sharedState.currentState()) === null || _api$editorViewMode5 === void 0 ? void 0 : _api$editorViewMode5.mode;
      if (viewMode === 'view') {
        return null;
      }
      if (options !== null && options !== void 0 && options.onGiveFeedback) {
        var feedbackContexts = trs.flatMap(function (tr) {
          var feedbackContext = tr.getMeta(syncedBlockPromptedFeedbackMetaKey);
          return feedbackContext ? [feedbackContext] : [];
        });
        if (feedbackContexts.length > 0 && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_sync_block_activation', 'isEnabled', true)) {
          feedbackContexts.forEach(function (feedbackContext) {
            ctx.handleAppliedFeedback(feedbackContext);
          });
        }
      }

      // Update source sync block cache for user-initiated changes only.
      // Cache updates are handled here instead of in the nodeview update() so we
      // can filter out non-user changes (remote collab, dirty programmatic txns).
      var isUserChange = function isUserChange(tr) {
        return tr.docChanged && !(0, _collab.isDirtyTransaction)(tr) && !tr.getMeta('isRemote');
      };
      var hasSourceBlockEdit = trs.some(function (tr) {
        return isUserChange(tr) && (0, _trackSyncBlocks7.hasEditInSyncBlock)(tr, oldState);
      });
      if (hasSourceBlockEdit) {
        newState.doc.forEach(function (node) {
          if (syncBlockStore.sourceManager.isSourceBlock(node)) {
            syncBlockStore.sourceManager.updateSyncBlockData(node, false);
          }
        });
      }
      var _iterator2 = _createForOfIteratorHelper(trs),
        _step2;
      try {
        var _loop = function _loop() {
            var tr = _step2.value;
            if (tr.getMeta(_utils.pmHistoryPluginKey)) {
              var _trackSyncBlocks6 = (0, _trackSyncBlocks7.trackSyncBlocks)(syncBlockStore.sourceManager.isSourceBlock, tr, oldState),
                added = _trackSyncBlocks6.added;
              if (added.length > 0) {
                // Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
                // See filterTransaction above for more details
                var _tr = newState.tr;
                added.forEach(function (node) {
                  if (node.from !== undefined && node.to !== undefined) {
                    _tr.delete(node.from, node.to);
                  }
                });
                return {
                  v: _tr
                };
              }
            }
          },
          _ret;
        for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
          _ret = _loop();
          if (_ret) return _ret.v;
        }

        // Detect and remove duplicate bodiedSyncBlock resourceIds.
        // When a block template containing a source sync block is inserted into the
        // same document, it creates a duplicate with the same resourceId. We keep the
        // first occurrence and delete subsequent duplicates entirely (including their
        // contents), since a document must not contain two source sync blocks with the
        // same resourceId.
      } catch (err) {
        _iterator2.e(err);
      } finally {
        _iterator2.f();
      }
      if (trs.some(function (tr) {
        return tr.docChanged && !tr.getMeta('isRemote');
      })) {
        // Quick check: only walk the full document when at least one
        // transaction inserted a source synced block. This avoids an
        // expensive descendants() traversal on every local edit.
        var hasInsertedSourceBlock = trs.some(function (tr) {
          if (!tr.docChanged || tr.getMeta('isRemote')) {
            return false;
          }
          return tr.steps.some(function (step) {
            if (!(step instanceof _transform.ReplaceStep || step instanceof _transform.ReplaceAroundStep) || !('slice' in step)) {
              return false;
            }
            var _ref10 = step,
              slice = _ref10.slice;
            var found = false;
            slice.content.descendants(function (node) {
              if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
                found = true;
              }
              return false;
            });
            return found;
          });
        });
        if (!hasInsertedSourceBlock) {
          return null;
        }
        var seenResourceIds = new Set();
        var duplicates = [];
        newState.doc.descendants(function (node, pos) {
          if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
            if (seenResourceIds.has(node.attrs.resourceId)) {
              duplicates.push({
                pos: pos,
                nodeSize: node.nodeSize
              });
            } else {
              seenResourceIds.add(node.attrs.resourceId);
            }
            return false;
          }
        });
        if (duplicates.length > 0) {
          var tr = newState.tr;

          // Delete in reverse document order so positions remain valid
          for (var i = duplicates.length - 1; i >= 0; i--) {
            var dup = duplicates[i];
            tr.delete(dup.pos, dup.pos + dup.nodeSize);
          }
          tr.setMeta('addToHistory', false);
          (0, _utils3.deferDispatch)(function () {
            var _api$core;
            api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref11) {
              var tr = _ref11.tr;
              return tr.setMeta(syncedBlockPluginKey, {
                activeFlag: {
                  id: _types.FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
                }
              });
            });
          });
          return tr;
        }
      }
      return null;
    }
  });
};