@atlaskit/editor-plugin-show-diff 2.1.3 → 3.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atlaskit/editor-plugin-show-diff
2
2
 
3
+ ## 3.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`16d89ac68ca47`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/16d89ac68ca47) -
8
+ Improve how large number of small steps are grouped together in the diff.
9
+ - Updated dependencies
10
+
11
+ ## 3.0.0
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+
3
17
  ## 2.1.3
4
18
 
5
19
  ### Patch Changes
@@ -1,30 +1,109 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
7
  exports.calculateDiffDecorations = void 0;
8
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+ var _isEqual = _interopRequireDefault(require("lodash/isEqual"));
12
+ var _memoizeOne = _interopRequireDefault(require("memoize-one"));
7
13
  var _prosemirrorChangeset = require("prosemirror-changeset");
14
+ var _steps = require("@atlaskit/adf-schema/steps");
8
15
  var _document = require("@atlaskit/editor-common/utils/document");
16
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
9
17
  var _view = require("@atlaskit/editor-prosemirror/view");
10
18
  var _decorations = require("./decorations");
11
19
  var _markDecorations = require("./markDecorations");
12
20
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
13
21
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
14
- function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } // eslint-disable-next-line @atlassian/tangerine/import/entry-points
15
- var calculateDiffDecorations = exports.calculateDiffDecorations = function calculateDiffDecorations(_ref) {
22
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
23
+ 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; }
24
+ 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; } // eslint-disable-next-line @atlassian/tangerine/import/entry-points
25
+ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration(doc, from, to, colourScheme) {
26
+ var decorations = [];
27
+ // Iterate over the document nodes within the range
28
+ doc.nodesBetween(from, to, function (node, pos) {
29
+ if (node.isBlock) {
30
+ decorations.push((0, _decorations.createBlockChangedDecoration)({
31
+ from: pos,
32
+ to: pos + node.nodeSize,
33
+ name: node.type.name
34
+ }, colourScheme));
35
+ }
36
+ });
37
+ return decorations;
38
+ };
39
+
40
+ /**
41
+ * Groups adjacent changes to reduce visual fragmentation in diffs.
42
+ * Merges consecutive insertions and deletions that are close together.
43
+ */
44
+ function optimizeChanges(changes) {
45
+ if (changes.length <= 1) {
46
+ return changes;
47
+ }
48
+ var optimized = [];
49
+ var current = _objectSpread({}, changes[0]);
50
+ for (var i = 1; i < changes.length; i++) {
51
+ var next = changes[i];
52
+
53
+ // Check if changes are adjacent or very close (within 2 positions)
54
+ var isAdjacent = next.fromB <= current.toB + 2;
55
+ if (isAdjacent) {
56
+ current = {
57
+ fromA: current.fromA,
58
+ toA: Math.max(current.toA, next.toA),
59
+ fromB: current.fromB,
60
+ toB: Math.max(current.toB, next.toB),
61
+ deleted: [].concat((0, _toConsumableArray2.default)(current.deleted), (0, _toConsumableArray2.default)(next.deleted)),
62
+ inserted: [].concat((0, _toConsumableArray2.default)(current.inserted), (0, _toConsumableArray2.default)(next.inserted))
63
+ };
64
+ } else {
65
+ optimized.push(current);
66
+ current = _objectSpread({}, next);
67
+ }
68
+ }
69
+ optimized.push(current);
70
+ return optimized;
71
+ }
72
+
73
+ // Simplifies the steps to improve performance and reduce fragmentation in diffs
74
+ function simplifySteps(steps) {
75
+ return steps
76
+ // Remove steps that don't affect document structure or content
77
+ .filter(function (step) {
78
+ return !(step instanceof _steps.AnalyticsStep || step instanceof _transform.AttrStep || step instanceof _steps.SetAttrsStep);
79
+ })
80
+ // Merge consecutive steps where possible
81
+ .reduce(function (acc, step) {
82
+ var _lastStep$merge;
83
+ var lastStep = acc[acc.length - 1];
84
+ var merged = lastStep === null || lastStep === void 0 || (_lastStep$merge = lastStep.merge) === null || _lastStep$merge === void 0 ? void 0 : _lastStep$merge.call(lastStep, step);
85
+ if (merged) {
86
+ acc[acc.length - 1] = merged;
87
+ } else {
88
+ acc.push(step);
89
+ }
90
+ return acc;
91
+ }, []);
92
+ }
93
+ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref) {
16
94
  var state = _ref.state,
17
95
  pluginState = _ref.pluginState,
18
96
  nodeViewSerializer = _ref.nodeViewSerializer,
19
97
  colourScheme = _ref.colourScheme;
20
98
  var originalDoc = pluginState.originalDoc,
21
- steps = pluginState.steps;
99
+ rawSteps = pluginState.steps;
100
+ var steps = simplifySteps(rawSteps);
22
101
  if (!originalDoc || !pluginState.isDisplayingChanges) {
23
102
  return _view.DecorationSet.empty;
24
103
  }
25
104
  var tr = state.tr;
26
105
  var steppedDoc = originalDoc;
27
- var changeset = _prosemirrorChangeset.ChangeSet.create(originalDoc);
106
+ var stepMaps = [];
28
107
  var _iterator = _createForOfIteratorHelper(steps),
29
108
  _step;
30
109
  try {
@@ -33,9 +112,10 @@ var calculateDiffDecorations = exports.calculateDiffDecorations = function calcu
33
112
  var result = step.apply(steppedDoc);
34
113
  if (result.failed === null && result.doc) {
35
114
  steppedDoc = result.doc;
36
- changeset = changeset.addSteps(steppedDoc, [step.getMap()], tr.doc);
115
+ stepMaps.push(step.getMap());
37
116
  }
38
117
  }
118
+
39
119
  // Rather than using .eq() we use a custom function that only checks for structural
40
120
  // changes and ignores differences in attributes which don't affect decoration positions
41
121
  } catch (err) {
@@ -46,11 +126,14 @@ var calculateDiffDecorations = exports.calculateDiffDecorations = function calcu
46
126
  if (!(0, _document.areNodesEqualIgnoreAttrs)(steppedDoc, tr.doc)) {
47
127
  return _view.DecorationSet.empty;
48
128
  }
129
+ var changeset = _prosemirrorChangeset.ChangeSet.create(originalDoc).addSteps(steppedDoc, stepMaps, tr.doc);
49
130
  var changes = (0, _prosemirrorChangeset.simplifyChanges)(changeset.changes, tr.doc);
131
+ var optimizedChanges = optimizeChanges(changes);
50
132
  var decorations = [];
51
- changes.forEach(function (change) {
133
+ optimizedChanges.forEach(function (change) {
52
134
  if (change.inserted.length > 0) {
53
135
  decorations.push((0, _decorations.createInlineChangedDecoration)(change, colourScheme));
136
+ decorations.push.apply(decorations, (0, _toConsumableArray2.default)(calculateNodesForBlockDecoration(tr.doc, change.fromB, change.toB, colourScheme)));
54
137
  }
55
138
  if (change.deleted.length > 0) {
56
139
  var decoration = (0, _decorations.createDeletedContentDecoration)({
@@ -68,4 +151,21 @@ var calculateDiffDecorations = exports.calculateDiffDecorations = function calcu
68
151
  decorations.push((0, _decorations.createInlineChangedDecoration)(change, colourScheme));
69
152
  });
70
153
  return _view.DecorationSet.empty.add(tr.doc, decorations);
71
- };
154
+ };
155
+ var calculateDiffDecorations = exports.calculateDiffDecorations = (0, _memoizeOne.default)(calculateDiffDecorationsInner,
156
+ // Cache results unless relevant inputs change
157
+ function (_ref2, _ref3) {
158
+ var _ref6;
159
+ var _ref4 = (0, _slicedToArray2.default)(_ref2, 1),
160
+ _ref4$ = _ref4[0],
161
+ pluginState = _ref4$.pluginState,
162
+ state = _ref4$.state,
163
+ colourScheme = _ref4$.colourScheme;
164
+ var _ref5 = (0, _slicedToArray2.default)(_ref3, 1),
165
+ _ref5$ = _ref5[0],
166
+ lastPluginState = _ref5$.pluginState,
167
+ lastState = _ref5$.state,
168
+ lastColourScheme = _ref5$.colourScheme;
169
+ var originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
170
+ return (_ref6 = originalDocIsSame && (0, _isEqual.default)(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc) && colourScheme === lastColourScheme) !== null && _ref6 !== void 0 ? _ref6 : false;
171
+ });
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createInlineChangedDecoration = exports.createDeletedContentDecoration = void 0;
6
+ exports.createInlineChangedDecoration = exports.createDeletedContentDecoration = exports.createBlockChangedDecoration = void 0;
7
7
  var _lazyNodeView = require("@atlaskit/editor-common/lazy-node-view");
8
8
  var _view = require("@atlaskit/editor-prosemirror/view");
9
9
  var editingStyle = (0, _lazyNodeView.convertToInlineCss)({
@@ -33,6 +33,84 @@ var createInlineChangedDecoration = exports.createInlineChangedDecoration = func
33
33
  'data-testid': 'show-diff-changed-decoration'
34
34
  }, {});
35
35
  };
36
+ var getEditorStyleNode = function getEditorStyleNode(nodeName, colourScheme) {
37
+ switch (nodeName) {
38
+ case 'blockquote':
39
+ return colourScheme === 'traditional' ? traditionalStyleQuoteNode : editingStyleQuoteNode;
40
+ case 'mediaSingle':
41
+ case 'mediaGroup':
42
+ case 'embedCard':
43
+ case 'table':
44
+ case 'tableRow':
45
+ case 'tableCell':
46
+ case 'tableHeader':
47
+ return undefined;
48
+ // Handle table separately to avoid border issues
49
+ case 'paragraph':
50
+ case 'heading':
51
+ case 'hardBreak':
52
+ return undefined;
53
+ // Paragraph and heading nodes do not need special styling
54
+ case 'decisionList':
55
+ case 'taskList':
56
+ case 'taskItem':
57
+ case 'bulletList':
58
+ case 'orderedList':
59
+ case 'listItem':
60
+ return undefined;
61
+ // Lists do not need special styling
62
+ case 'layoutSection':
63
+ return undefined;
64
+ // Layout nodes do not need special styling
65
+ case 'rule':
66
+ return colourScheme === 'traditional' ? traditionalStyleRuleNode : editingStyleRuleNode;
67
+ case 'blockCard':
68
+ return colourScheme === 'traditional' ? traditionalStyleCardBlockNode : editingStyleCardBlockNode;
69
+ default:
70
+ return colourScheme === 'traditional' ? traditionalStyleNode : editingStyleNode;
71
+ }
72
+ };
73
+ var editingStyleQuoteNode = (0, _lazyNodeView.convertToInlineCss)({
74
+ borderLeft: "2px solid ".concat("var(--ds-border-accent-purple, #8270DB)")
75
+ });
76
+ var traditionalStyleQuoteNode = (0, _lazyNodeView.convertToInlineCss)({
77
+ borderLeft: "2px solid ".concat("var(--ds-border-accent-green, #22A06B)")
78
+ });
79
+ var editingStyleRuleNode = (0, _lazyNodeView.convertToInlineCss)({
80
+ backgroundColor: "var(--ds-border-accent-purple, #8270DB)"
81
+ });
82
+ var traditionalStyleRuleNode = (0, _lazyNodeView.convertToInlineCss)({
83
+ backgroundColor: "var(--ds-border-accent-green, #22A06B)"
84
+ });
85
+ var editingStyleNode = (0, _lazyNodeView.convertToInlineCss)({
86
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-purple, #8270DB)"),
87
+ borderRadius: "var(--ds-radius-small, 4px)"
88
+ });
89
+ var traditionalStyleNode = (0, _lazyNodeView.convertToInlineCss)({
90
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-green, #22A06B)"),
91
+ borderRadius: "var(--ds-radius-small, 4px)"
92
+ });
93
+ var editingStyleCardBlockNode = (0, _lazyNodeView.convertToInlineCss)({
94
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-purple, #8270DB)"),
95
+ borderRadius: "var(--ds-radius-medium, 6px)"
96
+ });
97
+ var traditionalStyleCardBlockNode = (0, _lazyNodeView.convertToInlineCss)({
98
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-green, #22A06B)"),
99
+ borderRadius: "var(--ds-radius-medium, 6px)"
100
+ });
101
+ /**
102
+ * Inline decoration used for insertions as the content already exists in the document
103
+ *
104
+ * @param change Changeset "change" containing information about the change content + range
105
+ * @returns Prosemirror inline decoration
106
+ */
107
+ var createBlockChangedDecoration = exports.createBlockChangedDecoration = function createBlockChangedDecoration(change, colourScheme) {
108
+ return _view.Decoration.node(change.from, change.to, {
109
+ style: getEditorStyleNode(change.name, colourScheme),
110
+ 'data-testid': 'show-diff-changed-decoration-node',
111
+ class: "show-diff-changed-decoration-node-".concat(colourScheme || 'standard')
112
+ }, {});
113
+ };
36
114
  var deletedContentStyle = (0, _lazyNodeView.convertToInlineCss)({
37
115
  color: "var(--ds-text-accent-gray, #44546F)",
38
116
  textDecoration: 'line-through',
@@ -1,10 +1,84 @@
1
1
  // eslint-disable-next-line @atlassian/tangerine/import/entry-points
2
+ import isEqual from 'lodash/isEqual';
3
+ import memoizeOne from 'memoize-one';
2
4
  import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
5
+ import { AnalyticsStep, SetAttrsStep } from '@atlaskit/adf-schema/steps';
3
6
  import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
7
+ import { AttrStep } from '@atlaskit/editor-prosemirror/transform';
4
8
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
5
- import { createInlineChangedDecoration, createDeletedContentDecoration } from './decorations';
9
+ import { createInlineChangedDecoration, createDeletedContentDecoration, createBlockChangedDecoration } from './decorations';
6
10
  import { getMarkChangeRanges } from './markDecorations';
7
- export const calculateDiffDecorations = ({
11
+ const calculateNodesForBlockDecoration = (doc, from, to, colourScheme) => {
12
+ const decorations = [];
13
+ // Iterate over the document nodes within the range
14
+ doc.nodesBetween(from, to, (node, pos) => {
15
+ if (node.isBlock) {
16
+ decorations.push(createBlockChangedDecoration({
17
+ from: pos,
18
+ to: pos + node.nodeSize,
19
+ name: node.type.name
20
+ }, colourScheme));
21
+ }
22
+ });
23
+ return decorations;
24
+ };
25
+
26
+ /**
27
+ * Groups adjacent changes to reduce visual fragmentation in diffs.
28
+ * Merges consecutive insertions and deletions that are close together.
29
+ */
30
+ function optimizeChanges(changes) {
31
+ if (changes.length <= 1) {
32
+ return changes;
33
+ }
34
+ const optimized = [];
35
+ let current = {
36
+ ...changes[0]
37
+ };
38
+ for (let i = 1; i < changes.length; i++) {
39
+ const next = changes[i];
40
+
41
+ // Check if changes are adjacent or very close (within 2 positions)
42
+ const isAdjacent = next.fromB <= current.toB + 2;
43
+ if (isAdjacent) {
44
+ current = {
45
+ fromA: current.fromA,
46
+ toA: Math.max(current.toA, next.toA),
47
+ fromB: current.fromB,
48
+ toB: Math.max(current.toB, next.toB),
49
+ deleted: [...current.deleted, ...next.deleted],
50
+ inserted: [...current.inserted, ...next.inserted]
51
+ };
52
+ } else {
53
+ optimized.push(current);
54
+ current = {
55
+ ...next
56
+ };
57
+ }
58
+ }
59
+ optimized.push(current);
60
+ return optimized;
61
+ }
62
+
63
+ // Simplifies the steps to improve performance and reduce fragmentation in diffs
64
+ function simplifySteps(steps) {
65
+ return steps
66
+ // Remove steps that don't affect document structure or content
67
+ .filter(step => !(step instanceof AnalyticsStep || step instanceof AttrStep || step instanceof SetAttrsStep))
68
+ // Merge consecutive steps where possible
69
+ .reduce((acc, step) => {
70
+ var _lastStep$merge;
71
+ const lastStep = acc[acc.length - 1];
72
+ const merged = lastStep === null || lastStep === void 0 ? void 0 : (_lastStep$merge = lastStep.merge) === null || _lastStep$merge === void 0 ? void 0 : _lastStep$merge.call(lastStep, step);
73
+ if (merged) {
74
+ acc[acc.length - 1] = merged;
75
+ } else {
76
+ acc.push(step);
77
+ }
78
+ return acc;
79
+ }, []);
80
+ }
81
+ const calculateDiffDecorationsInner = ({
8
82
  state,
9
83
  pluginState,
10
84
  nodeViewSerializer,
@@ -12,8 +86,9 @@ export const calculateDiffDecorations = ({
12
86
  }) => {
13
87
  const {
14
88
  originalDoc,
15
- steps
89
+ steps: rawSteps
16
90
  } = pluginState;
91
+ const steps = simplifySteps(rawSteps);
17
92
  if (!originalDoc || !pluginState.isDisplayingChanges) {
18
93
  return DecorationSet.empty;
19
94
  }
@@ -21,24 +96,28 @@ export const calculateDiffDecorations = ({
21
96
  tr
22
97
  } = state;
23
98
  let steppedDoc = originalDoc;
24
- let changeset = ChangeSet.create(originalDoc);
99
+ const stepMaps = [];
25
100
  for (const step of steps) {
26
101
  const result = step.apply(steppedDoc);
27
102
  if (result.failed === null && result.doc) {
28
103
  steppedDoc = result.doc;
29
- changeset = changeset.addSteps(steppedDoc, [step.getMap()], tr.doc);
104
+ stepMaps.push(step.getMap());
30
105
  }
31
106
  }
107
+
32
108
  // Rather than using .eq() we use a custom function that only checks for structural
33
109
  // changes and ignores differences in attributes which don't affect decoration positions
34
110
  if (!areNodesEqualIgnoreAttrs(steppedDoc, tr.doc)) {
35
111
  return DecorationSet.empty;
36
112
  }
113
+ const changeset = ChangeSet.create(originalDoc).addSteps(steppedDoc, stepMaps, tr.doc);
37
114
  const changes = simplifyChanges(changeset.changes, tr.doc);
115
+ const optimizedChanges = optimizeChanges(changes);
38
116
  const decorations = [];
39
- changes.forEach(change => {
117
+ optimizedChanges.forEach(change => {
40
118
  if (change.inserted.length > 0) {
41
119
  decorations.push(createInlineChangedDecoration(change, colourScheme));
120
+ decorations.push(...calculateNodesForBlockDecoration(tr.doc, change.fromB, change.toB, colourScheme));
42
121
  }
43
122
  if (change.deleted.length > 0) {
44
123
  const decoration = createDeletedContentDecoration({
@@ -56,4 +135,19 @@ export const calculateDiffDecorations = ({
56
135
  decorations.push(createInlineChangedDecoration(change, colourScheme));
57
136
  });
58
137
  return DecorationSet.empty.add(tr.doc, decorations);
59
- };
138
+ };
139
+ export const calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner,
140
+ // Cache results unless relevant inputs change
141
+ ([{
142
+ pluginState,
143
+ state,
144
+ colourScheme
145
+ }], [{
146
+ pluginState: lastPluginState,
147
+ state: lastState,
148
+ colourScheme: lastColourScheme
149
+ }]) => {
150
+ var _ref;
151
+ const originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
152
+ return (_ref = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc) && colourScheme === lastColourScheme) !== null && _ref !== void 0 ? _ref : false;
153
+ });
@@ -25,6 +25,82 @@ export const createInlineChangedDecoration = (change, colourScheme) => Decoratio
25
25
  style: colourScheme === 'traditional' ? traditionalInsertStyle : editingStyle,
26
26
  'data-testid': 'show-diff-changed-decoration'
27
27
  }, {});
28
+ const getEditorStyleNode = (nodeName, colourScheme) => {
29
+ switch (nodeName) {
30
+ case 'blockquote':
31
+ return colourScheme === 'traditional' ? traditionalStyleQuoteNode : editingStyleQuoteNode;
32
+ case 'mediaSingle':
33
+ case 'mediaGroup':
34
+ case 'embedCard':
35
+ case 'table':
36
+ case 'tableRow':
37
+ case 'tableCell':
38
+ case 'tableHeader':
39
+ return undefined;
40
+ // Handle table separately to avoid border issues
41
+ case 'paragraph':
42
+ case 'heading':
43
+ case 'hardBreak':
44
+ return undefined;
45
+ // Paragraph and heading nodes do not need special styling
46
+ case 'decisionList':
47
+ case 'taskList':
48
+ case 'taskItem':
49
+ case 'bulletList':
50
+ case 'orderedList':
51
+ case 'listItem':
52
+ return undefined;
53
+ // Lists do not need special styling
54
+ case 'layoutSection':
55
+ return undefined;
56
+ // Layout nodes do not need special styling
57
+ case 'rule':
58
+ return colourScheme === 'traditional' ? traditionalStyleRuleNode : editingStyleRuleNode;
59
+ case 'blockCard':
60
+ return colourScheme === 'traditional' ? traditionalStyleCardBlockNode : editingStyleCardBlockNode;
61
+ default:
62
+ return colourScheme === 'traditional' ? traditionalStyleNode : editingStyleNode;
63
+ }
64
+ };
65
+ const editingStyleQuoteNode = convertToInlineCss({
66
+ borderLeft: `2px solid ${"var(--ds-border-accent-purple, #8270DB)"}`
67
+ });
68
+ const traditionalStyleQuoteNode = convertToInlineCss({
69
+ borderLeft: `2px solid ${"var(--ds-border-accent-green, #22A06B)"}`
70
+ });
71
+ const editingStyleRuleNode = convertToInlineCss({
72
+ backgroundColor: "var(--ds-border-accent-purple, #8270DB)"
73
+ });
74
+ const traditionalStyleRuleNode = convertToInlineCss({
75
+ backgroundColor: "var(--ds-border-accent-green, #22A06B)"
76
+ });
77
+ const editingStyleNode = convertToInlineCss({
78
+ boxShadow: `0 0 0 1px ${"var(--ds-border-accent-purple, #8270DB)"}`,
79
+ borderRadius: "var(--ds-radius-small, 4px)"
80
+ });
81
+ const traditionalStyleNode = convertToInlineCss({
82
+ boxShadow: `0 0 0 1px ${"var(--ds-border-accent-green, #22A06B)"}`,
83
+ borderRadius: "var(--ds-radius-small, 4px)"
84
+ });
85
+ const editingStyleCardBlockNode = convertToInlineCss({
86
+ boxShadow: `0 0 0 1px ${"var(--ds-border-accent-purple, #8270DB)"}`,
87
+ borderRadius: "var(--ds-radius-medium, 6px)"
88
+ });
89
+ const traditionalStyleCardBlockNode = convertToInlineCss({
90
+ boxShadow: `0 0 0 1px ${"var(--ds-border-accent-green, #22A06B)"}`,
91
+ borderRadius: "var(--ds-radius-medium, 6px)"
92
+ });
93
+ /**
94
+ * Inline decoration used for insertions as the content already exists in the document
95
+ *
96
+ * @param change Changeset "change" containing information about the change content + range
97
+ * @returns Prosemirror inline decoration
98
+ */
99
+ export const createBlockChangedDecoration = (change, colourScheme) => Decoration.node(change.from, change.to, {
100
+ style: getEditorStyleNode(change.name, colourScheme),
101
+ 'data-testid': 'show-diff-changed-decoration-node',
102
+ class: `show-diff-changed-decoration-node-${colourScheme || 'standard'}`
103
+ }, {});
28
104
  const deletedContentStyle = convertToInlineCss({
29
105
  color: "var(--ds-text-accent-gray, #44546F)",
30
106
  textDecoration: 'line-through',
@@ -1,25 +1,103 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
1
4
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
2
5
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
3
6
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
7
+ 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; }
8
+ 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
9
  // eslint-disable-next-line @atlassian/tangerine/import/entry-points
10
+ import isEqual from 'lodash/isEqual';
11
+ import memoizeOne from 'memoize-one';
5
12
  import { ChangeSet, simplifyChanges } from 'prosemirror-changeset';
13
+ import { AnalyticsStep, SetAttrsStep } from '@atlaskit/adf-schema/steps';
6
14
  import { areNodesEqualIgnoreAttrs } from '@atlaskit/editor-common/utils/document';
15
+ import { AttrStep } from '@atlaskit/editor-prosemirror/transform';
7
16
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
8
- import { createInlineChangedDecoration, createDeletedContentDecoration } from './decorations';
17
+ import { createInlineChangedDecoration, createDeletedContentDecoration, createBlockChangedDecoration } from './decorations';
9
18
  import { getMarkChangeRanges } from './markDecorations';
10
- export var calculateDiffDecorations = function calculateDiffDecorations(_ref) {
19
+ var calculateNodesForBlockDecoration = function calculateNodesForBlockDecoration(doc, from, to, colourScheme) {
20
+ var decorations = [];
21
+ // Iterate over the document nodes within the range
22
+ doc.nodesBetween(from, to, function (node, pos) {
23
+ if (node.isBlock) {
24
+ decorations.push(createBlockChangedDecoration({
25
+ from: pos,
26
+ to: pos + node.nodeSize,
27
+ name: node.type.name
28
+ }, colourScheme));
29
+ }
30
+ });
31
+ return decorations;
32
+ };
33
+
34
+ /**
35
+ * Groups adjacent changes to reduce visual fragmentation in diffs.
36
+ * Merges consecutive insertions and deletions that are close together.
37
+ */
38
+ function optimizeChanges(changes) {
39
+ if (changes.length <= 1) {
40
+ return changes;
41
+ }
42
+ var optimized = [];
43
+ var current = _objectSpread({}, changes[0]);
44
+ for (var i = 1; i < changes.length; i++) {
45
+ var next = changes[i];
46
+
47
+ // Check if changes are adjacent or very close (within 2 positions)
48
+ var isAdjacent = next.fromB <= current.toB + 2;
49
+ if (isAdjacent) {
50
+ current = {
51
+ fromA: current.fromA,
52
+ toA: Math.max(current.toA, next.toA),
53
+ fromB: current.fromB,
54
+ toB: Math.max(current.toB, next.toB),
55
+ deleted: [].concat(_toConsumableArray(current.deleted), _toConsumableArray(next.deleted)),
56
+ inserted: [].concat(_toConsumableArray(current.inserted), _toConsumableArray(next.inserted))
57
+ };
58
+ } else {
59
+ optimized.push(current);
60
+ current = _objectSpread({}, next);
61
+ }
62
+ }
63
+ optimized.push(current);
64
+ return optimized;
65
+ }
66
+
67
+ // Simplifies the steps to improve performance and reduce fragmentation in diffs
68
+ function simplifySteps(steps) {
69
+ return steps
70
+ // Remove steps that don't affect document structure or content
71
+ .filter(function (step) {
72
+ return !(step instanceof AnalyticsStep || step instanceof AttrStep || step instanceof SetAttrsStep);
73
+ })
74
+ // Merge consecutive steps where possible
75
+ .reduce(function (acc, step) {
76
+ var _lastStep$merge;
77
+ var lastStep = acc[acc.length - 1];
78
+ var merged = lastStep === null || lastStep === void 0 || (_lastStep$merge = lastStep.merge) === null || _lastStep$merge === void 0 ? void 0 : _lastStep$merge.call(lastStep, step);
79
+ if (merged) {
80
+ acc[acc.length - 1] = merged;
81
+ } else {
82
+ acc.push(step);
83
+ }
84
+ return acc;
85
+ }, []);
86
+ }
87
+ var calculateDiffDecorationsInner = function calculateDiffDecorationsInner(_ref) {
11
88
  var state = _ref.state,
12
89
  pluginState = _ref.pluginState,
13
90
  nodeViewSerializer = _ref.nodeViewSerializer,
14
91
  colourScheme = _ref.colourScheme;
15
92
  var originalDoc = pluginState.originalDoc,
16
- steps = pluginState.steps;
93
+ rawSteps = pluginState.steps;
94
+ var steps = simplifySteps(rawSteps);
17
95
  if (!originalDoc || !pluginState.isDisplayingChanges) {
18
96
  return DecorationSet.empty;
19
97
  }
20
98
  var tr = state.tr;
21
99
  var steppedDoc = originalDoc;
22
- var changeset = ChangeSet.create(originalDoc);
100
+ var stepMaps = [];
23
101
  var _iterator = _createForOfIteratorHelper(steps),
24
102
  _step;
25
103
  try {
@@ -28,9 +106,10 @@ export var calculateDiffDecorations = function calculateDiffDecorations(_ref) {
28
106
  var result = step.apply(steppedDoc);
29
107
  if (result.failed === null && result.doc) {
30
108
  steppedDoc = result.doc;
31
- changeset = changeset.addSteps(steppedDoc, [step.getMap()], tr.doc);
109
+ stepMaps.push(step.getMap());
32
110
  }
33
111
  }
112
+
34
113
  // Rather than using .eq() we use a custom function that only checks for structural
35
114
  // changes and ignores differences in attributes which don't affect decoration positions
36
115
  } catch (err) {
@@ -41,11 +120,14 @@ export var calculateDiffDecorations = function calculateDiffDecorations(_ref) {
41
120
  if (!areNodesEqualIgnoreAttrs(steppedDoc, tr.doc)) {
42
121
  return DecorationSet.empty;
43
122
  }
123
+ var changeset = ChangeSet.create(originalDoc).addSteps(steppedDoc, stepMaps, tr.doc);
44
124
  var changes = simplifyChanges(changeset.changes, tr.doc);
125
+ var optimizedChanges = optimizeChanges(changes);
45
126
  var decorations = [];
46
- changes.forEach(function (change) {
127
+ optimizedChanges.forEach(function (change) {
47
128
  if (change.inserted.length > 0) {
48
129
  decorations.push(createInlineChangedDecoration(change, colourScheme));
130
+ decorations.push.apply(decorations, _toConsumableArray(calculateNodesForBlockDecoration(tr.doc, change.fromB, change.toB, colourScheme)));
49
131
  }
50
132
  if (change.deleted.length > 0) {
51
133
  var decoration = createDeletedContentDecoration({
@@ -63,4 +145,21 @@ export var calculateDiffDecorations = function calculateDiffDecorations(_ref) {
63
145
  decorations.push(createInlineChangedDecoration(change, colourScheme));
64
146
  });
65
147
  return DecorationSet.empty.add(tr.doc, decorations);
66
- };
148
+ };
149
+ export var calculateDiffDecorations = memoizeOne(calculateDiffDecorationsInner,
150
+ // Cache results unless relevant inputs change
151
+ function (_ref2, _ref3) {
152
+ var _ref6;
153
+ var _ref4 = _slicedToArray(_ref2, 1),
154
+ _ref4$ = _ref4[0],
155
+ pluginState = _ref4$.pluginState,
156
+ state = _ref4$.state,
157
+ colourScheme = _ref4$.colourScheme;
158
+ var _ref5 = _slicedToArray(_ref3, 1),
159
+ _ref5$ = _ref5[0],
160
+ lastPluginState = _ref5$.pluginState,
161
+ lastState = _ref5$.state,
162
+ lastColourScheme = _ref5$.colourScheme;
163
+ var originalDocIsSame = lastPluginState.originalDoc && pluginState.originalDoc && pluginState.originalDoc.eq(lastPluginState.originalDoc);
164
+ return (_ref6 = originalDocIsSame && isEqual(pluginState.steps, lastPluginState.steps) && state.doc.eq(lastState.doc) && colourScheme === lastColourScheme) !== null && _ref6 !== void 0 ? _ref6 : false;
165
+ });
@@ -27,6 +27,84 @@ export var createInlineChangedDecoration = function createInlineChangedDecoratio
27
27
  'data-testid': 'show-diff-changed-decoration'
28
28
  }, {});
29
29
  };
30
+ var getEditorStyleNode = function getEditorStyleNode(nodeName, colourScheme) {
31
+ switch (nodeName) {
32
+ case 'blockquote':
33
+ return colourScheme === 'traditional' ? traditionalStyleQuoteNode : editingStyleQuoteNode;
34
+ case 'mediaSingle':
35
+ case 'mediaGroup':
36
+ case 'embedCard':
37
+ case 'table':
38
+ case 'tableRow':
39
+ case 'tableCell':
40
+ case 'tableHeader':
41
+ return undefined;
42
+ // Handle table separately to avoid border issues
43
+ case 'paragraph':
44
+ case 'heading':
45
+ case 'hardBreak':
46
+ return undefined;
47
+ // Paragraph and heading nodes do not need special styling
48
+ case 'decisionList':
49
+ case 'taskList':
50
+ case 'taskItem':
51
+ case 'bulletList':
52
+ case 'orderedList':
53
+ case 'listItem':
54
+ return undefined;
55
+ // Lists do not need special styling
56
+ case 'layoutSection':
57
+ return undefined;
58
+ // Layout nodes do not need special styling
59
+ case 'rule':
60
+ return colourScheme === 'traditional' ? traditionalStyleRuleNode : editingStyleRuleNode;
61
+ case 'blockCard':
62
+ return colourScheme === 'traditional' ? traditionalStyleCardBlockNode : editingStyleCardBlockNode;
63
+ default:
64
+ return colourScheme === 'traditional' ? traditionalStyleNode : editingStyleNode;
65
+ }
66
+ };
67
+ var editingStyleQuoteNode = convertToInlineCss({
68
+ borderLeft: "2px solid ".concat("var(--ds-border-accent-purple, #8270DB)")
69
+ });
70
+ var traditionalStyleQuoteNode = convertToInlineCss({
71
+ borderLeft: "2px solid ".concat("var(--ds-border-accent-green, #22A06B)")
72
+ });
73
+ var editingStyleRuleNode = convertToInlineCss({
74
+ backgroundColor: "var(--ds-border-accent-purple, #8270DB)"
75
+ });
76
+ var traditionalStyleRuleNode = convertToInlineCss({
77
+ backgroundColor: "var(--ds-border-accent-green, #22A06B)"
78
+ });
79
+ var editingStyleNode = convertToInlineCss({
80
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-purple, #8270DB)"),
81
+ borderRadius: "var(--ds-radius-small, 4px)"
82
+ });
83
+ var traditionalStyleNode = convertToInlineCss({
84
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-green, #22A06B)"),
85
+ borderRadius: "var(--ds-radius-small, 4px)"
86
+ });
87
+ var editingStyleCardBlockNode = convertToInlineCss({
88
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-purple, #8270DB)"),
89
+ borderRadius: "var(--ds-radius-medium, 6px)"
90
+ });
91
+ var traditionalStyleCardBlockNode = convertToInlineCss({
92
+ boxShadow: "0 0 0 1px ".concat("var(--ds-border-accent-green, #22A06B)"),
93
+ borderRadius: "var(--ds-radius-medium, 6px)"
94
+ });
95
+ /**
96
+ * Inline decoration used for insertions as the content already exists in the document
97
+ *
98
+ * @param change Changeset "change" containing information about the change content + range
99
+ * @returns Prosemirror inline decoration
100
+ */
101
+ export var createBlockChangedDecoration = function createBlockChangedDecoration(change, colourScheme) {
102
+ return Decoration.node(change.from, change.to, {
103
+ style: getEditorStyleNode(change.name, colourScheme),
104
+ 'data-testid': 'show-diff-changed-decoration-node',
105
+ class: "show-diff-changed-decoration-node-".concat(colourScheme || 'standard')
106
+ }, {});
107
+ };
30
108
  var deletedContentStyle = convertToInlineCss({
31
109
  color: "var(--ds-text-accent-gray, #44546F)",
32
110
  textDecoration: 'line-through',
@@ -2,9 +2,9 @@ import { type EditorState } from '@atlaskit/editor-prosemirror/state';
2
2
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
3
3
  import type { ShowDiffPluginState } from './main';
4
4
  import type { NodeViewSerializer } from './NodeViewSerializer';
5
- export declare const calculateDiffDecorations: ({ state, pluginState, nodeViewSerializer, colourScheme, }: {
5
+ export declare const calculateDiffDecorations: import("memoize-one").MemoizedFn<({ state, pluginState, nodeViewSerializer, colourScheme, }: {
6
6
  colourScheme?: "standard" | "traditional";
7
7
  nodeViewSerializer: NodeViewSerializer;
8
8
  pluginState: Omit<ShowDiffPluginState, "decorations">;
9
9
  state: EditorState;
10
- }) => DecorationSet;
10
+ }) => DecorationSet>;
@@ -12,6 +12,17 @@ export declare const createInlineChangedDecoration: (change: {
12
12
  fromB: number;
13
13
  toB: number;
14
14
  }, colourScheme?: "standard" | "traditional") => Decoration;
15
+ /**
16
+ * Inline decoration used for insertions as the content already exists in the document
17
+ *
18
+ * @param change Changeset "change" containing information about the change content + range
19
+ * @returns Prosemirror inline decoration
20
+ */
21
+ export declare const createBlockChangedDecoration: (change: {
22
+ from: number;
23
+ name: string;
24
+ to: number;
25
+ }, colourScheme?: "standard" | "traditional") => Decoration;
15
26
  interface DeletedContentDecorationProps {
16
27
  change: Change;
17
28
  colourScheme?: 'standard' | 'traditional';
@@ -2,9 +2,9 @@ import { type EditorState } from '@atlaskit/editor-prosemirror/state';
2
2
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
3
3
  import type { ShowDiffPluginState } from './main';
4
4
  import type { NodeViewSerializer } from './NodeViewSerializer';
5
- export declare const calculateDiffDecorations: ({ state, pluginState, nodeViewSerializer, colourScheme, }: {
5
+ export declare const calculateDiffDecorations: import("memoize-one").MemoizedFn<({ state, pluginState, nodeViewSerializer, colourScheme, }: {
6
6
  colourScheme?: "standard" | "traditional";
7
7
  nodeViewSerializer: NodeViewSerializer;
8
8
  pluginState: Omit<ShowDiffPluginState, "decorations">;
9
9
  state: EditorState;
10
- }) => DecorationSet;
10
+ }) => DecorationSet>;
@@ -12,6 +12,17 @@ export declare const createInlineChangedDecoration: (change: {
12
12
  fromB: number;
13
13
  toB: number;
14
14
  }, colourScheme?: "standard" | "traditional") => Decoration;
15
+ /**
16
+ * Inline decoration used for insertions as the content already exists in the document
17
+ *
18
+ * @param change Changeset "change" containing information about the change content + range
19
+ * @returns Prosemirror inline decoration
20
+ */
21
+ export declare const createBlockChangedDecoration: (change: {
22
+ from: number;
23
+ name: string;
24
+ to: number;
25
+ }, colourScheme?: "standard" | "traditional") => Decoration;
15
26
  interface DeletedContentDecorationProps {
16
27
  change: Change;
17
28
  colourScheme?: 'standard' | 'traditional';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-show-diff",
3
- "version": "2.1.3",
3
+ "version": "3.0.1",
4
4
  "description": "ShowDiff plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -28,13 +28,16 @@
28
28
  "sideEffects": false,
29
29
  "atlaskit:src": "src/index.ts",
30
30
  "dependencies": {
31
+ "@atlaskit/adf-schema": "^51.2.0",
31
32
  "@atlaskit/editor-prosemirror": "7.0.0",
32
- "@atlaskit/tokens": "^6.3.0",
33
+ "@atlaskit/tokens": "^6.5.0",
33
34
  "@babel/runtime": "^7.0.0",
35
+ "lodash": "^4.17.21",
36
+ "memoize-one": "^6.0.0",
34
37
  "prosemirror-changeset": "^2.2.1"
35
38
  },
36
39
  "peerDependencies": {
37
- "@atlaskit/editor-common": "^109.14.0",
40
+ "@atlaskit/editor-common": "^110.12.0",
38
41
  "react": "^18.2.0"
39
42
  },
40
43
  "techstack": {