@atlaskit/editor-core 219.5.1 → 219.6.0
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/CHANGELOG.md +19 -0
- package/dist/cjs/create-editor/ReactEditorView.js +45 -0
- package/dist/cjs/create-editor/create-editor.js +6 -1
- package/dist/cjs/create-editor/filter-plugins-for-reconfigure.js +49 -0
- package/dist/cjs/test-utils.js +3 -1
- package/dist/cjs/ui/PluginSlot/mount-plugin-hooks.js +10 -3
- package/dist/cjs/version-wrapper.js +1 -1
- package/dist/es2019/create-editor/ReactEditorView.js +43 -1
- package/dist/es2019/create-editor/create-editor.js +6 -1
- package/dist/es2019/create-editor/filter-plugins-for-reconfigure.js +35 -0
- package/dist/es2019/test-utils.js +3 -1
- package/dist/es2019/ui/PluginSlot/mount-plugin-hooks.js +18 -9
- package/dist/es2019/version-wrapper.js +1 -1
- package/dist/esm/create-editor/ReactEditorView.js +45 -0
- package/dist/esm/create-editor/create-editor.js +6 -1
- package/dist/esm/create-editor/filter-plugins-for-reconfigure.js +43 -0
- package/dist/esm/test-utils.js +3 -1
- package/dist/esm/ui/PluginSlot/mount-plugin-hooks.js +10 -3
- package/dist/esm/version-wrapper.js +1 -1
- package/dist/types/create-editor/filter-plugins-for-reconfigure.d.ts +19 -0
- package/dist/types/ui/PluginSlot/mount-plugin-hooks.d.ts +2 -2
- package/dist/types-ts4.5/create-editor/filter-plugins-for-reconfigure.d.ts +19 -0
- package/dist/types-ts4.5/ui/PluginSlot/mount-plugin-hooks.d.ts +2 -2
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @atlaskit/editor-core
|
|
2
2
|
|
|
3
|
+
## 219.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`80e6f41ddfce4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/80e6f41ddfce4) -
|
|
8
|
+
Reconcile editor plugin set on reconfigureState across preset switches: drop plugins whose schema
|
|
9
|
+
nodes/marks aren't in the current schema, and evict plugins from the injection API that aren't
|
|
10
|
+
part of the new preset. Behind feature gate `platform_editor_reconfigure_filter_plugins`.
|
|
11
|
+
|
|
12
|
+
Adds the `NamedReactHookFactory` type to `@atlaskit/editor-common/types` and annotates each
|
|
13
|
+
plugin's `usePluginHook` with its plugin name in `processPluginsList` so `MountPluginHooks` can
|
|
14
|
+
key React fibers stably per plugin. The shape of `EditorConfig.pluginHooks` is unchanged
|
|
15
|
+
(`ReactHookFactory[]`); the annotation is a non-breaking additive property on the function value
|
|
16
|
+
that falls back to the array index when absent.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
|
|
3
22
|
## 219.5.1
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -49,6 +49,7 @@ var _consts = require("./consts");
|
|
|
49
49
|
var _createEditor = require("./create-editor");
|
|
50
50
|
var _createPluginsList = _interopRequireDefault(require("./create-plugins-list"));
|
|
51
51
|
var _createSchema = require("./create-schema");
|
|
52
|
+
var _filterPluginsForReconfigure = require("./filter-plugins-for-reconfigure");
|
|
52
53
|
var _messages = require("./messages");
|
|
53
54
|
var _focusEditorElement = require("./ReactEditorView/focusEditorElement");
|
|
54
55
|
var _getUAPrefix = require("./ReactEditorView/getUAPrefix");
|
|
@@ -362,6 +363,10 @@ function ReactEditorView(props) {
|
|
|
362
363
|
var _useState = (0, _react.useState)(0),
|
|
363
364
|
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
|
|
364
365
|
bumpConfigVersion = _useState2[1];
|
|
366
|
+
|
|
367
|
+
// Preset reference last processed by the schema/API reconciliation below.
|
|
368
|
+
// Used to skip that work when reconfigure is called with the same preset.
|
|
369
|
+
var lastFilteredPresetRef = (0, _react.useRef)(null);
|
|
365
370
|
var reconfigureState = (0, _react.useCallback)(function (props) {
|
|
366
371
|
if (!viewRef.current) {
|
|
367
372
|
return;
|
|
@@ -371,7 +376,47 @@ function ReactEditorView(props) {
|
|
|
371
376
|
// so we blur here to stop ProseMirror from trying to apply selection to detached nodes or
|
|
372
377
|
// nodes that haven't been re-rendered to the document yet.
|
|
373
378
|
blur();
|
|
379
|
+
|
|
380
|
+
// Snapshot plugin names registered before createPluginsList runs, so
|
|
381
|
+
// we can tell which plugins are newly added by the new preset vs.
|
|
382
|
+
// which ones already coexisted with the current schema.
|
|
383
|
+
var previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames());
|
|
374
384
|
var editorPlugins = (0, _createPluginsList.default)(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
|
|
385
|
+
|
|
386
|
+
// `state.reconfigure` keeps the original schema, so switching presets
|
|
387
|
+
// can leave the editor inconsistent in two ways:
|
|
388
|
+
// 1. The new preset may add plugins that reference schema nodes or
|
|
389
|
+
// marks the original schema doesn't have.
|
|
390
|
+
// 2. Plugins registered by a previous preset can linger in the
|
|
391
|
+
// injection API even when the new preset doesn't re-register
|
|
392
|
+
// them, so listeners still fire against a state that no longer
|
|
393
|
+
// has their pmPlugin.
|
|
394
|
+
if (lastFilteredPresetRef.current !== props.preset && (0, _platformFeatureFlags.fg)('platform_editor_reconfigure_filter_plugins')) {
|
|
395
|
+
var _filterPluginsForReco = (0, _filterPluginsForReconfigure.filterPluginsForReconfigure)(editorPlugins, viewRef.current.state.schema, previousPluginNames),
|
|
396
|
+
kept = _filterPluginsForReco.kept,
|
|
397
|
+
dropped = _filterPluginsForReco.dropped;
|
|
398
|
+
editorPlugins = kept;
|
|
399
|
+
|
|
400
|
+
// Reconcile the injection API with the post-filter plugin set.
|
|
401
|
+
// This evicts both the plugins we just dropped above (re-registered
|
|
402
|
+
// by createPluginsList but no longer in editorPlugins) AND any
|
|
403
|
+
// plugin from a previous preset that the new preset doesn't
|
|
404
|
+
// re-register.
|
|
405
|
+
var keptPluginNames = new Set(editorPlugins.map(function (p) {
|
|
406
|
+
return p === null || p === void 0 ? void 0 : p.name;
|
|
407
|
+
}).filter(function (n) {
|
|
408
|
+
return Boolean(n);
|
|
409
|
+
}));
|
|
410
|
+
var evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames);
|
|
411
|
+
if (dropped.length > 0 || evictedFromApi.length > 0) {
|
|
412
|
+
// eslint-disable-next-line no-console
|
|
413
|
+
console.warn('[reconfigureState] Cleanup summary:', {
|
|
414
|
+
dropped: dropped,
|
|
415
|
+
evictedFromApi: evictedFromApi
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
lastFilteredPresetRef.current = props.preset;
|
|
419
|
+
}
|
|
375
420
|
config.current = (0, _createEditor.processPluginsList)(editorPlugins);
|
|
376
421
|
if ((0, _expValEquals.expValEquals)('platform_editor_appearance_shared_state', 'isEnabled', true)) {
|
|
377
422
|
var _config$current$pmPlu2;
|
|
@@ -72,7 +72,12 @@ function processPluginsList(plugins) {
|
|
|
72
72
|
acc.contentComponents.push(plugin.contentComponent);
|
|
73
73
|
}
|
|
74
74
|
if (plugin.usePluginHook) {
|
|
75
|
-
|
|
75
|
+
// Wrap with .bind(null) so we can annotate the function with the
|
|
76
|
+
// plugin name without mutating the plugin's original hook reference.
|
|
77
|
+
// MountPluginHooks reads `pluginName` to derive a stable React key.
|
|
78
|
+
var named = plugin.usePluginHook.bind(null);
|
|
79
|
+
named.pluginName = plugin.name;
|
|
80
|
+
acc.pluginHooks.push(named);
|
|
76
81
|
}
|
|
77
82
|
if (plugin.primaryToolbarComponent) {
|
|
78
83
|
acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.filterPluginsForReconfigure = filterPluginsForReconfigure;
|
|
7
|
+
/**
|
|
8
|
+
* Used by `reconfigureState` to drop plugins from a freshly-built preset that
|
|
9
|
+
* declare schema nodes/marks the current schema doesn't have. Plugins already
|
|
10
|
+
* present before this reconfigure are kept untouched: they have demonstrably
|
|
11
|
+
* coexisted with the schema (e.g. tests that mock a stripped-down schema), so
|
|
12
|
+
* removing them now would be a regression.
|
|
13
|
+
*/
|
|
14
|
+
function filterPluginsForReconfigure(editorPlugins, schema, previousPluginNames) {
|
|
15
|
+
var availableNodeNames = new Set(Object.keys(schema.nodes));
|
|
16
|
+
var availableMarkNames = new Set(Object.keys(schema.marks));
|
|
17
|
+
var dropped = [];
|
|
18
|
+
var kept = editorPlugins.filter(function (plugin) {
|
|
19
|
+
if (!plugin) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (previousPluginNames.has(plugin.name)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
var missingNodes = plugin.nodes ? plugin.nodes().map(function (n) {
|
|
26
|
+
return n.name;
|
|
27
|
+
}).filter(function (n) {
|
|
28
|
+
return !availableNodeNames.has(n);
|
|
29
|
+
}) : [];
|
|
30
|
+
var missingMarks = plugin.marks ? plugin.marks().map(function (m) {
|
|
31
|
+
return m.name;
|
|
32
|
+
}).filter(function (m) {
|
|
33
|
+
return !availableMarkNames.has(m);
|
|
34
|
+
}) : [];
|
|
35
|
+
if (missingNodes.length > 0 || missingMarks.length > 0) {
|
|
36
|
+
dropped.push({
|
|
37
|
+
name: plugin.name,
|
|
38
|
+
missingNodes: missingNodes,
|
|
39
|
+
missingMarks: missingMarks
|
|
40
|
+
});
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
kept: kept,
|
|
47
|
+
dropped: dropped
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/cjs/test-utils.js
CHANGED
|
@@ -56,7 +56,9 @@ function lightProcessPluginsList(editorPlugins) {
|
|
|
56
56
|
acc.contentComponents.push(editorPlugin.contentComponent);
|
|
57
57
|
}
|
|
58
58
|
if (editorPlugin.usePluginHook) {
|
|
59
|
-
|
|
59
|
+
var named = editorPlugin.usePluginHook.bind(null);
|
|
60
|
+
named.pluginName = editorPlugin.name;
|
|
61
|
+
acc.pluginHooks.push(named);
|
|
60
62
|
}
|
|
61
63
|
if (editorPlugin.onEditorViewStateUpdated) {
|
|
62
64
|
acc.onEditorViewStateUpdatedCallbacks.push(editorPlugin.onEditorViewStateUpdated);
|
|
@@ -23,12 +23,19 @@ function MountPluginHooks(_ref2) {
|
|
|
23
23
|
if (!editorView) {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// Key each fiber by the plugin name carried on the hook function (set by
|
|
28
|
+
// `processPluginsList`). This keeps fibers stable across `reconfigureState`
|
|
29
|
+
// calls that change the plugin set — keying by array index would let React
|
|
30
|
+
// reuse the same fiber for a different `usePluginHook`, which calls a
|
|
31
|
+
// different sequence of hooks (Rules of Hooks violation).
|
|
32
|
+
// Falls back to the array index for any hook that wasn't annotated.
|
|
33
|
+
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map(function (usePluginHook, index) {
|
|
34
|
+
var pluginName = usePluginHook.pluginName;
|
|
27
35
|
return /*#__PURE__*/_react.default.createElement(MountPluginHook
|
|
28
|
-
// Ignored via go/ees005
|
|
29
36
|
// eslint-disable-next-line react/no-array-index-key
|
|
30
37
|
, {
|
|
31
|
-
key:
|
|
38
|
+
key: pluginName !== null && pluginName !== void 0 ? pluginName : index,
|
|
32
39
|
usePluginHook: usePluginHook,
|
|
33
40
|
editorView: editorView,
|
|
34
41
|
containerElement: containerElement
|
|
@@ -38,6 +38,7 @@ import { PROSEMIRROR_RENDERED_DEGRADED_SEVERITY_THRESHOLD, PROSEMIRROR_RENDERED_
|
|
|
38
38
|
import { createErrorReporter, createPMPlugins, processPluginsList } from './create-editor';
|
|
39
39
|
import createPluginsList from './create-plugins-list';
|
|
40
40
|
import { createSchema } from './create-schema';
|
|
41
|
+
import { filterPluginsForReconfigure } from './filter-plugins-for-reconfigure';
|
|
41
42
|
import { editorMessages } from './messages';
|
|
42
43
|
import { focusEditorElement } from './ReactEditorView/focusEditorElement';
|
|
43
44
|
import { getUAPrefix } from './ReactEditorView/getUAPrefix';
|
|
@@ -336,6 +337,10 @@ export function ReactEditorView(props) {
|
|
|
336
337
|
// in-place-mutated `config.current` (contentComponents / toolbar
|
|
337
338
|
// components from the rebuilt preset).
|
|
338
339
|
const [, bumpConfigVersion] = useState(0);
|
|
340
|
+
|
|
341
|
+
// Preset reference last processed by the schema/API reconciliation below.
|
|
342
|
+
// Used to skip that work when reconfigure is called with the same preset.
|
|
343
|
+
const lastFilteredPresetRef = useRef(null);
|
|
339
344
|
const reconfigureState = useCallback(props => {
|
|
340
345
|
if (!viewRef.current) {
|
|
341
346
|
return;
|
|
@@ -345,7 +350,44 @@ export function ReactEditorView(props) {
|
|
|
345
350
|
// so we blur here to stop ProseMirror from trying to apply selection to detached nodes or
|
|
346
351
|
// nodes that haven't been re-rendered to the document yet.
|
|
347
352
|
blur();
|
|
348
|
-
|
|
353
|
+
|
|
354
|
+
// Snapshot plugin names registered before createPluginsList runs, so
|
|
355
|
+
// we can tell which plugins are newly added by the new preset vs.
|
|
356
|
+
// which ones already coexisted with the current schema.
|
|
357
|
+
const previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames());
|
|
358
|
+
let editorPlugins = createPluginsList(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
|
|
359
|
+
|
|
360
|
+
// `state.reconfigure` keeps the original schema, so switching presets
|
|
361
|
+
// can leave the editor inconsistent in two ways:
|
|
362
|
+
// 1. The new preset may add plugins that reference schema nodes or
|
|
363
|
+
// marks the original schema doesn't have.
|
|
364
|
+
// 2. Plugins registered by a previous preset can linger in the
|
|
365
|
+
// injection API even when the new preset doesn't re-register
|
|
366
|
+
// them, so listeners still fire against a state that no longer
|
|
367
|
+
// has their pmPlugin.
|
|
368
|
+
if (lastFilteredPresetRef.current !== props.preset && fg('platform_editor_reconfigure_filter_plugins')) {
|
|
369
|
+
const {
|
|
370
|
+
kept,
|
|
371
|
+
dropped
|
|
372
|
+
} = filterPluginsForReconfigure(editorPlugins, viewRef.current.state.schema, previousPluginNames);
|
|
373
|
+
editorPlugins = kept;
|
|
374
|
+
|
|
375
|
+
// Reconcile the injection API with the post-filter plugin set.
|
|
376
|
+
// This evicts both the plugins we just dropped above (re-registered
|
|
377
|
+
// by createPluginsList but no longer in editorPlugins) AND any
|
|
378
|
+
// plugin from a previous preset that the new preset doesn't
|
|
379
|
+
// re-register.
|
|
380
|
+
const keptPluginNames = new Set(editorPlugins.map(p => p === null || p === void 0 ? void 0 : p.name).filter(n => Boolean(n)));
|
|
381
|
+
const evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames);
|
|
382
|
+
if (dropped.length > 0 || evictedFromApi.length > 0) {
|
|
383
|
+
// eslint-disable-next-line no-console
|
|
384
|
+
console.warn('[reconfigureState] Cleanup summary:', {
|
|
385
|
+
dropped,
|
|
386
|
+
evictedFromApi
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
lastFilteredPresetRef.current = props.preset;
|
|
390
|
+
}
|
|
349
391
|
config.current = processPluginsList(editorPlugins);
|
|
350
392
|
if (expValEquals('platform_editor_appearance_shared_state', 'isEnabled', true)) {
|
|
351
393
|
config.current.pmPlugins.push(...pluginInjectionAPI.current.getInternalPMPlugins());
|
|
@@ -53,7 +53,12 @@ export function processPluginsList(plugins) {
|
|
|
53
53
|
acc.contentComponents.push(plugin.contentComponent);
|
|
54
54
|
}
|
|
55
55
|
if (plugin.usePluginHook) {
|
|
56
|
-
|
|
56
|
+
// Wrap with .bind(null) so we can annotate the function with the
|
|
57
|
+
// plugin name without mutating the plugin's original hook reference.
|
|
58
|
+
// MountPluginHooks reads `pluginName` to derive a stable React key.
|
|
59
|
+
const named = plugin.usePluginHook.bind(null);
|
|
60
|
+
named.pluginName = plugin.name;
|
|
61
|
+
acc.pluginHooks.push(named);
|
|
57
62
|
}
|
|
58
63
|
if (plugin.primaryToolbarComponent) {
|
|
59
64
|
acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Used by `reconfigureState` to drop plugins from a freshly-built preset that
|
|
3
|
+
* declare schema nodes/marks the current schema doesn't have. Plugins already
|
|
4
|
+
* present before this reconfigure are kept untouched: they have demonstrably
|
|
5
|
+
* coexisted with the schema (e.g. tests that mock a stripped-down schema), so
|
|
6
|
+
* removing them now would be a regression.
|
|
7
|
+
*/
|
|
8
|
+
export function filterPluginsForReconfigure(editorPlugins, schema, previousPluginNames) {
|
|
9
|
+
const availableNodeNames = new Set(Object.keys(schema.nodes));
|
|
10
|
+
const availableMarkNames = new Set(Object.keys(schema.marks));
|
|
11
|
+
const dropped = [];
|
|
12
|
+
const kept = editorPlugins.filter(plugin => {
|
|
13
|
+
if (!plugin) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (previousPluginNames.has(plugin.name)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const missingNodes = plugin.nodes ? plugin.nodes().map(n => n.name).filter(n => !availableNodeNames.has(n)) : [];
|
|
20
|
+
const missingMarks = plugin.marks ? plugin.marks().map(m => m.name).filter(m => !availableMarkNames.has(m)) : [];
|
|
21
|
+
if (missingNodes.length > 0 || missingMarks.length > 0) {
|
|
22
|
+
dropped.push({
|
|
23
|
+
name: plugin.name,
|
|
24
|
+
missingNodes,
|
|
25
|
+
missingMarks
|
|
26
|
+
});
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
kept,
|
|
33
|
+
dropped
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -43,7 +43,9 @@ function lightProcessPluginsList(editorPlugins) {
|
|
|
43
43
|
acc.contentComponents.push(editorPlugin.contentComponent);
|
|
44
44
|
}
|
|
45
45
|
if (editorPlugin.usePluginHook) {
|
|
46
|
-
|
|
46
|
+
const named = editorPlugin.usePluginHook.bind(null);
|
|
47
|
+
named.pluginName = editorPlugin.name;
|
|
48
|
+
acc.pluginHooks.push(named);
|
|
47
49
|
}
|
|
48
50
|
if (editorPlugin.onEditorViewStateUpdated) {
|
|
49
51
|
acc.onEditorViewStateUpdatedCallbacks.push(editorPlugin.onEditorViewStateUpdated);
|
|
@@ -18,13 +18,22 @@ export function MountPluginHooks({
|
|
|
18
18
|
if (!editorView) {
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
|
|
22
|
+
// Key each fiber by the plugin name carried on the hook function (set by
|
|
23
|
+
// `processPluginsList`). This keeps fibers stable across `reconfigureState`
|
|
24
|
+
// calls that change the plugin set — keying by array index would let React
|
|
25
|
+
// reuse the same fiber for a different `usePluginHook`, which calls a
|
|
26
|
+
// different sequence of hooks (Rules of Hooks violation).
|
|
27
|
+
// Falls back to the array index for any hook that wasn't annotated.
|
|
28
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map((usePluginHook, index) => {
|
|
29
|
+
const pluginName = usePluginHook.pluginName;
|
|
30
|
+
return /*#__PURE__*/React.createElement(MountPluginHook
|
|
31
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
32
|
+
, {
|
|
33
|
+
key: pluginName !== null && pluginName !== void 0 ? pluginName : index,
|
|
34
|
+
usePluginHook: usePluginHook,
|
|
35
|
+
editorView: editorView,
|
|
36
|
+
containerElement: containerElement
|
|
37
|
+
});
|
|
38
|
+
}));
|
|
30
39
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const name = "@atlaskit/editor-core";
|
|
2
|
-
export const version = "219.5.
|
|
2
|
+
export const version = "219.5.1";
|
|
@@ -46,6 +46,7 @@ import { PROSEMIRROR_RENDERED_DEGRADED_SEVERITY_THRESHOLD, PROSEMIRROR_RENDERED_
|
|
|
46
46
|
import { createErrorReporter, createPMPlugins, processPluginsList } from './create-editor';
|
|
47
47
|
import createPluginsList from './create-plugins-list';
|
|
48
48
|
import { createSchema } from './create-schema';
|
|
49
|
+
import { filterPluginsForReconfigure } from './filter-plugins-for-reconfigure';
|
|
49
50
|
import { editorMessages } from './messages';
|
|
50
51
|
import { focusEditorElement } from './ReactEditorView/focusEditorElement';
|
|
51
52
|
import { getUAPrefix } from './ReactEditorView/getUAPrefix';
|
|
@@ -353,6 +354,10 @@ export function ReactEditorView(props) {
|
|
|
353
354
|
var _useState = useState(0),
|
|
354
355
|
_useState2 = _slicedToArray(_useState, 2),
|
|
355
356
|
bumpConfigVersion = _useState2[1];
|
|
357
|
+
|
|
358
|
+
// Preset reference last processed by the schema/API reconciliation below.
|
|
359
|
+
// Used to skip that work when reconfigure is called with the same preset.
|
|
360
|
+
var lastFilteredPresetRef = useRef(null);
|
|
356
361
|
var reconfigureState = useCallback(function (props) {
|
|
357
362
|
if (!viewRef.current) {
|
|
358
363
|
return;
|
|
@@ -362,7 +367,47 @@ export function ReactEditorView(props) {
|
|
|
362
367
|
// so we blur here to stop ProseMirror from trying to apply selection to detached nodes or
|
|
363
368
|
// nodes that haven't been re-rendered to the document yet.
|
|
364
369
|
blur();
|
|
370
|
+
|
|
371
|
+
// Snapshot plugin names registered before createPluginsList runs, so
|
|
372
|
+
// we can tell which plugins are newly added by the new preset vs.
|
|
373
|
+
// which ones already coexisted with the current schema.
|
|
374
|
+
var previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames());
|
|
365
375
|
var editorPlugins = createPluginsList(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current);
|
|
376
|
+
|
|
377
|
+
// `state.reconfigure` keeps the original schema, so switching presets
|
|
378
|
+
// can leave the editor inconsistent in two ways:
|
|
379
|
+
// 1. The new preset may add plugins that reference schema nodes or
|
|
380
|
+
// marks the original schema doesn't have.
|
|
381
|
+
// 2. Plugins registered by a previous preset can linger in the
|
|
382
|
+
// injection API even when the new preset doesn't re-register
|
|
383
|
+
// them, so listeners still fire against a state that no longer
|
|
384
|
+
// has their pmPlugin.
|
|
385
|
+
if (lastFilteredPresetRef.current !== props.preset && fg('platform_editor_reconfigure_filter_plugins')) {
|
|
386
|
+
var _filterPluginsForReco = filterPluginsForReconfigure(editorPlugins, viewRef.current.state.schema, previousPluginNames),
|
|
387
|
+
kept = _filterPluginsForReco.kept,
|
|
388
|
+
dropped = _filterPluginsForReco.dropped;
|
|
389
|
+
editorPlugins = kept;
|
|
390
|
+
|
|
391
|
+
// Reconcile the injection API with the post-filter plugin set.
|
|
392
|
+
// This evicts both the plugins we just dropped above (re-registered
|
|
393
|
+
// by createPluginsList but no longer in editorPlugins) AND any
|
|
394
|
+
// plugin from a previous preset that the new preset doesn't
|
|
395
|
+
// re-register.
|
|
396
|
+
var keptPluginNames = new Set(editorPlugins.map(function (p) {
|
|
397
|
+
return p === null || p === void 0 ? void 0 : p.name;
|
|
398
|
+
}).filter(function (n) {
|
|
399
|
+
return Boolean(n);
|
|
400
|
+
}));
|
|
401
|
+
var evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames);
|
|
402
|
+
if (dropped.length > 0 || evictedFromApi.length > 0) {
|
|
403
|
+
// eslint-disable-next-line no-console
|
|
404
|
+
console.warn('[reconfigureState] Cleanup summary:', {
|
|
405
|
+
dropped: dropped,
|
|
406
|
+
evictedFromApi: evictedFromApi
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
lastFilteredPresetRef.current = props.preset;
|
|
410
|
+
}
|
|
366
411
|
config.current = processPluginsList(editorPlugins);
|
|
367
412
|
if (expValEquals('platform_editor_appearance_shared_state', 'isEnabled', true)) {
|
|
368
413
|
var _config$current$pmPlu2;
|
|
@@ -61,7 +61,12 @@ export function processPluginsList(plugins) {
|
|
|
61
61
|
acc.contentComponents.push(plugin.contentComponent);
|
|
62
62
|
}
|
|
63
63
|
if (plugin.usePluginHook) {
|
|
64
|
-
|
|
64
|
+
// Wrap with .bind(null) so we can annotate the function with the
|
|
65
|
+
// plugin name without mutating the plugin's original hook reference.
|
|
66
|
+
// MountPluginHooks reads `pluginName` to derive a stable React key.
|
|
67
|
+
var named = plugin.usePluginHook.bind(null);
|
|
68
|
+
named.pluginName = plugin.name;
|
|
69
|
+
acc.pluginHooks.push(named);
|
|
65
70
|
}
|
|
66
71
|
if (plugin.primaryToolbarComponent) {
|
|
67
72
|
acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Used by `reconfigureState` to drop plugins from a freshly-built preset that
|
|
3
|
+
* declare schema nodes/marks the current schema doesn't have. Plugins already
|
|
4
|
+
* present before this reconfigure are kept untouched: they have demonstrably
|
|
5
|
+
* coexisted with the schema (e.g. tests that mock a stripped-down schema), so
|
|
6
|
+
* removing them now would be a regression.
|
|
7
|
+
*/
|
|
8
|
+
export function filterPluginsForReconfigure(editorPlugins, schema, previousPluginNames) {
|
|
9
|
+
var availableNodeNames = new Set(Object.keys(schema.nodes));
|
|
10
|
+
var availableMarkNames = new Set(Object.keys(schema.marks));
|
|
11
|
+
var dropped = [];
|
|
12
|
+
var kept = editorPlugins.filter(function (plugin) {
|
|
13
|
+
if (!plugin) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (previousPluginNames.has(plugin.name)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
var missingNodes = plugin.nodes ? plugin.nodes().map(function (n) {
|
|
20
|
+
return n.name;
|
|
21
|
+
}).filter(function (n) {
|
|
22
|
+
return !availableNodeNames.has(n);
|
|
23
|
+
}) : [];
|
|
24
|
+
var missingMarks = plugin.marks ? plugin.marks().map(function (m) {
|
|
25
|
+
return m.name;
|
|
26
|
+
}).filter(function (m) {
|
|
27
|
+
return !availableMarkNames.has(m);
|
|
28
|
+
}) : [];
|
|
29
|
+
if (missingNodes.length > 0 || missingMarks.length > 0) {
|
|
30
|
+
dropped.push({
|
|
31
|
+
name: plugin.name,
|
|
32
|
+
missingNodes: missingNodes,
|
|
33
|
+
missingMarks: missingMarks
|
|
34
|
+
});
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
kept: kept,
|
|
41
|
+
dropped: dropped
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/esm/test-utils.js
CHANGED
|
@@ -50,7 +50,9 @@ function lightProcessPluginsList(editorPlugins) {
|
|
|
50
50
|
acc.contentComponents.push(editorPlugin.contentComponent);
|
|
51
51
|
}
|
|
52
52
|
if (editorPlugin.usePluginHook) {
|
|
53
|
-
|
|
53
|
+
var named = editorPlugin.usePluginHook.bind(null);
|
|
54
|
+
named.pluginName = editorPlugin.name;
|
|
55
|
+
acc.pluginHooks.push(named);
|
|
54
56
|
}
|
|
55
57
|
if (editorPlugin.onEditorViewStateUpdated) {
|
|
56
58
|
acc.onEditorViewStateUpdatedCallbacks.push(editorPlugin.onEditorViewStateUpdated);
|
|
@@ -16,12 +16,19 @@ export function MountPluginHooks(_ref2) {
|
|
|
16
16
|
if (!editorView) {
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
// Key each fiber by the plugin name carried on the hook function (set by
|
|
21
|
+
// `processPluginsList`). This keeps fibers stable across `reconfigureState`
|
|
22
|
+
// calls that change the plugin set — keying by array index would let React
|
|
23
|
+
// reuse the same fiber for a different `usePluginHook`, which calls a
|
|
24
|
+
// different sequence of hooks (Rules of Hooks violation).
|
|
25
|
+
// Falls back to the array index for any hook that wasn't annotated.
|
|
26
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, pluginHooks === null || pluginHooks === void 0 ? void 0 : pluginHooks.map(function (usePluginHook, index) {
|
|
27
|
+
var pluginName = usePluginHook.pluginName;
|
|
20
28
|
return /*#__PURE__*/React.createElement(MountPluginHook
|
|
21
|
-
// Ignored via go/ees005
|
|
22
29
|
// eslint-disable-next-line react/no-array-index-key
|
|
23
30
|
, {
|
|
24
|
-
key:
|
|
31
|
+
key: pluginName !== null && pluginName !== void 0 ? pluginName : index,
|
|
25
32
|
usePluginHook: usePluginHook,
|
|
26
33
|
editorView: editorView,
|
|
27
34
|
containerElement: containerElement
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export var name = "@atlaskit/editor-core";
|
|
2
|
-
export var version = "219.5.
|
|
2
|
+
export var version = "219.5.1";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EditorPlugin } from '@atlaskit/editor-common/types';
|
|
2
|
+
import type { Schema } from '@atlaskit/editor-prosemirror/model';
|
|
3
|
+
export interface DroppedPlugin {
|
|
4
|
+
missingMarks: string[];
|
|
5
|
+
missingNodes: string[];
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FilterPluginsForReconfigureResult {
|
|
9
|
+
dropped: DroppedPlugin[];
|
|
10
|
+
kept: EditorPlugin[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Used by `reconfigureState` to drop plugins from a freshly-built preset that
|
|
14
|
+
* declare schema nodes/marks the current schema doesn't have. Plugins already
|
|
15
|
+
* present before this reconfigure are kept untouched: they have demonstrably
|
|
16
|
+
* coexisted with the schema (e.g. tests that mock a stripped-down schema), so
|
|
17
|
+
* removing them now would be a regression.
|
|
18
|
+
*/
|
|
19
|
+
export declare function filterPluginsForReconfigure(editorPlugins: Array<EditorPlugin | undefined | null>, schema: Schema, previousPluginNames: ReadonlySet<string>): FilterPluginsForReconfigureResult;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { NamedReactHookFactory } from '@atlaskit/editor-common/types';
|
|
3
3
|
import type { EditorView } from '@atlaskit/editor-prosemirror/view';
|
|
4
4
|
interface MountPluginHooksProps {
|
|
5
5
|
containerElement: HTMLElement | null;
|
|
6
6
|
editorView: EditorView | undefined;
|
|
7
|
-
pluginHooks:
|
|
7
|
+
pluginHooks: NamedReactHookFactory[] | undefined;
|
|
8
8
|
}
|
|
9
9
|
export declare function MountPluginHooks({ pluginHooks, editorView, containerElement, }: MountPluginHooksProps): React.JSX.Element | null;
|
|
10
10
|
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EditorPlugin } from '@atlaskit/editor-common/types';
|
|
2
|
+
import type { Schema } from '@atlaskit/editor-prosemirror/model';
|
|
3
|
+
export interface DroppedPlugin {
|
|
4
|
+
missingMarks: string[];
|
|
5
|
+
missingNodes: string[];
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FilterPluginsForReconfigureResult {
|
|
9
|
+
dropped: DroppedPlugin[];
|
|
10
|
+
kept: EditorPlugin[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Used by `reconfigureState` to drop plugins from a freshly-built preset that
|
|
14
|
+
* declare schema nodes/marks the current schema doesn't have. Plugins already
|
|
15
|
+
* present before this reconfigure are kept untouched: they have demonstrably
|
|
16
|
+
* coexisted with the schema (e.g. tests that mock a stripped-down schema), so
|
|
17
|
+
* removing them now would be a regression.
|
|
18
|
+
*/
|
|
19
|
+
export declare function filterPluginsForReconfigure(editorPlugins: Array<EditorPlugin | undefined | null>, schema: Schema, previousPluginNames: ReadonlySet<string>): FilterPluginsForReconfigureResult;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { NamedReactHookFactory } from '@atlaskit/editor-common/types';
|
|
3
3
|
import type { EditorView } from '@atlaskit/editor-prosemirror/view';
|
|
4
4
|
interface MountPluginHooksProps {
|
|
5
5
|
containerElement: HTMLElement | null;
|
|
6
6
|
editorView: EditorView | undefined;
|
|
7
|
-
pluginHooks:
|
|
7
|
+
pluginHooks: NamedReactHookFactory[] | undefined;
|
|
8
8
|
}
|
|
9
9
|
export declare function MountPluginHooks({ pluginHooks, editorView, containerElement, }: MountPluginHooksProps): React.JSX.Element | null;
|
|
10
10
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-core",
|
|
3
|
-
"version": "219.
|
|
3
|
+
"version": "219.6.0",
|
|
4
4
|
"description": "A package contains Atlassian editor core functionality",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@atlaskit/editor-ssr-renderer": "^5.1.0",
|
|
65
65
|
"@atlaskit/editor-toolbar": "^1.0.0",
|
|
66
66
|
"@atlaskit/editor-toolbar-model": "^0.4.0",
|
|
67
|
-
"@atlaskit/emoji": "^70.
|
|
67
|
+
"@atlaskit/emoji": "^70.5.0",
|
|
68
68
|
"@atlaskit/feature-gate-js-client": "^5.5.0",
|
|
69
69
|
"@atlaskit/icon": "^34.5.0",
|
|
70
70
|
"@atlaskit/link": "^3.4.0",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"uuid": "^3.1.0"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@atlaskit/editor-common": "^114.
|
|
95
|
+
"@atlaskit/editor-common": "^114.27.0",
|
|
96
96
|
"@atlaskit/link-provider": "^4.4.0",
|
|
97
97
|
"@atlaskit/media-core": "^37.0.0",
|
|
98
98
|
"react": "^18.2.0",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"@atlaskit/adf-utils": "^19.29.0",
|
|
107
107
|
"@atlaskit/analytics-listeners": "^10.0.0",
|
|
108
108
|
"@atlaskit/collab-provider": "^18.2.0",
|
|
109
|
-
"@atlaskit/editor-plugin-annotation": "^10.
|
|
109
|
+
"@atlaskit/editor-plugin-annotation": "^10.3.0",
|
|
110
110
|
"@atlaskit/editor-plugin-card": "^16.5.0",
|
|
111
111
|
"@atlaskit/editor-plugin-list": "^12.0.0",
|
|
112
112
|
"@atlaskit/editor-plugin-paste": "^11.1.0",
|
|
@@ -167,6 +167,9 @@
|
|
|
167
167
|
}
|
|
168
168
|
},
|
|
169
169
|
"platform-feature-flags": {
|
|
170
|
+
"platform_editor_reconfigure_filter_plugins": {
|
|
171
|
+
"type": "boolean"
|
|
172
|
+
},
|
|
170
173
|
"linking_platform_datasource_assets_objects": {
|
|
171
174
|
"type": "boolean",
|
|
172
175
|
"referenceOnly": "true"
|