@atlaskit/editor-plugin-annotation 29.0.5 → 29.1.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @atlaskit/editor-plugin-annotation
2
2
 
3
+ ## 29.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`afd887e901de1`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/afd887e901de1) -
8
+ Discover annotations in legacy content macros and refresh their state when nested annotations are
9
+ added while platform_editor_fix_lcm_editor_annotations is enabled. Editor-scoped renderers can
10
+ load nested LCM annotations independently of the renderer experiment.
11
+
12
+ Allow the ProseMirror test editor helper to be checked with unused declaration checks enabled.
13
+
14
+ ## 29.0.6
15
+
16
+ ### Patch Changes
17
+
18
+ - [`de0b3e0a118bc`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/de0b3e0a118bc) -
19
+ Add comment target attribution to inline comment draft-open analytics.
20
+ - Updated dependencies
21
+
3
22
  ## 29.0.5
4
23
 
5
24
  ### Patch Changes
@@ -14,6 +14,7 @@ var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
14
14
  var _annotation2 = require("@atlaskit/editor-common/utils/annotation");
15
15
  var _view = require("@atlaskit/editor-prosemirror/view");
16
16
  var _fg = require("@atlaskit/platform-feature-flags/fg");
17
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
17
18
  var _editorCommands = require("../editor-commands");
18
19
  var _utils = require("../editor-commands/utils");
19
20
  var _nodeviews = require("../nodeviews");
@@ -409,6 +410,12 @@ var inlineCommentPlugin = exports.inlineCommentPlugin = function inlineCommentPl
409
410
  if (!(event.target instanceof HTMLElement)) {
410
411
  return false;
411
412
  }
413
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_fix_lcm_editor_annotations')) {
414
+ // The LCM renderer bridge owns selection so its draft-close guard runs first.
415
+ if (event.target.closest('[data-lcm-annotation-renderer]')) {
416
+ return false;
417
+ }
418
+ }
412
419
 
413
420
  // Find the nearest ancestor (or self) with the data-id attribute
414
421
  var annotationId = (_event$target$closest = event.target.closest('[data-id]')) === null || _event$target$closest === void 0 ? void 0 : _event$target$closest.getAttribute('data-id');
@@ -5,8 +5,10 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.shouldClearBookMarkCheck = exports.createPluginState = exports.createCommand = void 0;
8
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
8
9
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
10
  var _pluginStateFactory = require("@atlaskit/editor-common/utils/plugin-state-factory");
11
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
10
12
  var _state = require("@atlaskit/editor-prosemirror/state");
11
13
  var _view = require("@atlaskit/editor-prosemirror/view");
12
14
  var _reducer = _interopRequireDefault(require("./reducer"));
@@ -14,12 +16,11 @@ var _utils = require("./utils");
14
16
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
15
17
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
16
18
  /**
17
- * Creates a handleDocChanged function with its own deleted annotations cache.
18
- * This ensures each editor instance has its own cache, avoiding cross-contamination.
19
+ * Creates a document-sync handler with a deleted annotations cache in its closure.
19
20
  */
20
21
  var createHandleDocChanged = function createHandleDocChanged() {
21
22
  // Cache for preserving deleted annotation resolved states through delete/undo cycles
22
- // This lives in closure scope per editor instance to avoid serialization and reduce state size
23
+ // This stays outside plugin state so the undo cache is not serialized.
23
24
  var deletedAnnotationsCache = {};
24
25
  return function (tr, prevPluginState) {
25
26
  if (tr.getMeta('replaceDocument')) {
@@ -28,16 +29,24 @@ var createHandleDocChanged = function createHandleDocChanged() {
28
29
  });
29
30
  }
30
31
  var updatedState = getSelectionChangedHandler(false)(tr, prevPluginState);
32
+ var legacyContentExperimentEnabled = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_fix_lcm_editor_annotations');
33
+ var currentDiscovery = legacyContentExperimentEnabled ? (0, _utils.getAnnotationDiscovery)(tr.doc) : undefined;
34
+ var previousAnnotationIds = currentDiscovery ? new Set((0, _utils.getAnnotationDiscovery)(tr.before).annotationIds) : undefined;
35
+ var newNestedAnnotationIds = currentDiscovery ? currentDiscovery.nestedAnnotationIds.filter(function (id) {
36
+ return !(previousAnnotationIds !== null && previousAnnotationIds !== void 0 && previousAnnotationIds.has(id)) && prevPluginState.annotations[id] === undefined && deletedAnnotationsCache[id] === undefined;
37
+ }) : [];
31
38
 
32
39
  // Collect annotation IDs currently in the document
33
- var annotationIdsInDocument = new Set();
34
- tr.doc.descendants(function (node) {
35
- node.marks.forEach(function (mark) {
36
- if (mark.type.name === 'annotation') {
37
- annotationIdsInDocument.add(mark.attrs.id);
38
- }
40
+ var annotationIdsInDocument = currentDiscovery ? new Set([].concat((0, _toConsumableArray2.default)(currentDiscovery.rootAnnotationIds), (0, _toConsumableArray2.default)(currentDiscovery.nestedAnnotationIds))) : new Set();
41
+ if (!currentDiscovery) {
42
+ tr.doc.descendants(function (node) {
43
+ node.marks.forEach(function (mark) {
44
+ if (mark.type.name === 'annotation') {
45
+ annotationIdsInDocument.add(mark.attrs.id);
46
+ }
47
+ });
39
48
  });
40
- });
49
+ }
41
50
  var annotationIdsInState = Object.keys(prevPluginState.annotations);
42
51
 
43
52
  // Early return if annotations haven't changed
@@ -45,7 +54,9 @@ var createHandleDocChanged = function createHandleDocChanged() {
45
54
  return !annotationIdsInDocument.has(id);
46
55
  });
47
56
  if (!annotationsHaveChanged) {
48
- return updatedState;
57
+ return newNestedAnnotationIds.length > 0 ? _objectSpread(_objectSpread({}, updatedState), {}, {
58
+ dirtyAnnotations: true
59
+ }) : updatedState;
49
60
  }
50
61
 
51
62
  // Cache deleted annotations to be able to restore their resolved states on undo
@@ -68,6 +79,8 @@ var createHandleDocChanged = function createHandleDocChanged() {
68
79
  });
69
80
  return _objectSpread(_objectSpread({}, updatedState), {}, {
70
81
  annotations: updatedAnnotations
82
+ }, newNestedAnnotationIds.length > 0 && {
83
+ dirtyAnnotations: true
71
84
  });
72
85
  };
73
86
  };
@@ -195,47 +208,52 @@ var getSelectionChangedHandler = function getSelectionChangedHandler(reopenComme
195
208
  getSelectionChangeHandlerOld(reopenCommentView)(tr, pluginState);
196
209
  };
197
210
  };
