@atlaskit/editor-plugin-show-diff 15.0.0 → 15.1.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/afm-cc/tsconfig.json +0 -12
  3. package/afm-products/tsconfig.json +0 -12
  4. package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +131 -79
  5. package/dist/cjs/pm-plugins/decorations/colorSchemes/factory.js +32 -0
  6. package/dist/cjs/pm-plugins/decorations/createBlockChangedDecoration.js +87 -25
  7. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +27 -4
  8. package/dist/cjs/pm-plugins/decorations/createInlineChangedDecoration.js +17 -3
  9. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +5 -3
  10. package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +43 -14
  11. package/dist/cjs/pm-plugins/decorations/revealStyles.js +156 -0
  12. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +25 -5
  13. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -2
  14. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  15. package/dist/cjs/pm-plugins/main.js +28 -10
  16. package/dist/cjs/pm-plugins/resolveDiffContributors.js +4 -0
  17. package/dist/cjs/pm-plugins/revealAnimation.js +524 -0
  18. package/dist/cjs/showDiffPlugin.js +42 -7
  19. package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +59 -9
  20. package/dist/es2019/pm-plugins/decorations/colorSchemes/factory.js +30 -0
  21. package/dist/es2019/pm-plugins/decorations/createBlockChangedDecoration.js +62 -4
  22. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +25 -3
  23. package/dist/es2019/pm-plugins/decorations/createInlineChangedDecoration.js +18 -1
  24. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +5 -3
  25. package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +36 -9
  26. package/dist/es2019/pm-plugins/decorations/revealStyles.js +139 -0
  27. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +25 -6
  28. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +6 -3
  29. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +5 -2
  30. package/dist/es2019/pm-plugins/main.js +28 -9
  31. package/dist/es2019/pm-plugins/resolveDiffContributors.js +4 -0
  32. package/dist/es2019/pm-plugins/revealAnimation.js +495 -0
  33. package/dist/es2019/showDiffPlugin.js +37 -7
  34. package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +131 -79
  35. package/dist/esm/pm-plugins/decorations/colorSchemes/factory.js +30 -0
  36. package/dist/esm/pm-plugins/decorations/createBlockChangedDecoration.js +87 -25
  37. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +27 -4
  38. package/dist/esm/pm-plugins/decorations/createInlineChangedDecoration.js +17 -3
  39. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +5 -3
  40. package/dist/esm/pm-plugins/decorations/extractContributorTags.js +43 -14
  41. package/dist/esm/pm-plugins/decorations/revealStyles.js +149 -0
  42. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +25 -5
  43. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.js +4 -2
  44. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.js +2 -1
  45. package/dist/esm/pm-plugins/main.js +28 -10
  46. package/dist/esm/pm-plugins/resolveDiffContributors.js +4 -0
  47. package/dist/esm/pm-plugins/revealAnimation.js +518 -0
  48. package/dist/esm/showDiffPlugin.js +42 -7
  49. package/dist/types/entry-points/show-diff-plugin-type.d.ts +1 -1
  50. package/dist/types/pm-plugins/calculateDiff/calculateDiffDecorations.d.ts +2 -1
  51. package/dist/types/pm-plugins/decorations/colorSchemes/factory.d.ts +24 -0
  52. package/dist/types/pm-plugins/decorations/createBlockChangedDecoration.d.ts +5 -1
  53. package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +9 -3
  54. package/dist/types/pm-plugins/decorations/createInlineChangedDecoration.d.ts +3 -2
  55. package/dist/types/pm-plugins/decorations/createNodeChangedDecorationWidget.d.ts +3 -2
  56. package/dist/types/pm-plugins/decorations/revealStyles.d.ts +49 -0
  57. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeView.d.ts +4 -3
  58. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.d.ts +1 -1
  59. package/dist/types/pm-plugins/decorations/utils/wrapBlockNodeViewStyles.legacy.d.ts +1 -1
  60. package/dist/types/pm-plugins/main.d.ts +6 -1
  61. package/dist/types/pm-plugins/revealAnimation.d.ts +33 -0
  62. package/dist/types/showDiffPluginType.d.ts +29 -1
  63. package/package.json +10 -4
