@atlaskit/editor-plugin-show-diff 16.0.1 → 16.0.3
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 +45 -0
- package/afm-products/tsconfig.json +3 -0
- package/dist/cjs/pm-plugins/calculateDiff/calculateDiffDecorations.js +91 -51
- package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +20 -1
- package/dist/cjs/pm-plugins/decorations/extractContributorTags.js +121 -21
- package/dist/cjs/pm-plugins/decorations/utils/getAttrChangeRanges.js +19 -8
- package/dist/cjs/pm-plugins/decorations/utils/getMarkChangeRanges.js +18 -4
- package/dist/cjs/pm-plugins/decorations/utils/mapStepRangeToFinal.js +34 -0
- package/dist/cjs/pm-plugins/main.js +18 -0
- package/dist/cjs/ui/ContributorTag/buildContributorTagDom.js +51 -5
- package/dist/cjs/ui/ContributorTag/contributorLabel.js +55 -0
- package/dist/cjs/ui/ContributorTag/contributorTagController.js +40 -43
- package/dist/cjs/ui/ContributorTag/messages.js +5 -0
- package/dist/cjs/ui/DiffNavigation/announceActiveDiff.js +54 -0
- package/dist/cjs/ui/DiffNavigation/messages.js +19 -0
- package/dist/es2019/pm-plugins/calculateDiff/calculateDiffDecorations.js +48 -13
- package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +18 -1
- package/dist/es2019/pm-plugins/decorations/extractContributorTags.js +70 -10
- package/dist/es2019/pm-plugins/decorations/utils/getAttrChangeRanges.js +16 -0
- package/dist/es2019/pm-plugins/decorations/utils/getMarkChangeRanges.js +12 -4
- package/dist/es2019/pm-plugins/decorations/utils/mapStepRangeToFinal.js +28 -0
- package/dist/es2019/pm-plugins/main.js +18 -0
- package/dist/es2019/ui/ContributorTag/buildContributorTagDom.js +48 -4
- package/dist/es2019/ui/ContributorTag/contributorLabel.js +49 -0
- package/dist/es2019/ui/ContributorTag/contributorTagController.js +39 -44
- package/dist/es2019/ui/ContributorTag/messages.js +5 -0
- package/dist/es2019/ui/DiffNavigation/announceActiveDiff.js +49 -0
- package/dist/es2019/ui/DiffNavigation/messages.js +13 -0
- package/dist/esm/pm-plugins/calculateDiff/calculateDiffDecorations.js +91 -51
- package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +20 -1
- package/dist/esm/pm-plugins/decorations/extractContributorTags.js +121 -21
- package/dist/esm/pm-plugins/decorations/utils/getAttrChangeRanges.js +19 -8
- package/dist/esm/pm-plugins/decorations/utils/getMarkChangeRanges.js +17 -4
- package/dist/esm/pm-plugins/decorations/utils/mapStepRangeToFinal.js +28 -0
- package/dist/esm/pm-plugins/main.js +18 -0
- package/dist/esm/ui/ContributorTag/buildContributorTagDom.js +50 -4
- package/dist/esm/ui/ContributorTag/contributorLabel.js +49 -0
- package/dist/esm/ui/ContributorTag/contributorTagController.js +41 -44
- package/dist/esm/ui/ContributorTag/messages.js +5 -0
- package/dist/esm/ui/DiffNavigation/announceActiveDiff.js +48 -0
- package/dist/esm/ui/DiffNavigation/messages.js +13 -0
- package/dist/types/pm-plugins/decorations/extractContributorTags.d.ts +14 -1
- package/dist/types/pm-plugins/decorations/utils/getAttrChangeRanges.d.ts +3 -0
- package/dist/types/pm-plugins/decorations/utils/getMarkChangeRanges.d.ts +4 -1
- package/dist/types/pm-plugins/decorations/utils/mapStepRangeToFinal.d.ts +8 -0
- package/dist/types/showDiffPluginType.d.ts +7 -1
- package/dist/types/ui/ContributorTag/buildContributorTagDom.d.ts +12 -2
- package/dist/types/ui/ContributorTag/contributorLabel.d.ts +13 -0
- package/dist/types/ui/ContributorTag/contributorTagController.d.ts +11 -0
- package/dist/types/ui/ContributorTag/messages.d.ts +1 -0
- package/dist/types/ui/DiffNavigation/announceActiveDiff.d.ts +18 -0
- package/dist/types/ui/DiffNavigation/messages.d.ts +5 -0
- package/package.json +5 -4
|
@@ -39,7 +39,24 @@ export const mountContributorTag = ({
|
|
|
39
39
|
diffId
|
|
40
40
|
});
|
|
41
41
|
controller.mount(host);
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Kept alive when ProseMirror only rebuilt the widget desc around this same host: a document
|
|
45
|
+
* change remaps the decoration and fires `destroy`, but an element `toDOM` is reused verbatim
|
|
46
|
+
* and runs no mount callback, so tearing down here left an active change with no tag
|
|
47
|
+
* (EDITOR-8971).
|
|
48
|
+
*
|
|
49
|
+
* Deferred, because the host is still attached while that update is in flight. A host outside a
|
|
50
|
+
* live editor root is a real removal — including the view being destroyed, which drops the root.
|
|
51
|
+
*/
|
|
52
|
+
return {
|
|
53
|
+
destroy: () => queueMicrotask(() => {
|
|
54
|
+
var _mountContext$getEdit;
|
|
55
|
+
if (!host.isConnected || !((_mountContext$getEdit = mountContext.getEditorRoot()) !== null && _mountContext$getEdit !== void 0 && _mountContext$getEdit.contains(host))) {
|
|
56
|
+
controller.destroy();
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
};
|
|
43
60
|
};
|
|
44
61
|
export const unmountContributorTag = mount => {
|
|
45
62
|
mount === null || mount === void 0 ? void 0 : mount.destroy();
|
|
@@ -34,12 +34,44 @@ const leadsReplacement = widget => fg('confluence_ncs_step_diffing_version_histo
|
|
|
34
34
|
*/
|
|
35
35
|
const isSameBlockChange = (inner, block) => inner.spec.attributionKey === block.spec.attributionKey && (block.from <= inner.from && inner.to <= block.to || inner.from <= block.from && block.to <= inner.to);
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Which of two changes in the same range leads, and so is the one a tag captions. Position first,
|
|
39
|
+
* then `side`, since deleted content shares its `from` with the content that replaced it and paints
|
|
40
|
+
* above; then the narrower range, so a change nested in another captions itself.
|
|
41
|
+
*/
|
|
42
|
+
const byLeadingPosition = (a, b) => {
|
|
43
|
+
var _a$decoration$spec$si, _b$decoration$spec$si;
|
|
44
|
+
return a.decoration.from - b.decoration.from || ((_a$decoration$spec$si = a.decoration.spec.side) !== null && _a$decoration$spec$si !== void 0 ? _a$decoration$spec$si : 0) - ((_b$decoration$spec$si = b.decoration.spec.side) !== null && _b$decoration$spec$si !== void 0 ? _b$decoration$spec$si : 0) || a.decoration.to - a.decoration.from - (b.decoration.to - b.decoration.from);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The targets a navigation range wholly covers, leader first. */
|
|
48
|
+
const containedBy = (targets, {
|
|
49
|
+
from,
|
|
50
|
+
to
|
|
51
|
+
}) => targets.filter(({
|
|
52
|
+
decoration
|
|
53
|
+
}) => from <= decoration.from && decoration.to <= to).sort(byLeadingPosition);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The one tag a navigation step reveals. `spec.isActive` is no use here: it covers everything the
|
|
57
|
+
* group's union range touches, so several tags revealed over each other (EDITOR-8971). Hence
|
|
58
|
+
* containment, and the leading change of what it covers.
|
|
59
|
+
*/
|
|
60
|
+
const resolveActiveTarget = (surviving, activeIndexPos) => containedBy(surviving, activeIndexPos)[0];
|
|
61
|
+
|
|
37
62
|
/**
|
|
38
63
|
* One model per diff decoration whose attribution resolves to a supplied contributor; anything
|
|
39
64
|
* unattributed, untaggable or unresolved is skipped. A replacement's two decorations are collapsed
|
|
40
65
|
* into a single tag, on whichever half renders first — see `isSameChange` and `leadsReplacement`.
|
|
66
|
+
*
|
|
67
|
+
* `activeIndexPos`, when given, reveals exactly one of the tags — see `resolveActiveTarget`.
|
|
68
|
+
* Absent, each tag keeps the active state its own decoration was drawn with.
|
|
69
|
+
*
|
|
70
|
+
* `stops`, when given, is the navigation stop list the step buttons walk
|
|
71
|
+
* (`getScrollableDecorations`). One contributor cannot hold two tags in one stop, since only the
|
|
72
|
+
* leading one would ever be reachable. Absent, every decoration keeps its own tag.
|
|
41
73
|
*/
|
|
42
|
-
export const extractContributorTags = (decorations, contributors) => {
|
|
74
|
+
export const extractContributorTags = (decorations, contributors, activeIndexPos, stops) => {
|
|
43
75
|
if (!contributors) {
|
|
44
76
|
return [];
|
|
45
77
|
}
|
|
@@ -77,10 +109,15 @@ export const extractContributorTags = (decorations, contributors) => {
|
|
|
77
109
|
const linkedDiffIds = new Map();
|
|
78
110
|
const folded = new Set();
|
|
79
111
|
const fold = (host, target) => {
|
|
80
|
-
var _linkedDiffIds$get;
|
|
112
|
+
var _linkedDiffIds$get, _linkedDiffIds$get2;
|
|
81
113
|
folded.add(target);
|
|
82
114
|
const hostDiffId = host.decoration.spec.diffId;
|
|
83
|
-
|
|
115
|
+
const targetDiffId = target.decoration.spec.diffId;
|
|
116
|
+
// A target that already hosts folded tags hands them up, so no hover target is orphaned when
|
|
117
|
+
// one host folds into another.
|
|
118
|
+
const inherited = (_linkedDiffIds$get = linkedDiffIds.get(targetDiffId)) !== null && _linkedDiffIds$get !== void 0 ? _linkedDiffIds$get : [];
|
|
119
|
+
linkedDiffIds.delete(targetDiffId);
|
|
120
|
+
linkedDiffIds.set(hostDiffId, [...((_linkedDiffIds$get2 = linkedDiffIds.get(hostDiffId)) !== null && _linkedDiffIds$get2 !== void 0 ? _linkedDiffIds$get2 : []), targetDiffId, ...inherited]);
|
|
84
121
|
};
|
|
85
122
|
for (const target of targets) {
|
|
86
123
|
const {
|
|
@@ -120,15 +157,38 @@ export const extractContributorTags = (decorations, contributors) => {
|
|
|
120
157
|
fold(host, target);
|
|
121
158
|
}
|
|
122
159
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
160
|
+
|
|
161
|
+
// One tag per contributor per navigation stop. The folds above only catch a replacement's two
|
|
162
|
+
// halves and a block's own highlights; two of one contributor's changes that merely touch are a
|
|
163
|
+
// single stop (`groupTouchingDecorations`) yet kept a tag each, so the stop's trailing tag was
|
|
164
|
+
// drawn but could never be stepped to (EDITOR-8971). It folds into the leading tag of its
|
|
165
|
+
// contributor, staying a hover target. Contributors are kept apart: a stop spanning two of them
|
|
166
|
+
// still captions each, rather than crediting one for the other's change.
|
|
167
|
+
for (const stop of stops !== null && stops !== void 0 ? stops : []) {
|
|
168
|
+
const byContributor = new Map();
|
|
169
|
+
for (const target of containedBy(targets.filter(target => !folded.has(target)), stop)) {
|
|
170
|
+
var _target$decoration$sp, _byContributor$get;
|
|
171
|
+
const key = (_target$decoration$sp = target.decoration.spec.attributionKey) !== null && _target$decoration$sp !== void 0 ? _target$decoration$sp : '';
|
|
172
|
+
byContributor.set(key, [...((_byContributor$get = byContributor.get(key)) !== null && _byContributor$get !== void 0 ? _byContributor$get : []), target]);
|
|
127
173
|
}
|
|
128
|
-
|
|
174
|
+
for (const [leader, ...trailing] of byContributor.values()) {
|
|
175
|
+
trailing.forEach(target => fold(leader, target));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const surviving = targets.filter(target => !folded.has(target));
|
|
179
|
+
const activeTarget = activeIndexPos === undefined ? undefined : resolveActiveTarget(surviving, activeIndexPos);
|
|
180
|
+
return surviving.map(target => {
|
|
181
|
+
const {
|
|
182
|
+
contributor,
|
|
183
|
+
decoration: {
|
|
184
|
+
spec
|
|
185
|
+
}
|
|
186
|
+
} = target;
|
|
129
187
|
const connected = contributor.connectedToKey ? contributors[contributor.connectedToKey] : undefined;
|
|
130
188
|
const connectedContributor = connected ? toTagContributor(connected) : undefined;
|
|
131
189
|
const linked = linkedDiffIds.get(spec.diffId);
|
|
190
|
+
// Only the navigated tag reveals; with no active range, the decoration's own state stands.
|
|
191
|
+
const isActive = activeIndexPos === undefined ? spec.isActive : target === activeTarget;
|
|
132
192
|
return {
|
|
133
193
|
contributor: toTagContributor(contributor),
|
|
134
194
|
diffId: spec.diffId,
|
|
@@ -138,8 +198,8 @@ export const extractContributorTags = (decorations, contributors) => {
|
|
|
138
198
|
...(spec.colorScheme ? {
|
|
139
199
|
colorScheme: spec.colorScheme
|
|
140
200
|
} : {}),
|
|
141
|
-
...(
|
|
142
|
-
isActive
|
|
201
|
+
...(isActive !== undefined ? {
|
|
202
|
+
isActive
|
|
143
203
|
} : {}),
|
|
144
204
|
...(spec.isInserted !== undefined ? {
|
|
145
205
|
isInserted: spec.isInserted
|
|
@@ -49,6 +49,7 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
|
|
|
49
49
|
return attrStepContexts.map(attrStepContext => {
|
|
50
50
|
const {
|
|
51
51
|
afterNode,
|
|
52
|
+
attributionKey,
|
|
52
53
|
beforeNode,
|
|
53
54
|
finalPos,
|
|
54
55
|
originalPos,
|
|
@@ -74,6 +75,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
|
|
|
74
75
|
const watchedAttrs = inlineNodeAttrMap[nodeName];
|
|
75
76
|
if (attrsToCheck.some(v => watchedAttrs.includes(v))) {
|
|
76
77
|
return {
|
|
78
|
+
...(attributionKey ? {
|
|
79
|
+
attributionKey
|
|
80
|
+
} : {}),
|
|
77
81
|
fromB: finalPos,
|
|
78
82
|
toB: finalPos + nodeAtPos.nodeSize,
|
|
79
83
|
isInline: true,
|
|
@@ -90,6 +94,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
|
|
|
90
94
|
// taskItem node: state attribute change — highlight the taskItem node
|
|
91
95
|
if (attrsToCheck.some(v => taskItemAttrs.includes(v)) && (nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.type.name) === 'taskItem') {
|
|
92
96
|
return {
|
|
97
|
+
...(attributionKey ? {
|
|
98
|
+
attributionKey
|
|
99
|
+
} : {}),
|
|
93
100
|
fromB: finalPos,
|
|
94
101
|
toB: finalPos + nodeAtPos.nodeSize
|
|
95
102
|
};
|
|
@@ -101,6 +108,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
|
|
|
101
108
|
// panel as a "deleted" widget for a before/after comparison.
|
|
102
109
|
if (attrsToCheck.some(v => panelAttrs.includes(v)) && nodeAtPos && getBaseNodeTypeName(nodeAtPos.type) === 'panel') {
|
|
103
110
|
return {
|
|
111
|
+
...(attributionKey ? {
|
|
112
|
+
attributionKey
|
|
113
|
+
} : {}),
|
|
104
114
|
fromB: finalPos,
|
|
105
115
|
toB: finalPos + nodeAtPos.nodeSize,
|
|
106
116
|
...(originalPos !== undefined && originalNodeAtPos && {
|
|
@@ -118,6 +128,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
|
|
|
118
128
|
}
|
|
119
129
|
const isInline = nodeAtPos.type.name === 'inlineExtension';
|
|
120
130
|
return {
|
|
131
|
+
...(attributionKey ? {
|
|
132
|
+
attributionKey
|
|
133
|
+
} : {}),
|
|
121
134
|
fromB: finalPos,
|
|
122
135
|
toB: finalPos + nodeAtPos.nodeSize,
|
|
123
136
|
isInline
|
|
@@ -128,6 +141,9 @@ export const getAttrChangeRanges = (doc, attrStepContexts, originalDoc) => {
|
|
|
128
141
|
if (attrsToCheck.some(v => mediaAttrs.includes(v)) && $pos.parent.type === doc.type.schema.nodes.mediaSingle) {
|
|
129
142
|
const startPos = $pos.pos + $pos.parentOffset;
|
|
130
143
|
return {
|
|
144
|
+
...(attributionKey ? {
|
|
145
|
+
attributionKey
|
|
146
|
+
} : {}),
|
|
131
147
|
fromB: startPos,
|
|
132
148
|
toB: startPos + $pos.parent.nodeSize - 1
|
|
133
149
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AddMarkStep, RemoveMarkStep } from '@atlaskit/editor-prosemirror/transform';
|
|
2
|
+
import { getAttributionKey } from '../colorSchemes/attributions';
|
|
3
|
+
import { mapStepRangeToFinal } from './mapStepRangeToFinal';
|
|
2
4
|
const extractMarkStep = step => {
|
|
3
5
|
if (step instanceof AddMarkStep) {
|
|
4
6
|
return {
|
|
@@ -18,10 +20,10 @@ const extractMarkStep = step => {
|
|
|
18
20
|
}
|
|
19
21
|
return undefined;
|
|
20
22
|
};
|
|
21
|
-
export const getMarkChangeRanges = steps => {
|
|
23
|
+
export const getMarkChangeRanges = (steps, stepAttributions = [], stepMaps = []) => {
|
|
22
24
|
const resultRanges = [];
|
|
23
25
|
let lastOp;
|
|
24
|
-
for (const step of steps) {
|
|
26
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
25
27
|
const op = extractMarkStep(step);
|
|
26
28
|
if (!op) {
|
|
27
29
|
continue;
|
|
@@ -31,9 +33,15 @@ export const getMarkChangeRanges = steps => {
|
|
|
31
33
|
if (lastOp && lastOp.from === op.from && lastOp.to === op.to && lastOp.markName === op.markName && lastOp.type !== op.type) {
|
|
32
34
|
resultRanges.pop();
|
|
33
35
|
} else {
|
|
36
|
+
const finalRange = mapStepRangeToFinal(op.from, op.to, stepIndex, stepMaps);
|
|
37
|
+
if (!finalRange) {
|
|
38
|
+
lastOp = op;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
34
41
|
resultRanges.push({
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
attributionKey: getAttributionKey(stepAttributions[stepIndex]),
|
|
43
|
+
fromB: finalRange.fromB,
|
|
44
|
+
toB: finalRange.toB
|
|
37
45
|
});
|
|
38
46
|
}
|
|
39
47
|
lastOp = op;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Mapping } from '@atlaskit/editor-prosemirror/transform';
|
|
2
|
+
// ProseMirror's assoc value chooses which side of an insertion a mapped boundary stays on.
|
|
3
|
+
const FROM_ASSOC = -1;
|
|
4
|
+
const TO_ASSOC = 1;
|
|
5
|
+
|
|
6
|
+
/** Maps a step's range into final-document coordinates when the rollout path supplies step maps. */
|
|
7
|
+
export const mapStepRangeToFinal = (from, to, stepIndex, stepMaps) => {
|
|
8
|
+
if (!stepMaps) {
|
|
9
|
+
return {
|
|
10
|
+
fromB: from,
|
|
11
|
+
toB: to
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// The range is already in the document immediately after this step, so skip its own map and
|
|
16
|
+
// apply only the maps from subsequent steps.
|
|
17
|
+
const firstSubsequentStepIndex = stepIndex + 1;
|
|
18
|
+
const finalRange = new Mapping(stepMaps.slice(firstSubsequentStepIndex));
|
|
19
|
+
const fromB = finalRange.mapResult(from, FROM_ASSOC);
|
|
20
|
+
const toB = finalRange.mapResult(to, TO_ASSOC);
|
|
21
|
+
if (fromB.deleted && toB.deleted) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
fromB: fromB.pos,
|
|
26
|
+
toB: toB.pos
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -4,6 +4,7 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
|
4
4
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
5
5
|
import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform-override';
|
|
6
6
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
7
|
+
import { getActiveDiffAnnouncement } from '../ui/DiffNavigation/announceActiveDiff';
|
|
7
8
|
import { calculateDiffDecorations } from './calculateDiff/calculateDiffDecorations';
|
|
8
9
|
import { isDiffDecoration, isDiffDecorationSpec } from './decorations/decorationKeys';
|
|
9
10
|
import { enforceCustomStepRegisters } from './enforceCustomStepRegisters';
|
|
@@ -285,6 +286,23 @@ export const createPlugin = (config, getIntl, api, onEditorView) => {
|
|
|
285
286
|
api === null || api === void 0 ? void 0 : api.core.actions.execute(toggleExpandRange(activeDecoration.from, activeDecoration.to, true));
|
|
286
287
|
}
|
|
287
288
|
cancelPendingScrollToDecoration = scrollToDecoration(view, scrollableDecorations, pluginState.activeIndex);
|
|
289
|
+
|
|
290
|
+
// Stepping only scrolls — no focus moves and no content changes — so without this a
|
|
291
|
+
// screen-reader user gets silence. Announced through the live region rather than by
|
|
292
|
+
// focusing the change, since the "next change" control has to stay focused to be
|
|
293
|
+
// pressed again. After the scroll above, so the announcement never precedes it.
|
|
294
|
+
if (isExtendedEnabled(pluginState === null || pluginState === void 0 ? void 0 : pluginState.diffType)) {
|
|
295
|
+
const announcement = getActiveDiffAnnouncement({
|
|
296
|
+
activeIndex: pluginState.activeIndex,
|
|
297
|
+
contributorTags: pluginState.contributorTags,
|
|
298
|
+
decorations: scrollableDecorations,
|
|
299
|
+
intl: getIntl()
|
|
300
|
+
});
|
|
301
|
+
if (announcement) {
|
|
302
|
+
var _api$accessibilityUti;
|
|
303
|
+
api === null || api === void 0 ? void 0 : (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 ? void 0 : _api$accessibilityUti.actions.ariaNotify(announcement);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
288
306
|
}
|
|
289
307
|
},
|
|
290
308
|
destroy() {
|
|
@@ -41,6 +41,30 @@ export const CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE = 'data-revealed';
|
|
|
41
41
|
*/
|
|
42
42
|
export const TAG_EXIT_FALLBACK_MS = 1200;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The tooltip's look, inline rather than through `VANILLA_TOOLTIP_DEFAULT_CLASS`: this tooltip is
|
|
46
|
+
* hoisted out of the tag, and that class's rule is scoped under `.ProseMirror` — see
|
|
47
|
+
* `resolveTooltipContainer`. Otherwise `vanillaTooltipDefaultStyles` in editor-core, which is the
|
|
48
|
+
* look every other vanilla tooltip has; keep the two in step.
|
|
49
|
+
*/
|
|
50
|
+
export const CONTRIBUTOR_TAG_TOOLTIP_STYLES = {
|
|
51
|
+
boxSizing: 'border-box',
|
|
52
|
+
maxWidth: '240px',
|
|
53
|
+
backgroundColor: "var(--ds-background-neutral-bold, #292A2E)",
|
|
54
|
+
// A `popover` is given one by the UA stylesheet.
|
|
55
|
+
border: 'none',
|
|
56
|
+
borderRadius: "var(--ds-radius-small, 3px)",
|
|
57
|
+
color: "var(--ds-text-inverse, #FFFFFF)",
|
|
58
|
+
font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
|
|
59
|
+
fontFamily: "var(--ds-font-family-body, \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
|
|
60
|
+
overflowWrap: 'break-word',
|
|
61
|
+
paddingBlock: "var(--ds-space-050, 4px)",
|
|
62
|
+
paddingInline: "var(--ds-space-075, 6px)",
|
|
63
|
+
whiteSpace: 'normal',
|
|
64
|
+
// A tooltip is never a hit target — it hangs over the document's own content.
|
|
65
|
+
pointerEvents: 'none'
|
|
66
|
+
};
|
|
67
|
+
|
|
44
68
|
// Positioned against the host widget its decoration renders on the change's first character:
|
|
45
69
|
// `bottom: 100%` lifts the tag onto the line above, `inset-inline-start` starts it on that
|
|
46
70
|
// character.
|
|
@@ -124,6 +148,17 @@ const srLabelStyle = convertToInlineCss({
|
|
|
124
148
|
userSelect: 'none',
|
|
125
149
|
whiteSpace: 'nowrap'
|
|
126
150
|
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Ids for the hidden label that the tag's `aria-labelledby` points at. Namespaced and random because
|
|
154
|
+
* the tag renders inside the document and shares the host page's id space — the same reason
|
|
155
|
+
* `VanillaTooltip` does it this way. The counter is a fallback for non-secure contexts, where
|
|
156
|
+
* `crypto.randomUUID` is unavailable.
|
|
157
|
+
*/
|
|
158
|
+
const generateLabelId = (() => {
|
|
159
|
+
let count = 0;
|
|
160
|
+
return () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? `${CONTRIBUTOR_TAG_CLASS}-label-${crypto.randomUUID()}` : `${CONTRIBUTOR_TAG_CLASS}-label-${count += 1}`;
|
|
161
|
+
})();
|
|
127
162
|
const createSpan = (doc, style, attributes = {}) => {
|
|
128
163
|
const span = doc.createElement('span');
|
|
129
164
|
span.setAttribute('style', style);
|
|
@@ -139,21 +174,30 @@ const createSpan = (doc, style, attributes = {}) => {
|
|
|
139
174
|
* would need a cast per reference below.
|
|
140
175
|
*/
|
|
141
176
|
export const buildContributorTagDom = doc => {
|
|
177
|
+
const labelId = generateLabelId();
|
|
142
178
|
const root = createSpan(doc, constraintStyle);
|
|
143
179
|
const tag = createSpan(doc, tagStyle, {
|
|
144
180
|
class: CONTRIBUTOR_TAG_CLASS,
|
|
145
181
|
'data-testid': CONTRIBUTOR_TAG_TESTID,
|
|
146
|
-
//
|
|
147
|
-
//
|
|
182
|
+
// Named by the hidden label rather than `aria-label`, which would be a second copy of the same
|
|
183
|
+
// sentence. `role="note"` takes its name from the author, not its contents, so without this
|
|
184
|
+
// the tag is a focus stop with no accessible name.
|
|
185
|
+
'aria-labelledby': labelId,
|
|
148
186
|
role: 'note',
|
|
149
|
-
// Focusable so keyboard and screen-reader users can reach the tag and its tooltip.
|
|
187
|
+
// Focusable so keyboard and screen-reader users can reach the tag and its tooltip. Its focus
|
|
188
|
+
// ring is drawn inward — see `contributorTagStyles` in editor-core.
|
|
150
189
|
tabindex: '0'
|
|
151
190
|
});
|
|
152
191
|
const avatars = createSpan(doc, avatarsStyle);
|
|
192
|
+
// `aria-hidden`: the hidden label restates this name in a full sentence, so leaving it in the
|
|
193
|
+
// tree announced the contributor twice ("Priya Changed by Priya").
|
|
153
194
|
const name = createSpan(doc, nameStyle, {
|
|
195
|
+
'aria-hidden': 'true',
|
|
154
196
|
'data-testid': CONTRIBUTOR_TAG_NAME_TESTID
|
|
155
197
|
});
|
|
156
|
-
const srLabel = createSpan(doc, srLabelStyle
|
|
198
|
+
const srLabel = createSpan(doc, srLabelStyle, {
|
|
199
|
+
id: labelId
|
|
200
|
+
});
|
|
157
201
|
tag.append(avatars, name, srLabel);
|
|
158
202
|
root.appendChild(tag);
|
|
159
203
|
return {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { contributorTagMessages } from './messages';
|
|
2
|
+
const getContributorName = (contributor, formatMessage) => {
|
|
3
|
+
var _contributor$name, _contributor$agentKin;
|
|
4
|
+
const name = (_contributor$name = contributor.name) === null || _contributor$name === void 0 ? void 0 : _contributor$name.trim();
|
|
5
|
+
if (name) {
|
|
6
|
+
return name;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// `agentKind` defaults to `'external'` per the plugin type contract; an unidentified external
|
|
10
|
+
// agent falls back to a generic label rather than rendering a nameless tag.
|
|
11
|
+
if (contributor.kind === 'agent' && ((_contributor$agentKin = contributor.agentKind) !== null && _contributor$agentKin !== void 0 ? _contributor$agentKin : 'external') === 'external') {
|
|
12
|
+
return formatMessage(contributorTagMessages.externalAgentName);
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether the label has to spell out that the contributor is an agent. A sighted user reads that off
|
|
19
|
+
* the `aria-hidden` avatar, so the label is the only other carrier.
|
|
20
|
+
*
|
|
21
|
+
* Excluded: a connected pair, which says it via `changedByConnected`, and an unnamed external agent,
|
|
22
|
+
* whose name `getContributorName` has already resolved to "External agent".
|
|
23
|
+
*/
|
|
24
|
+
const isUnaccompaniedNamedAgent = model => {
|
|
25
|
+
var _model$contributor$na;
|
|
26
|
+
return !model.connectedContributor && model.contributor.kind === 'agent' && Boolean((_model$contributor$na = model.contributor.name) === null || _model$contributor$na === void 0 ? void 0 : _model$contributor$na.trim());
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The two strings a tag's model resolves to. Shared with `getActiveDiffAnnouncement`, so stepping
|
|
30
|
+
* onto a change says the same sentence as the tag pinned to it rather than a copy free to drift.
|
|
31
|
+
*/
|
|
32
|
+
export const formatContributorLabel = (model, formatMessage) => {
|
|
33
|
+
const isPrimaryAgent = model.contributor.kind === 'agent';
|
|
34
|
+
const primaryName = getContributorName(model.contributor, formatMessage);
|
|
35
|
+
const secondaryName = model.connectedContributor ? getContributorName(model.connectedContributor, formatMessage) : '';
|
|
36
|
+
const agentName = isPrimaryAgent ? primaryName : secondaryName;
|
|
37
|
+
const userName = isPrimaryAgent ? secondaryName : primaryName;
|
|
38
|
+
return {
|
|
39
|
+
fullLabel: model.connectedContributor ? formatMessage(contributorTagMessages.changedByConnected, {
|
|
40
|
+
agentName,
|
|
41
|
+
userName
|
|
42
|
+
}) : formatMessage(isUnaccompaniedNamedAgent(model) ? contributorTagMessages.changedByAgent : contributorTagMessages.changedBy, {
|
|
43
|
+
name: primaryName
|
|
44
|
+
}),
|
|
45
|
+
// A connected pair only ever shows the agent; the pair is spelled out in `fullLabel`, because
|
|
46
|
+
// the tag is capped at `MAX_TAG_WIDTH` and two names rarely fit inside it.
|
|
47
|
+
visibleName: model.connectedContributor ? agentName || primaryName : primaryName
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -3,24 +3,9 @@ import { bind, bindAll } from 'bind-event-listener';
|
|
|
3
3
|
import { VanillaTooltip } from '@atlaskit/editor-common/vanilla-tooltip';
|
|
4
4
|
import { getAccentTokens } from '../../pm-plugins/decorations/colorSchemes/factory';
|
|
5
5
|
import { colorSchemeRegistry } from '../../pm-plugins/decorations/colorSchemes/schemes';
|
|
6
|
-
import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
|
|
6
|
+
import { buildContributorTagDom, CONTRIBUTOR_TAG_REVEALED_ATTRIBUTE, CONTRIBUTOR_TAG_TOOLTIP_STYLES, TAG_EXIT_FALLBACK_MS } from './buildContributorTagDom';
|
|
7
7
|
import { contributorAvatarRenderer } from './contributorAvatarRenderer';
|
|
8
|
-
import {
|
|
9
|
-
const getContributorName = (contributor, formatMessage) => {
|
|
10
|
-
var _contributor$name, _contributor$agentKin;
|
|
11
|
-
const name = (_contributor$name = contributor.name) === null || _contributor$name === void 0 ? void 0 : _contributor$name.trim();
|
|
12
|
-
if (name) {
|
|
13
|
-
return name;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// `agentKind` defaults to `'external'` per the plugin type contract; an unidentified external
|
|
17
|
-
// agent falls back to a generic label rather than rendering a nameless tag.
|
|
18
|
-
if (contributor.kind === 'agent' && ((_contributor$agentKin = contributor.agentKind) !== null && _contributor$agentKin !== void 0 ? _contributor$agentKin : 'external') === 'external') {
|
|
19
|
-
return formatMessage(contributorTagMessages.externalAgentName);
|
|
20
|
-
}
|
|
21
|
-
return '';
|
|
22
|
-
};
|
|
23
|
-
|
|
8
|
+
import { formatContributorLabel } from './contributorLabel';
|
|
24
9
|
/**
|
|
25
10
|
* Resolves the tag's accent from the same scheme value the highlight was drawn from, so the colour
|
|
26
11
|
* the two carry can never drift — only the tone does, which is `getAccentTokens`'s to pick.
|
|
@@ -99,8 +84,8 @@ const fadeTagOut = (tag, release) => {
|
|
|
99
84
|
|
|
100
85
|
/** The contributors a tag draws, keyed so a republished model can be recognised as the same pair. */
|
|
101
86
|
const identityOf = model => [model === null || model === void 0 ? void 0 : model.contributor, model === null || model === void 0 ? void 0 : model.connectedContributor].map(contributor => {
|
|
102
|
-
var _contributor$
|
|
103
|
-
return contributor ? `${contributor.kind}:${(_contributor$
|
|
87
|
+
var _contributor$agentKin, _contributor$avatarUr;
|
|
88
|
+
return contributor ? `${contributor.kind}:${(_contributor$agentKin = contributor.agentKind) !== null && _contributor$agentKin !== void 0 ? _contributor$agentKin : ''}:${contributor.name}:${(_contributor$avatarUr = contributor.avatarUrl) !== null && _contributor$avatarUr !== void 0 ? _contributor$avatarUr : ''}` : '';
|
|
104
89
|
}).join('|');
|
|
105
90
|
|
|
106
91
|
/**
|
|
@@ -322,24 +307,12 @@ export class ContributorTagController {
|
|
|
322
307
|
return;
|
|
323
308
|
}
|
|
324
309
|
const {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const userName = isPrimaryAgent ? secondaryName : primaryName;
|
|
332
|
-
|
|
333
|
-
// A connected pair only ever shows the agent; the pair is spelled out in the label, because the
|
|
334
|
-
// tag is capped at `MAX_TAG_WIDTH` and two names rarely fit inside it.
|
|
335
|
-
this.dom.name.textContent = model.connectedContributor ? agentName || primaryName : primaryName;
|
|
336
|
-
this.fullLabel = model.connectedContributor ? formatMessage(contributorTagMessages.changedByConnected, {
|
|
337
|
-
agentName,
|
|
338
|
-
userName
|
|
339
|
-
}) : formatMessage(contributorTagMessages.changedBy, {
|
|
340
|
-
name: primaryName
|
|
341
|
-
});
|
|
342
|
-
this.dom.srLabel.textContent = this.fullLabel;
|
|
310
|
+
fullLabel,
|
|
311
|
+
visibleName
|
|
312
|
+
} = formatContributorLabel(model, this.options.getIntl().formatMessage);
|
|
313
|
+
this.dom.name.textContent = visibleName;
|
|
314
|
+
this.fullLabel = fullLabel;
|
|
315
|
+
this.dom.srLabel.textContent = fullLabel;
|
|
343
316
|
}
|
|
344
317
|
accentOf(model) {
|
|
345
318
|
var _model$isInserted;
|
|
@@ -395,15 +368,37 @@ export class ContributorTagController {
|
|
|
395
368
|
if (!content || !this.dom) {
|
|
396
369
|
return;
|
|
397
370
|
}
|
|
398
|
-
this.tooltip = new VanillaTooltip(this.dom.tag, content,
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
|
|
371
|
+
this.tooltip = new VanillaTooltip(this.dom.tag, content,
|
|
372
|
+
// Generated id.
|
|
373
|
+
undefined,
|
|
374
|
+
// No class: the look is inline, because a hoisted tooltip is outside the `.ProseMirror`
|
|
375
|
+
// scope `VANILLA_TOOLTIP_DEFAULT_CLASS` is keyed on — see `CONTRIBUTOR_TAG_TOOLTIP_STYLES`.
|
|
376
|
+
'',
|
|
377
|
+
// Default delay, no `onShow`, and the default `top` placement.
|
|
378
|
+
undefined, CONTRIBUTOR_TAG_TOOLTIP_STYLES, undefined, undefined, this.resolveTooltipContainer());
|
|
403
379
|
// `VanillaTooltip` sets `aria-describedby` on its trigger, which would announce the label a
|
|
404
|
-
// second time — the
|
|
405
|
-
// requires.
|
|
380
|
+
// second time — the hidden child already announces it, and names the tag with it.
|
|
406
381
|
this.dom.tag.removeAttribute('aria-describedby');
|
|
382
|
+
// Out of the tag but still in the document, so its text would otherwise be a third copy of the
|
|
383
|
+
// same sentence. It stays visible for sighted users, who need the part of the name the tag
|
|
384
|
+
// clipped.
|
|
385
|
+
this.tooltip.element.setAttribute('aria-hidden', 'true');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Where the tooltip is appended, rather than inside the tag.
|
|
390
|
+
*
|
|
391
|
+
* Popper and the browser only agree on a top-layer popover's origin when nothing above it is
|
|
392
|
+
* transformed, and the tag hangs under the editor's own transformed nodes — inside a wide image
|
|
393
|
+
* that is `.rich-media-item`, whose `translateX(-50%)` threw the tooltip off screen
|
|
394
|
+
* (EDITOR-8971). The content area is the nearest ancestor with none of that above it, and keeps
|
|
395
|
+
* the tooltip inside the editor it belongs to; the document body covers an appearance that has
|
|
396
|
+
* no content area.
|
|
397
|
+
*/
|
|
398
|
+
resolveTooltipContainer() {
|
|
399
|
+
var _editorRoot$closest;
|
|
400
|
+
const editorRoot = this.options.getEditorRoot();
|
|
401
|
+
return (_editorRoot$closest = editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.closest('.ak-editor-content-area')) !== null && _editorRoot$closest !== void 0 ? _editorRoot$closest : editorRoot === null || editorRoot === void 0 ? void 0 : editorRoot.ownerDocument.body;
|
|
407
402
|
}
|
|
408
403
|
|
|
409
404
|
/**
|
|
@@ -5,6 +5,11 @@ export const contributorTagMessages = defineMessages({
|
|
|
5
5
|
defaultMessage: 'Changed by {name}',
|
|
6
6
|
description: 'Screen-reader label and tooltip for the tag pinned to a highlighted change in the version-history diff, naming the single person or agent that made the change. The name placeholder is the contributor display name.'
|
|
7
7
|
},
|
|
8
|
+
changedByAgent: {
|
|
9
|
+
id: 'editor-plugin-show-diff.ContributorTag.changedByAgent',
|
|
10
|
+
defaultMessage: 'Changed by {name}, an AI agent',
|
|
11
|
+
description: 'Screen-reader label and tooltip for the tag pinned to a highlighted change in the version-history diff, when a named AI agent made the change rather than a person. The name placeholder is the agent display name.'
|
|
12
|
+
},
|
|
8
13
|
changedByConnected: {
|
|
9
14
|
id: 'editor-plugin-show-diff.ContributorTag.changedByConnected',
|
|
10
15
|
defaultMessage: 'Changed by {agentName} with {userName}',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isDiffDecoration } from '../../pm-plugins/decorations/decorationKeys';
|
|
2
|
+
import { formatContributorLabel } from '../ContributorTag/contributorLabel';
|
|
3
|
+
import { diffNavigationMessages } from './messages';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The tag that captions the given decoration, if any. `linkedDiffIds` is checked as well as
|
|
7
|
+
* `diffId`, because a replacement's two decorations share one tag and navigation can land on either.
|
|
8
|
+
*/
|
|
9
|
+
const findTagFor = (decoration, contributorTags) => {
|
|
10
|
+
if (!(contributorTags !== null && contributorTags !== void 0 && contributorTags.length) || !isDiffDecoration(decoration)) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const {
|
|
14
|
+
diffId
|
|
15
|
+
} = decoration.spec;
|
|
16
|
+
return contributorTags.find(tag => {
|
|
17
|
+
var _tag$linkedDiffIds;
|
|
18
|
+
return tag.diffId === diffId || ((_tag$linkedDiffIds = tag.linkedDiffIds) === null || _tag$linkedDiffIds === void 0 ? void 0 : _tag$linkedDiffIds.includes(diffId));
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a screen reader should say once the reader has stepped onto a change: where they are in the
|
|
24
|
+
* diff, and — when the plugin can credit the change — who made it. Stepping otherwise only scrolls,
|
|
25
|
+
* which is silent.
|
|
26
|
+
*
|
|
27
|
+
* Returns `undefined` for an index that resolves no change, so the caller says nothing at all
|
|
28
|
+
* rather than "Change 4 of 3".
|
|
29
|
+
*/
|
|
30
|
+
export const getActiveDiffAnnouncement = ({
|
|
31
|
+
activeIndex,
|
|
32
|
+
contributorTags,
|
|
33
|
+
decorations,
|
|
34
|
+
intl
|
|
35
|
+
}) => {
|
|
36
|
+
const activeDecoration = decorations[activeIndex];
|
|
37
|
+
if (!activeDecoration) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const position = {
|
|
41
|
+
index: activeIndex + 1,
|
|
42
|
+
total: decorations.length
|
|
43
|
+
};
|
|
44
|
+
const tag = findTagFor(activeDecoration, contributorTags);
|
|
45
|
+
return tag ? intl.formatMessage(diffNavigationMessages.activeChangeWithContributor, {
|
|
46
|
+
...position,
|
|
47
|
+
contributor: formatContributorLabel(tag, intl.formatMessage).fullLabel
|
|
48
|
+
}) : intl.formatMessage(diffNavigationMessages.activeChange, position);
|
|
49
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
export const diffNavigationMessages = defineMessages({
|
|
3
|
+
activeChange: {
|
|
4
|
+
id: 'editor-plugin-show-diff.DiffNavigation.activeChange',
|
|
5
|
+
defaultMessage: 'Change {index} of {total}',
|
|
6
|
+
description: 'Announced to screen readers when the reader steps to the next or previous change in the version-history diff. The index placeholder is the position of the change stepped to, counting from 1; total is how many changes the diff has.'
|
|
7
|
+
},
|
|
8
|
+
activeChangeWithContributor: {
|
|
9
|
+
id: 'editor-plugin-show-diff.DiffNavigation.activeChangeWithContributor',
|
|
10
|
+
defaultMessage: 'Change {index} of {total}. {contributor}',
|
|
11
|
+
description: 'Announced to screen readers when the reader steps to the next or previous change in the version-history diff and that change is credited to someone. The index and total placeholders are as in activeChange. The contributor placeholder is an already-translated sentence (changedBy, changedByAgent or changedByConnected), so it must be kept whole and only repositioned.'
|
|
12
|
+
}
|
|
13
|
+
});
|