211
+ var createAnnotationPluginFactory = function createAnnotationPluginFactory(onDocChanged) {
212
+ return (0, _pluginStateFactory.pluginFactory)(_utils.inlineCommentPluginKey, _reducer.default, {
213
+ onSelectionChanged: getSelectionChangedHandler(true),
214
+ onDocChanged: onDocChanged,
215
+ mapping: function mapping(tr, pluginState, editorState) {
216
+ var draftDecorationSet = pluginState.draftDecorationSet,
217
+ bookmark = pluginState.bookmark;
218
+ var mappedDecorationSet = _view.DecorationSet.empty,
219
+ mappedBookmark;
220
+ var hasMappedDecorations = false;
221
+ if (draftDecorationSet) {
222
+ mappedDecorationSet = draftDecorationSet.map(tr.mapping, tr.doc);
223
+ }
224
+ hasMappedDecorations = mappedDecorationSet.find(undefined, undefined, function (spec) {
225
+ return Object.values(_utils.decorationKey).includes(spec.key);
226
+ }).length > 0;
198
227
 
199
- // Create the handler with cache once at module level
200
- var handleDocChangedWithSync = createHandleDocChanged();
201
- var dest = (0, _pluginStateFactory.pluginFactory)(_utils.inlineCommentPluginKey, _reducer.default, {
202
- onSelectionChanged: getSelectionChangedHandler(true),
203
- onDocChanged: handleDocChangedWithSync,
204
- mapping: function mapping(tr, pluginState, editorState) {
205
- var draftDecorationSet = pluginState.draftDecorationSet,
206
- bookmark = pluginState.bookmark;
207
- var mappedDecorationSet = _view.DecorationSet.empty,
208
- mappedBookmark;
209
- var hasMappedDecorations = false;
210
- if (draftDecorationSet) {
211
- mappedDecorationSet = draftDecorationSet.map(tr.mapping, tr.doc);
212
- }
213
- hasMappedDecorations = mappedDecorationSet.find(undefined, undefined, function (spec) {
214
- return Object.values(_utils.decorationKey).includes(spec.key);
215
- }).length > 0;
228
+ // When changes to decoration target make decoration invalid (e.g. delete text, add mark to node),
229
+ // we need to reset bookmark to hide create component and to avoid invalid draft being published
230
+ // We only perform this change when document selection has changed.
231
+ if (!hasMappedDecorations && shouldClearBookMarkCheck(tr, editorState, bookmark)) {
232
+ return _objectSpread(_objectSpread({}, pluginState), {}, {
233
+ draftDecorationSet: mappedDecorationSet,
234
+ bookmark: undefined
235
+ });
236
+ }
237
+ if (bookmark) {
238
+ mappedBookmark = bookmark.map(tr.mapping);
239
+ }
216
240
 
217
- // When changes to decoration target make decoration invalid (e.g. delete text, add mark to node),
218
- // we need to reset bookmark to hide create component and to avoid invalid draft being published
219
- // We only perform this change when document selection has changed.
220
- if (!hasMappedDecorations && shouldClearBookMarkCheck(tr, editorState, bookmark)) {
241
+ // return same pluginState if mappings did not change
242
+ if (mappedBookmark === bookmark && mappedDecorationSet === draftDecorationSet) {
243
+ return pluginState;
244
+ }
221
245
  return _objectSpread(_objectSpread({}, pluginState), {}, {
222
246
  draftDecorationSet: mappedDecorationSet,
223
- bookmark: undefined
247
+ bookmark: mappedBookmark
224
248
  });
225
249
  }
226
- if (bookmark) {
227
- mappedBookmark = bookmark.map(tr.mapping);
228
- }
229
-
230
- // return same pluginState if mappings did not change
231
- if (mappedBookmark === bookmark && mappedDecorationSet === draftDecorationSet) {
232
- return pluginState;
233
- }
234
- return _objectSpread(_objectSpread({}, pluginState), {}, {
235
- draftDecorationSet: mappedDecorationSet,
236
- bookmark: mappedBookmark
237
- });
238
- }
239
- });
240
- var createPluginState = exports.createPluginState = dest.createPluginState;
241
- var createCommand = exports.createCommand = dest.createCommand;
250
+ });
251
+ };
252
+ var handleDocChangedWithSync = createHandleDocChanged();
253
+ // Keep the control cohort's existing module-level cache lifetime unchanged.
254
+ var legacyDest = createAnnotationPluginFactory(handleDocChangedWithSync);
255
+ var commandFactory = (0, _pluginStateFactory.pluginFactory)(_utils.inlineCommentPluginKey, _reducer.default);
256
+ var createPluginState = exports.createPluginState = function createPluginState(dispatch, initialState) {
257
+ return ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_fix_lcm_editor_annotations') ? createAnnotationPluginFactory(createHandleDocChanged()) : legacyDest).createPluginState(dispatch, initialState);
258
+ };
259
+ var createCommand = exports.createCommand = commandFactory.createCommand;
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.addDraftDecoration = void 0;
8
8
  exports.annotationExists = annotationExists;
9
- exports.getPluginState = exports.getDraftCommandAnalyticsPayload = exports.getAnnotationViewKey = exports.getAllAnnotations = exports.findAnnotationsInSelection = exports.decorationKey = void 0;
9
+ exports.getPluginState = exports.getDraftCommandAnalyticsPayload = exports.getAnnotationViewKey = exports.getAnnotationDiscovery = exports.getAllAnnotations = exports.findAnnotationsInSelection = exports.decorationKey = void 0;
10
10
  exports.getSelectionPositions = getSelectionPositions;
11
11
  exports.hasInvalidNodes = exports.hasAnyUnResolvedAnnotationInPage = void 0;
12
12
  exports.hasInvalidWhitespaceNode = hasInvalidWhitespaceNode;
@@ -16,12 +16,14 @@ exports.shouldHideRemixAnnotation = exports.resolveDraftBookmark = exports.isSup
16
16
  exports.stripNonExistingAnnotations = stripNonExistingAnnotations;
17
17
  exports.surroundingMarks = void 0;
18
18
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
19
+ var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
19
20
  var _annotation = require("@atlaskit/adf-schema/annotation");
20
21
  var _enums = require("@atlaskit/editor-common/analytics/types/enums");
21
22
  var _mediaSingle = require("@atlaskit/editor-common/media-single");
22
23
  var _annotation2 = require("@atlaskit/editor-common/styles/annotation");
23
24
  var _annotation3 = require("@atlaskit/editor-common/utils/annotation");
24
25
  var _nodes = require("@atlaskit/editor-common/utils/nodes");
26
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
25
27
  var _state = require("@atlaskit/editor-prosemirror/state");
26
28
  var _view = require("@atlaskit/editor-prosemirror/view");
27
29
  var _fg = require("@atlaskit/platform-feature-flags/fg");
@@ -79,7 +81,111 @@ var reorderAnnotations = function reorderAnnotations(annotations, $from) {
79
81
  });
80
82
  });
81
83
  };
84
+ var legacyContentAnnotationsByDoc = new WeakMap();
85
+ var legacyContentAnnotationIdsByExtension = new WeakMap();
86
+ var readNestedContentNodes = function readNestedContentNodes(parameters) {
87
+ if (!parameters || (0, _typeof2.default)(parameters) !== 'object') {
88
+ return undefined;
89
+ }
90
+ var nestedContent = parameters.nestedContent;
91
+ if (!nestedContent || (0, _typeof2.default)(nestedContent) !== 'object') {
92
+ return undefined;
93
+ }
94
+ var nestedDocument = nestedContent;
95
+ return nestedDocument.type === 'doc' && Array.isArray(nestedDocument.content) ? nestedDocument.content : undefined;
96
+ };
97
+ var getNestedContentAnnotationIds = function getNestedContentAnnotationIds(extension) {
98
+ var cachedIds = legacyContentAnnotationIdsByExtension.get(extension);
99
+ if (cachedIds) {
100
+ return cachedIds;
101
+ }
102
+ var nestedNodes = readNestedContentNodes(extension.attrs.parameters);
103
+ if (!nestedNodes) {
104
+ var noIds = Object.freeze([]);
105
+ legacyContentAnnotationIdsByExtension.set(extension, noIds);
106
+ return noIds;
107
+ }
108
+ var annotationIds = new Set();
109
+ var visitedAdfNodes = new WeakSet();
110
+ var pending = (0, _toConsumableArray2.default)(nestedNodes).reverse();
111
+ while (pending.length > 0) {
112
+ var _adfNode$attrs;
113
+ var value = pending.pop();
114
+ if (!value || (0, _typeof2.default)(value) !== 'object' || visitedAdfNodes.has(value)) {
115
+ continue;
116
+ }
117
+ visitedAdfNodes.add(value);
118
+ var adfNode = value;
119
+ if (Array.isArray(adfNode.marks)) {
120
+ adfNode.marks.forEach(function (rawMark) {
121
+ if (!rawMark || (0, _typeof2.default)(rawMark) !== 'object') {
122
+ return;
123
+ }
124
+ var annotationMark = rawMark;
125
+ var attrs = annotationMark.attrs;
126
+ if (annotationMark.type !== 'annotation' || !validateAnnotationAttributes(attrs)) {
127
+ return;
128
+ }
129
+ annotationIds.add(attrs.id);
130
+ });
131
+ }
132
+ if (adfNode.type === 'extension' && ((_adfNode$attrs = adfNode.attrs) === null || _adfNode$attrs === void 0 ? void 0 : _adfNode$attrs.extensionType) === 'com.atlassian.confluence.migration' && adfNode.attrs.extensionKey === 'legacy-content') {
133
+ var _nestedNodes = readNestedContentNodes(adfNode.attrs.parameters);
134
+ if (_nestedNodes) {
135
+ for (var i = _nestedNodes.length - 1; i >= 0; i--) {
136
+ pending.push(_nestedNodes[i]);
137
+ }
138
+ }
139
+ }
140
+ if (Array.isArray(adfNode.content)) {
141
+ for (var _i = adfNode.content.length - 1; _i >= 0; _i--) {
142
+ pending.push(adfNode.content[_i]);
143
+ }
144
+ }
145
+ }
146
+ var ids = Object.freeze((0, _toConsumableArray2.default)(annotationIds));
147
+ legacyContentAnnotationIdsByExtension.set(extension, ids);
148
+ return ids;
149
+ };
150
+
151
+ /** Discover annotation IDs from immutable legacy-content ADF without repeatedly scanning it. */
152
+ var getAnnotationDiscovery = exports.getAnnotationDiscovery = function getAnnotationDiscovery(doc) {
153
+ var cachedDiscovery = legacyContentAnnotationsByDoc.get(doc);
154
+ if (cachedDiscovery) {
155
+ return cachedDiscovery;
156
+ }
157
+ var allAnnotationIds = new Set();
158
+ var rootAnnotationIds = new Set();
159
+ var nestedAnnotationIds = new Set();
160
+ doc.descendants(function (node) {
161
+ node.marks.filter(function (mark) {
162
+ return mark.type.name === 'annotation';
163
+ }).forEach(function (mark) {
164
+ if (validateAnnotationMark(mark)) {
165
+ rootAnnotationIds.add(mark.attrs.id);
166
+ allAnnotationIds.add(mark.attrs.id);
167
+ }
168
+ });
169
+ if (node.type.name === 'extension' && node.attrs.extensionType === 'com.atlassian.confluence.migration' && node.attrs.extensionKey === 'legacy-content') {
170
+ getNestedContentAnnotationIds(node).forEach(function (id) {
171
+ nestedAnnotationIds.add(id);
172
+ allAnnotationIds.add(id);
173
+ });
174
+ }
175
+ return true;
176
+ });
177
+ var discovery = {
178
+ annotationIds: Object.freeze((0, _toConsumableArray2.default)(allAnnotationIds)),
179
+ rootAnnotationIds: Object.freeze((0, _toConsumableArray2.default)(rootAnnotationIds)),
180
+ nestedAnnotationIds: Object.freeze((0, _toConsumableArray2.default)(nestedAnnotationIds))
181
+ };
182
+ legacyContentAnnotationsByDoc.set(doc, discovery);
183
+ return discovery;
184
+ };
82
185
  var getAllAnnotations = exports.getAllAnnotations = function getAllAnnotations(doc) {
186
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_fix_lcm_editor_annotations')) {
187
+ return (0, _toConsumableArray2.default)(getAnnotationDiscovery(doc).annotationIds);
188
+ }
83
189
  var allAnnotationIds = new Set();
