@atlaskit/editor-plugin-synced-block 8.2.9 → 8.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,9 @@ import { Experience, EXPERIENCE_ID, ExperienceCheckDomMutation, ExperienceCheckT
4
4
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
5
5
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
6
6
  import { fg } from '@atlaskit/platform-feature-flags';
7
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
7
8
  import { SYNCED_BLOCK_BUTTON_TEST_ID } from '../types';
9
+ import { syncedBlockPluginKey } from './main';
8
10
  const TIMEOUT_DURATION = 30000;
9
11
  const pluginKey = new PluginKey('syncedBlockMenuAndToolbarExperience');
10
12
  const SYNCED_BLOCK_BUTTON_TEST_IDS = Object.values(SYNCED_BLOCK_BUTTON_TEST_ID);
@@ -89,69 +91,106 @@ export const getMenuAndToolbarExperiencesPlugin = ({
89
91
  durationMs: TIMEOUT_DURATION
90
92
  }), syncedLocationsDropdownOpenedCheck()]
91
93
  });
92
- const unbindClickListener = bind(document, {
93
- type: 'click',
94
- listener: event => {
95
- const target = event.target;
96
- if (!target) {
97
- return;
98
- }
99
- const button = target.closest('button[data-testid]');
100
- if (!button || !(button instanceof HTMLButtonElement)) {
101
- return;
102
- }
103
- const testId = button.dataset.testid;
104
- if (!isSyncedBlockButtonId(testId)) {
105
- return;
106
- }
107
- if (button.disabled) {
108
- return;
109
- }
110
- handleButtonClick({
111
- testId,
112
- button,
113
- createSourcePrimaryToolbarExperience,
114
- createSourceBlockMenuExperience,
115
- createSourceQuickInsertMenuExperience,
116
- deleteReferenceSyncedBlockExperience,
117
- unsyncReferenceSyncedBlockExperience,
118
- unsyncSourceSyncedBlockExperience,
119
- deleteSourceSyncedBlockExperience,
120
- syncedLocationsExperience
121
- });
122
- },
123
- options: {
124
- capture: true
125
- }
126
- });
127
- const unbindKeydownListener = bind(document, {
128
- type: 'keydown',
129
- listener: event => {
130
- if (isEnterKey(event.key)) {
131
- const typeaheadPopup = popupWithNestedElement(getPopupsTarget(), '.fabric-editor-typeahead');
132
- if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
94
+ const bindListeners = () => {
95
+ const unbindClickListener = bind(document, {
96
+ type: 'click',
97
+ listener: event => {
98
+ const target = event.target;
99
+ if (!target) {
100
+ return;
101
+ }
102
+ const button = target.closest('button[data-testid]');
103
+ if (!button || !(button instanceof HTMLButtonElement)) {
133
104
  return;
134
105
  }
135
- const targetElement = fg('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
136
- if (!targetElement || !(targetElement instanceof HTMLElement)) {
106
+ const testId = button.dataset.testid;
107
+ if (!isSyncedBlockButtonId(testId)) {
137
108
  return;
138
109
  }
139
- const testId = targetElement.dataset.testid;
140
- if (testId === SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
141
- createSourceQuickInsertMenuExperience.start();
110
+ if (button.disabled) {
111
+ return;
112
+ }
113
+ handleButtonClick({
114
+ testId,
115
+ button,
116
+ createSourcePrimaryToolbarExperience,
117
+ createSourceBlockMenuExperience,
118
+ createSourceQuickInsertMenuExperience,
119
+ deleteReferenceSyncedBlockExperience,
120
+ unsyncReferenceSyncedBlockExperience,
121
+ unsyncSourceSyncedBlockExperience,
122
+ deleteSourceSyncedBlockExperience,
123
+ syncedLocationsExperience
124
+ });
125
+ },
126
+ options: {
127
+ capture: true
128
+ }
129
+ });
130
+ const unbindKeydownListener = bind(document, {
131
+ type: 'keydown',
132
+ listener: event => {
133
+ if (isEnterKey(event.key)) {
134
+ const typeaheadPopup = popupWithNestedElement(getPopupsTarget(), '.fabric-editor-typeahead');
135
+ if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
136
+ return;
137
+ }
138
+ const targetElement = fg('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
139
+ if (!targetElement || !(targetElement instanceof HTMLElement)) {
140
+ return;
141
+ }
142
+ const testId = targetElement.dataset.testid;
143
+ if (testId === SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
144
+ createSourceQuickInsertMenuExperience.start();
145
+ }
142
146
  }
147
+ },
148
+ options: {
149
+ capture: true
143
150
  }
144
- },
145
- options: {
146
- capture: true
147
- }
148
- });
151
+ });
152
+ return {
153
+ unbindClickListener,
154
+ unbindKeydownListener
155
+ };
156
+ };
149
157
  return new SafePlugin({
150
158
  key: pluginKey,
151
159
  view: view => {
160
+ var _syncedBlockPluginKey;
152
161
  editorViewRef.current = view;
162
+
163
+ // Track whether listeners have been bound. When the experiment is
164
+ // ON and the document initially has no synced blocks, we defer
165
+ // binding until `update()` detects that `hasSyncedBlocks` has
166
+ // flipped to `true` (e.g. via paste or collab insert). This avoids
167
+ // the ~2-5 ms TBT cost of capture-phase click/keydown handlers on
168
+ // the ~99.97 % of pages that never use synced blocks (EDITOR-6931).
169
+ let listenersBound = false;
170
+ let unbindClickListener;
171
+ let unbindKeydownListener;
172
+ const ensureListenersBound = () => {
173
+ if (listenersBound) {
174
+ return;
175
+ }
176
+ listenersBound = true;
177
+ const unbinders = bindListeners();
178
+ unbindClickListener = unbinders.unbindClickListener;
179
+ unbindKeydownListener = unbinders.unbindKeydownListener;
180
+ };
181
+ if ((_syncedBlockPluginKey = syncedBlockPluginKey.getState(view.state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks || !expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
182
+ ensureListenersBound();
183
+ }
153
184
  return {
185
+ update: (view, prevState) => {
186
+ var _syncedBlockPluginKey2;
187
+ if (!listenersBound && view.state.doc !== prevState.doc && (_syncedBlockPluginKey2 = syncedBlockPluginKey.getState(view.state)) !== null && _syncedBlockPluginKey2 !== void 0 && _syncedBlockPluginKey2.hasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
188
+ // Bind listeners now that synced blocks are present.
189
+ ensureListenersBound();
190
+ }
191
+ },
154
192
  destroy: () => {
193
+ var _unbindClickListener, _unbindKeydownListene;
155
194
  createSourcePrimaryToolbarExperience.abort({
156
195
  reason: 'editorDestroyed'
157
196
  });
@@ -176,8 +215,8 @@ export const getMenuAndToolbarExperiencesPlugin = ({
176
215
  syncedLocationsExperience === null || syncedLocationsExperience === void 0 ? void 0 : syncedLocationsExperience.abort({
177
216
  reason: 'editorDestroyed'
178
217
  });
179
- unbindClickListener();
180
- unbindKeydownListener();
218
+ (_unbindClickListener = unbindClickListener) === null || _unbindClickListener === void 0 ? void 0 : _unbindClickListener();
219
+ (_unbindKeydownListene = unbindKeydownListener) === null || _unbindKeydownListene === void 0 ? void 0 : _unbindKeydownListene();
181
220
  }
182
221
  };
183
222
  }
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
2
  import { syncBlock, bodiedSyncBlock } from '@atlaskit/adf-schema';
3
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
3
4
  import { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
5
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
6
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
5
7
  import { flushBodiedSyncBlocks, flushSyncBlocks, discardUnpublishedSyncBlocks } from './editor-actions';
6
8
  import { copySyncedBlockReferenceToClipboardEditorCommand, createSyncedBlock } from './editor-commands';
@@ -14,6 +16,36 @@ import { getToolbarConfig } from './ui/floating-toolbar';
14
16
  import { getQuickInsertConfig } from './ui/quick-insert';
15
17
  import { SyncBlockRefresher } from './ui/SyncBlockRefresher';
16
18
  import { getToolbarComponents } from './ui/toolbar-components';
19
+
20
+ /**
21
+ * EDITOR-6929 / PR-G: Guard contentComponent rendering.
22
+ * When `hasSyncedBlocks` is false return null
23
+ * to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag —
24
+ * their hooks (useSharedPluginStateWithSelector) would execute selectors on
25
+ * every transaction for no benefit on the ~99.98% of pages with zero synced
26
+ * blocks.
27
+ */
28
+ const LazySyncedBlockUI = ({
29
+ syncBlockStore: syncBlockStoreManager,
30
+ api
31
+ }) => {
32
+ const hasSyncBlocks = useSharedPluginStateWithSelector(api, ['syncedBlock'], states => {
33
+ var _states$syncedBlockSt;
34
+ return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks;
35
+ });
36
+ if (!hasSyncBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
37
+ return null;
38
+ }
39
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, {
40
+ syncBlockStoreManager: syncBlockStoreManager,
41
+ api: api
42
+ }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, {
43
+ syncBlockStoreManager: syncBlockStoreManager,
44
+ api: api
45
+ }), /*#__PURE__*/React.createElement(Flag, {
46
+ api: api
47
+ }));
48
+ };
17
49
  export const syncedBlockPlugin = ({
18
50
  config,
19
51
  api
@@ -22,7 +54,15 @@ export const syncedBlockPlugin = ({
22
54
  const refs = {};
23
55
  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;
24
56
  const syncBlockStore = new SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
57
+ const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
25
58
  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);
59
+
60
+ // --- Memoized getSharedState (EDITOR-6929 / PR-F) ---
61
+ // Cache the last returned shared state object. On each call, perform a
62
+ // shallow comparison of all fields against the cached value. If nothing
63
+ // changed, return the cached reference so SharedStateAPI subscribers
64
+ // (React components) skip re-rendering.
65
+ let cachedSharedState;
26
66
  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));
27
67
  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));
28
68
  return {
@@ -84,7 +124,19 @@ export const syncedBlockPlugin = ({
84
124
  },
85
125
  pluginsOptions: {
86
126
  quickInsert: getQuickInsertConfig(config, api, syncBlockStore),
87
- floatingToolbar: (state, intl) => getToolbarConfig(state, intl, api, syncBlockStore)
127
+ floatingToolbar: (state, intl) => {
128
+ var _syncedBlockPluginKey;
129
+ // When the experiment is ON and the document has no synced blocks,
130
+ // skip the toolbar config entirely to avoid the per-selection-change
131
+ // cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931).
132
+ // Save the expValEquals('editor_synced_block_perf', 'isEnabled', true) in a const
133
+ // because floatingToolbar is called on every selection change.
134
+ // computing it once at plugin initialisation is more efficient.
135
+ if (!((_syncedBlockPluginKey = syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks) && isPerfExperimentOn) {
136
+ return undefined;
137
+ }
138
+ return getToolbarConfig(state, intl, api, syncBlockStore);
139
+ }
88
140
  },
89
141
  contentComponent: ({
90
142
  containerElement,
@@ -94,20 +146,16 @@ export const syncedBlockPlugin = ({
94
146
  refs.containerElement = containerElement || undefined;
95
147
  refs.popupsMountPoint = popupsMountPoint || undefined;
96
148
  refs.wrapperElement = wrapperElement || undefined;
97
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SyncBlockRefresher, {
98
- syncBlockStoreManager: syncBlockStore,
149
+ return /*#__PURE__*/React.createElement(LazySyncedBlockUI, {
150
+ syncBlockStore: syncBlockStore,
99
151
  api: api
100
- }), /*#__PURE__*/React.createElement(DeleteConfirmationModal, {
101
- syncBlockStoreManager: syncBlockStore,
102
- api: api
103
- }), /*#__PURE__*/React.createElement(Flag, {
104
- api: api
105
- }));
152
+ });
106
153
  },
107
154
  getSharedState: editorState => {
108
155
  if (!editorState) {
109
156
  return;
110
157
  }
158
+ const pluginState = syncedBlockPluginKey.getState(editorState);
111
159
  const {
112
160
  activeFlag,
113
161
  syncBlockStore: currentSyncBlockStore,
@@ -115,8 +163,14 @@ export const syncedBlockPlugin = ({
115
163
  retryCreationPosMap,
116
164
  hasSyncedBlocks,
117
165
  hasUnsavedBodiedSyncBlockChanges
118
- } = syncedBlockPluginKey.getState(editorState);
119
- return {
166
+ } = pluginState;
167
+
168
+ // --- EDITOR-6929 / PR-F: return a stable reference when all
169
+ // fields are unchanged to prevent unnecessary React re-renders. ---
170
+ if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
171
+ return cachedSharedState;
172
+ }
173
+ const nextSharedState = {
120
174
  activeFlag,
121
175
  syncBlockStore: currentSyncBlockStore,
122
176
  bodiedSyncBlockDeletionStatus,
@@ -124,6 +178,8 @@ export const syncedBlockPlugin = ({
124
178
  hasSyncedBlocks,
125
179
  hasUnsavedBodiedSyncBlockChanges
126
180
  };
181
+ cachedSharedState = nextSharedState;
182
+ return nextSharedState;
127
183
  }
128
184
  };
129
185
  };
@@ -350,7 +350,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
350
350
  // When the perf gate is ON and the doc has synced blocks we do a
351
351
  // single traversal here; afterwards `apply()` will map or rebuild
352
352
  // only when a status signal changes.
353
- var initStatusDecorationSet = expValEquals('editor_synced_block_perf', 'isEnabled', true) && docHasSyncedBlocks ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
353
+ var initStatusDecorationSet = docHasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
354
354
  return {
355
355
  selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
356
356
  activeFlag: false,
@@ -367,6 +367,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
367
367
  apply: function apply(tr, currentPluginState, oldEditorState) {
368
368
  var _meta$activeFlag, _meta$bodiedSyncBlock;
369
369
  var meta = tr.getMeta(syncedBlockPluginKey);
370
+ var isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
370
371
  var activeFlag = currentPluginState.activeFlag,
371
372
  selectionDecorationSet = currentPluginState.selectionDecorationSet,
372
373
  bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
@@ -380,11 +381,19 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
380
381
  // Lazy-init bookkeeping: once a synced block enters the document we
381
382
  // flip `hasSyncedBlocks` to `true` for the lifetime of this editor
382
383
  var nextHasSyncedBlocks = prevHasSyncedBlocks;
383
- if (!prevHasSyncedBlocks && tr.docChanged && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
384
+ if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
384
385
  if (transactionInsertsSyncedBlock(tr)) {
385
386
  nextHasSyncedBlocks = true;
386
387
  }
387
388
  }
389
+
390
+ // --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
391
+ // no meta is set, and the selection/doc haven't changed in a way
392
+ // that affects our state, return the SAME object reference so
393
+ // SharedStateAPI skips notifying subscribers. ---
394
+ if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
395
+ return currentPluginState;
396
+ }
388
397
  var newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
389
398
  : selectionDecorationSet;
390
399
  if (!tr.selection.eq(oldEditorState.selection)) {
@@ -408,7 +417,7 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
408
417
  var nextIsOffline = prevOffline;
409
418
  var nextIsViewMode = prevViewMode;
410
419
  var nextIsDragging = prevDragging;
411
- if (expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
420
+ if (isPerfExperimentOn) {
412
421
  if (!nextHasSyncedBlocks) {
413
422
  // No synced blocks → keep empty status decorations
414
423
  nextStatusDecorationSet = DecorationSet.empty;
@@ -437,14 +446,24 @@ export var createPlugin = function createPlugin(options, pmPluginFactoryParams,
437
446
  }
438
447
  var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
439
448
  var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
449
+ var nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
450
+ var nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
451
+ var nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
452
+
453
+ // --- Reference equality (EDITOR-6929): return the same object
454
+ // when ALL fields are reference-equal to avoid SharedStateAPI
455
+ // notifying subscribers and triggering React re-renders. ---
456
+ if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
457
+ return currentPluginState;
458
+ }
440
459
  return {
441
- activeFlag: (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag,
460
+ activeFlag: nextActiveFlag,
442
461
  selectionDecorationSet: newDecorationSet,
443
462
  syncBlockStore: syncBlockStore,
444
463
  retryCreationPosMap: newRetryCreationPosMap,
445
464
  hasSyncedBlocks: nextHasSyncedBlocks,
446
- bodiedSyncBlockDeletionStatus: (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus,
447
- hasUnsavedBodiedSyncBlockChanges: syncBlockStore.sourceManager.hasUnsavedChanges(),
465
+ bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
466
+ hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
448
467
  statusDecorationSet: nextStatusDecorationSet,
449
468
  prevIsOffline: nextIsOffline,
450
469
  prevIsViewMode: nextIsViewMode,
@@ -5,7 +5,9 @@ import { Experience, EXPERIENCE_ID, ExperienceCheckDomMutation, ExperienceCheckT
5
5
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
6
6
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
7
7
  import { fg } from '@atlaskit/platform-feature-flags';
8
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
8
9
  import { SYNCED_BLOCK_BUTTON_TEST_ID } from '../types';
10
+ import { syncedBlockPluginKey } from './main';
9
11
  var TIMEOUT_DURATION = 30000;
10
12
  var pluginKey = new PluginKey('syncedBlockMenuAndToolbarExperience');
11
13
  var SYNCED_BLOCK_BUTTON_TEST_IDS = Object.values(SYNCED_BLOCK_BUTTON_TEST_ID);
@@ -89,69 +91,106 @@ export var getMenuAndToolbarExperiencesPlugin = function getMenuAndToolbarExperi
89
91
  durationMs: TIMEOUT_DURATION
90
92
  }), syncedLocationsDropdownOpenedCheck()]
91
93
  });
92
- var unbindClickListener = bind(document, {
93
- type: 'click',
94
- listener: function listener(event) {
95
- var target = event.target;
96
- if (!target) {
97
- return;
98
- }
99
- var button = target.closest('button[data-testid]');
100
- if (!button || !(button instanceof HTMLButtonElement)) {
101
- return;
102
- }
103
- var testId = button.dataset.testid;
104
- if (!isSyncedBlockButtonId(testId)) {
105
- return;
106
- }
107
- if (button.disabled) {
108
- return;
109
- }
110
- handleButtonClick({
111
- testId: testId,
112
- button: button,
113
- createSourcePrimaryToolbarExperience: createSourcePrimaryToolbarExperience,
114
- createSourceBlockMenuExperience: createSourceBlockMenuExperience,
115
- createSourceQuickInsertMenuExperience: createSourceQuickInsertMenuExperience,
116
- deleteReferenceSyncedBlockExperience: deleteReferenceSyncedBlockExperience,
117
- unsyncReferenceSyncedBlockExperience: unsyncReferenceSyncedBlockExperience,
118
- unsyncSourceSyncedBlockExperience: unsyncSourceSyncedBlockExperience,
119
- deleteSourceSyncedBlockExperience: deleteSourceSyncedBlockExperience,
120
- syncedLocationsExperience: syncedLocationsExperience
121
- });
122
- },
123
- options: {
124
- capture: true
125
- }
126
- });
127
- var unbindKeydownListener = bind(document, {
128
- type: 'keydown',
129
- listener: function listener(event) {
130
- if (isEnterKey(event.key)) {
131
- var typeaheadPopup = popupWithNestedElement(getPopupsTarget(), '.fabric-editor-typeahead');
132
- if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
94
+ var bindListeners = function bindListeners() {
95
+ var unbindClickListener = bind(document, {
96
+ type: 'click',
97
+ listener: function listener(event) {
98
+ var target = event.target;
99
+ if (!target) {
100
+ return;
101
+ }
102
+ var button = target.closest('button[data-testid]');
103
+ if (!button || !(button instanceof HTMLButtonElement)) {
104
+ return;
105
+ }
106
+ var testId = button.dataset.testid;
107
+ if (!isSyncedBlockButtonId(testId)) {
133
108
  return;
134
109
  }
135
- var targetElement = fg('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
136
- if (!targetElement || !(targetElement instanceof HTMLElement)) {
110
+ if (button.disabled) {
137
111
  return;
138
112
  }
139
- var testId = targetElement.dataset.testid;
140
- if (testId === SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
141
- createSourceQuickInsertMenuExperience.start();
113
+ handleButtonClick({
114
+ testId: testId,
115
+ button: button,
116
+ createSourcePrimaryToolbarExperience: createSourcePrimaryToolbarExperience,
117
+ createSourceBlockMenuExperience: createSourceBlockMenuExperience,
118
+ createSourceQuickInsertMenuExperience: createSourceQuickInsertMenuExperience,
119
+ deleteReferenceSyncedBlockExperience: deleteReferenceSyncedBlockExperience,
120
+ unsyncReferenceSyncedBlockExperience: unsyncReferenceSyncedBlockExperience,
121
+ unsyncSourceSyncedBlockExperience: unsyncSourceSyncedBlockExperience,
122
+ deleteSourceSyncedBlockExperience: deleteSourceSyncedBlockExperience,
123
+ syncedLocationsExperience: syncedLocationsExperience
124
+ });
125
+ },
126
+ options: {
127
+ capture: true
128
+ }
129
+ });
130
+ var unbindKeydownListener = bind(document, {
131
+ type: 'keydown',
132
+ listener: function listener(event) {
133
+ if (isEnterKey(event.key)) {
134
+ var typeaheadPopup = popupWithNestedElement(getPopupsTarget(), '.fabric-editor-typeahead');
135
+ if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
136
+ return;
137
+ }
138
+ var targetElement = fg('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
139
+ if (!targetElement || !(targetElement instanceof HTMLElement)) {
140
+ return;
141
+ }
142
+ var testId = targetElement.dataset.testid;
143
+ if (testId === SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
144
+ createSourceQuickInsertMenuExperience.start();
145
+ }
142
146
  }
147
+ },
148
+ options: {
149
+ capture: true
143
150
  }
144
- },
145
- options: {
146
- capture: true
147
- }
148
- });
151
+ });
152
+ return {
153
+ unbindClickListener: unbindClickListener,
154
+ unbindKeydownListener: unbindKeydownListener
155
+ };
156
+ };
149
157
  return new SafePlugin({
150
158
  key: pluginKey,
151
159
  view: function view(_view) {
160
+ var _syncedBlockPluginKey;
152
161
  editorViewRef.current = _view;
162
+
163
+ // Track whether listeners have been bound. When the experiment is
164
+ // ON and the document initially has no synced blocks, we defer
165
+ // binding until `update()` detects that `hasSyncedBlocks` has
166
+ // flipped to `true` (e.g. via paste or collab insert). This avoids
167
+ // the ~2-5 ms TBT cost of capture-phase click/keydown handlers on
168
+ // the ~99.97 % of pages that never use synced blocks (EDITOR-6931).
169
+ var listenersBound = false;
170
+ var unbindClickListener;
171
+ var unbindKeydownListener;
172
+ var ensureListenersBound = function ensureListenersBound() {
173
+ if (listenersBound) {
174
+ return;
175
+ }
176
+ listenersBound = true;
177
+ var unbinders = bindListeners();
178
+ unbindClickListener = unbinders.unbindClickListener;
179
+ unbindKeydownListener = unbinders.unbindKeydownListener;
180
+ };
181
+ if ((_syncedBlockPluginKey = syncedBlockPluginKey.getState(_view.state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks || !expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
182
+ ensureListenersBound();
183
+ }
153
184
  return {
185
+ update: function update(view, prevState) {
186
+ var _syncedBlockPluginKey2;
187
+ if (!listenersBound && view.state.doc !== prevState.doc && (_syncedBlockPluginKey2 = syncedBlockPluginKey.getState(view.state)) !== null && _syncedBlockPluginKey2 !== void 0 && _syncedBlockPluginKey2.hasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true)) {
188
+ // Bind listeners now that synced blocks are present.
189
+ ensureListenersBound();
190
+ }
191
+ },
154
192
  destroy: function destroy() {
193
+ var _unbindClickListener, _unbindKeydownListene;
155
194
  createSourcePrimaryToolbarExperience.abort({
156
195
  reason: 'editorDestroyed'
157
196
  });
@@ -176,8 +215,8 @@ export var getMenuAndToolbarExperiencesPlugin = function getMenuAndToolbarExperi
176
215
  syncedLocationsExperience === null || syncedLocationsExperience === void 0 || syncedLocationsExperience.abort({
177
216
  reason: 'editorDestroyed'
178
217
  });
179
- unbindClickListener();
180
- unbindKeydownListener();
218
+ (_unbindClickListener = unbindClickListener) === null || _unbindClickListener === void 0 || _unbindClickListener();
219
+ (_unbindKeydownListene = unbindKeydownListener) === null || _unbindKeydownListene === void 0 || _unbindKeydownListene();
181
220
  }
182
221
  };
183
222
  }