@@ -0,0 +1,518 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ import { buildWipeableBackground, REVEAL_ATTR, REVEAL_BG_VAR, REVEAL_BORDER_VAR } from './decorations/revealStyles';
3
+
4
+ /**
5
+ * Reveal animation timing. Phase A (0–0.6D): outgoing state fades in, agent highlight wipes out.
6
+ * Phase B (0.6D–D): all highlights wipe in. Uses Web Animations API for element reusability and
7
+ * per-reveal cancellation.
8
+ */
9
+
10
+ /** Total choreography length when the caller does not specify one. */
11
+ export var REVEAL_DEFAULT_DURATION_MS = 950;
12
+
13
+ /**
14
+ * The reflow settles slightly ahead of the highlights. Running it for the full duration makes the
15
+ * movement read as sluggish next to the cross-fade, which is finished well before it.
16
+ */
17
+ var REFLOW_DURATION_FRACTION = 0.8;
18
+
19
+ /** Share of the total spent cross-fading before the highlights come back. */
20
+ var PHASE_A_FRACTION = 0.6;
21
+ var EASING = 'ease-in-out';
22
+
23
+ /** Selector for agent highlights in the outgoing snapshot. */
24
+ var CHANGED_DECORATION_SELECTOR = '[data-testid="show-diff-changed-decoration"]';
25
+
26
+ /**
27
+ * Every marker that identifies a block as changed, in either the outgoing or incoming render.
28
+ * Deleted content is a widget in the incoming state only; reveal markers exist only while revealing.
29
+ */
30
+ var CHANGED_BLOCK_SELECTOR = ['[data-testid="show-diff-changed-decoration"]', '[data-testid="show-diff-deleted-decoration"]', "[".concat(REVEAL_ATTR, "]")].join(', ');
31
+
32
+ /** Elements that re-mount on clone; replace with placeholders to preserve layout. */
33
+ var REMOUNT_UNSAFE_SELECTOR = 'iframe, object, embed, video, audio, canvas';
34
+
35
+ /**
36
+ * Marks animations this module owns, so a re-bind can tell them from anything else on the node.
37
+ * Suffixed per purpose because one element can legitimately carry several — a changed block that
38
+ * also grows takes both the cross-fade and the clip.
39
+ */
40
+ var REVEAL_ID = {
41
+ clock: 'show-diff-reveal-clock',
42
+ fade: 'show-diff-reveal-fade',
43
+ reflow: 'show-diff-reveal-reflow',
44
+ wipe: 'show-diff-reveal-wipe'
45
+ };
46
+ /** `property` distinguishes the clip from the carry, since one block can need both. */
47
+
48
+ var running = new WeakMap();
49
+
50
+ /**
51
+ * Stop any running reveal and drop it from plugin state.
52
+ *
53
+ * Clearing the state is what restores the resting appearance: revealing decorations paint their
54
+ * highlight at zero width, so the repaint that follows is what renders them normally again. Doing
55
+ * it in state rather than by writing styles onto the current elements means decorations rendered
56
+ * later are correct too — an imperative fix only reaches the elements that exist at that instant.
57
+ */
58
+ export var cancelReveal = function cancelReveal(editorView) {
59
+ var _current$overlay, _current$restoreReflo;
60
+ var current = running.get(editorView);
61
+ if (!current) {
62
+ return;
63
+ }
64
+ running.delete(editorView);
65
+ [].concat(_toConsumableArray(current.outgoing), _toConsumableArray(current.incoming)).forEach(function (animation) {
66
+ return animation.cancel();
67
+ });
68
+ (_current$overlay = current.overlay) === null || _current$overlay === void 0 || _current$overlay.remove();
69
+ (_current$restoreReflo = current.restoreReflow) === null || _current$restoreReflo === void 0 || _current$restoreReflo.call(current);
70
+ current.complete();
71
+ };
72
+
73
+ /**
74
+ * Sanitise cloned subtree: remove IDs, contenteditable, re-mount-unsafe elements.
75
+ * Measures embeds from LIVE element (clone is detached, would measure 0x0).
76
+ */
77
+ var sanitiseClone = function sanitiseClone(live, clone) {
78
+ clone.removeAttribute('id');
79
+ clone.querySelectorAll('[id]').forEach(function (element) {
80
+ return element.removeAttribute('id');
81
+ });
82
+ clone.removeAttribute('contenteditable');
83
+ clone.querySelectorAll('[contenteditable]').forEach(function (element) {
84
+ element.removeAttribute('contenteditable');
85
+ });
86
+ // Revealing decorations paint at zero width; snapshot must show full width so clear markers here.
87
+ clone.querySelectorAll("[".concat(REVEAL_ATTR, "]")).forEach(function (element) {
88
+ element.removeAttribute(REVEAL_ATTR);
89
+ element.style.removeProperty('background-size');
90
+ });
91
+ var liveEmbeds = live.querySelectorAll(REMOUNT_UNSAFE_SELECTOR);
92
+ clone.querySelectorAll(REMOUNT_UNSAFE_SELECTOR).forEach(function (element, index) {
93
+ var placeholder = document.createElement('div');
94
+ var source = liveEmbeds[index];
95
+ if (source) {
96
+ var _source$getBoundingCl = source.getBoundingClientRect(),
97
+ width = _source$getBoundingCl.width,
98
+ height = _source$getBoundingCl.height;
99
+ placeholder.style.width = "".concat(width, "px");
100
+ placeholder.style.height = "".concat(height, "px");
101
+ }
102
+ element.replaceWith(placeholder);
103
+ });
104
+ };
105
+
106
+ /**
107
+ * Convert flat highlights (from non-revealing render) to wipeable gradients.
108
+ * Reads colour from LIVE element (clone is detached, no computed style).
109
+ */
110
+ var makeOutgoingHighlightsWipeable = function makeOutgoingHighlightsWipeable(liveBlock, clonedBlock) {
111
+ var live = liveBlock.querySelectorAll(CHANGED_DECORATION_SELECTOR);
112
+ var cloned = clonedBlock.querySelectorAll(CHANGED_DECORATION_SELECTOR);
113
+ var wipeable = [];
114
+ cloned.forEach(function (clone, index) {
115
+ var _clone$getAttribute;
116
+ var source = live[index];
117
+ if (!source) {
118
+ return;
119
+ }
120
+ // Read colour from custom property (revealing render) or computed style (flat render).
121
+ var color = source.style.getPropertyValue(REVEAL_BG_VAR).trim() || window.getComputedStyle(source).backgroundColor;
122
+ if (!color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)') {
123
+ return;
124
+ }
125
+ clone.setAttribute('style', "".concat((_clone$getAttribute = clone.getAttribute('style')) !== null && _clone$getAttribute !== void 0 ? _clone$getAttribute : '').concat(buildWipeableBackground(color)));
126
+ // Anchor right: shrinking retracts the highlight through the right edge.
127
+ clone.style.backgroundSize = '100% 100%';
128
+ clone.style.backgroundPosition = '100% 0';
129
+ wipeable.push(clone);
130
+ });
131
+ return wipeable;
132
+ };
133
+
134
+ /**
135
+ * The contiguous run of top-level blocks containing a change.
136
+ *
137
+ * Only these cross-fade. Fading the whole content area would also fade blocks that did not change
138
+ * and have merely been pushed up or down by the reflow, which reads as the entire document
139
+ * flickering.
140
+ *
141
+ * Exact rather than contiguous: an agent can touch two paragraphs either side of untouched ones,
142
+ * and a first-to-last range would sweep up everything between them. The index travels with each
143
+ * block so its outgoing height can be paired with its incoming height across the swap.
144
+ */
145
+ var findChangedBlocks = function findChangedBlocks(content) {
146
+ return Array.from(content.children).filter(function (child) {
147
+ return child instanceof HTMLElement;
148
+ }).map(function (block, index) {
149
+ return {
150
+ block: block,
151
+ index: index
152
+ };
153
+ }).filter(function (_ref) {
154
+ var block = _ref.block;
155
+ return block.matches(CHANGED_BLOCK_SELECTOR) || block.querySelector(CHANGED_BLOCK_SELECTOR);
156
+ });
157
+ };
158
+
159
+ /** Outgoing height of every changed block, keyed by its position among the content's children. */
160
+ var measureBlocks = function measureBlocks(changed) {
161
+ return new Map(changed.map(function (_ref2) {
162
+ var block = _ref2.block,
163
+ index = _ref2.index;
164
+ return [index, block.getBoundingClientRect().height];
165
+ }));
166
+ };
167
+
168
+ /**
169
+ * Build and position a snapshot of the outgoing state over the changed blocks.
170
+ *
171
+ * The wrapper is a shallow clone of the ProseMirror element so the snapshot keeps the class-based
172
+ * typography the real content has; a plain div would render the text differently. Inserted as a
173
+ * sibling rather than a child so ProseMirror does not reconcile it away.
174
+ */
175
+ var buildOverlay = function buildOverlay(content, changed) {
176
+ var host = content.parentElement;
177
+ if (!host || !content.offsetParent || changed.length === 0) {
178
+ return undefined;
179
+ }
180
+ var wrapper = content.cloneNode(false);
181
+ if (!(wrapper instanceof HTMLElement)) {
182
+ return undefined;
183
+ }
184
+ var contentRect = content.getBoundingClientRect();
185
+ changed.forEach(function (_ref3) {
186
+ var block = _ref3.block;
187
+ var clone = block.cloneNode(true);
188
+ if (!(clone instanceof HTMLElement)) {
189
+ return;
190
+ }
191
+ sanitiseClone(block, clone);
192
+ makeOutgoingHighlightsWipeable(block, clone);
193
+
194
+ // Each clone is placed at its own offset. Stacking them in flow would close the gaps left by
195
+ // the unchanged blocks that were not copied, so a later block would sit too high.
196
+ var rect = block.getBoundingClientRect();
197
+ clone.style.position = 'absolute';
198
+ clone.style.top = "".concat(rect.top - contentRect.top, "px");
199
+ clone.style.left = "".concat(rect.left - contentRect.left, "px");
200
+ clone.style.width = "".concat(rect.width, "px");
201
+ clone.style.margin = '0';
202
+ wrapper.appendChild(clone);
203
+ });
204
+ wrapper.removeAttribute('id');
205
+ wrapper.removeAttribute('contenteditable');
206
+ // aria-hidden + inert removes overlay from focus, hit-testing and AT (aria-hidden-focus safe).
207
+ wrapper.setAttribute('aria-hidden', 'true');
208
+ wrapper.setAttribute('inert', '');
209
+ wrapper.style.position = 'absolute';
210
+ // Sits on the content box, with the clones positioned relative to it.
211
+ wrapper.style.top = "".concat(content.offsetTop, "px");
212
+ wrapper.style.left = "".concat(content.offsetLeft, "px");
213
+ wrapper.style.width = "".concat(content.offsetWidth, "px");
214
+ // The wrapper inherits the editor's own padding and margin, which would offset the copied blocks
215
+ // a second time on top of the position already measured from them.
216
+ wrapper.style.margin = '0';
217
+ wrapper.style.padding = '0';
218
+ wrapper.style.boxSizing = 'border-box';
219
+ wrapper.style.pointerEvents = 'none';
220
+ wrapper.style.zIndex = '1';
221
+ host.insertBefore(wrapper, content);
222
+ return wrapper;
223
+ };
224
+
225
+ /**
226
+ * Slide the content below the change from where it used to sit to where it now sits.
227
+ *
228
+ * The incoming blocks are already at their final height, so without this everything below them
229
+ * jumps the instant the diff is applied. `margin-bottom` carries the following content; when the
230
+ * change has grown, the last block is also clipped back to its old height and released, so the
231
+ * content below is not overlapped while it catches up.
232
+ */
233
+ var animateReflow = function animateReflow(changed, outgoingHeights, duration) {
234
+ var _changed$;
235
+ var specs = [];
236
+ var restores = [];
237
+ var deltas = new Map();
238
+ changed.forEach(function (_ref4) {
239
+ var block = _ref4.block,
240
+ index = _ref4.index;
241
+ var before = outgoingHeights.get(index);
242
+ if (before === undefined) {
243
+ return;
244
+ }
245
+ var delta = before - block.getBoundingClientRect().height;
246
+ if (Math.abs(delta) < 1) {
247
+ return;
248
+ }
249
+ deltas.set(index, delta);
250
+
251
+ // Growth only: clip the block back to its old height and open it up. Without this the taller
252
+ // new content would overlap the content below, which has not caught up yet.
253
+ var growth = Math.max(0, -delta);
254
+ if (growth === 0) {
255
+ return;
256
+ }
257
+ var previousOverflow = block.style.overflow;
258
+ block.style.overflow = 'hidden';
259
+ restores.push(function () {
260
+ block.style.overflow = previousOverflow;
261
+ });
262
+ specs.push({
263
+ index: index,
264
+ keyframes: [{
265
+ clipPath: "inset(0 0 ".concat(growth, "px 0)")
266
+ }, {
267
+ clipPath: 'inset(0 0 0 0)'
268
+ }],
269
+ property: 'clip'
270
+ });
271
+ });
272
+ var parent = (_changed$ = changed[0]) === null || _changed$ === void 0 ? void 0 : _changed$.block.parentElement;
273
+ if (deltas.size === 0 || !parent) {
274
+ return {
275
+ restore: function restore() {
276
+ return restores.forEach(function (restore) {
277
+ return restore();
278
+ });
279
+ },
280
+ specs: specs
281
+ };
282
+ }
283
+
284
+ // Everything below a change is carried with `transform`, never `margin`. Margin is a layout
285
+ // property, so each frame would re-lay-out the content below at a fractional offset and
286
+ // re-rasterise its text — that reads as shimmer even at a locked 60fps. Transforms run on the
287
+ // compositor: the glyphs are rasterised once and moved.
288
+ var carried = 0;
289
+ Array.from(parent.children).forEach(function (child, index) {
290
+ var _deltas$get;
291
+ if (child instanceof HTMLElement && carried !== 0) {
292
+ specs.push({
293
+ index: index,
294
+ keyframes: [{
295
+ transform: "translateY(".concat(carried, "px)")
296
+ }, {
297
+ transform: 'translateY(0px)'
298
+ }],
299
+ property: 'carry'
300
+ });
301
+ }
302
+ // Applied after the block itself: a change moves the content below it, not its own top edge.
303
+ carried += (_deltas$get = deltas.get(index)) !== null && _deltas$get !== void 0 ? _deltas$get : 0;
304
+ });
305
+ return {
306
+ restore: function restore() {
307
+ return restores.forEach(function (restore) {
308
+ return restore();
309
+ });
310
+ },
311
+ specs: specs
312
+ };
313
+ };
314
+
315
+ /** Animate incoming highlights; re-run if diff repaints mid-reveal. */
316
+ /**
317
+ * Creates an animation unless the element already carries one of ours, and resumes it at `elapsed`.
318
+ *
319
+ * ProseMirror rebuilds inline decorations on any repaint, which destroys animations bound to them.
320
+ * Widget DOM is reused and so survives, which is why deleted highlights used to animate while added
321
+ * ones snapped in. Re-binding at the elapsed time keeps a re-rendered element in step rather than
322
+ * restarting it from the beginning.
323
+ */
324
+ var bind = function bind(entry, element, id, keyframes, options, elapsed) {
325
+ if (element.getAnimations().some(function (animation) {
326
+ return animation.id === id;
327
+ })) {
328
+ return;
329
+ }
330
+ var animation = element.animate(keyframes, options);
331
+ animation.id = id;
332
+ if (elapsed > 0) {
333
+ animation.currentTime = Math.min(elapsed, options.duration);
334
+ }
335
+ entry.incoming.push(animation);
336
+ };
337
+ var highlightKeyframes = function highlightKeyframes(element) {
338
+ var border = element.style.getPropertyValue(REVEAL_BORDER_VAR).trim();
339
+ return [{
340
+ backgroundSize: '0% 100%',
341
+ borderBottomColor: 'transparent',
342
+ offset: 0
343
+ }, {
344
+ backgroundSize: '0% 100%',
345
+ borderBottomColor: 'transparent',
346
+ easing: EASING,
347
+ offset: PHASE_A_FRACTION
348
+ }, {
349
+ backgroundSize: '100% 100%',
350
+ borderBottomColor: border,
351
+ offset: 1
352
+ }];
353
+ };
354
+
355
+ /** (Re)binds every animation that runs on live, ProseMirror-managed DOM. */
356
+ var applyIncoming = function applyIncoming(entry, content, total, phaseA) {
357
+ var elapsed = Math.max(0, performance.now() - entry.startedAt);
358
+ var changedBlocks = findChangedBlocks(content);
359
+
360
+ // Only the changed blocks cross-fade. Everything else is unchanged content that has merely
361
+ // moved, and fading it would read as the whole document flickering.
362
+ if (entry.overlay) {
363
+ var fading = changedBlocks.length > 0 ? changedBlocks.map(function (_ref5) {
364
+ var block = _ref5.block;
365
+ return block;
366
+ }) : [content];
367
+ fading.forEach(function (block) {
368
+ bind(entry, block, REVEAL_ID.fade, [{
369
+ opacity: 0
370
+ }, {
371
+ opacity: 1
372
+ }], {
373
+ duration: phaseA,
374
+ easing: EASING,
375
+ fill: 'forwards'
376
+ }, elapsed);
377
+ });
378
+ }
379
+ content.querySelectorAll("[".concat(REVEAL_ATTR, "]")).forEach(function (element) {
380
+ bind(entry, element, REVEAL_ID.wipe, highlightKeyframes(element), {
381
+ duration: total,
382
+ fill: 'forwards'
383
+ }, elapsed);
384
+ });
385
+ entry.reflowSpecs.forEach(function (_ref6) {
386
+ var index = _ref6.index,
387
+ keyframes = _ref6.keyframes,
388
+ property = _ref6.property;
389
+ var child = content.children[index];
390
+ if (child instanceof HTMLElement) {
391
+ bind(entry, child, "".concat(REVEAL_ID.reflow, "-").concat(property), keyframes, {
392
+ duration: entry.reflowDuration,
393
+ easing: EASING,
394
+ fill: 'backwards'
395
+ }, elapsed);
396
+ }
397
+ });
398
+ };
399
+
400
+ /**
401
+ * Re-binds the reveal to the current decorations. Called on every repaint while a reveal is in
402
+ * flight, because a repaint silently destroys any animation attached to re-rendered DOM.
403
+ */
404
+ export var rebindReveal = function rebindReveal(editorView, content) {
405
+ var entry = running.get(editorView);
406
+ if (entry) {
407
+ applyIncoming(entry, content, entry.total, entry.phaseA);
408
+ }
409
+ };
410
+ var scheduleIncoming = function scheduleIncoming(editorView, entry, content, total, phaseA) {
411
+ requestAnimationFrame(function () {
412
+ if (running.get(editorView) !== entry) {
413
+ return;
414
+ }
415
+ entry.startedAt = performance.now();
416
+ var reflow = animateReflow(findChangedBlocks(content), entry.outgoingHeights, entry.reflowDuration);
417
+ entry.reflowSpecs = reflow.specs;
418
+ entry.restoreReflow = reflow.restore;
419
+ applyIncoming(entry, content, total, phaseA);
420
+
421
+ // The clock runs on the content root, which ProseMirror never replaces. Hanging completion
422
+ // off a decoration animation would strand the reveal whenever that decoration was rebuilt.
423
+ var clock = content.animate([{
424
+ opacity: 1
425
+ }, {
426
+ opacity: 1
427
+ }], {
428
+ duration: total
429
+ });
430
+ clock.id = REVEAL_ID.clock;
431
+ clock.onfinish = function () {
432
+ if (running.get(editorView) === entry) {
433
+ cancelReveal(editorView);
434
+ }
435
+ };
436
+ entry.incoming.push(clock);
437
+ });
438
+ };
439
+
440
+ /**
441
+ * Start the reveal. Must be called BEFORE transaction dispatch (last moment outgoing DOM exists
442
+ * for capture). Incoming half animates a frame later, post-decoration-render.
443
+ */
444
+ export var beginReveal = function beginReveal(_ref7) {
445
+ var _reveal$durationMs;
446
+ var editorView = _ref7.editorView,
447
+ onComplete = _ref7.onComplete,
448
+ reveal = _ref7.reveal;
449
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
450
+ return;
451
+ }
452
+ var content = editorView.dom;
453
+ if (!(content instanceof HTMLElement)) {
454
+ return;
455
+ }
456
+ var total = (_reveal$durationMs = reveal.durationMs) !== null && _reveal$durationMs !== void 0 ? _reveal$durationMs : REVEAL_DEFAULT_DURATION_MS;
457
+ var phaseA = Math.round(total * PHASE_A_FRACTION);
458
+
459
+ // Repaint mid-reveal: reuse snapshot (not retake), rebind incoming animations to fresh decorations.
460
+ var existing = running.get(editorView);
461
+ if (existing) {
462
+ existing.incoming.forEach(function (animation) {
463
+ return animation.cancel();
464
+ });
465
+ existing.incoming = [];
466
+ scheduleIncoming(editorView, existing, content, total, phaseA);
467
+ return;
468
+ }
469
+
470
+ // Captured before dispatch: this is the last moment the outgoing layout can be measured.
471
+ var outgoingBlocks = findChangedBlocks(content);
472
+ var overlay = buildOverlay(content, outgoingBlocks);
473
+ var outgoing = [];
474
+ if (overlay) {
475
+ // fill: 'forwards' is load-bearing: default fill: 'none' snaps snapshot to opaque (reads as swap).
476
+ var fadeOut = overlay.animate([{
477
+ opacity: 1
478
+ }, {
479
+ opacity: 0
480
+ }], {
481
+ duration: phaseA,
482
+ easing: EASING,
483
+ fill: 'forwards'
484
+ });
485
+ fadeOut.onfinish = function () {
486
+ overlay.remove();
487
+ };
488
+ outgoing.push(fadeOut);
489
+ overlay.querySelectorAll(CHANGED_DECORATION_SELECTOR).forEach(function (element) {
490
+ if (!element.style.backgroundImage) {
491
+ return;
492
+ }
493
+ outgoing.push(element.animate([{
494
+ backgroundSize: '100% 100%'
495
+ }, {
496
+ backgroundSize: '0% 100%'
497
+ }], {
498
+ duration: phaseA,
499
+ easing: EASING,
500
+ fill: 'forwards'
501
+ }));
502
+ });
503
+ }
504
+ var entry = {
505
+ complete: onComplete,
506
+ incoming: [],
507
+ outgoing: outgoing,
508
+ outgoingHeights: measureBlocks(outgoingBlocks),
509
+ phaseA: phaseA,
510
+ reflowDuration: Math.round(total * REFLOW_DURATION_FRACTION),
511
+ reflowSpecs: [],
512
+ startedAt: performance.now(),
513
+ total: total,
514
+ overlay: overlay
515
+ };
516
+ running.set(editorView, entry);
517
+ scheduleIncoming(editorView, entry, content, total, phaseA);
518
+ };
@@ -4,16 +4,18 @@ var _excluded = ["stepsWithAttribution", "contributorProfiles"];
4
4
  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; }