84
190
  doc.descendants(function (node) {
85
191
  node.marks.filter(function (mark) {
@@ -93,29 +199,30 @@ var getAllAnnotations = exports.getAllAnnotations = function getAllAnnotations(d
93
199
  });
94
200
  return Array.from(allAnnotationIds);
95
201
  };
96
-
97
- /*
98
- * verifies if annotation mark contains valid attributes
99
- */
100
- var validateAnnotationMark = function validateAnnotationMark(annotationMark) {
101
- var _ref = annotationMark.attrs,
102
- id = _ref.id,
103
- annotationType = _ref.annotationType;
104
- return validateAnnotationId(id) && validateAnnotationType(annotationType);
105
- function validateAnnotationId(id) {
106
- if (!id || typeof id !== 'string') {
107
- return false;
108
- }
109
- var invalidIds = ['null', 'undefined'];
110
- return !invalidIds.includes(id.toLowerCase());
202
+ var validateAnnotationId = function validateAnnotationId(id) {
203
+ if (!id || typeof id !== 'string') {
204
+ return false;
111
205
  }
112
- function validateAnnotationType(type) {
113
- if (!type || typeof type !== 'string') {
114
- return false;
115
- }
116
- var allowedTypes = Object.values(_annotation.AnnotationTypes);
117
- return allowedTypes.includes(type);
206
+ var invalidIds = ['null', 'undefined'];
207
+ return !invalidIds.includes(id.toLowerCase());
208
+ };
209
+ var validateAnnotationType = function validateAnnotationType(type) {
210
+ if (!type || typeof type !== 'string') {
211
+ return false;
212
+ }
213
+ var allowedTypes = Object.values(_annotation.AnnotationTypes);
214
+ return allowedTypes.some(function (allowedType) {
215
+ return allowedType === type;
216
+ });
217
+ };
218
+ var validateAnnotationAttributes = function validateAnnotationAttributes(attrs) {
219
+ if (!attrs || (0, _typeof2.default)(attrs) !== 'object') {
220
+ return false;
118
221
  }
222
+ return 'id' in attrs && 'annotationType' in attrs && validateAnnotationId(attrs.id) && validateAnnotationType(attrs.annotationType);
223
+ };
224
+ var validateAnnotationMark = function validateAnnotationMark(annotationMark) {
225
+ return validateAnnotationAttributes(annotationMark.attrs);
119
226
  };
120
227
  var decorationKey = exports.decorationKey = {
121
228
  block: 'blockCommentDecoration',
@@ -288,14 +395,26 @@ var getDraftCommandAnalyticsPayload = exports.getDraftCommandAnalyticsPayload =
288
395
  var payload = function payload(state) {
289
396
  var attributes = {};
290
397
  if (drafting) {
398
+ var _currentMediaNodeWith;
399
+ var selection = state.selection;
400
+ var targetNode = selection instanceof _state.NodeSelection ? selection.node : (_currentMediaNodeWith = (0, _mediaSingle.currentMediaNodeWithPos)(state)) === null || _currentMediaNodeWith === void 0 ? void 0 : _currentMediaNodeWith.node;
401
+ var commentTargetType = 'other';
402
+ if ((targetNode === null || targetNode === void 0 ? void 0 : targetNode.type.name) === 'extension') {
403
+ commentTargetType = targetNode.attrs.extensionKey === 'native-embed:maui' ? 'remixEmbed' : 'other';
404
+ } else if (targetNode && ['media', 'mediaSingle', 'mediaInline'].includes(targetNode.type.name)) {
405
+ commentTargetType = 'media';
406
+ } else if (selection instanceof _state.TextSelection || selection instanceof _state.AllSelection) {
407
+ commentTargetType = 'text';
408
+ }
291
409
  attributes = {
292
410
  inputMethod: inputMethod,
293
- overlap: getAnnotationsInSelectionCount(state)
411
+ overlap: getAnnotationsInSelectionCount(state),
412
+ commentTargetType: commentTargetType
294
413
  };
295
414
  }
296
415
  if ((0, _fg.fg)('editor_inline_comments_on_inline_nodes')) {
297
- var _ref2 = getPluginState(state) || {},
298
- bookmark = _ref2.bookmark;
416
+ var _ref = getPluginState(state) || {},
417
+ bookmark = _ref.bookmark;
299
418
  attributes.inlineNodeNames = (0, _annotation3.getRangeInlineNodeNames)({
300
419
  doc: state.doc,
301
420
  pos: resolveDraftBookmark(state, bookmark)
@@ -312,12 +431,12 @@ var getDraftCommandAnalyticsPayload = exports.getDraftCommandAnalyticsPayload =
312
431
  return payload;
313
432
  };
314
433
  var isSelectionValid = exports.isSelectionValid = function isSelectionValid(state) {
315
- var _currentMediaNodeWith;
434
+ var _currentMediaNodeWith2;
316
435
  var supportedBlockNodes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
317
436
  var isBlockNodeSupported = arguments.length > 2 ? arguments[2] : undefined;
318
437
  var selection = state.selection;
319
- var _ref3 = getPluginState(state) || {},
320
- disallowOnWhitespace = _ref3.disallowOnWhitespace;
438
+ var _ref2 = getPluginState(state) || {},
439
+ disallowOnWhitespace = _ref2.disallowOnWhitespace;
321
440
  var allowedInlineNodes = ['emoji', 'status', 'date', 'mention', 'inlineCard'];
322
441
  var isSelectionEmpty = selection.empty;
323
442
  var isTextOrAllSelection = selection instanceof _state.TextSelection || selection instanceof _state.AllSelection;
@@ -326,7 +445,7 @@ var isSelectionValid = exports.isSelectionValid = function isSelectionValid(stat
326
445
  var isValidSelection = isTextOrAllSelection || isValidNodeSelection;
327
446
 
328
447
  // Allow media so that it can enter draft mode
329
- if ((_currentMediaNodeWith = (0, _mediaSingle.currentMediaNodeWithPos)(state)) !== null && _currentMediaNodeWith !== void 0 && _currentMediaNodeWith.node) {
448
+ if ((_currentMediaNodeWith2 = (0, _mediaSingle.currentMediaNodeWithPos)(state)) !== null && _currentMediaNodeWith2 !== void 0 && _currentMediaNodeWith2.node) {
330
449
  return _types.AnnotationSelectionType.VALID;
331
450
  }
332
451
  if (isSelectionEmpty || !isValidSelection) {
@@ -4,6 +4,7 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
4
4
  import { getAnnotationInlineNodeTypes } from '@atlaskit/editor-common/utils/annotation';
5
5
  import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
6
6
  import { fg } from '@atlaskit/platform-feature-flags/fg';
7
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
7
8
  import { clearDirtyMark, closeComponent, setHoveredAnnotation, setInlineCommentsVisibility, setSelectedAnnotation, flushPendingSelections, updateInlineCommentResolvedState, updateMouseState, setPendingSelectedAnnotation, setInlineCommentDraftState, setInlineCommentsFetched } from '../editor-commands';
8
9
  import { resetUserIntent, setUserIntent } from '../editor-commands/utils';
9
10
  import { getAnnotationViewClassname, getBlockAnnotationViewClassname } from '../nodeviews';
@@ -353,6 +354,12 @@ export const inlineCommentPlugin = options => {
353
354
  if (!(event.target instanceof HTMLElement)) {
354
355
  return false;
355
356
  }
357
+ if (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations')) {
358
+ // The LCM renderer bridge owns selection so its draft-close guard runs first.
359
+ if (event.target.closest('[data-lcm-annotation-renderer]')) {
360
+ return false;
361
+ }
362
+ }
356
363
 
357
364
  // Find the nearest ancestor (or self) with the data-id attribute
358
365
  const annotationId = (_event$target$closest = event.target.closest('[data-id]')) === null || _event$target$closest === void 0 ? void 0 : _event$target$closest.getAttribute('data-id');
@@ -1,16 +1,16 @@
1
1
  import { pluginFactory } from '@atlaskit/editor-common/utils/plugin-state-factory';
2
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
2
3
  import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
3
4
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
4
5
  import reducer from './reducer';
5
- import { decorationKey, findAnnotationsInSelection, inlineCommentPluginKey, isBlockNodeAnnotationsSelected, isSelectedAnnotationsChanged } from './utils';
6
+ import { decorationKey, getAnnotationDiscovery, findAnnotationsInSelection, inlineCommentPluginKey, isBlockNodeAnnotationsSelected, isSelectedAnnotationsChanged } from './utils';
6
7
 
7
8
  /**
8
- * Creates a handleDocChanged function with its own deleted annotations cache.
9
- * This ensures each editor instance has its own cache, avoiding cross-contamination.
9
+ * Creates a document-sync handler with a deleted annotations cache in its closure.
10
10
  */
11
11
  const createHandleDocChanged = () => {
12
12
  // Cache for preserving deleted annotation resolved states through delete/undo cycles
13
- // This lives in closure scope per editor instance to avoid serialization and reduce state size
13
+ // This stays outside plugin state so the undo cache is not serialized.
14
14
  const deletedAnnotationsCache = {};
15
15
  return (tr, prevPluginState) => {
16
16
  if (tr.getMeta('replaceDocument')) {
@@ -20,22 +20,31 @@ const createHandleDocChanged = () => {
20
20
  };
21
21
  }
22
22
  const updatedState = getSelectionChangedHandler(false)(tr, prevPluginState);
23
+ const legacyContentExperimentEnabled = isExperimentEnabled('platform_editor_fix_lcm_editor_annotations');
24
+ const currentDiscovery = legacyContentExperimentEnabled ? getAnnotationDiscovery(tr.doc) : undefined;
25
+ const previousAnnotationIds = currentDiscovery ? new Set(getAnnotationDiscovery(tr.before).annotationIds) : undefined;
26
+ const newNestedAnnotationIds = currentDiscovery ? currentDiscovery.nestedAnnotationIds.filter(id => !(previousAnnotationIds !== null && previousAnnotationIds !== void 0 && previousAnnotationIds.has(id)) && prevPluginState.annotations[id] === undefined && deletedAnnotationsCache[id] === undefined) : [];
23
27
 
24
28
  // Collect annotation IDs currently in the document
25
- const annotationIdsInDocument = new Set();
26
- tr.doc.descendants(node => {
27
- node.marks.forEach(mark => {
28
- if (mark.type.name === 'annotation') {
29
- annotationIdsInDocument.add(mark.attrs.id);
30
- }
29
+ const annotationIdsInDocument = currentDiscovery ? new Set([...currentDiscovery.rootAnnotationIds, ...currentDiscovery.nestedAnnotationIds]) : new Set();
30
+ if (!currentDiscovery) {
31
+ tr.doc.descendants(node => {
32
+ node.marks.forEach(mark => {
33
+ if (mark.type.name === 'annotation') {
34
+ annotationIdsInDocument.add(mark.attrs.id);
35
+ }
36
+ });
31
37
  });
32
- });
38
+ }
33
39
  const annotationIdsInState = Object.keys(prevPluginState.annotations);
34
40
 
35
41
  // Early return if annotations haven't changed
36
42
  const annotationsHaveChanged = annotationIdsInDocument.size !== annotationIdsInState.length || annotationIdsInState.some(id => !annotationIdsInDocument.has(id));
37
43
  if (!annotationsHaveChanged) {
38
- return updatedState;
44
+ return newNestedAnnotationIds.length > 0 ? {
45
+ ...updatedState,
46
+ dirtyAnnotations: true
47
+ } : updatedState;
39
48
  }
40
49
 
41
50
  // Cache deleted annotations to be able to restore their resolved states on undo
@@ -58,7 +67,10 @@ const createHandleDocChanged = () => {
58
67
  });
59
68
  return {
60
69
  ...updatedState,
61
- annotations: updatedAnnotations
70
+ annotations: updatedAnnotations,
71
+ ...(newNestedAnnotationIds.length > 0 && {
72
+ dirtyAnnotations: true
73
+ })
62
74
  };
63
75
  };
64
76
  };
@@ -194,12 +206,9 @@ const getSelectionChangedHandler = reopenCommentView => (tr, pluginState) => plu
194
206
  getSelectionChangeHandlerNew(reopenCommentView)(tr, pluginState) :
195
207
  // else if platform_editor_comments_api_manager == false
196
208
  getSelectionChangeHandlerOld(reopenCommentView)(tr, pluginState);
197
-
198
- // Create the handler with cache once at module level
199
- const handleDocChangedWithSync = createHandleDocChanged();
200
- const dest = pluginFactory(inlineCommentPluginKey, reducer, {
209
+ const createAnnotationPluginFactory = onDocChanged => pluginFactory(inlineCommentPluginKey, reducer, {
201
210
  onSelectionChanged: getSelectionChangedHandler(true),
202
- onDocChanged: handleDocChangedWithSync,
211
+ onDocChanged,
203
212
  mapping: (tr, pluginState, editorState) => {
204
213
  const {
205
214
  draftDecorationSet,
@@ -238,5 +247,9 @@ const dest = pluginFactory(inlineCommentPluginKey, reducer, {
238
247
  };
239
248
  }
240
249
  });
241
- export const createPluginState = dest.createPluginState;
242
- export const createCommand = dest.createCommand;
250
+ const handleDocChangedWithSync = createHandleDocChanged();
251
+ // Keep the control cohort's existing module-level cache lifetime unchanged.
252
+ const legacyDest = createAnnotationPluginFactory(handleDocChangedWithSync);
253
+ const commandFactory = pluginFactory(inlineCommentPluginKey, reducer);
254
+ export const createPluginState = (dispatch, initialState) => (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations') ? createAnnotationPluginFactory(createHandleDocChanged()) : legacyDest).createPluginState(dispatch, initialState);
255
+ export const createCommand = commandFactory.createCommand;
@@ -4,6 +4,7 @@ import { currentMediaNodeWithPos } from '@atlaskit/editor-common/media-single';
4
4
  import { AnnotationSharedClassNames, BlockAnnotationSharedClassNames } from '@atlaskit/editor-common/styles/annotation';
5
5
  import { canApplyAnnotationOnRange, getAnnotationIdsFromRange, getRangeInlineNodeNames, hasAnnotationMark, isEmptyTextSelection } from '@atlaskit/editor-common/utils/annotation';
6
6
  import { isParagraph, isText } from '@atlaskit/editor-common/utils/nodes';
7
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
7
8
  import { AllSelection, NodeSelection, PluginKey, TextSelection } from '@atlaskit/editor-prosemirror/state';
8
9
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
9
10
  import { fg } from '@atlaskit/platform-feature-flags/fg';
@@ -53,7 +54,109 @@ const reorderAnnotations = (annotations, $from) => {
53
54
  const idSet = surroundingMarks($from).map(filterAnnotationIds);
54
55
  annotations.sort((a, b) => sum(idSet, ids => ids.indexOf(a.id)) - sum(idSet, ids => ids.indexOf(b.id)));
55
56
  };
57
+ const legacyContentAnnotationsByDoc = new WeakMap();
58
+ const legacyContentAnnotationIdsByExtension = new WeakMap();
59
+ const readNestedContentNodes = parameters => {
60
+ if (!parameters || typeof parameters !== 'object') {
61
+ return undefined;
62
+ }
63
+ const nestedContent = parameters.nestedContent;
64
+ if (!nestedContent || typeof nestedContent !== 'object') {
65
+ return undefined;
66
+ }
67
+ const nestedDocument = nestedContent;
68
+ return nestedDocument.type === 'doc' && Array.isArray(nestedDocument.content) ? nestedDocument.content : undefined;
69
+ };
70
+ const getNestedContentAnnotationIds = extension => {
71
+ const cachedIds = legacyContentAnnotationIdsByExtension.get(extension);
72
+ if (cachedIds) {
73
+ return cachedIds;
74
+ }
75
+ const nestedNodes = readNestedContentNodes(extension.attrs.parameters);
76
+ if (!nestedNodes) {
77
+ const noIds = Object.freeze([]);
78
+ legacyContentAnnotationIdsByExtension.set(extension, noIds);
79
+ return noIds;
80
+ }
81
+ const annotationIds = new Set();
82
+ const visitedAdfNodes = new WeakSet();
83
+ const pending = [...nestedNodes].reverse();
84
+ while (pending.length > 0) {
85
+ var _adfNode$attrs;
86
+ const value = pending.pop();
87
+ if (!value || typeof value !== 'object' || visitedAdfNodes.has(value)) {
88
+ continue;
89
+ }
90
+ visitedAdfNodes.add(value);
91
+ const adfNode = value;
92
+ if (Array.isArray(adfNode.marks)) {
93
+ adfNode.marks.forEach(rawMark => {
94
+ if (!rawMark || typeof rawMark !== 'object') {
95
+ return;
96
+ }
97
+ const annotationMark = rawMark;
98
+ const attrs = annotationMark.attrs;
99
+ if (annotationMark.type !== 'annotation' || !validateAnnotationAttributes(attrs)) {
100
+ return;
101
+ }
102
+ annotationIds.add(attrs.id);
103
+ });
104
+ }
105
+ if (adfNode.type === 'extension' && ((_adfNode$attrs = adfNode.attrs) === null || _adfNode$attrs === void 0 ? void 0 : _adfNode$attrs.extensionType) === 'com.atlassian.confluence.migration' && adfNode.attrs.extensionKey === 'legacy-content') {
106
+ const nestedNodes = readNestedContentNodes(adfNode.attrs.parameters);
107
+ if (nestedNodes) {
108
+ for (let i = nestedNodes.length - 1; i >= 0; i--) {
109
+ pending.push(nestedNodes[i]);
110
+ }
111
+ }
112
+ }
113
+ if (Array.isArray(adfNode.content)) {
114
+ for (let i = adfNode.content.length - 1; i >= 0; i--) {
115
+ pending.push(adfNode.content[i]);
116
+ }
117
+ }
118
+ }
119
+ const ids = Object.freeze([...annotationIds]);
120
+ legacyContentAnnotationIdsByExtension.set(extension, ids);
121
+ return ids;
122
+ };
123
+
124
+ /** Discover annotation IDs from immutable legacy-content ADF without repeatedly scanning it. */
125
+ export const getAnnotationDiscovery = doc => {
126
+ const cachedDiscovery = legacyContentAnnotationsByDoc.get(doc);
127
+ if (cachedDiscovery) {
128
+ return cachedDiscovery;
129
+ }
130
+ const allAnnotationIds = new Set();
131
+ const rootAnnotationIds = new Set();
132
+ const nestedAnnotationIds = new Set();
133
+ doc.descendants(node => {
134
+ node.marks.filter(mark => mark.type.name === 'annotation').forEach(mark => {
135
+ if (validateAnnotationMark(mark)) {
136
+ rootAnnotationIds.add(mark.attrs.id);
137
+ allAnnotationIds.add(mark.attrs.id);
138
+ }
139
+ });
140
+ if (node.type.name === 'extension' && node.attrs.extensionType === 'com.atlassian.confluence.migration' && node.attrs.extensionKey === 'legacy-content') {
141
+ getNestedContentAnnotationIds(node).forEach(id => {
142
+ nestedAnnotationIds.add(id);
143
+ allAnnotationIds.add(id);
144
+ });
145
+ }
146
+ return true;
147
+ });
148
+ const discovery = {
149
+ annotationIds: Object.freeze([...allAnnotationIds]),
150
+ rootAnnotationIds: Object.freeze([...rootAnnotationIds]),
151
+ nestedAnnotationIds: Object.freeze([...nestedAnnotationIds])
152
+ };
153
+ legacyContentAnnotationsByDoc.set(doc, discovery);
154
+ return discovery;
155
+ };
56
156
  export const getAllAnnotations = doc => {
157
+ if (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations')) {
158
+ return [...getAnnotationDiscovery(doc).annotationIds];
159
+ }
57
160
  const allAnnotationIds = new Set();
58
161
  doc.descendants(node => {
59
162
  node.marks.filter(mark => mark.type.name === 'annotation')
@@ -63,31 +166,27 @@ export const getAllAnnotations = doc => {
63
166
  });
64
167
  return Array.from(allAnnotationIds);
65
168
  };
66
-
67
- /*
68
- * verifies if annotation mark contains valid attributes
69
- */
70
- const validateAnnotationMark = annotationMark => {
71
- const {
72
- id,
73
- annotationType
74
- } = annotationMark.attrs;
75
- return validateAnnotationId(id) && validateAnnotationType(annotationType);
76
- function validateAnnotationId(id) {
77
- if (!id || typeof id !== 'string') {
78
- return false;
79
- }
80
- const invalidIds = ['null', 'undefined'];
81
- return !invalidIds.includes(id.toLowerCase());
169
+ const validateAnnotationId = id => {
170
+ if (!id || typeof id !== 'string') {
171
+ return false;
82
172
  }
83
- function validateAnnotationType(type) {
84
- if (!type || typeof type !== 'string') {
85
- return false;
86
- }
87
- const allowedTypes = Object.values(AnnotationTypes);
88
- return allowedTypes.includes(type);
173
+ const invalidIds = ['null', 'undefined'];
174
+ return !invalidIds.includes(id.toLowerCase());
175
+ };
176
+ const validateAnnotationType = type => {
177
+ if (!type || typeof type !== 'string') {
178
+ return false;
89
179
  }
180
+ const allowedTypes = Object.values(AnnotationTypes);
181
+ return allowedTypes.some(allowedType => allowedType === type);
90
182
  };
183
+ const validateAnnotationAttributes = attrs => {
184
+ if (!attrs || typeof attrs !== 'object') {
185
+ return false;
186
+ }
187
+ return 'id' in attrs && 'annotationType' in attrs && validateAnnotationId(attrs.id) && validateAnnotationType(attrs.annotationType);
188
+ };
189
+ const validateAnnotationMark = annotationMark => validateAnnotationAttributes(annotationMark.attrs);
91
190
  export const decorationKey = {
92
191
  block: 'blockCommentDecoration',
93
192
  inline: 'inlineCommentDecoration'
@@ -246,9 +345,23 @@ export const getDraftCommandAnalyticsPayload = (drafting, inputMethod) => {
246
345
  const payload = state => {
247
346
  let attributes = {};
248
347
  if (drafting) {
348
+ var _currentMediaNodeWith;
349
+ const {
350
+ selection
351
+ } = state;
352
+ const targetNode = selection instanceof NodeSelection ? selection.node : (_currentMediaNodeWith = currentMediaNodeWithPos(state)) === null || _currentMediaNodeWith === void 0 ? void 0 : _currentMediaNodeWith.node;
353
+ let commentTargetType = 'other';
354
+ if ((targetNode === null || targetNode === void 0 ? void 0 : targetNode.type.name) === 'extension') {
355
+ commentTargetType = targetNode.attrs.extensionKey === 'native-embed:maui' ? 'remixEmbed' : 'other';
356
+ } else if (targetNode && ['media', 'mediaSingle', 'mediaInline'].includes(targetNode.type.name)) {
357
+ commentTargetType = 'media';
358
+ } else if (selection instanceof TextSelection || selection instanceof AllSelection) {
359
+ commentTargetType = 'text';
360
+ }
249
361
  attributes = {
250
362
  inputMethod,
251
- overlap: getAnnotationsInSelectionCount(state)
363
+ overlap: getAnnotationsInSelectionCount(state),
364
+ commentTargetType
252
365
  };
253
366
  }
254
367
  if (fg('editor_inline_comments_on_inline_nodes')) {
@@ -271,7 +384,7 @@ export const getDraftCommandAnalyticsPayload = (drafting, inputMethod) => {
271
384
  return payload;
272
385
  };
273
386
  export const isSelectionValid = (state, supportedBlockNodes = [], isBlockNodeSupported) => {
274
- var _currentMediaNodeWith;
387
+ var _currentMediaNodeWith2;
275
388
  const {
276
389
  selection
277
390
  } = state;
@@ -286,7 +399,7 @@ export const isSelectionValid = (state, supportedBlockNodes = [], isBlockNodeSup
286
399
  const isValidSelection = isTextOrAllSelection || isValidNodeSelection;
287
400
 
288
401
  // Allow media so that it can enter draft mode
289
- if ((_currentMediaNodeWith = currentMediaNodeWithPos(state)) !== null && _currentMediaNodeWith !== void 0 && _currentMediaNodeWith.node) {
402
+ if ((_currentMediaNodeWith2 = currentMediaNodeWithPos(state)) !== null && _currentMediaNodeWith2 !== void 0 && _currentMediaNodeWith2.node) {
290
403
  return AnnotationSelectionType.VALID;
291
404
  }
292
405
  if (isSelectionEmpty || !isValidSelection) {
@@ -7,6 +7,7 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
7
7
  import { getAnnotationInlineNodeTypes } from '@atlaskit/editor-common/utils/annotation';
8
8
  import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
9
9
  import { fg } from '@atlaskit/platform-feature-flags/fg';
10
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
10
11
  import { clearDirtyMark, closeComponent, setHoveredAnnotation, setInlineCommentsVisibility, setSelectedAnnotation, flushPendingSelections, updateInlineCommentResolvedState, updateMouseState, setPendingSelectedAnnotation, setInlineCommentDraftState, setInlineCommentsFetched } from '../editor-commands';
11
12
  import { resetUserIntent, setUserIntent } from '../editor-commands/utils';
12
13
  import { getAnnotationViewClassname, getBlockAnnotationViewClassname } from '../nodeviews';
@@ -402,6 +403,12 @@ export var inlineCommentPlugin = function inlineCommentPlugin(options) {
402
403
  if (!(event.target instanceof HTMLElement)) {
403
404
  return false;
404
405
  }
406
+ if (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations')) {
407
+ // The LCM renderer bridge owns selection so its draft-close guard runs first.
408
+ if (event.target.closest('[data-lcm-annotation-renderer]')) {
409
+ return false;
410
+ }
411
+ }
405
412
 
406
413
  // Find the nearest ancestor (or self) with the data-id attribute
407
414
  var annotationId = (_event$target$closest = event.target.closest('[data-id]')) === null || _event$target$closest === void 0 ? void 0 : _event$target$closest.getAttribute('data-id');
@@ -1,19 +1,20 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
3
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
4
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
5
  import { pluginFactory } from '@atlaskit/editor-common/utils/plugin-state-factory';
6
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
5
7
  import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
6
8
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
9
  import reducer from './reducer';
8
- import { decorationKey, findAnnotationsInSelection, inlineCommentPluginKey, isBlockNodeAnnotationsSelected, isSelectedAnnotationsChanged } from './utils';
10
+ import { decorationKey, getAnnotationDiscovery, findAnnotationsInSelection, inlineCommentPluginKey, isBlockNodeAnnotationsSelected, isSelectedAnnotationsChanged } from './utils';
9
11
 
10
12
  /**
11
- * Creates a handleDocChanged function with its own deleted annotations cache.
12
- * This ensures each editor instance has its own cache, avoiding cross-contamination.
13
+ * Creates a document-sync handler with a deleted annotations cache in its closure.
13
14
  */
14
15
  var createHandleDocChanged = function createHandleDocChanged() {
15
16
  // Cache for preserving deleted annotation resolved states through delete/undo cycles
16
- // This lives in closure scope per editor instance to avoid serialization and reduce state size
17
+ // This stays outside plugin state so the undo cache is not serialized.
17
18
  var deletedAnnotationsCache = {};
18
19
  return function (tr, prevPluginState) {
19
20
  if (tr.getMeta('replaceDocument')) {
@@ -22,16 +23,24 @@ var createHandleDocChanged = function createHandleDocChanged() {
22
23
  });
23
24
  }
24
25
  var updatedState = getSelectionChangedHandler(false)(tr, prevPluginState);
26
+ var legacyContentExperimentEnabled = isExperimentEnabled('platform_editor_fix_lcm_editor_annotations');
27
+ var currentDiscovery = legacyContentExperimentEnabled ? getAnnotationDiscovery(tr.doc) : undefined;
28
+ var previousAnnotationIds = currentDiscovery ? new Set(getAnnotationDiscovery(tr.before).annotationIds) : undefined;
29
+ var newNestedAnnotationIds = currentDiscovery ? currentDiscovery.nestedAnnotationIds.filter(function (id) {
30
+ return !(previousAnnotationIds !== null && previousAnnotationIds !== void 0 && previousAnnotationIds.has(id)) && prevPluginState.annotations[id] === undefined && deletedAnnotationsCache[id] === undefined;
31
+ }) : [];
25
32
 
26
33
  // Collect annotation IDs currently in the document
27
- var annotationIdsInDocument = new Set();
28
- tr.doc.descendants(function (node) {
29
- node.marks.forEach(function (mark) {
30
- if (mark.type.name === 'annotation') {
31
- annotationIdsInDocument.add(mark.attrs.id);
32
- }
34
+ var annotationIdsInDocument = currentDiscovery ? new Set([].concat(_toConsumableArray(currentDiscovery.rootAnnotationIds), _toConsumableArray(currentDiscovery.nestedAnnotationIds))) : new Set();
35
+ if (!currentDiscovery) {
36
+ tr.doc.descendants(function (node) {
37
+ node.marks.forEach(function (mark) {
38
+ if (mark.type.name === 'annotation') {
39
+ annotationIdsInDocument.add(mark.attrs.id);
40
+ }
41
+ });
33
42
  });
34
- });
43
+ }
35
44
  var annotationIdsInState = Object.keys(prevPluginState.annotations);
36
45
 
37
46
  // Early return if annotations haven't changed
@@ -39,7 +48,9 @@ var createHandleDocChanged = function createHandleDocChanged() {
39
48
  return !annotationIdsInDocument.has(id);
40
49
  });
41
50
  if (!annotationsHaveChanged) {
42
- return updatedState;
51
+ return newNestedAnnotationIds.length > 0 ? _objectSpread(_objectSpread({}, updatedState), {}, {
52
+ dirtyAnnotations: true
53
+ }) : updatedState;
43
54
  }
44
55
 
45
56
  // Cache deleted annotations to be able to restore their resolved states on undo
@@ -62,6 +73,8 @@ var createHandleDocChanged = function createHandleDocChanged() {
62
73
  });
63
74
  return _objectSpread(_objectSpread({}, updatedState), {}, {
64
75
  annotations: updatedAnnotations
76
+ }, newNestedAnnotationIds.length > 0 && {
77
+ dirtyAnnotations: true
65
78
  });
66
79
  };
67
80
  };
@@ -189,47 +202,52 @@ var getSelectionChangedHandler = function getSelectionChangedHandler(reopenComme
189
202
  getSelectionChangeHandlerOld(reopenCommentView)(tr, pluginState);
190
203
  };
191
204
  };
205
+ var createAnnotationPluginFactory = function createAnnotationPluginFactory(onDocChanged) {
206
+ return pluginFactory(inlineCommentPluginKey, reducer, {
207
+ onSelectionChanged: getSelectionChangedHandler(true),
208
+ onDocChanged: onDocChanged,
209
+ mapping: function mapping(tr, pluginState, editorState) {
210
+ var draftDecorationSet = pluginState.draftDecorationSet,
211
+ bookmark = pluginState.bookmark;
212
+ var mappedDecorationSet = DecorationSet.empty,
213
+ mappedBookmark;
214
+ var hasMappedDecorations = false;
215
+ if (draftDecorationSet) {
216
+ mappedDecorationSet = draftDecorationSet.map(tr.mapping, tr.doc);
217
+ }
218
+ hasMappedDecorations = mappedDecorationSet.find(undefined, undefined, function (spec) {
219
+ return Object.values(decorationKey).includes(spec.key);
220
+ }).length > 0;
192
221
 
193
- // Create the handler with cache once at module level
194
- var handleDocChangedWithSync = createHandleDocChanged();
195
- var dest = pluginFactory(inlineCommentPluginKey, reducer, {
196
- onSelectionChanged: getSelectionChangedHandler(true),
197
- onDocChanged: handleDocChangedWithSync,
198
- mapping: function mapping(tr, pluginState, editorState) {
199
- var draftDecorationSet = pluginState.draftDecorationSet,
200
- bookmark = pluginState.bookmark;
201
- var mappedDecorationSet = DecorationSet.empty,
202
- mappedBookmark;
203
- var hasMappedDecorations = false;
204
- if (draftDecorationSet) {
205
- mappedDecorationSet = draftDecorationSet.map(tr.mapping, tr.doc);
206
- }
207
- hasMappedDecorations = mappedDecorationSet.find(undefined, undefined, function (spec) {
208
- return Object.values(decorationKey).includes(spec.key);
209
- }).length > 0;
222
+ // When changes to decoration target make decoration invalid (e.g. delete text, add mark to node),
223
+ // we need to reset bookmark to hide create component and to avoid invalid draft being published
224
+ // We only perform this change when document selection has changed.
225
+ if (!hasMappedDecorations && shouldClearBookMarkCheck(tr, editorState, bookmark)) {
226
+ return _objectSpread(_objectSpread({}, pluginState), {}, {
227
+ draftDecorationSet: mappedDecorationSet,
228
+ bookmark: undefined
229
+ });
230
+ }
231
+ if (bookmark) {
232
+ mappedBookmark = bookmark.map(tr.mapping);
233
+ }
210
234
 
211
- // When changes to decoration target make decoration invalid (e.g. delete text, add mark to node),
212
- // we need to reset bookmark to hide create component and to avoid invalid draft being published
213
- // We only perform this change when document selection has changed.
214
- if (!hasMappedDecorations && shouldClearBookMarkCheck(tr, editorState, bookmark)) {
235
+ // return same pluginState if mappings did not change
236
+ if (mappedBookmark === bookmark && mappedDecorationSet === draftDecorationSet) {
237
+ return pluginState;
238
+ }
215
239
  return _objectSpread(_objectSpread({}, pluginState), {}, {
216
240
  draftDecorationSet: mappedDecorationSet,
217
- bookmark: undefined
241
+ bookmark: mappedBookmark
218
242
  });
219
243
  }
220
- if (bookmark) {
221
- mappedBookmark = bookmark.map(tr.mapping);
222
- }
223
-
224
- // return same pluginState if mappings did not change
225
- if (mappedBookmark === bookmark && mappedDecorationSet === draftDecorationSet) {
226
- return pluginState;
227
- }
228
- return _objectSpread(_objectSpread({}, pluginState), {}, {
229
- draftDecorationSet: mappedDecorationSet,
230
- bookmark: mappedBookmark
231
- });
232
- }
233
- });
234
- export var createPluginState = dest.createPluginState;
235
- export var createCommand = dest.createCommand;
244
+ });
245
+ };
246
+ var handleDocChangedWithSync = createHandleDocChanged();
247
+ // Keep the control cohort's existing module-level cache lifetime unchanged.
248
+ var legacyDest = createAnnotationPluginFactory(handleDocChangedWithSync);
249
+ var commandFactory = pluginFactory(inlineCommentPluginKey, reducer);
250
+ export var createPluginState = function createPluginState(dispatch, initialState) {
251
+ return (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations') ? createAnnotationPluginFactory(createHandleDocChanged()) : legacyDest).createPluginState(dispatch, initialState);
252
+ };
253
+ export var createCommand = commandFactory.createCommand;
@@ -1,10 +1,12 @@
1
1
  import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ import _typeof from "@babel/runtime/helpers/typeof";
2
3
  import { AnnotationTypes } from '@atlaskit/adf-schema/annotation';
3
4
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics/types/enums';
4
5
  import { currentMediaNodeWithPos } from '@atlaskit/editor-common/media-single';
5
6
  import { AnnotationSharedClassNames, BlockAnnotationSharedClassNames } from '@atlaskit/editor-common/styles/annotation';
6
7
  import { canApplyAnnotationOnRange, getAnnotationIdsFromRange, getRangeInlineNodeNames, hasAnnotationMark, isEmptyTextSelection } from '@atlaskit/editor-common/utils/annotation';
7
8
  import { isParagraph, isText } from '@atlaskit/editor-common/utils/nodes';
9
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
8
10
  import { AllSelection, NodeSelection, PluginKey, TextSelection } from '@atlaskit/editor-prosemirror/state';
9
11
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
10
12
  import { fg } from '@atlaskit/platform-feature-flags/fg';
@@ -62,7 +64,111 @@ var reorderAnnotations = function reorderAnnotations(annotations, $from) {
62
64
  });
63
65
  });
64
66
  };
67
+ var legacyContentAnnotationsByDoc = new WeakMap();
68
+ var legacyContentAnnotationIdsByExtension = new WeakMap();
69
+ var readNestedContentNodes = function readNestedContentNodes(parameters) {
70
+ if (!parameters || _typeof(parameters) !== 'object') {
71
+ return undefined;
72
+ }
73
+ var nestedContent = parameters.nestedContent;
74
+ if (!nestedContent || _typeof(nestedContent) !== 'object') {
75
+ return undefined;
76
+ }
77
+ var nestedDocument = nestedContent;
78
+ return nestedDocument.type === 'doc' && Array.isArray(nestedDocument.content) ? nestedDocument.content : undefined;
79
+ };
80
+ var getNestedContentAnnotationIds = function getNestedContentAnnotationIds(extension) {
81
+ var cachedIds = legacyContentAnnotationIdsByExtension.get(extension);
82
+ if (cachedIds) {
83
+ return cachedIds;
84
+ }
85
+ var nestedNodes = readNestedContentNodes(extension.attrs.parameters);
86
+ if (!nestedNodes) {
87
+ var noIds = Object.freeze([]);
88
+ legacyContentAnnotationIdsByExtension.set(extension, noIds);
89
+ return noIds;
90
+ }
91
+ var annotationIds = new Set();
92
+ var visitedAdfNodes = new WeakSet();
93
+ var pending = _toConsumableArray(nestedNodes).reverse();
94
+ while (pending.length > 0) {
95
+ var _adfNode$attrs;
96
+ var value = pending.pop();
97
+ if (!value || _typeof(value) !== 'object' || visitedAdfNodes.has(value)) {
98
+ continue;
99
+ }
100
+ visitedAdfNodes.add(value);
101
+ var adfNode = value;
102
+ if (Array.isArray(adfNode.marks)) {
103
+ adfNode.marks.forEach(function (rawMark) {
104
+ if (!rawMark || _typeof(rawMark) !== 'object') {
105
+ return;
106
+ }
107
+ var annotationMark = rawMark;
108
+ var attrs = annotationMark.attrs;
109
+ if (annotationMark.type !== 'annotation' || !validateAnnotationAttributes(attrs)) {
110
+ return;
111
+ }
112
+ annotationIds.add(attrs.id);
113
+ });
114
+ }
115
+ if (adfNode.type === 'extension' && ((_adfNode$attrs = adfNode.attrs) === null || _adfNode$attrs === void 0 ? void 0 : _adfNode$attrs.extensionType) === 'com.atlassian.confluence.migration' && adfNode.attrs.extensionKey === 'legacy-content') {
116
+ var _nestedNodes = readNestedContentNodes(adfNode.attrs.parameters);
117
+ if (_nestedNodes) {
118
+ for (var i = _nestedNodes.length - 1; i >= 0; i--) {
119
+ pending.push(_nestedNodes[i]);
120
+ }
121
+ }
122
+ }
123
+ if (Array.isArray(adfNode.content)) {
124
+ for (var _i = adfNode.content.length - 1; _i >= 0; _i--) {
125
+ pending.push(adfNode.content[_i]);
126
+ }
127
+ }
128
+ }
129
+ var ids = Object.freeze(_toConsumableArray(annotationIds));
130
+ legacyContentAnnotationIdsByExtension.set(extension, ids);
131
+ return ids;
132
+ };
133
+
134
+ /** Discover annotation IDs from immutable legacy-content ADF without repeatedly scanning it. */
135
+ export var getAnnotationDiscovery = function getAnnotationDiscovery(doc) {
136
+ var cachedDiscovery = legacyContentAnnotationsByDoc.get(doc);
137
+ if (cachedDiscovery) {
138
+ return cachedDiscovery;
139
+ }
140
+ var allAnnotationIds = new Set();
141
+ var rootAnnotationIds = new Set();
142
+ var nestedAnnotationIds = new Set();
143
+ doc.descendants(function (node) {
144
+ node.marks.filter(function (mark) {
145
+ return mark.type.name === 'annotation';
146
+ }).forEach(function (mark) {
147
+ if (validateAnnotationMark(mark)) {
148
+ rootAnnotationIds.add(mark.attrs.id);
149
+ allAnnotationIds.add(mark.attrs.id);
150
+ }
151
+ });
152
+ if (node.type.name === 'extension' && node.attrs.extensionType === 'com.atlassian.confluence.migration' && node.attrs.extensionKey === 'legacy-content') {
153
+ getNestedContentAnnotationIds(node).forEach(function (id) {
154
+ nestedAnnotationIds.add(id);
155
+ allAnnotationIds.add(id);
156
+ });
157
+ }
158
+ return true;
159
+ });
160
+ var discovery = {
161
+ annotationIds: Object.freeze(_toConsumableArray(allAnnotationIds)),
162
+ rootAnnotationIds: Object.freeze(_toConsumableArray(rootAnnotationIds)),
163
+ nestedAnnotationIds: Object.freeze(_toConsumableArray(nestedAnnotationIds))
164
+ };
165
+ legacyContentAnnotationsByDoc.set(doc, discovery);
166
+ return discovery;
167
+ };
65
168
  export var getAllAnnotations = function getAllAnnotations(doc) {
169
+ if (isExperimentEnabled('platform_editor_fix_lcm_editor_annotations')) {
170
+ return _toConsumableArray(getAnnotationDiscovery(doc).annotationIds);
171
+ }
66
172
  var allAnnotationIds = new Set();
67
173
  doc.descendants(function (node) {
68
174
  node.marks.filter(function (mark) {
@@ -76,29 +182,30 @@ export var getAllAnnotations = function getAllAnnotations(doc) {
76
182
  });
77
183
  return Array.from(allAnnotationIds);
78
184
  };
79
-
80
- /*
81
- * verifies if annotation mark contains valid attributes
82
- */
83
- var validateAnnotationMark = function validateAnnotationMark(annotationMark) {
84
- var _ref = annotationMark.attrs,
85
- id = _ref.id,
86
- annotationType = _ref.annotationType;
87
- return validateAnnotationId(id) && validateAnnotationType(annotationType);
88
- function validateAnnotationId(id) {
89
- if (!id || typeof id !== 'string') {
90
- return false;
91
- }
92
- var invalidIds = ['null', 'undefined'];
93
- return !invalidIds.includes(id.toLowerCase());
185
+ var validateAnnotationId = function validateAnnotationId(id) {
186
+ if (!id || typeof id !== 'string') {
187
+ return false;
94
188
  }
95
- function validateAnnotationType(type) {
96
- if (!type || typeof type !== 'string') {
97
- return false;
98
- }
99
- var allowedTypes = Object.values(AnnotationTypes);
100
- return allowedTypes.includes(type);
189
+ var invalidIds = ['null', 'undefined'];
190
+ return !invalidIds.includes(id.toLowerCase());
191
+ };
192
+ var validateAnnotationType = function validateAnnotationType(type) {
193
+ if (!type || typeof type !== 'string') {
194
+ return false;
195
+ }
196
+ var allowedTypes = Object.values(AnnotationTypes);
197
+ return allowedTypes.some(function (allowedType) {
198
+ return allowedType === type;
199
+ });
200
+ };
201
+ var validateAnnotationAttributes = function validateAnnotationAttributes(attrs) {
202
+ if (!attrs || _typeof(attrs) !== 'object') {
203
+ return false;
101
204
  }
205
+ return 'id' in attrs && 'annotationType' in attrs && validateAnnotationId(attrs.id) && validateAnnotationType(attrs.annotationType);
206
+ };
207
+ var validateAnnotationMark = function validateAnnotationMark(annotationMark) {
208
+ return validateAnnotationAttributes(annotationMark.attrs);
102
209
  };
103
210
  export var decorationKey = {
104
211
  block: 'blockCommentDecoration',
@@ -271,14 +378,26 @@ export var getDraftCommandAnalyticsPayload = function getDraftCommandAnalyticsPa
271
378
  var payload = function payload(state) {
272
379
  var attributes = {};
273
380
  if (drafting) {
381
+ var _currentMediaNodeWith;
382
+ var selection = state.selection;
383
+ var targetNode = selection instanceof NodeSelection ? selection.node : (_currentMediaNodeWith = currentMediaNodeWithPos(state)) === null || _currentMediaNodeWith === void 0 ? void 0 : _currentMediaNodeWith.node;
384
+ var commentTargetType = 'other';
385
+ if ((targetNode === null || targetNode === void 0 ? void 0 : targetNode.type.name) === 'extension') {
386
+ commentTargetType = targetNode.attrs.extensionKey === 'native-embed:maui' ? 'remixEmbed' : 'other';
387
+ } else if (targetNode && ['media', 'mediaSingle', 'mediaInline'].includes(targetNode.type.name)) {
388
+ commentTargetType = 'media';
389
+ } else if (selection instanceof TextSelection || selection instanceof AllSelection) {
390
+ commentTargetType = 'text';
391
+ }
274
392
  attributes = {
275
393
  inputMethod: inputMethod,
276
- overlap: getAnnotationsInSelectionCount(state)
394
+ overlap: getAnnotationsInSelectionCount(state),
395
+ commentTargetType: commentTargetType
277
396
  };
278
397
  }
279
398
  if (fg('editor_inline_comments_on_inline_nodes')) {
280
- var _ref2 = getPluginState(state) || {},
281
- bookmark = _ref2.bookmark;
399
+ var _ref = getPluginState(state) || {},
400
+ bookmark = _ref.bookmark;
282
401
  attributes.inlineNodeNames = getRangeInlineNodeNames({
283
402
  doc: state.doc,
284
403
  pos: resolveDraftBookmark(state, bookmark)
@@ -295,12 +414,12 @@ export var getDraftCommandAnalyticsPayload = function getDraftCommandAnalyticsPa
295
414
  return payload;
296
415
  };
297
416
  export var isSelectionValid = function isSelectionValid(state) {
298
- var _currentMediaNodeWith;
417
+ var _currentMediaNodeWith2;
299
418
  var supportedBlockNodes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
300
419
  var isBlockNodeSupported = arguments.length > 2 ? arguments[2] : undefined;
301
420
  var selection = state.selection;
302
- var _ref3 = getPluginState(state) || {},
303
- disallowOnWhitespace = _ref3.disallowOnWhitespace;
421
+ var _ref2 = getPluginState(state) || {},
422
+ disallowOnWhitespace = _ref2.disallowOnWhitespace;
304
423
  var allowedInlineNodes = ['emoji', 'status', 'date', 'mention', 'inlineCard'];
305
424
  var isSelectionEmpty = selection.empty;
306
425
  var isTextOrAllSelection = selection instanceof TextSelection || selection instanceof AllSelection;
@@ -309,7 +428,7 @@ export var isSelectionValid = function isSelectionValid(state) {
309
428
  var isValidSelection = isTextOrAllSelection || isValidNodeSelection;
310
429
 
311
430
  // Allow media so that it can enter draft mode
312
- if ((_currentMediaNodeWith = currentMediaNodeWithPos(state)) !== null && _currentMediaNodeWith !== void 0 && _currentMediaNodeWith.node) {
431
+ if ((_currentMediaNodeWith2 = currentMediaNodeWithPos(state)) !== null && _currentMediaNodeWith2 !== void 0 && _currentMediaNodeWith2.node) {
313
432
  return AnnotationSelectionType.VALID;
314
433
  }
315
434
  if (isSelectionEmpty || !isValidSelection) {
@@ -11,6 +11,13 @@ import type { InlineCommentPluginState } from './types';
11
11
  * @param $pos Position to center search around
12
12
  */
13
13
  export declare const surroundingMarks: ($pos: ResolvedPos) => (readonly Mark[])[];
14
+ type AnnotationDiscovery = {
15
+ annotationIds: readonly string[];
16
+ nestedAnnotationIds: readonly string[];
17
+ rootAnnotationIds: readonly string[];
18
+ };
19
+ /** Discover annotation IDs from immutable legacy-content ADF without repeatedly scanning it. */
20
+ export declare const getAnnotationDiscovery: (doc: Node) => AnnotationDiscovery;
14
21
  export declare const getAllAnnotations: (doc: Node) => string[];
15
22
  export declare const decorationKey: {
16
23
  block: string;
@@ -52,3 +59,4 @@ export declare function isSelectedAnnotationsChanged(oldSelectedAnnotations: Ann
52
59
  */
53
60
  export declare const isBlockNodeAnnotationsSelected: (selection: Selection, selectedAnnotations?: AnnotationInfo[]) => boolean;
54
61
  export declare const hasAnyUnResolvedAnnotationInPage: (state: EditorState) => boolean;
62
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-annotation",
3
- "version": "29.0.5",
3
+ "version": "29.1.0",
4
4
  "description": "Annotation plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -30,12 +30,13 @@
30
30
  "@atlaskit/editor-toolbar": "^2.8.0",
31
31
  "@atlaskit/editor-toolbar-model": "^1.2.0",
32
32
  "@atlaskit/icon": "^38.0.0",
33
+ "@atlaskit/platform-feature-experiments": "^0.3.0",
33
34
  "@atlaskit/platform-feature-flags": "^2.2.0",
34
35
  "@atlaskit/tmp-editor-statsig": "^246.0.0",
35
36
  "@babel/runtime": "^7.0.0"
36
37
  },
37
38
  "peerDependencies": {
38
- "@atlaskit/editor-common": "^133.6.0",
39
+ "@atlaskit/editor-common": "^133.7.0",
39
40
  "react": "^18.2.0 || ^19.2.0",
40
41
  "react-dom": "^18.2.0 || ^19.2.0",
41
42
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"