@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.
- package/AGENTS.md +11 -11
- package/CHANGELOG.md +20 -0
- package/dist/cjs/pm-plugins/main.js +25 -6
- package/dist/cjs/pm-plugins/menu-and-toolbar-experiences.js +92 -53
- package/dist/cjs/syncedBlockPlugin.js +76 -25
- package/dist/es2019/pm-plugins/main.js +25 -6
- package/dist/es2019/pm-plugins/menu-and-toolbar-experiences.js +92 -53
- package/dist/es2019/syncedBlockPlugin.js +67 -11
- package/dist/esm/pm-plugins/main.js +25 -6
- package/dist/esm/pm-plugins/menu-and-toolbar-experiences.js +92 -53
- package/dist/esm/syncedBlockPlugin.js +77 -25
- package/package.json +4 -4
|
@@ -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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
136
|
-
if (!
|
|
106
|
+
const testId = button.dataset.testid;
|
|
107
|
+
if (!isSyncedBlockButtonId(testId)) {
|
|
137
108
|
return;
|
|
138
109
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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) =>
|
|
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(
|
|
98
|
-
|
|
149
|
+
return /*#__PURE__*/React.createElement(LazySyncedBlockUI, {
|
|
150
|
+
syncBlockStore: syncBlockStore,
|
|
99
151
|
api: api
|
|
100
|
-
})
|
|
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
|
-
} =
|
|
119
|
-
|
|
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)
|
|
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 &&
|
|
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 (
|
|
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:
|
|
460
|
+
activeFlag: nextActiveFlag,
|
|
442
461
|
selectionDecorationSet: newDecorationSet,
|
|
443
462
|
syncBlockStore: syncBlockStore,
|
|
444
463
|
retryCreationPosMap: newRetryCreationPosMap,
|
|
445
464
|
hasSyncedBlocks: nextHasSyncedBlocks,
|
|
446
|
-
bodiedSyncBlockDeletionStatus:
|
|
447
|
-
hasUnsavedBodiedSyncBlockChanges:
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
136
|
-
if (!targetElement || !(targetElement instanceof HTMLElement)) {
|
|
110
|
+
if (button.disabled) {
|
|
137
111
|
return;
|
|
138
112
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
}
|