@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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
O(n) `doc.descendants()` walk on every transaction.
|
|
84
|
-
- Uses `sourceSyncBlockStoreManager.hasPendingCreations()` for an O(1)
|
|
85
|
-
|
|
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,25 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-synced-block
|
|
2
2
|
|
|
3
|
+
## 8.2.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`a160344820ea5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a160344820ea5) -
|
|
8
|
+
EDITOR-6929: Fix React re-render cascade by (1) returning same plugin state reference from apply()
|
|
9
|
+
when nothing changed, (2) memoizing getSharedState to return a stable reference, and (3) guarding
|
|
10
|
+
contentComponent to skip rendering when hasSyncedBlocks is false. All gated behind
|
|
11
|
+
editor_synced_block_perf experiment.
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
|
|
14
|
+
## 8.2.10
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [`0a702a2b251d1`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/0a702a2b251d1) -
|
|
19
|
+
Guard menuAndToolbarExperiencesPlugin DOM listeners and floatingToolbar config behind
|
|
20
|
+
hasSyncedBlocks to avoid unnecessary work on pages without synced blocks
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
|
|
3
23
|
## 8.2.9
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -357,7 +357,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
357
357
|
// When the perf gate is ON and the doc has synced blocks we do a
|
|
358
358
|
// single traversal here; afterwards `apply()` will map or rebuild
|
|
359
359
|
// only when a status signal changes.
|
|
360
|
-
var initStatusDecorationSet = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)
|
|
360
|
+
var initStatusDecorationSet = docHasSyncedBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : _view.DecorationSet.empty;
|
|
361
361
|
return {
|
|
362
362
|
selectionDecorationSet: (0, _selectionDecorations.calculateDecorations)(instance.doc, instance.selection, instance.schema),
|
|
363
363
|
activeFlag: false,
|
|
@@ -374,6 +374,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
374
374
|
apply: function apply(tr, currentPluginState, oldEditorState) {
|
|
375
375
|
var _meta$activeFlag, _meta$bodiedSyncBlock;
|
|
376
376
|
var meta = tr.getMeta(syncedBlockPluginKey);
|
|
377
|
+
var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
|
|
377
378
|
var activeFlag = currentPluginState.activeFlag,
|
|
378
379
|
selectionDecorationSet = currentPluginState.selectionDecorationSet,
|
|
379
380
|
bodiedSyncBlockDeletionStatus = currentPluginState.bodiedSyncBlockDeletionStatus,
|
|
@@ -387,11 +388,19 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
387
388
|
// Lazy-init bookkeeping: once a synced block enters the document we
|
|
388
389
|
// flip `hasSyncedBlocks` to `true` for the lifetime of this editor
|
|
389
390
|
var nextHasSyncedBlocks = prevHasSyncedBlocks;
|
|
390
|
-
if (!prevHasSyncedBlocks && tr.docChanged &&
|
|
391
|
+
if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
|
|
391
392
|
if ((0, _transactionInsertsSyncedBlock.transactionInsertsSyncedBlock)(tr)) {
|
|
392
393
|
nextHasSyncedBlocks = true;
|
|
393
394
|
}
|
|
394
395
|
}
|
|
396
|
+
|
|
397
|
+
// --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
|
|
398
|
+
// no meta is set, and the selection/doc haven't changed in a way
|
|
399
|
+
// that affects our state, return the SAME object reference so
|
|
400
|
+
// SharedStateAPI skips notifying subscribers. ---
|
|
401
|
+
if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
|
|
402
|
+
return currentPluginState;
|
|
403
|
+
}
|
|
395
404
|
var newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
|
|
396
405
|
: selectionDecorationSet;
|
|
397
406
|
if (!tr.selection.eq(oldEditorState.selection)) {
|
|
@@ -415,7 +424,7 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
415
424
|
var nextIsOffline = prevOffline;
|
|
416
425
|
var nextIsViewMode = prevViewMode;
|
|
417
426
|
var nextIsDragging = prevDragging;
|
|
418
|
-
if (
|
|
427
|
+
if (isPerfExperimentOn) {
|
|
419
428
|
if (!nextHasSyncedBlocks) {
|
|
420
429
|
// No synced blocks → keep empty status decorations
|
|
421
430
|
nextStatusDecorationSet = _view.DecorationSet.empty;
|
|
@@ -444,14 +453,24 @@ var createPlugin = exports.createPlugin = function createPlugin(options, pmPlugi
|
|
|
444
453
|
}
|
|
445
454
|
var newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
|
|
446
455
|
var newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
|
|
456
|
+
var nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
|
|
457
|
+
var nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
|
|
458
|
+
var nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
|
|
459
|
+
|
|
460
|
+
// --- Reference equality (EDITOR-6929): return the same object
|
|
461
|
+
// when ALL fields are reference-equal to avoid SharedStateAPI
|
|
462
|
+
// notifying subscribers and triggering React re-renders. ---
|
|
463
|
+
if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
|
|
464
|
+
return currentPluginState;
|
|
465
|
+
}
|
|
447
466
|
return {
|
|
448
|
-
activeFlag:
|
|
467
|
+
activeFlag: nextActiveFlag,
|
|
449
468
|
selectionDecorationSet: newDecorationSet,
|
|
450
469
|
syncBlockStore: syncBlockStore,
|
|
451
470
|
retryCreationPosMap: newRetryCreationPosMap,
|
|
452
471
|
hasSyncedBlocks: nextHasSyncedBlocks,
|
|
453
|
-
bodiedSyncBlockDeletionStatus:
|
|
454
|
-
hasUnsavedBodiedSyncBlockChanges:
|
|
472
|
+
bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
|
|
473
|
+
hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
|
|
455
474
|
statusDecorationSet: nextStatusDecorationSet,
|
|
456
475
|
prevIsOffline: nextIsOffline,
|
|
457
476
|
prevIsViewMode: nextIsViewMode,
|
|
@@ -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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
143
|
-
if (!
|
|
113
|
+
var testId = button.dataset.testid;
|
|
114
|
+
if (!isSyncedBlockButtonId(testId)) {
|
|
144
115
|
return;
|
|
145
116
|
}
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
}
|
|
@@ -7,7 +7,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
7
7
|
exports.syncedBlockPlugin = void 0;
|
|
8
8
|
var _react = _interopRequireDefault(require("react"));
|
|
9
9
|
var _adfSchema = require("@atlaskit/adf-schema");
|
|
10
|
+
var _hooks = require("@atlaskit/editor-common/hooks");
|
|
10
11
|
var _editorSyncedBlockProvider = require("@atlaskit/editor-synced-block-provider");
|
|
12
|
+
var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
|
|
11
13
|
var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
|
|
12
14
|
var _editorActions = require("./editor-actions");
|
|
13
15
|
var _editorCommands = require("./editor-commands");
|
|
@@ -21,14 +23,50 @@ var _floatingToolbar = require("./ui/floating-toolbar");
|
|
|
21
23
|
var _quickInsert = require("./ui/quick-insert");
|
|
22
24
|
var _SyncBlockRefresher = require("./ui/SyncBlockRefresher");
|
|
23
25
|
var _toolbarComponents = require("./ui/toolbar-components");
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* EDITOR-6929 / PR-G: Guard contentComponent rendering.
|
|
28
|
+
* When `hasSyncedBlocks` is false return null
|
|
29
|
+
* to avoid mounting SyncBlockRefresher, DeleteConfirmationModal, and Flag —
|
|
30
|
+
* their hooks (useSharedPluginStateWithSelector) would execute selectors on
|
|
31
|
+
* every transaction for no benefit on the ~99.98% of pages with zero synced
|
|
32
|
+
* blocks.
|
|
33
|
+
*/
|
|
34
|
+
var LazySyncedBlockUI = function LazySyncedBlockUI(_ref) {
|
|
35
|
+
var syncBlockStoreManager = _ref.syncBlockStore,
|
|
27
36
|
api = _ref.api;
|
|
37
|
+
var hasSyncBlocks = (0, _hooks.useSharedPluginStateWithSelector)(api, ['syncedBlock'], function (states) {
|
|
38
|
+
var _states$syncedBlockSt;
|
|
39
|
+
return (_states$syncedBlockSt = states.syncedBlockState) === null || _states$syncedBlockSt === void 0 ? void 0 : _states$syncedBlockSt.hasSyncedBlocks;
|
|
40
|
+
});
|
|
41
|
+
if (!hasSyncBlocks && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_SyncBlockRefresher.SyncBlockRefresher, {
|
|
45
|
+
syncBlockStoreManager: syncBlockStoreManager,
|
|
46
|
+
api: api
|
|
47
|
+
}), /*#__PURE__*/_react.default.createElement(_DeleteConfirmationModal.DeleteConfirmationModal, {
|
|
48
|
+
syncBlockStoreManager: syncBlockStoreManager,
|
|
49
|
+
api: api
|
|
50
|
+
}), /*#__PURE__*/_react.default.createElement(_Flag.Flag, {
|
|
51
|
+
api: api
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_ref2) {
|
|
55
|
+
var _api$editorViewMode, _api$analytics, _api$blockMenu, _config$enableSourceC, _api$toolbar, _config$enableSourceC2;
|
|
56
|
+
var config = _ref2.config,
|
|
57
|
+
api = _ref2.api;
|
|
28
58
|
var refs = {};
|
|
29
59
|
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
60
|
var syncBlockStore = new _editorSyncedBlockProvider.SyncBlockStoreManager(config === null || config === void 0 ? void 0 : config.syncBlockDataProvider, viewMode, config === null || config === void 0 ? void 0 : config.__livePage);
|
|
61
|
+
var isPerfExperimentOn = (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true);
|
|
31
62
|
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);
|
|
63
|
+
|
|
64
|
+
// --- Memoized getSharedState (EDITOR-6929 / PR-F) ---
|
|
65
|
+
// Cache the last returned shared state object. On each call, perform a
|
|
66
|
+
// shallow comparison of all fields against the cached value. If nothing
|
|
67
|
+
// changed, return the cached reference so SharedStateAPI subscribers
|
|
68
|
+
// (React components) skip re-rendering.
|
|
69
|
+
var cachedSharedState;
|
|
32
70
|
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
71
|
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));
|
|
34
72
|
return {
|
|
@@ -70,9 +108,9 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
|
|
|
70
108
|
return (0, _editorCommands.copySyncedBlockReferenceToClipboardEditorCommand)(syncBlockStore, inputMethod, api);
|
|
71
109
|
},
|
|
72
110
|
insertSyncedBlock: function insertSyncedBlock() {
|
|
73
|
-
return function (
|
|
111
|
+
return function (_ref3) {
|
|
74
112
|
var _api$analytics3;
|
|
75
|
-
var tr =
|
|
113
|
+
var tr = _ref3.tr;
|
|
76
114
|
if (!(config !== null && config !== void 0 && config.enableSourceCreation)) {
|
|
77
115
|
return null;
|
|
78
116
|
}
|
|
@@ -98,38 +136,49 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
|
|
|
98
136
|
pluginsOptions: {
|
|
99
137
|
quickInsert: (0, _quickInsert.getQuickInsertConfig)(config, api, syncBlockStore),
|
|
100
138
|
floatingToolbar: function floatingToolbar(state, intl) {
|
|
139
|
+
var _syncedBlockPluginKey;
|
|
140
|
+
// When the experiment is ON and the document has no synced blocks,
|
|
141
|
+
// skip the toolbar config entirely to avoid the per-selection-change
|
|
142
|
+
// cost of findSyncBlockOrBodiedSyncBlock (EDITOR-6931).
|
|
143
|
+
// Save the expValEquals('editor_synced_block_perf', 'isEnabled', true) in a const
|
|
144
|
+
// because floatingToolbar is called on every selection change.
|
|
145
|
+
// computing it once at plugin initialisation is more efficient.
|
|
146
|
+
if (!((_syncedBlockPluginKey = _main.syncedBlockPluginKey.getState(state)) !== null && _syncedBlockPluginKey !== void 0 && _syncedBlockPluginKey.hasSyncedBlocks) && isPerfExperimentOn) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
101
149
|
return (0, _floatingToolbar.getToolbarConfig)(state, intl, api, syncBlockStore);
|
|
102
150
|
}
|
|
103
151
|
},
|
|
104
|
-
contentComponent: function contentComponent(
|
|
105
|
-
var containerElement =
|
|
106
|
-
wrapperElement =
|
|
107
|
-
popupsMountPoint =
|
|
152
|
+
contentComponent: function contentComponent(_ref4) {
|
|
153
|
+
var containerElement = _ref4.containerElement,
|
|
154
|
+
wrapperElement = _ref4.wrapperElement,
|
|
155
|
+
popupsMountPoint = _ref4.popupsMountPoint;
|
|
108
156
|
refs.containerElement = containerElement || undefined;
|
|
109
157
|
refs.popupsMountPoint = popupsMountPoint || undefined;
|
|
110
158
|
refs.wrapperElement = wrapperElement || undefined;
|
|
111
|
-
return /*#__PURE__*/_react.default.createElement(
|
|
112
|
-
|
|
159
|
+
return /*#__PURE__*/_react.default.createElement(LazySyncedBlockUI, {
|
|
160
|
+
syncBlockStore: syncBlockStore,
|
|
113
161
|
api: api
|
|
114
|
-
})
|
|
115
|
-
syncBlockStoreManager: syncBlockStore,
|
|
116
|
-
api: api
|
|
117
|
-
}), /*#__PURE__*/_react.default.createElement(_Flag.Flag, {
|
|
118
|
-
api: api
|
|
119
|
-
}));
|
|
162
|
+
});
|
|
120
163
|
},
|
|
121
164
|
getSharedState: function getSharedState(editorState) {
|
|
122
165
|
if (!editorState) {
|
|
123
166
|
return;
|
|
124
167
|
}
|
|
125
|
-
var
|
|
126
|
-
|
|
127
|
-
currentSyncBlockStore =
|
|
128
|
-
bodiedSyncBlockDeletionStatus =
|
|
129
|
-
retryCreationPosMap =
|
|
130
|
-
hasSyncedBlocks =
|
|
131
|
-
hasUnsavedBodiedSyncBlockChanges =
|
|
132
|
-
|
|
168
|
+
var pluginState = _main.syncedBlockPluginKey.getState(editorState);
|
|
169
|
+
var activeFlag = pluginState.activeFlag,
|
|
170
|
+
currentSyncBlockStore = pluginState.syncBlockStore,
|
|
171
|
+
bodiedSyncBlockDeletionStatus = pluginState.bodiedSyncBlockDeletionStatus,
|
|
172
|
+
retryCreationPosMap = pluginState.retryCreationPosMap,
|
|
173
|
+
hasSyncedBlocks = pluginState.hasSyncedBlocks,
|
|
174
|
+
hasUnsavedBodiedSyncBlockChanges = pluginState.hasUnsavedBodiedSyncBlockChanges;
|
|
175
|
+
|
|
176
|
+
// --- EDITOR-6929 / PR-F: return a stable reference when all
|
|
177
|
+
// fields are unchanged to prevent unnecessary React re-renders. ---
|
|
178
|
+
if (cachedSharedState !== undefined && cachedSharedState.activeFlag === activeFlag && cachedSharedState.syncBlockStore === currentSyncBlockStore && cachedSharedState.bodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && cachedSharedState.retryCreationPosMap === retryCreationPosMap && cachedSharedState.hasSyncedBlocks === hasSyncedBlocks && cachedSharedState.hasUnsavedBodiedSyncBlockChanges === hasUnsavedBodiedSyncBlockChanges && (0, _expValEquals.expValEquals)('editor_synced_block_perf', 'isEnabled', true)) {
|
|
179
|
+
return cachedSharedState;
|
|
180
|
+
}
|
|
181
|
+
var nextSharedState = {
|
|
133
182
|
activeFlag: activeFlag,
|
|
134
183
|
syncBlockStore: currentSyncBlockStore,
|
|
135
184
|
bodiedSyncBlockDeletionStatus: bodiedSyncBlockDeletionStatus,
|
|
@@ -137,6 +186,8 @@ var syncedBlockPlugin = exports.syncedBlockPlugin = function syncedBlockPlugin(_
|
|
|
137
186
|
hasSyncedBlocks: hasSyncedBlocks,
|
|
138
187
|
hasUnsavedBodiedSyncBlockChanges: hasUnsavedBodiedSyncBlockChanges
|
|
139
188
|
};
|
|
189
|
+
cachedSharedState = nextSharedState;
|
|
190
|
+
return nextSharedState;
|
|
140
191
|
}
|
|
141
192
|
};
|
|
142
193
|
};
|
|
@@ -324,7 +324,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
324
324
|
// When the perf gate is ON and the doc has synced blocks we do a
|
|
325
325
|
// single traversal here; afterwards `apply()` will map or rebuild
|
|
326
326
|
// only when a status signal changes.
|
|
327
|
-
const initStatusDecorationSet = expValEquals('editor_synced_block_perf', 'isEnabled', true)
|
|
327
|
+
const initStatusDecorationSet = docHasSyncedBlocks && expValEquals('editor_synced_block_perf', 'isEnabled', true) ? buildStatusDecorations(instance.doc, syncBlockStore, initIsOffline, initIsViewMode, initIsDragging) : DecorationSet.empty;
|
|
328
328
|
return {
|
|
329
329
|
selectionDecorationSet: calculateDecorations(instance.doc, instance.selection, instance.schema),
|
|
330
330
|
activeFlag: false,
|
|
@@ -341,6 +341,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
341
341
|
apply: (tr, currentPluginState, oldEditorState) => {
|
|
342
342
|
var _meta$activeFlag, _meta$bodiedSyncBlock;
|
|
343
343
|
const meta = tr.getMeta(syncedBlockPluginKey);
|
|
344
|
+
const isPerfExperimentOn = expValEquals('editor_synced_block_perf', 'isEnabled', true);
|
|
344
345
|
const {
|
|
345
346
|
activeFlag,
|
|
346
347
|
selectionDecorationSet,
|
|
@@ -356,11 +357,19 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
356
357
|
// Lazy-init bookkeeping: once a synced block enters the document we
|
|
357
358
|
// flip `hasSyncedBlocks` to `true` for the lifetime of this editor
|
|
358
359
|
let nextHasSyncedBlocks = prevHasSyncedBlocks;
|
|
359
|
-
if (!prevHasSyncedBlocks && tr.docChanged &&
|
|
360
|
+
if (!prevHasSyncedBlocks && tr.docChanged && isPerfExperimentOn) {
|
|
360
361
|
if (transactionInsertsSyncedBlock(tr)) {
|
|
361
362
|
nextHasSyncedBlocks = true;
|
|
362
363
|
}
|
|
363
364
|
}
|
|
365
|
+
|
|
366
|
+
// --- Fast path (EDITOR-6929): when `hasSyncedBlocks` is false,
|
|
367
|
+
// no meta is set, and the selection/doc haven't changed in a way
|
|
368
|
+
// that affects our state, return the SAME object reference so
|
|
369
|
+
// SharedStateAPI skips notifying subscribers. ---
|
|
370
|
+
if (!nextHasSyncedBlocks && !meta && !tr.docChanged && tr.selection.eq(oldEditorState.selection) && isPerfExperimentOn) {
|
|
371
|
+
return currentPluginState;
|
|
372
|
+
}
|
|
364
373
|
let newDecorationSet = tr.docChanged ? selectionDecorationSet.map(tr.mapping, tr.doc) // only map if document changed
|
|
365
374
|
: selectionDecorationSet;
|
|
366
375
|
if (!tr.selection.eq(oldEditorState.selection)) {
|
|
@@ -384,7 +393,7 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
384
393
|
let nextIsOffline = prevOffline;
|
|
385
394
|
let nextIsViewMode = prevViewMode;
|
|
386
395
|
let nextIsDragging = prevDragging;
|
|
387
|
-
if (
|
|
396
|
+
if (isPerfExperimentOn) {
|
|
388
397
|
if (!nextHasSyncedBlocks) {
|
|
389
398
|
// No synced blocks → keep empty status decorations
|
|
390
399
|
nextStatusDecorationSet = DecorationSet.empty;
|
|
@@ -413,14 +422,24 @@ export const createPlugin = (options, pmPluginFactoryParams, syncBlockStore, api
|
|
|
413
422
|
}
|
|
414
423
|
const newPosEntry = meta === null || meta === void 0 ? void 0 : meta.retryCreationPos;
|
|
415
424
|
const newRetryCreationPosMap = mapRetryCreationPosMap(retryCreationPosMap, newPosEntry, tr.mapping.map.bind(tr.mapping));
|
|
425
|
+
const nextActiveFlag = (_meta$activeFlag = meta === null || meta === void 0 ? void 0 : meta.activeFlag) !== null && _meta$activeFlag !== void 0 ? _meta$activeFlag : activeFlag;
|
|
426
|
+
const nextBodiedSyncBlockDeletionStatus = (_meta$bodiedSyncBlock = meta === null || meta === void 0 ? void 0 : meta.bodiedSyncBlockDeletionStatus) !== null && _meta$bodiedSyncBlock !== void 0 ? _meta$bodiedSyncBlock : bodiedSyncBlockDeletionStatus;
|
|
427
|
+
const nextHasUnsavedBodiedSyncBlockChanges = syncBlockStore.sourceManager.hasUnsavedChanges();
|
|
428
|
+
|
|
429
|
+
// --- Reference equality (EDITOR-6929): return the same object
|
|
430
|
+
// when ALL fields are reference-equal to avoid SharedStateAPI
|
|
431
|
+
// notifying subscribers and triggering React re-renders. ---
|
|
432
|
+
if (nextActiveFlag === activeFlag && newDecorationSet === selectionDecorationSet && newRetryCreationPosMap === retryCreationPosMap && nextHasSyncedBlocks === prevHasSyncedBlocks && nextBodiedSyncBlockDeletionStatus === bodiedSyncBlockDeletionStatus && nextHasUnsavedBodiedSyncBlockChanges === currentPluginState.hasUnsavedBodiedSyncBlockChanges && nextStatusDecorationSet === prevStatusDecorationSet && nextIsOffline === prevOffline && nextIsViewMode === prevViewMode && nextIsDragging === prevDragging && isPerfExperimentOn) {
|
|
433
|
+
return currentPluginState;
|
|
434
|
+
}
|
|
416
435
|
return {
|
|
417
|
-
activeFlag:
|
|
436
|
+
activeFlag: nextActiveFlag,
|
|
418
437
|
selectionDecorationSet: newDecorationSet,
|
|
419
438
|
syncBlockStore: syncBlockStore,
|
|
420
439
|
retryCreationPosMap: newRetryCreationPosMap,
|
|
421
440
|
hasSyncedBlocks: nextHasSyncedBlocks,
|
|
422
|
-
bodiedSyncBlockDeletionStatus:
|
|
423
|
-
hasUnsavedBodiedSyncBlockChanges:
|
|
441
|
+
bodiedSyncBlockDeletionStatus: nextBodiedSyncBlockDeletionStatus,
|
|
442
|
+
hasUnsavedBodiedSyncBlockChanges: nextHasUnsavedBodiedSyncBlockChanges,
|
|
424
443
|
statusDecorationSet: nextStatusDecorationSet,
|
|
425
444
|
prevIsOffline: nextIsOffline,
|
|
426
445
|
prevIsViewMode: nextIsViewMode,
|