@atlaskit/editor-plugin-synced-block 8.2.8 → 8.2.10

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.
package/AGENTS.md CHANGED
@@ -65,24 +65,24 @@ src/
65
65
 
66
66
  ### Editor Actions
67
67
 
68
- This package exposes top-level **editor actions** (in `editor-actions/index.ts`)
69
- that products call from outside the plugin lifecycle:
68
+ This package exposes top-level **editor actions** (in `editor-actions/index.ts`) that products call
69
+ from outside the plugin lifecycle:
70
70
 
71
71
  - `flushBodiedSyncBlocks(store)` — flush all dirty source blocks
72
72
  - `flushSyncBlocks(store)` — flush reference manager (e.g. on save)
73
- - `discardUnpublishedSyncBlocks(store)` — delete unpublished blocks on cancel
74
- (added in EDITOR-6473; used by Confluence's editor cancel flow)
73
+ - `discardUnpublishedSyncBlocks(store)` — delete unpublished blocks on cancel (added in EDITOR-6473;
74
+ used by Confluence's editor cancel flow)
75
75
 
76
76
  ### Lazy Init & Perf (EDITOR-6928 / EDITOR-6930)
77
77
 
78
78
  Behind the `editor_synced_block_perf` experiment, `main.ts`:
79
- - Skips creating synced-block plugin state and node-views for documents
80
- with no synced blocks (`hasSyncedBlocks(doc)`).
81
- - Computes `statusDecorationSet` inside `apply()` and stores it on plugin
82
- state, then exposes it via an O(1) `decorations` prop instead of an
83
- O(n) `doc.descendants()` walk on every transaction.
84
- - Uses `sourceSyncBlockStoreManager.hasPendingCreations()` for an O(1)
85
- pending-creation early return in `buildStatusDecorations()`.
79
+
80
+ - Skips creating synced-block plugin state and node-views for documents with no synced blocks
81
+ (`hasSyncedBlocks(doc)`).
82
+ - Computes `statusDecorationSet` inside `apply()` and stores it on plugin state, then exposes it via
83
+ an O(1) `decorations` prop instead of an O(n) `doc.descendants()` walk on every transaction.
84
+ - Uses `sourceSyncBlockStoreManager.hasPendingCreations()` for an O(1) pending-creation early return
85
+ in `buildStatusDecorations()`.
86
86
 
87
87
  ### Key Code Patterns
88
88
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atlaskit/editor-plugin-synced-block
2
2
 
3
+ ## 8.2.10
4
+
5
+ ### Patch Changes
6
+
7
+ - [`0a702a2b251d1`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/0a702a2b251d1) -
8
+ Guard menuAndToolbarExperiencesPlugin DOM listeners and floatingToolbar config behind
9
+ hasSyncedBlocks to avoid unnecessary work on pages without synced blocks
10
+ - Updated dependencies
11
+
12
+ ## 8.2.9
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies
17
+
3
18
  ## 8.2.8
4
19
 
5
20
  ### Patch Changes
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "extends": "../../../../tsconfig.local-consumption.json",
3
3
  "compilerOptions": {
4
- "target": "es5",
5
4
  "outDir": "../../../../../confluence/tsDist/@atlaskit__editor-plugin-synced-block",
6
5
  "rootDir": "../",
7
6
  "composite": true,
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "extends": "../../../../tsconfig.local-consumption.json",
3
3
  "compilerOptions": {
4
- "target": "es5",
5
4
  "outDir": "../../../../../tsDist/@atlaskit__editor-plugin-synced-block/app",
6
5
  "rootDir": "../",
7
6
  "composite": true,
@@ -12,7 +12,9 @@ var _experiences = require("@atlaskit/editor-common/experiences");
12
12
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
13
13
  var _state = require("@atlaskit/editor-prosemirror/state");
14
14
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
15
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
15
16
  var _types = require("../types");
17
+ var _main = require("./main");
16
18
  var TIMEOUT_DURATION = 30000;
17
19
  var pluginKey = new _state.PluginKey('syncedBlockMenuAndToolbarExperience');
18
20
  var SYNCED_BLOCK_BUTTON_TEST_IDS = Object.values(_types.SYNCED_BLOCK_BUTTON_TEST_ID);
@@ -96,69 +98,106 @@ var getMenuAndToolbarExperiencesPlugin = exports.getMenuAndToolbarExperiencesPlu
96
98
  durationMs: TIMEOUT_DURATION
97
99
  }), syncedLocationsDropdownOpenedCheck()]
98
100
  });
