UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

204 lines (201 loc) 10.8 kB
import React from 'react'; import { syncBlock, bodiedSyncBlock } from '@atlaskit/adf-schema'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { flushBodiedSyncBlocks, flushSyncBlocks, discardUnpublishedSyncBlocks } from './editor-actions'; import { copySyncedBlockReferenceToClipboardEditorCommand, createSyncedBlock } from './editor-commands'; import { createPlugin, syncedBlockPluginKey } from './pm-plugins/main'; import { getMenuAndToolbarExperiencesPlugin } from './pm-plugins/menu-and-toolbar-experiences'; import { getBlockMenuComponents } from './ui/block-menu-components'; import { DeleteConfirmationModal } from './ui/DeleteConfirmationModal'; import { Flag } from './ui/Flag'; import { getToolbarConfig } from './ui/floating-toolbar'; import { getQuickInsertConfig } from './ui/quick-insert'; import { getSyncedBlockQuickInsertComponents } from './ui/quick-insert/getSyncedBlockQuickInsertComponents'; import { SourceSyncBlockPlaceholder } from './ui/SourceSyncBlockPlaceholder'; import { SyncBlockRefresher } from './ui/SyncBlockRefresher'; import { getToolbarComponents } from './ui/toolbar-components'; /** * EDITOR-6929 / PR-G: Guard contentComponent rendering. * When `hasSyncedBlocks` is false return null * to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag — * their hooks (useSharedPluginStateWithSelector) would execute selectors on * every transaction for no benefit on the ~99.98% of pages with zero synced * blocks. */ const LazySyncedBlockUI = ({ syncBlockStore: syncBlockStoreManager, api, editorView, onFeedbackPromptShown, onGiveFeedback }) => { const hasSyncBlocks = useSharedPluginStateWithSelector(api, ['syncedBlock'], states => { var _states$syncedBlockSt; return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks; }); if (!hasSyncBlocks) { return null; } return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, { syncBlockStoreManager: syncBlockStoreManager, api: api }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, { syncBlockStoreManager: syncBlockStoreManager, api: api, editorView: editorView }), /*#__PURE__*/React.createElement(Flag, { api: api, onFeedbackPromptShown: onFeedbackPromptShown, onGiveFeedback: onGiveFeedback })); }; export const syncedBlockPlugin = ({ config, api }) => { var _api$editorViewMode, _api$editorViewMode$s, _api$analytics, _api$analytics$action, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2; const refs = {}; const viewMode = 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; const syncBlockStore = new SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage); syncBlockStore.setFireAnalyticsEvent(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); // --- Memoized getSharedState (EDITOR-6929 / PR-F) --- // Cache the last returned shared state object. On each call, perform a // shallow comparison of all fields against the cached value. If nothing // changed, return the cached reference so SharedStateAPI subscribers // (React components) skip re-rendering. let cachedSharedState; api === null || api === void 0 ? void 0 : (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 ? void 0 : _api$blockMenu.actions.registerBlockMenuComponents(getBlockMenuComponents(api, (_config$enableSourceC = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC !== void 0 ? _config$enableSourceC : false)); api === null || api === void 0 ? void 0 : (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 ? void 0 : _api$toolbar.actions.registerComponents(getToolbarComponents(api, (_config$enableSourceC2 = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC2 !== void 0 ? _config$enableSourceC2 : false)); const isRegisteredSlashCommandEnabled = isExperimentEnabled('platform_editor_slash_command'); if (isRegisteredSlashCommandEnabled && config !== null && config !== void 0 && config.enableSourceCreation) { var _api$uiControlRegistr; api === null || api === void 0 ? void 0 : (_api$uiControlRegistr = api.uiControlRegistry) === null || _api$uiControlRegistr === void 0 ? void 0 : _api$uiControlRegistr.actions.register(getSyncedBlockQuickInsertComponents({ api, syncBlockStore })); } return { name: 'syncedBlock', nodes() { return [{ name: 'syncBlock', node: syncBlock }, { name: 'bodiedSyncBlock', node: bodiedSyncBlock }]; }, pmPlugins() { return [{ name: 'syncedBlockPlugin', plugin: params => createPlugin(config, params, syncBlockStore, api) }, { name: 'menuAndToolbarExperiencesPlugin', plugin: () => getMenuAndToolbarExperiencesPlugin({ refs, dispatchAnalyticsEvent: payload => { var _api$analytics2, _api$analytics2$actio; return 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(payload); } }) }]; }, commands: { copySyncedBlockReferenceToClipboard: inputMethod => copySyncedBlockReferenceToClipboardEditorCommand(syncBlockStore, inputMethod, api, config === null || config === void 0 ? void 0 : config.__livePage), insertSyncedBlock: inputMethod => ({ tr }) => { var _api$analytics3; if (!(config !== null && config !== void 0 && config.enableSourceCreation)) { return null; } return createSyncedBlock({ tr, syncBlockStore, fireAnalyticsEvent: api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions.fireAnalyticsEvent, inputMethod }) || null; } }, actions: { flushBodiedSyncBlocks: () => { return flushBodiedSyncBlocks(syncBlockStore); }, flushSyncedBlocks: () => { return flushSyncBlocks(syncBlockStore); }, discardUnpublishedSyncBlocks: () => { return discardUnpublishedSyncBlocks(syncBlockStore); } }, pluginsOptions: { ...(!isRegisteredSlashCommandEnabled && { quickInsert: getQuickInsertConfig(config, api, syncBlockStore) }), floatingToolbar: (state, intl) => { var _syncedBlockPluginKey; // When registered slash-command support is on and the document has no synced blocks, // skip the toolbar config entirely to avoid the per-selection-change // cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931). if (!((_syncedBlockPluginKey = syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks)) { return undefined; } return getToolbarConfig(state, intl, api, syncBlockStore, config === null || config === void 0 ? void 0 : config.__livePage, config === null || config === void 0 ? void 0 : config.onGiveFeedback); } }, contentComponent: ({ containerElement, wrapperElement, popupsMountPoint, editorView }) => { refs.containerElement = containerElement || undefined; refs.popupsMountPoint = popupsMountPoint || undefined; refs.wrapperElement = wrapperElement || undefined; return /*#__PURE__*/React.createElement(React.Fragment, null, expValEquals('platform_editor_sync_block_activation', 'isEnabled', true) && /*#__PURE__*/React.createElement(SourceSyncBlockPlaceholder, null), /*#__PURE__*/React.createElement(LazySyncedBlockUI, { syncBlockStore: syncBlockStore, api: api, editorView: editorView, onFeedbackPromptShown: config === null || config === void 0 ? void 0 : config.onFeedbackPromptShown, onGiveFeedback: config === null || config === void 0 ? void 0 : config.onGiveFeedback })); }, getSharedState: editorState => { if (!editorState) { return; } const pluginState = syncedBlockPluginKey.getState(editorState); const { activeFlag, syncBlockStore: currentSyncBlockStore, bodiedSyncBlockDeletionStatus, retryCreationPosMap, hasSyncedBlocks, hasUnsavedBodiedSyncBlockChanges } = pluginState; // --- EDITOR-6929 / PR-F: return a stable reference when all // fields are unchanged to prevent unnecessary React re-renders. --- if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges) { return cachedSharedState; } const nextSharedState = { activeFlag, syncBlockStore: currentSyncBlockStore, bodiedSyncBlockDeletionStatus, retryCreationPosMap, hasSyncedBlocks, hasUnsavedBodiedSyncBlockChanges }; cachedSharedState = nextSharedState; return nextSharedState; }, // Destroy the SyncBlockStoreManager on editor unmount to cancel // pending timers, subscriptions, and in-flight fetches. destroy() { syncBlockStore.destroy(); } }; };