@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
629 lines (619 loc) • 29.6 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { isDirtyTransaction } from '@atlaskit/editor-common/collab';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
import { BodiedSyncBlockSharedCssClassName, SyncBlockStateCssClassName } from '@atlaskit/editor-common/sync-block';
import { mapSlice, pmHistoryPluginKey } from '@atlaskit/editor-common/utils';
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
import { DecorationSet, Decoration } from '@atlaskit/editor-prosemirror/view';
import { convertPMNodesToSyncBlockNodes, rebaseTransaction } from '@atlaskit/editor-synced-block-provider';
import { fg } from '@atlaskit/platform-feature-flags';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { bodiedSyncBlockNodeView, bodiedSyncBlockNodeViewOld } from '../nodeviews/bodiedSyncedBlock';
import { SyncBlock as SyncBlockView } from '../nodeviews/syncedBlock';
import { FLAG_ID } from '../types';
import { handleBodiedSyncBlockCreation } from './utils/handle-bodied-sync-block-creation';
import { handleBodiedSyncBlockRemoval } from './utils/handle-bodied-sync-block-removal';
import { shouldIgnoreDomEvent } from './utils/ignore-dom-event';
import { calculateDecorations } from './utils/selection-decorations';
import { hasEditInSyncBlock, trackSyncBlocks } from './utils/track-sync-blocks';
import { deferDispatch, wasExtensionInsertedInBodiedSyncBlock, sliceFullyContainsNode } from './utils/utils';
export const syncedBlockPluginKey = new PluginKey('syncedBlockPlugin');
const mapRetryCreationPosMap = (oldMap, newRetryCreationPos, mapPos) => {
const resourceId = newRetryCreationPos === null || newRetryCreationPos === void 0 ? void 0 : newRetryCreationPos.resourceId;
const newMap = new Map(oldMap);
if (resourceId) {
const {
pos
} = newRetryCreationPos;
if (!pos) {
newMap.delete(resourceId);
} else {
newMap.set(resourceId, pos);
}
}
if (newMap.size === 0) {
return newMap;
}
for (const [id, pos] of newMap.entries()) {
newMap.set(id, {
from: mapPos(pos.from),
to: mapPos(pos.to)
});
}
return newMap;
};
const showCopiedFlag = api => {
deferDispatch(() => {
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr.setMeta(syncedBlockPluginKey, {
activeFlag: {
id: FLAG_ID.SYNC_BLOCK_COPIED
}
}));
});
};
const showExtensionInSyncBlockWarningIfNeeded = (tr, state, api, extensionFlagShown) => {
var _api$connectivity, _api$connectivity$sha;
if (!tr.docChanged || tr.getMeta('isRemote') || Boolean(tr.getMeta(pmHistoryPluginKey)) || isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity = api.connectivity) === null || _api$connectivity === void 0 ? void 0 : (_api$connectivity$sha = _api$connectivity.sharedState.currentState()) === null || _api$connectivity$sha === void 0 ? void 0 : _api$connectivity$sha.mode)) {
return;
}
const resourceId = 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);
deferDispatch(() => {
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr.setMeta(syncedBlockPluginKey, {
activeFlag: {
id: editorExperiment('platform_synced_block_patch_6', true, {
exposure: true
}) ? FLAG_ID.EXTENSION_IN_SYNC_BLOCK : FLAG_ID.INLINE_EXTENSION_IN_SYNC_BLOCK
}
}));
});
}
};
const getDeleteReason = tr => {
const reason = tr.getMeta('deletionReason');
if (!reason) {
return 'source-block-deleted';
}
return reason;
};
const filterTransactionOnline = ({
tr,
state,
syncBlockStore,
api,
confirmationTransactionRef,
bodiedSyncBlockRemoved,
bodiedSyncBlockAdded,
extensionFlagShown
}) => {
const {
removed: syncBlockRemoved,
added: syncBlockAdded
} = trackSyncBlocks(syncBlockStore.referenceManager.isReferenceBlock, tr, state);
syncBlockRemoved.forEach(syncBlock => {
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({
action: ACTION.DELETED,
actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
actionSubjectId: ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_DELETE,
attributes: {
resourceId: syncBlock.attrs.resourceId,
blockInstanceId: syncBlock.attrs.localId
},
eventType: EVENT_TYPE.OPERATIONAL
});
});
syncBlockAdded.forEach(syncBlock => {
var _api$analytics2, _api$analytics2$actio;
api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : (_api$analytics2$actio = _api$analytics2.actions) === null || _api$analytics2$actio === void 0 ? void 0 : _api$analytics2$actio.fireAnalyticsEvent({
action: ACTION.INSERTED,
actionSubject: ACTION_SUBJECT.DOCUMENT,
actionSubjectId: ACTION_SUBJECT_ID.SYNCED_BLOCK,
attributes: {
resourceId: syncBlock.attrs.resourceId,
blockInstanceId: syncBlock.attrs.localId
},
eventType: EVENT_TYPE.TRACK
});
});
if (bodiedSyncBlockRemoved.length > 0) {
// eslint-disable-next-line no-param-reassign
confirmationTransactionRef.current = tr;
return handleBodiedSyncBlockRemoval(bodiedSyncBlockRemoved, syncBlockStore, api, confirmationTransactionRef, getDeleteReason(tr));
}
if (bodiedSyncBlockAdded.length > 0) {
if (tr.getMeta(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;
}
handleBodiedSyncBlockCreation(bodiedSyncBlockAdded, state, api);
return true;
}
showExtensionInSyncBlockWarningIfNeeded(tr, state, api, extensionFlagShown);
return true;
};
const filterTransactionOffline = ({
tr,
state,
syncBlockStore,
api,
isConfirmedSyncBlockDeletion,
bodiedSyncBlockRemoved,
bodiedSyncBlockAdded
}) => {
const {
removed: syncBlockRemoved,
added: syncBlockAdded
} = trackSyncBlocks(syncBlockStore.referenceManager.isReferenceBlock, tr, state);
let errorFlag = false;
if (isConfirmedSyncBlockDeletion || bodiedSyncBlockRemoved.length > 0 || syncBlockRemoved.length > 0) {
errorFlag = FLAG_ID.CANNOT_DELETE_WHEN_OFFLINE;
} else if (bodiedSyncBlockAdded.length > 0 || syncBlockAdded.length > 0) {
errorFlag = FLAG_ID.CANNOT_CREATE_WHEN_OFFLINE;
} else if (hasEditInSyncBlock(tr, state)) {
errorFlag = FLAG_ID.CANNOT_EDIT_WHEN_OFFLINE;
}
if (errorFlag) {
deferDispatch(() => {
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr.setMeta(syncedBlockPluginKey, {
activeFlag: {
id: errorFlag
}
}));
});
return false;
}
return true;
};
/**
* 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.
*/
class SyncedBlockPluginContext {
constructor() {
_defineProperty(this, "confirmationTransactionRef", {
current: undefined
});
_defineProperty(this, "_isCopyEvent", false);
_defineProperty(this, "unpublishedFlagShown", new Set());
_defineProperty(this, "extensionFlagShown", new Set());
}
get isCopyEvent() {
return this._isCopyEvent;
}
markCopyEvent() {
this._isCopyEvent = true;
}
consumeCopyEvent() {
const was = this._isCopyEvent;
this._isCopyEvent = false;
return was;
}
}
export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api) => {
const {
useLongPressSelection = false
} = options || {};
const ctx = new SyncedBlockPluginContext();
const confirmationTransactionRef = ctx.confirmationTransactionRef;
const unpublishedFlagShown = ctx.unpublishedFlagShown;
const 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(() => {
deferDispatch(() => {
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr);
});
});
// Set up callback to detect unpublished sync blocks when they're fetched
syncBlockStore.referenceManager.setOnUnpublishedSyncBlockDetected(resourceId => {
// Only show the flag once per sync block
if (!unpublishedFlagShown.has(resourceId)) {
unpublishedFlagShown.add(resourceId);
deferDispatch(() => {
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
tr
}) => tr.setMeta(syncedBlockPluginKey, {
activeFlag: {
id: FLAG_ID.UNPUBLISHED_SYNC_BLOCK_PASTED
}
}));
});
}
});
return new SafePlugin({
key: syncedBlockPluginKey,
state: {
init(_, instance) {
const syncBlockNodes = instance.doc.children.filter(syncBlockStore.referenceManager.isReferenceBlock);
syncBlockStore.referenceManager.fetchSyncBlocksData(convertPMNodesToSyncBlockNodes(syncBlockNodes));
// Populate source sync block cache from initial document
// When fg is ON, this replaces the constructor call in the nodeview
if (fg('platform_synced_block_update_refactor')) {
instance.doc.forEach(node => {
if (syncBlockStore.sourceManager.isSourceBlock(node)) {
syncBlockStore.sourceManager.updateSyncBlockData(node, false);
}
});
}
return {
selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
activeFlag: false,
syncBlockStore: syncBlockStore,
retryCreationPosMap: new Map(),
hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
};
},
apply: (tr, currentPluginState, oldEditorState) => {
var _meta$activeFlag, _meta$bodiedSyncBlock;
const meta = tr.getMeta(syncedBlockPluginKey);
const {
activeFlag,
selectionDecorationSet,
bodiedSyncBlockDeletionStatus,
retryCreationPosMap
} = currentPluginState;
let newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
: selectionDecorationSet;
if (!tr.selection.eq(oldEditorState.selection)) {
newDecorationSet = calculateDecorations(tr.doc, tr.selection, tr.doc.type.schema);
} else if (tr.docChanged) {
const existingDecorationsLength = selectionDecorationSet.find().length;
const 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 = calculateDecorations(tr.doc, tr.selection, tr.doc.type.schema);
}
}
const newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
const newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
return {
activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
selectionDecorationSet: newDecorationSet,
syncBlockStore: syncBlockStore,
retryCreationPosMap: newRetryCreationPosMap,
bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges()
};
}
},
props: {
nodeViews: {
syncBlock: (node, view, getPos, _decorations) =>
// 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 SyncBlockView({
api,
options,
node,
view,
getPos,
portalProviderAPI: pmPluginFactoryParams.portalProviderAPI,
eventDispatcher: pmPluginFactoryParams.eventDispatcher,
syncBlockStore: syncBlockStore
}).init(),
bodiedSyncBlock: editorExperiment('platform_synced_block_use_new_source_nodeview', true, {
exposure: true
}) ? bodiedSyncBlockNodeView({
pluginOptions: options,
pmPluginFactoryParams,
api,
syncBlockStore
}) : bodiedSyncBlockNodeViewOld({
pluginOptions: options,
pmPluginFactoryParams,
api,
syncBlockStore
})
},
decorations: state => {
var _currentPluginState$s, _api$connectivity2, _api$connectivity2$sh, _api$editorViewMode, _api$editorViewMode$s, _api$userIntent, _api$userIntent$share, _api$focus, _api$focus$sharedStat, _api$focus$sharedStat2;
const currentPluginState = syncedBlockPluginKey.getState(state);
const selectionDecorationSet = (_currentPluginState$s = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.selectionDecorationSet) !== null && _currentPluginState$s !== void 0 ? _currentPluginState$s : DecorationSet.empty;
const syncBlockStore = currentPluginState === null || currentPluginState === void 0 ? void 0 : currentPluginState.syncBlockStore;
const {
doc
} = state;
const isOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity2 = api.connectivity) === null || _api$connectivity2 === void 0 ? void 0 : (_api$connectivity2$sh = _api$connectivity2.sharedState.currentState()) === null || _api$connectivity2$sh === void 0 ? void 0 : _api$connectivity2$sh.mode);
const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view';
const isDragging = (api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : (_api$userIntent$share = _api$userIntent.sharedState.currentState()) === null || _api$userIntent$share === void 0 ? void 0 : _api$userIntent$share.currentUserIntent) === 'dragging';
const offlineDecorations = [];
const viewModeDecorations = [];
const loadingDecorations = [];
const dragDecorations = [];
state.doc.descendants((node, pos) => {
if (node.type.name === 'bodiedSyncBlock' && isOffline) {
offlineDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
class: SyncBlockStateCssClassName.disabledClassName
}));
}
if (syncBlockStore.isSyncBlock(node) && isViewMode) {
viewModeDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
class: SyncBlockStateCssClassName.viewModeClassName
}));
}
if (node.type.name === 'bodiedSyncBlock' && syncBlockStore.sourceManager.isPendingCreation(node.attrs.resourceId)) {
loadingDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
class: SyncBlockStateCssClassName.creationLoadingClassName
}));
}
// Show sync block border while the user is dragging
if (isDragging && syncBlockStore.isSyncBlock(node)) {
dragDecorations.push(Decoration.node(pos, pos + node.nodeSize, {
class: SyncBlockStateCssClassName.draggingClassName
}));
}
});
if (api !== null && api !== void 0 && (_api$focus = api.focus) !== null && _api$focus !== void 0 && (_api$focus$sharedStat = _api$focus.sharedState) !== null && _api$focus$sharedStat !== void 0 && (_api$focus$sharedStat2 = _api$focus$sharedStat.currentState()) !== null && _api$focus$sharedStat2 !== void 0 && _api$focus$sharedStat2.hasFocus || !editorExperiment('platform_synced_block_patch_6', true, {
exposure: true
})) {
// Don't show decorations if the editor is not focused
return selectionDecorationSet.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
} else {
return DecorationSet.empty.add(doc, offlineDecorations).add(doc, viewModeDecorations).add(doc, loadingDecorations).add(doc, dragDecorations);
}
},
handleClickOn: createSelectionClickHandler(['bodiedSyncBlock'], target => !!target.closest(`.${BodiedSyncBlockSharedCssClassName.prefix}`), {
useLongPressSelection
}),
handleDOMEvents: {
mouseover(view, event) {
return shouldIgnoreDomEvent(view, event, api);
},
mousedown(view, event) {
return shouldIgnoreDomEvent(view, event, api);
},
copy: () => {
ctx.markCopyEvent();
return false;
}
},
transformCopied: (slice, {
state
}) => {
const pluginState = syncedBlockPluginKey.getState(state);
const syncBlockStore = pluginState === null || pluginState === void 0 ? void 0 : pluginState.syncBlockStore;
const {
schema
} = state;
const isCopy = ctx.consumeCopyEvent();
if (!syncBlockStore || !isCopy) {
return slice;
}
return mapSlice(slice, node => {
if (syncBlockStore.referenceManager.isReferenceBlock(node)) {
showCopiedFlag(api);
return node;
}
if (node.type.name === 'bodiedSyncBlock' && node.attrs.resourceId) {
// if we only selected part of the bodied sync block content,
// remove the sync block node and only keep the content
if (!sliceFullyContainsNode(slice, node)) {
return node.content;
}
showCopiedFlag(api);
const newResourceId = syncBlockStore.referenceManager.generateResourceIdForReference(node.attrs.resourceId);
// Convert bodiedSyncBlock to syncBlock
// The paste transformation will regenrate the localId
const newAttrs = {
...node.attrs,
resourceId: newResourceId
};
const newMarks = schema.nodes.syncBlock.markSet ? node.marks.filter(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;
});
}
},
filterTransaction: (tr, state) => {
var _api$editorViewMode2, _api$editorViewMode2$, _api$connectivity3, _api$connectivity3$sh;
const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : (_api$editorViewMode2$ = _api$editorViewMode2.sharedState.currentState()) === null || _api$editorViewMode2$ === void 0 ? void 0 : _api$editorViewMode2$.mode;
if (viewMode === 'view' && fg('platform_synced_block_patch_8')) {
return true;
}
const isOffline = isOfflineMode(api === null || api === void 0 ? void 0 : (_api$connectivity3 = api.connectivity) === null || _api$connectivity3 === void 0 ? void 0 : (_api$connectivity3$sh = _api$connectivity3.sharedState.currentState()) === null || _api$connectivity3$sh === void 0 ? void 0 : _api$connectivity3$sh.mode);
const isConfirmedSyncBlockDeletion = Boolean(tr.getMeta('isConfirmedSyncBlockDeletion'));
// Track newly added reference sync blocks before processing the transaction
if (tr.docChanged && !tr.getMeta('isRemote')) {
const {
added
} = trackSyncBlocks(syncBlockStore.referenceManager.isReferenceBlock, tr, state);
added.forEach(nodeInfo => {
var _nodeInfo$attrs;
if ((_nodeInfo$attrs = nodeInfo.attrs) !== null && _nodeInfo$attrs !== void 0 && _nodeInfo$attrs.resourceId) {
syncBlockStore.referenceManager.markAsNewlyAdded(nodeInfo.attrs.resourceId);
}
});
}
if (fg('platform_synced_block_update_refactor')) {
// 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')) {
const {
added
} = trackSyncBlocks(node => syncBlockStore.isSyncBlock(node), tr, state);
const sourceSyncBlockNodes = added.filter(nodeInfo => nodeInfo.node && syncBlockStore.sourceManager.isSourceBlock(nodeInfo.node));
const referenceSyncBlockNodes = added.filter(nodeInfo => nodeInfo.node && syncBlockStore.referenceManager.isReferenceBlock(nodeInfo.node));
sourceSyncBlockNodes.forEach(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'));
}
});
const syncBlockNodes = referenceSyncBlockNodes.map(nodeInfo => nodeInfo.node).filter(node => node !== undefined);
syncBlockStore.referenceManager.fetchSyncBlocksData(convertPMNodesToSyncBlockNodes(syncBlockNodes));
}
}
if (!tr.docChanged || Boolean(tr.getMeta('isRemote')) || !isOffline && isConfirmedSyncBlockDeletion) {
return true;
}
const {
removed: bodiedSyncBlockRemoved,
added: bodiedSyncBlockAdded
} = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, state);
return isOffline ? filterTransactionOffline({
tr,
state,
syncBlockStore,
api,
isConfirmedSyncBlockDeletion,
bodiedSyncBlockRemoved,
bodiedSyncBlockAdded
}) : filterTransactionOnline({
tr,
state,
syncBlockStore,
api,
confirmationTransactionRef,
bodiedSyncBlockRemoved,
bodiedSyncBlockAdded,
extensionFlagShown
});
},
appendTransaction: (trs, oldState, newState) => {
var _api$editorViewMode3, _api$editorViewMode3$;
const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode3 = api.editorViewMode) === null || _api$editorViewMode3 === void 0 ? void 0 : (_api$editorViewMode3$ = _api$editorViewMode3.sharedState.currentState()) === null || _api$editorViewMode3$ === void 0 ? void 0 : _api$editorViewMode3$.mode;
if (viewMode === 'view' && fg('platform_synced_block_patch_8')) {
return null;
}
// Update source sync block cache for user-initiated changes only
// When fg is ON, cache updates are handled here instead of in the nodeview update()
if (fg('platform_synced_block_update_refactor')) {
const isUserChange = tr => tr.docChanged && !isDirtyTransaction(tr) && !tr.getMeta('isRemote');
const hasSourceBlockEdit = trs.some(tr => isUserChange(tr) && hasEditInSyncBlock(tr, oldState));
if (hasSourceBlockEdit) {
newState.doc.forEach(node => {
if (syncBlockStore.sourceManager.isSourceBlock(node)) {
syncBlockStore.sourceManager.updateSyncBlockData(node, false);
}
});
}
}
trs.filter(tr => tr.docChanged).forEach(tr => {
if (confirmationTransactionRef.current) {
confirmationTransactionRef.current = rebaseTransaction(confirmationTransactionRef.current, tr, newState);
}
});
for (const tr of trs) {
if (tr.getMeta(pmHistoryPluginKey)) {
const {
added
} = trackSyncBlocks(syncBlockStore.sourceManager.isSourceBlock, tr, oldState);
if (added.length > 0) {
// Delete bodiedSyncBlock if it's originated from history, i.e. redo creation
// See filterTransaction above for more details
const {
tr
} = newState;
added.forEach(node => {
if (node.from !== undefined && node.to !== undefined) {
tr.delete(node.from, node.to);
}
});
return tr;
}
}
}
// 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.
if (trs.some(tr => tr.docChanged && !tr.getMeta('isRemote')) && fg('platform_synced_block_patch_8')) {
// 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.
const hasInsertedSourceBlock = trs.some(tr => {
if (!tr.docChanged || tr.getMeta('isRemote')) {
return false;
}
return tr.steps.some(step => {
if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep) || !('slice' in step)) {
return false;
}
const {
slice
} = step;
let found = false;
slice.content.descendants(node => {
if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
found = true;
}
return false;
});
return found;
});
});
if (!hasInsertedSourceBlock) {
return null;
}
const seenResourceIds = new Set();
const duplicates = [];
newState.doc.descendants((node, pos) => {
if (syncBlockStore.sourceManager.isSourceBlock(node) && node.attrs.resourceId) {
if (seenResourceIds.has(node.attrs.resourceId)) {
duplicates.push({
pos,
nodeSize: node.nodeSize
});
} else {
seenResourceIds.add(node.attrs.resourceId);
}
return false;
}
});
if (duplicates.length > 0) {
const {
tr
} = newState;
// Delete in reverse document order so positions remain valid
for (let i = duplicates.length - 1; i >= 0; i--) {
const dup = duplicates[i];
tr.delete(dup.pos, dup.pos + dup.nodeSize);
}
tr.setMeta('addToHistory', false);
deferDispatch(() => {
var _api$core;
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
tr
}) => tr.setMeta(syncedBlockPluginKey, {
activeFlag: {
id: FLAG_ID.DUPLICATE_SOURCE_SYNC_BLOCK
}
}));
});
return tr;
}
}
return null;
}
});
};