99
- var unbindClickListener = (0, _bindEventListener.bind)(document, {
100
- type: 'click',
101
- listener: function listener(event) {
102
- var target = event.target;
103
- if (!target) {
104
- return;
105
- }
106
- var button = target.closest('button[data-testid]');
107
- if (!button || !(button instanceof HTMLButtonElement)) {
108
- return;
109
- }
110
- var testId = button.dataset.testid;
111
- if (!isSyncedBlockButtonId(testId)) {
112
- return;
113
- }
114
- if (button.disabled) {
115
- return;
116
- }
117
- handleButtonClick({
118
- testId: testId,
119
- button: button,
120
- createSourcePrimaryToolbarExperience: createSourcePrimaryToolbarExperience,
121
- createSourceBlockMenuExperience: createSourceBlockMenuExperience,
122
- createSourceQuickInsertMenuExperience: createSourceQuickInsertMenuExperience,
123
- deleteReferenceSyncedBlockExperience: deleteReferenceSyncedBlockExperience,
124
- unsyncReferenceSyncedBlockExperience: unsyncReferenceSyncedBlockExperience,
125
- unsyncSourceSyncedBlockExperience: unsyncSourceSyncedBlockExperience,
126
- deleteSourceSyncedBlockExperience: deleteSourceSyncedBlockExperience,
127
- syncedLocationsExperience: syncedLocationsExperience
128
- });
129
- },
130
- options: {
131
- capture: true
132
- }
133
- });
134
- var unbindKeydownListener = (0, _bindEventListener.bind)(document, {
135
- type: 'keydown',
136
- listener: function listener(event) {
137
- if (isEnterKey(event.key)) {
138
- var typeaheadPopup = (0, _experiences.popupWithNestedElement)(getPopupsTarget(), '.fabric-editor-typeahead');
139
- if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
101
+ var bindListeners = function bindListeners() {
102
+ var unbindClickListener = (0, _bindEventListener.bind)(document, {
103
+ type: 'click',
104
+ listener: function listener(event) {
105
+ var target = event.target;
106
+ if (!target) {
107
+ return;
108
+ }
109
+ var button = target.closest('button[data-testid]');
110
+ if (!button || !(button instanceof HTMLButtonElement)) {
140
111
  return;
141
112
  }
142
- var targetElement = (0, _platformFeatureFlags.fg)('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
143
- if (!targetElement || !(targetElement instanceof HTMLElement)) {
113
+ var testId = button.dataset.testid;
114
+ if (!isSyncedBlockButtonId(testId)) {
144
115
  return;
145
116
  }
146
- var testId = targetElement.dataset.testid;
147
- if (testId === _types.SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
148
- createSourceQuickInsertMenuExperience.start();
117
+ if (button.disabled) {
118
+ return;
149
119
  }
120
+ handleButtonClick({
121
+ testId: testId,
122
+ button: button,
123
+ createSourcePrimaryToolbarExperience: createSourcePrimaryToolbarExperience,
124
+ createSourceBlockMenuExperience: createSourceBlockMenuExperience,
125
+ createSourceQuickInsertMenuExperience: createSourceQuickInsertMenuExperience,
126
+ deleteReferenceSyncedBlockExperience: deleteReferenceSyncedBlockExperience,
127
+ unsyncReferenceSyncedBlockExperience: unsyncReferenceSyncedBlockExperience,
128
+ unsyncSourceSyncedBlockExperience: unsyncSourceSyncedBlockExperience,
129
+ deleteSourceSyncedBlockExperience: deleteSourceSyncedBlockExperience,
130
+ syncedLocationsExperience: syncedLocationsExperience
131
+ });
132
+ },
133
+ options: {
134
+ capture: true
150
135
  }
151
- },
152
- options: {
153
- capture: true
154
- }
155
- });
136
+ });
137
+ var unbindKeydownListener = (0, _bindEventListener.bind)(document, {
138
+ type: 'keydown',
139
+ listener: function listener(event) {
140
+ if (isEnterKey(event.key)) {
141
+ var typeaheadPopup = (0, _experiences.popupWithNestedElement)(getPopupsTarget(), '.fabric-editor-typeahead');
142
+ if (!typeaheadPopup || !(typeaheadPopup instanceof HTMLElement)) {
143
+ return;
144
+ }
145
+ var targetElement = (0, _platformFeatureFlags.fg)('platform_synced_block_fix_experience_tracking') ? typeaheadPopup.querySelector('[role="option"][aria-selected="true"]') : typeaheadPopup.querySelector('[role="option"]');
146
+ if (!targetElement || !(targetElement instanceof HTMLElement)) {
147
+ return;
148
+ }
149
+ var testId = targetElement.dataset.testid;
150
+ if (testId === _types.SYNCED_BLOCK_BUTTON_TEST_ID.quickInsertCreate) {
151
+ createSourceQuickInsertMenuExperience.start();
152
+ }
153
+ }
154
+ },
155
+ options: {
156
+ capture: true
157
+ }
158
+ });
159
+ return {
160
+ unbindClickListener: unbindClickListener,
161
+ unbindKeydownListener: unbindKeydownListener
162
+ };
163
+ };
156
164
  return new _safePlugin.SafePlugin({
157
165
  key: pluginKey,
158
166
  view: function view(_view) {
167
+ var _syncedBlockPluginKey;
159
168
  editorViewRef.current = _view;
169
+
170
+ // Track whether listeners have been bound. When the experiment is
171
+ // ON and the document initially has no synced blocks, we defer
172
+ // binding until `update()` detects that `hasSyncedBlocks` has
173
+ // flipped to `true` (e.g. via paste or collab insert). This avoids
174
+ // the ~2-5 ms TBT cost of capture-phase click/keydown handlers on
175
+ // the ~99.97 % of pages that never use synced blocks (EDITOR-6931).
176
+ var listenersBound = false;
177
+ var unbindClickListener;
178
+ var unbindKeydownListener;
179
+ var ensureListenersBound = function ensureListenersBound() {
180
+ if (listenersBound) {
181
+ return;
182
+ }
183
+ listenersBound = true;
184
+ var unbinders = bindListeners();
185
+ unbindClickListener = unbinders.unbindClickListener;
186
+ unbindKeydownListener = unbinders.unbindKeydownListener;
187
+ };
188
+ if ((_syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(_view.state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks || !(0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
189
+ ensureListenersBound();
190
+ }
160
191
  return {
192
+ update: function update(view, prevState) {
193
+ var _syncedBlockPluginKey2;
194
+ if (!listenersBound && view.state.doc !== prevState.doc && (_syncedBlockPluginKey2 = _main.syncedBlockPluginKey.getState(view.state)) !== null && _syncedBlockPluginKey2 !== void 0 && _syncedBlockPluginKey2.hasSyncedBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
195
+ // Bind listeners now that synced blocks are present.
196
+ ensureListenersBound();
197
+ }
198
+ },
161
199
  destroy: function destroy() {
200
+ var _unbindClickListener, _unbindKeydownListene;
162
201
  createSourcePrimaryToolbarExperience.abort({
163
202
  reason: 'editorDestroyed'
164
203
  });
@@ -183,8 +222,8 @@ var getMenuAndToolbarExperiencesPlugin = exports.getMenuAndToolbarExperiencesPlu
183
222
  syncedLocationsExperience === null || syncedLocationsExperience === void 0 || syncedLocationsExperience.abort({
184
223
  reason: 'editorDestroyed'
185
224
  });
186
- unbindClickListener();
187
- unbindKeydownListener();
225
+ (_unbindClickListener = unbindClickListener) === null || _unbindClickListener === void 0 || _unbindClickListener();
226
+ (_unbindKeydownListene = unbindKeydownListener) === null || _unbindKeydownListene === void 0 || _unbindKeydownListene();
188
227
  }
189
228
  };
190
229
  }
@@ -8,6 +8,7 @@ exports.syncedBlockPlugin = void 0;
8
8
  var _react = _interopRequireDefault(require("react"));
9
9
  var _adfSchema = require("@atlaskit/adf-schema");
10
10
  var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
11
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
11
12
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
12
13
  var _editorActions = require("./editor-actions");
13
14
  var _editorCommands = require("./editor-commands");
@@ -28,6 +29,7 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
28
29
  var refs = {};
29
30
  var viewMode = 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;
30
31
  var syncBlockStore = new _editorSyncedBlockProvider.SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
32
+ var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
31
33
  syncBlockStore.setFireAnalyticsEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 ? void 0 : _api$analytics.fireAnalyticsEvent);
32
34
  api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 || _api$blockMenu.actions.registerBlockMenuComponents((0, _blockMenuComponents.getBlockMenuComponents)(api, (_config$enableSourceC = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC !== void 0 ? _config$enableSourceC : false));
33
35
  api === null || api === void 0 || (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 || _api$toolbar.actions.registerComponents((0, _toolbarComponents.getToolbarComponents)(api, (_config$enableSourceC2 = config === null || config === void 0 ? void 0 : config.enableSourceCreation) !== null && _config$enableSourceC2 !== void 0 ? _config$enableSourceC2 : false));
@@ -98,6 +100,16 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
98
100
  pluginsOptions: {
99
101
  quickInsert: (0, _quickInsert.getQuickInsertConfig)(config, api, syncBlockStore),
100
102
  floatingToolbar: function floatingToolbar(state, intl) {
103
+ var _syncedBlockPluginKey;
104
+ // When the experiment is ON and the document has no synced blocks,
105
+ // skip the toolbar config entirely to avoid the per-selection-change
106
+ // cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931).
107
+ // Save the expValEquals('editor_synced_block_perf', 'isEnabled', true) in a const
108
+ // because floatingToolbar is called on every selection change.
109
+ // computing it once at plugin initialisation is more efficient.
110
+ if (!((_syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks) && isPerfExperimentOn) {
111
+ return undefined;
112
+ }
101
113
  return (0, _floatingToolbar.getToolbarConfig)(state, intl, api, syncBlockStore);
102
114
  }
103
115
  },
@@ -122,13 +134,13 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
122
134
  if (!editorState) {
123
135
  return;
124
136
  }
125
- var _syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(editorState),
126
- activeFlag = _syncedBlockPluginKey.activeFlag,
127
- currentSyncBlockStore = _syncedBlockPluginKey.syncBlockStore,
128
- bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey.bodiedSyncBlockDeletionStatus,
129
- retryCreationPosMap = _syncedBlockPluginKey.retryCreationPosMap,
130
- hasSyncedBlocks = _syncedBlockPluginKey.hasSyncedBlocks,
131
- hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey.hasUnsavedBodiedSyncBlockChanges;
137
+ var _syncedBlockPluginKey2 = _main.syncedBlockPluginKey.getState(editorState),
138
+ activeFlag = _syncedBlockPluginKey2.activeFlag,
139
+ currentSyncBlockStore = _syncedBlockPluginKey2.syncBlockStore,
140
+ bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey2.bodiedSyncBlockDeletionStatus,
141
+ retryCreationPosMap = _syncedBlockPluginKey2.retryCreationPosMap,
142
+ hasSyncedBlocks = _syncedBlockPluginKey2.hasSyncedBlocks,
143
+ hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey2.hasUnsavedBodiedSyncBlockChanges;
132
144
  return {
133
145
  activeFlag: activeFlag,
134
146
  syncBlockStore: currentSyncBlockStore,
@@ -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,7 @@
1
1
  import React from 'react';
2
2
  import { syncBlock, bodiedSyncBlock } from '@atlaskit/adf-schema';
3
3
  import { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
4
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
5
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
5
6
  import { flushBodiedSyncBlocks, flushSyncBlocks, discardUnpublishedSyncBlocks } from './editor-actions';
6
7
  import { copySyncedBlockReferenceToClipboardEditorCommand, createSyncedBlock } from './editor-commands';
@@ -22,6 +23,7 @@ export const syncedBlockPlugin = ({
22
23
  const refs = {};
23
24
  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
25
  const syncBlockStore = new SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
26
+ const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
25
27
  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);
26
28
  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
29
  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));
@@ -84,7 +86,19 @@ export const syncedBlockPlugin = ({
84
86
  },
85
87
  pluginsOptions: {
86
88
  quickInsert: getQuickInsertConfig(config, api, syncBlockStore),
87
- floatingToolbar: (state, intl) => getToolbarConfig(state, intl, api, syncBlockStore)
89
+ floatingToolbar: (state, intl) => {
90
+ var _syncedBlockPluginKey;
91
+ // When the experiment is ON and the document has no synced blocks,
92
+ // skip the toolbar config entirely to avoid the per-selection-change
93
+ // cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931).
94
+ // Save the expValEquals('editor_synced_block_perf', 'isEnabled', true) in a const
95
+ // because floatingToolbar is called on every selection change.
96
+ // computing it once at plugin initialisation is more efficient.
97
+ if (!((_syncedBlockPluginKey = syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks) && isPerfExperimentOn) {
98
+ return undefined;
99
+ }
100
+ return getToolbarConfig(state, intl, api, syncBlockStore);
101
+ }
88
102
  },
89
103
  contentComponent: ({
90
104
  containerElement,
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { syncBlock, bodiedSyncBlock } from '@atlaskit/adf-schema';
3
3
  import { SyncBlockStoreManager } from '@atlaskit/editor-synced-block-provider';
4
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
5
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
5
6
  import { flushBodiedSyncBlocks as _flushBodiedSyncBlocks, flushSyncBlocks, discardUnpublishedSyncBlocks as _discardUnpublishedSyncBlocks } from './editor-actions';
6
7
  import { copySyncedBlockReferenceToClipboardEditorCommand, createSyncedBlock } from './editor-commands';
@@ -21,6 +22,7 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
21
22
  var refs = {};
22
23
  var viewMode = 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;
23
24
  var syncBlockStore = new SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
25
+ var isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
24
26
  syncBlockStore.setFireAnalyticsEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 ? void 0 : _api$analytics.fireAnalyticsEvent);
25
27
  api === null || api === void 0 || (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === 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));
26
28
  api === null || api === void 0 || (_api$toolbar = api.toolbar) === null || _api$toolbar === 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));
@@ -91,6 +93,16 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
91
93
  pluginsOptions: {
92
94
  quickInsert: getQuickInsertConfig(config, api, syncBlockStore),
93
95
  floatingToolbar: function floatingToolbar(state, intl) {
96
+ var _syncedBlockPluginKey;
97
+ // When the experiment is ON and the document has no synced blocks,
98
+ // skip the toolbar config entirely to avoid the per-selection-change
99
+ // cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931).
100
+ // Save the expValEquals('editor_synced_block_perf', 'isEnabled', true) in a const
101
+ // because floatingToolbar is called on every selection change.
102
+ // computing it once at plugin initialisation is more efficient.
103
+ if (!((_syncedBlockPluginKey = syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks) && isPerfExperimentOn) {
104
+ return undefined;
105
+ }
94
106
  return getToolbarConfig(state, intl, api, syncBlockStore);
95
107
  }
96
108
  },
@@ -115,13 +127,13 @@ export var syncedBlockPlugin = function syncedBlockPlugin(_ref) {
115
127
  if (!editorState) {
116
128
  return;
117
129
  }
118
- var _syncedBlockPluginKey = syncedBlockPluginKey.getState(editorState),
119
- activeFlag = _syncedBlockPluginKey.activeFlag,
120
- currentSyncBlockStore = _syncedBlockPluginKey.syncBlockStore,
121
- bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey.bodiedSyncBlockDeletionStatus,
122
- retryCreationPosMap = _syncedBlockPluginKey.retryCreationPosMap,
123
- hasSyncedBlocks = _syncedBlockPluginKey.hasSyncedBlocks,
124
- hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey.hasUnsavedBodiedSyncBlockChanges;
130
+ var _syncedBlockPluginKey2 = syncedBlockPluginKey.getState(editorState),
131
+ activeFlag = _syncedBlockPluginKey2.activeFlag,
132
+ currentSyncBlockStore = _syncedBlockPluginKey2.syncBlockStore,
133
+ bodiedSyncBlockDeletionStatus = _syncedBlockPluginKey2.bodiedSyncBlockDeletionStatus,
134
+ retryCreationPosMap = _syncedBlockPluginKey2.retryCreationPosMap,
135
+ hasSyncedBlocks = _syncedBlockPluginKey2.hasSyncedBlocks,
136
+ hasUnsavedBodiedSyncBlockChanges = _syncedBlockPluginKey2.hasUnsavedBodiedSyncBlockChanges;
125
137
  return {
126
138
  activeFlag: activeFlag,
127
139
  syncBlockStore: currentSyncBlockStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-synced-block",
3
- "version": "8.2.8",
3
+ "version": "8.2.10",
4
4
  "description": "SyncedBlock plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -50,11 +50,11 @@
50
50
  "@atlaskit/icon-lab": "^6.7.0",
51
51
  "@atlaskit/logo": "^20.1.0",
52
52
  "@atlaskit/lozenge": "^13.8.0",
53
- "@atlaskit/modal-dialog": "^14.18.0",
53
+ "@atlaskit/modal-dialog": "^15.0.0",
54
54
  "@atlaskit/platform-feature-flags": "^1.1.0",
55
55
  "@atlaskit/primitives": "^19.0.0",
56
56
  "@atlaskit/spinner": "19.1.2",
57
- "@atlaskit/tmp-editor-statsig": "^77.1.0",
57
+ "@atlaskit/tmp-editor-statsig": "^77.3.0",
58
58
  "@atlaskit/tokens": "13.0.3",
59
59
  "@atlaskit/tooltip": "^22.0.0",
60
60
  "@atlaskit/visually-hidden": "^3.1.0",
@@ -64,7 +64,7 @@
64
64
  "date-fns": "^2.17.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@atlaskit/editor-common": "^114.21.0",
67
+ "@atlaskit/editor-common": "^114.26.0",
68
68
  "react": "^18.2.0",
69
69
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
70
70
  },