5
5
  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; }
6
6
  import React from 'react';
7
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
7
8
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
8
9
  import { areAttributionColorGatesEnabled, resolveContributors } from './pm-plugins/decorations/colorSchemes/attributions';
9
10
  import { getDeletedWidgets as _getDeletedWidgets } from './pm-plugins/getDeletedWidgets';
10
11
  import { getScrollableDecorations } from './pm-plugins/getScrollableDecorations';
11
12
  import { createPlugin, showDiffPluginKey } from './pm-plugins/main';
12
13
  import { resolveDiffContributors } from './pm-plugins/resolveDiffContributors';
14
+ import { beginReveal, cancelReveal } from './pm-plugins/revealAnimation';
13
15
  import { IndicatorBarContentComponent } from './ui/IndicatorBar/IndicatorBarContentComponent';
14
16
  var normalizeShowDiffParams = function normalizeShowDiffParams(params) {
15
17
  if (!params || !('stepsWithAttribution' in params)) {
16
- // Cleared so a previous attributed diff's contributors cannot leak onto an unattributed one.
18
+ // Clear contributors to prevent leaking from attributed to unattributed diffs.
17
19
  return params ? _objectSpread(_objectSpread({}, params), {}, {
18
20
  stepAttributions: [],
19
21
  contributors: undefined
@@ -23,8 +25,7 @@ var normalizeShowDiffParams = function normalizeShowDiffParams(params) {
23
25
  contributorProfiles = params.contributorProfiles,
24
26
  rest = _objectWithoutProperties(params, _excluded);
25
27
  return _objectSpread(_objectSpread({}, rest), {}, {
26
- // Resolved and keyed at the public boundary so both the contributor model and the attribution-key
27
- // format stay internal.
28
+ // Resolve at boundary to keep contributor model and attribution format internal.
28
29
  contributors: areAttributionColorGatesEnabled() ? resolveContributors(resolveDiffContributors(stepsWithAttribution, contributorProfiles)) : undefined,
29
30
  steps: stepsWithAttribution.map(function (_ref) {
30
31
  var step = _ref.step;
@@ -36,11 +37,25 @@ var normalizeShowDiffParams = function normalizeShowDiffParams(params) {
36
37
  })
37
38
  });
38
39
  };
40
+ var prefersReducedMotion = function prefersReducedMotion() {
41
+ return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
42
+ };
43
+
44
+ /**
45
+ * Resolve reveal preconditions once here so decorations and animation share one answer. If
46
+ * decorations think a reveal is happening but animation doesn't, highlight paints hidden forever.
47
+ * All preconditions resolved here (not just gate) ensures "unable to animate" means "do not reveal".
48
+ */
49
+ var resolveReveal = function resolveReveal(reveal, editorView) {
50
+ return (
51
+ // Gate first, so it is the primary switch and is always observable once a reveal is requested.
52
+ reveal && fg('platform_editor_diff_reveal_animation') && editorView && typeof window !== 'undefined' && !prefersReducedMotion() ? reveal : undefined
53
+ );
54
+ };
39
55
  export var showDiffPlugin = function showDiffPlugin(_ref3) {
40
56
  var api = _ref3.api,
41
57
  config = _ref3.config;
42
- // Captured from the plugin's view lifecycle (see `createPlugin`) so read-only actions can access
43
- // the current state without exposing the plugin key to consumers.
58
+ // Captured from view lifecycle so read-only actions access state without exposing plugin key.
44
59
  var editorView;
45
60
  var setEditorView = function setEditorView(view) {
46
61
  editorView = view;
@@ -62,13 +77,34 @@ export var showDiffPlugin = function showDiffPlugin(_ref3) {
62
77
  tr: tr
63
78
  });
64
79
  }
80
+ var reveal = resolveReveal(params === null || params === void 0 ? void 0 : params.reveal, editorView);
81
+
82
+ // Capture snapshot BEFORE transaction dispatch (last moment outgoing DOM exists).
83
+ if (reveal && editorView) {
84
+ var view = editorView;
85
+ beginReveal({
86
+ editorView: view,
87
+ reveal: reveal,
88
+ onComplete: function onComplete() {
89
+ // Repaint without the reveal so the decorations settle into their resting
90
+ // style. Kept out of history: this is presentation, not an edit.
91
+ view.dispatch(view.state.tr.setMeta(showDiffPluginKey, {
92
+ action: 'REVEAL_COMPLETE'
93
+ }).setMeta('addToHistory', false));
94
+ }
95
+ });
96
+ }
65
97
  return tr.setMeta(showDiffPluginKey, _objectSpread(_objectSpread({}, normalizeShowDiffParams(params)), {}, {
98
+ reveal: reveal,
66
99
  action: 'SHOW_DIFF'
67
100
  }));
68
101
  };
69
102
  },
70
103
  hideDiff: function hideDiff(_ref5) {
71
104
  var tr = _ref5.tr;
105
+ if (editorView) {
106
+ cancelReveal(editorView);
107
+ }
72
108
  if (expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
73
109
  var _api$userIntent2;
74
110
  api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 || _api$userIntent2.commands.setCurrentUserIntent('default')({
@@ -110,8 +146,7 @@ export var showDiffPlugin = function showDiffPlugin(_ref3) {
110
146
  if (!expValEquals('platform_editor_diff_plugin_extended', 'isEnabled', true)) {
111
147
  return null;
112
148
  }
113
- // Rendered here so it shares the DOM context of the decoration anchor spans. Contributor
114
- // tags are not: each one mounts into the host element its own decoration draws.
149
+ // Rendered here sharing decoration anchor span DOM context (tags mount separately).
115
150
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(IndicatorBarContentComponent, {
116
151
  api: api
117
152
  }));
@@ -1 +1 @@
1
- export type { DeletedDiffPlacement, DeletedDiffWidget, DiffContributorProfile, DiffParams, DiffStepAttribution, DiffType, PMDiffParams, ShowDiffParams, ShowDiffPlugin, SmartDiffThresholds, StepWithAttribution, } from '../showDiffPluginType';
1
+ export type { DeletedDiffPlacement, DeletedDiffWidget, DiffContributorProfile, DiffParams, DiffStepAttribution, DiffType, PMDiffParams, RevealMode, RevealOptions, ShowDiffParams, ShowDiffPlugin, SmartDiffThresholds, StepWithAttribution, } from '../showDiffPluginType';
@@ -4,7 +4,7 @@ import type { IntlShape } from 'react-intl';
4
4
  import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
5
5
  import type { EditorState } from '@atlaskit/editor-prosemirror/state';
6
6
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
7
- import type { ColorScheme, ContributorTagModel, DeletedDiffPlacement, DiffDescriptor, DiffType, InlineDeletedDiffPlacement, ShowDiffPlugin, SmartDiffThresholds } from '../../showDiffPluginType';
7
+ import type { ColorScheme, ContributorTagModel, DeletedDiffPlacement, DiffDescriptor, DiffType, InlineDeletedDiffPlacement, RevealOptions, ShowDiffPlugin, SmartDiffThresholds } from '../../showDiffPluginType';
8
8
  import type { ContributorTagMountContext } from '../decorations/createContributorTagWidget';
9
9
  import type { ShowDiffPluginState } from '../main';
10
10
  import type { NodeViewSerializer } from '../NodeViewSerializer';
@@ -46,6 +46,7 @@ export declare const calculateDiffDecorations: MemoizedFn<({ state, pluginState,
46
46
  isInverted?: boolean;
47
47
  nodeViewSerializer: NodeViewSerializer;
48
48
  pluginState: Omit<ShowDiffPluginState, 'decorations'>;
49
+ reveal?: RevealOptions;
49
50
  showIndicators?: boolean;
50
51
  smartThresholds?: Partial<SmartDiffThresholds>;
51
52
  state: EditorState;
@@ -112,6 +112,30 @@ export declare function buildDeletedCellOverlayRoundedStyle(colors: DiffColorSch
112
112
  * `text-decoration` vs a `border-bottom` rule; only the latter honours `hideAddedDiffsUnderline`.
113
113
  */
114
114
  export declare function buildInsertedInlineStyle(colors: DiffColorScheme, isActive: boolean, hideAddedDiffsUnderline: boolean): string;
115
+ /**
116
+ * The colours `buildInsertedInlineStyle()` above would paint, for the reveal to animate towards.
117
+ *
118
+ * MUST mirror that function: the reveal replaces the flat highlight with a sized gradient, so a
119
+ * divergence here means the animation settles on a different colour than a static render produces.
120
+ * `revealStyles.ts` in the test package asserts the two agree across the isActive /
121
+ * hideAddedDiffsUnderline matrix.
122
+ *
123
+ * A transparent border is how "no underline" is expressed: the box still reserves the 2px so the
124
+ * text does not shift when the underline arrives.
125
+ */
126
+ export declare function getInsertedInlineRevealColors(colors: DiffColorScheme, isActive: boolean, hideAddedDiffsUnderline: boolean): {
127
+ background: string;
128
+ border: string;
129
+ };
130
+ /**
131
+ * The colours `buildDeletedInlineContentStyleExtended()` paints, for the reveal to animate towards.
132
+ * Deleted content's highlight does not vary with the active state — only its text does, which the
133
+ * reveal keeps as-is.
134
+ */
135
+ export declare function getDeletedInlineRevealColors(colors: DiffColorScheme): {
136
+ background: string;
137
+ border: string;
138
+ };
115
139
  /** The three visual states deleted inline content can be in. */
116
140
  export type DeletedInlineState = 'active' | 'default' | 'new';
117
141
  /**