@atlaskit/editor-plugin-card 18.3.2 → 18.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/ui/PasteDisplayAsMenu.js +62 -16
- package/dist/cjs/ui/nativeEmbedNode.js +132 -0
- package/dist/cjs/ui/pasteDisplayAsUtils.js +86 -23
- package/dist/es2019/ui/PasteDisplayAsMenu.js +60 -16
- package/dist/es2019/ui/nativeEmbedNode.js +132 -0
- package/dist/es2019/ui/pasteDisplayAsUtils.js +78 -22
- package/dist/esm/ui/PasteDisplayAsMenu.js +62 -16
- package/dist/esm/ui/nativeEmbedNode.js +125 -0
- package/dist/esm/ui/pasteDisplayAsUtils.js +85 -23
- package/dist/types/ui/nativeEmbedNode.d.ts +43 -0
- package/dist/types/ui/pasteDisplayAsUtils.d.ts +17 -2
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isSafeUrl } from '@atlaskit/adf-schema/url';
|
|
2
|
+
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
3
|
+
import { NATIVE_EMBED_EXTENSION_KEY, NATIVE_EMBED_EXTENSION_TYPE } from '@atlaskit/editor-common/extensions';
|
|
4
|
+
/**
|
|
5
|
+
* A native embed is an `extension` node that `editor-plugin-native-embeds` builds from
|
|
6
|
+
* embedCard ADF (via the `embedCardNodeTransformer` registered on the card plugin).
|
|
7
|
+
* A 1P link that resolves to an embed appearance — a whiteboard, page, slide or
|
|
8
|
+
* database — therefore lands in the document as one of these rather than as an
|
|
9
|
+
* `embedCard`, so card node types alone are not enough to describe what was pasted.
|
|
10
|
+
*
|
|
11
|
+
* `editor-plugin-native-embeds` owns the equivalent predicate, but the card plugin
|
|
12
|
+
* cannot depend on it: that would close the cycle
|
|
13
|
+
* editor-plugin-card -> editor-plugin-native-embeds -> editor-plugin-card.
|
|
14
|
+
*/
|
|
15
|
+
export const isNativeEmbedNode = node => {
|
|
16
|
+
var _node$attrs, _node$attrs2;
|
|
17
|
+
return (node === null || node === void 0 ? void 0 : node.type.name) === 'extension' && ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.extensionType) === NATIVE_EMBED_EXTENSION_TYPE && typeof ((_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.extensionKey) === 'string' && node.attrs.extensionKey.includes(NATIVE_EMBED_EXTENSION_KEY);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reads the embedded URL from a native embed node. The URL is stored in
|
|
22
|
+
* `parameters.macroParams`, where values may be wrapped (`{ value }`) or bare, and is
|
|
23
|
+
* mirrored in `parameters.macroMetadata` so it survives a revert to a card.
|
|
24
|
+
*/
|
|
25
|
+
export const getNativeEmbedUrl = node => {
|
|
26
|
+
var _node$attrs3, _parameters$macroPara, _ref, _parameters$macroMeta;
|
|
27
|
+
const parameters = (_node$attrs3 = node.attrs) === null || _node$attrs3 === void 0 ? void 0 : _node$attrs3.parameters;
|
|
28
|
+
const macroParamUrl = parameters === null || parameters === void 0 ? void 0 : (_parameters$macroPara = parameters.macroParams) === null || _parameters$macroPara === void 0 ? void 0 : _parameters$macroPara.url;
|
|
29
|
+
const url = (_ref = macroParamUrl && typeof macroParamUrl === 'object' && 'value' in macroParamUrl ? macroParamUrl.value : macroParamUrl) !== null && _ref !== void 0 ? _ref : parameters === null || parameters === void 0 ? void 0 : (_parameters$macroMeta = parameters.macroMetadata) === null || _parameters$macroMeta === void 0 ? void 0 : _parameters$macroMeta.url;
|
|
30
|
+
return typeof url === 'string' ? url : undefined;
|
|
31
|
+
};
|
|
32
|
+
const canReplaceNodeWith = (view, pos, nodeType) => {
|
|
33
|
+
if (!nodeType) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const $pos = view.state.doc.resolve(pos);
|
|
38
|
+
const index = $pos.index();
|
|
39
|
+
return $pos.parent.canReplaceWith(index, index + 1, nodeType);
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a native embed at `pos` can be replaced by the given appearance. `url` and
|
|
47
|
+
* `inline` both become a paragraph, `block` becomes a blockCard.
|
|
48
|
+
*/
|
|
49
|
+
export const isNativeEmbedAppearanceSupported = ({
|
|
50
|
+
editorView,
|
|
51
|
+
pos,
|
|
52
|
+
appearance
|
|
53
|
+
}) => {
|
|
54
|
+
const {
|
|
55
|
+
paragraph,
|
|
56
|
+
inlineCard,
|
|
57
|
+
blockCard
|
|
58
|
+
} = editorView.state.schema.nodes;
|
|
59
|
+
if (appearance === 'block') {
|
|
60
|
+
return canReplaceNodeWith(editorView, pos, blockCard);
|
|
61
|
+
}
|
|
62
|
+
if (appearance === 'inline' && !inlineCard) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (appearance === 'url' && !editorView.state.schema.marks.link) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return canReplaceNodeWith(editorView, pos, paragraph);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replaces a native embed with a link, an inline card or a block card. Mirrors
|
|
73
|
+
* `setNativeEmbedAppearance` in `editor-plugin-native-embeds`, which cannot be reused
|
|
74
|
+
* here because of the package cycle described on `isNativeEmbedNode`.
|
|
75
|
+
*/
|
|
76
|
+
export const changeNativeEmbedAppearance = ({
|
|
77
|
+
editorView,
|
|
78
|
+
pos,
|
|
79
|
+
node,
|
|
80
|
+
appearance,
|
|
81
|
+
url,
|
|
82
|
+
editorAnalyticsApi
|
|
83
|
+
}) => {
|
|
84
|
+
const {
|
|
85
|
+
state,
|
|
86
|
+
dispatch
|
|
87
|
+
} = editorView;
|
|
88
|
+
const {
|
|
89
|
+
paragraph,
|
|
90
|
+
inlineCard,
|
|
91
|
+
blockCard
|
|
92
|
+
} = state.schema.nodes;
|
|
93
|
+
const {
|
|
94
|
+
link
|
|
95
|
+
} = state.schema.marks;
|
|
96
|
+
if (!isSafeUrl(url)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
let content;
|
|
100
|
+
if (appearance === 'url' && link && paragraph) {
|
|
101
|
+
content = paragraph.create(null, state.schema.text(url, [link.create({
|
|
102
|
+
href: url
|
|
103
|
+
})]));
|
|
104
|
+
} else if (appearance === 'inline' && inlineCard && paragraph) {
|
|
105
|
+
content = paragraph.create(null, inlineCard.create({
|
|
106
|
+
url
|
|
107
|
+
}));
|
|
108
|
+
} else if (appearance === 'block' && blockCard) {
|
|
109
|
+
content = blockCard.create({
|
|
110
|
+
url
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (!content) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const tr = state.tr.replaceWith(pos, pos + node.nodeSize, content).scrollIntoView();
|
|
118
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent({
|
|
119
|
+
action: ACTION.CHANGED_TYPE,
|
|
120
|
+
actionSubject: ACTION_SUBJECT.NATIVE_EMBED,
|
|
121
|
+
eventType: EVENT_TYPE.TRACK,
|
|
122
|
+
attributes: {
|
|
123
|
+
newType: appearance,
|
|
124
|
+
previousType: 'nativeEmbed'
|
|
125
|
+
}
|
|
126
|
+
})(tr);
|
|
127
|
+
dispatch(tr);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
@@ -1,38 +1,94 @@
|
|
|
1
|
+
import { getNativeEmbedUrl, isNativeEmbedNode } from './nativeEmbedNode';
|
|
1
2
|
export const DISPLAY_AS_OPTIONS = ['url', 'inline', 'block', 'embed'];
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
const cardAtPos = (node, pos) => {
|
|
4
|
+
switch (node === null || node === void 0 ? void 0 : node.type.name) {
|
|
5
|
+
case 'inlineCard':
|
|
6
|
+
return {
|
|
7
|
+
appearance: 'inline',
|
|
8
|
+
pos
|
|
9
|
+
};
|
|
10
|
+
case 'blockCard':
|
|
11
|
+
return {
|
|
12
|
+
appearance: 'block',
|
|
13
|
+
pos
|
|
14
|
+
};
|
|
15
|
+
case 'embedCard':
|
|
16
|
+
return {
|
|
17
|
+
appearance: 'embed',
|
|
18
|
+
pos
|
|
19
|
+
};
|
|
20
|
+
default:
|
|
21
|
+
return isNativeEmbedNode(node) ? {
|
|
22
|
+
appearance: 'embed',
|
|
23
|
+
isNativeEmbed: true,
|
|
24
|
+
pos
|
|
25
|
+
} : undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const getNodeUrl = node => {
|
|
29
|
+
var _attrs$url, _attrs$data;
|
|
30
|
+
if (isNativeEmbedNode(node)) {
|
|
31
|
+
return getNativeEmbedUrl(node);
|
|
32
|
+
}
|
|
33
|
+
const attrs = node.attrs;
|
|
34
|
+
const url = (_attrs$url = attrs === null || attrs === void 0 ? void 0 : attrs.url) !== null && _attrs$url !== void 0 ? _attrs$url : attrs === null || attrs === void 0 ? void 0 : (_attrs$data = attrs.data) === null || _attrs$data === void 0 ? void 0 : _attrs$data.url;
|
|
35
|
+
return typeof url === 'string' ? url : undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Finds the card that a paste produced.
|
|
39
|
+
*
|
|
40
|
+
* The paste range is recorded when the paste happens and then mapped through every later
|
|
41
|
+
* transaction, so by the time the link has resolved into a card the range can point just
|
|
42
|
+
* past that card. `pastedUrl` resolves the ambiguity: when it is known, a candidate
|
|
43
|
+
* holding that URL wins over one that merely sits in the range, which stops a pre-existing
|
|
44
|
+
* neighbouring card from being reported as the pasted one.
|
|
45
|
+
*/
|
|
46
|
+
export const getCardAtPasteRange = (state, pasteStartPos, pasteEndPos, pastedUrl) => {
|
|
4
47
|
const docContentSize = state.doc.content.size;
|
|
5
48
|
const clampedStart = Math.max(0, Math.min(pasteStartPos, docContentSize));
|
|
6
49
|
const clampedEnd = Math.max(0, Math.min(pasteEndPos, docContentSize));
|
|
7
50
|
const from = Math.min(clampedStart, clampedEnd);
|
|
8
51
|
const to = Math.max(clampedStart, clampedEnd);
|
|
9
52
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
const node = state.doc.nodeAt(pos);
|
|
16
|
-
if ((node === null || node === void 0 ? void 0 : node.type.name) === 'inlineCard' || (node === null || node === void 0 ? void 0 : node.type.name) === 'blockCard' || (node === null || node === void 0 ? void 0 : node.type.name) === 'embedCard') {
|
|
17
|
-
return {
|
|
18
|
-
appearance: node.type.name === 'inlineCard' ? 'inline' : node.type.name === 'blockCard' ? 'block' : 'embed',
|
|
19
|
-
pos
|
|
20
|
-
};
|
|
21
|
-
}
|
|
53
|
+
var _inRange;
|
|
54
|
+
const candidateAt = pos => {
|
|
55
|
+
if (pos < 0 || pos >= docContentSize) {
|
|
56
|
+
return undefined;
|
|
22
57
|
}
|
|
23
|
-
|
|
58
|
+
const node = state.doc.nodeAt(pos);
|
|
59
|
+
const card = cardAtPos(node, pos);
|
|
60
|
+
return node && card ? {
|
|
61
|
+
card,
|
|
62
|
+
url: getNodeUrl(node)
|
|
63
|
+
} : undefined;
|
|
64
|
+
};
|
|
65
|
+
const adjacent = [candidateAt(from - 1), candidateAt(from)].filter(candidate => Boolean(candidate));
|
|
66
|
+
const inRange = [];
|
|
24
67
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
68
|
+
const card = cardAtPos(node, pos);
|
|
69
|
+
if (card) {
|
|
70
|
+
inRange.push({
|
|
71
|
+
card,
|
|
72
|
+
url: getNodeUrl(node)
|
|
73
|
+
});
|
|
30
74
|
return false;
|
|
31
75
|
}
|
|
32
76
|
return true;
|
|
33
77
|
});
|
|
78
|
+
if (pastedUrl) {
|
|
79
|
+
const match = [...adjacent, ...inRange].find(candidate => candidate.url === pastedUrl);
|
|
80
|
+
if (match) {
|
|
81
|
+
return match.card;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (from === to) {
|
|
85
|
+
const [firstAdjacent] = adjacent;
|
|
86
|
+
if (firstAdjacent) {
|
|
87
|
+
return firstAdjacent.card;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return (_inRange = inRange[inRange.length - 1]) === null || _inRange === void 0 ? void 0 : _inRange.card;
|
|
34
91
|
} catch {
|
|
35
|
-
return;
|
|
92
|
+
return undefined;
|
|
36
93
|
}
|
|
37
|
-
return result;
|
|
38
94
|
};
|
|
@@ -23,6 +23,7 @@ import { Box, Flex, Pressable } from '@atlaskit/primitives/compiled';
|
|
|
23
23
|
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
24
24
|
import { changeSelectedCardToLink, setSelectedCardAppearance } from '../pm-plugins/doc';
|
|
25
25
|
import { getSingleSmartLinkUrlFromSlice } from './currentPastedSmartLink';
|
|
26
|
+
import { changeNativeEmbedAppearance, getNativeEmbedUrl, isNativeEmbedAppearanceSupported } from './nativeEmbedNode';
|
|
26
27
|
import { getCardAtPasteRange } from './pasteDisplayAsUtils';
|
|
27
28
|
export var SMART_LINK_DISPLAY_AS_PASTE_MENU_SECTION_KEY = 'smart-link-display-as-paste-menu-section';
|
|
28
29
|
|
|
@@ -114,12 +115,22 @@ var getCurrentPastedSlice = function getCurrentPastedSlice(api) {
|
|
|
114
115
|
return slice;
|
|
115
116
|
};
|
|
116
117
|
var getCardUrlAtPasteRange = function getCardUrlAtPasteRange(_ref5) {
|
|
117
|
-
var
|
|
118
|
+
var _maybeAttrs$url, _maybeAttrs$data;
|
|
118
119
|
var editorView = _ref5.editorView,
|
|
119
120
|
pasteStartPos = _ref5.pasteStartPos,
|
|
120
121
|
pasteEndPos = _ref5.pasteEndPos;
|
|
121
122
|
var cardAtPasteRange = getCardAtPasteRange(editorView.state, pasteStartPos, pasteEndPos);
|
|
122
|
-
|
|
123
|
+
if (!cardAtPasteRange) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
var node = editorView.state.doc.nodeAt(cardAtPasteRange.pos);
|
|
127
|
+
if (!node) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (cardAtPasteRange.isNativeEmbed) {
|
|
131
|
+
return getNativeEmbedUrl(node);
|
|
132
|
+
}
|
|
133
|
+
var maybeAttrs = node.attrs;
|
|
123
134
|
var maybeUrl = (_maybeAttrs$url = maybeAttrs === null || maybeAttrs === void 0 ? void 0 : maybeAttrs.url) !== null && _maybeAttrs$url !== void 0 ? _maybeAttrs$url : maybeAttrs === null || maybeAttrs === void 0 || (_maybeAttrs$data = maybeAttrs.data) === null || _maybeAttrs$data === void 0 ? void 0 : _maybeAttrs$data.url;
|
|
124
135
|
return typeof maybeUrl === 'string' ? maybeUrl : undefined;
|
|
125
136
|
};
|
|
@@ -179,7 +190,7 @@ export var normalizeSelectionToLinkRangeForUrlAppearance = function normalizeSel
|
|
|
179
190
|
editorView.dispatch(linkRangeSelectionTr);
|
|
180
191
|
};
|
|
181
192
|
var PasteDisplayAsMenuHorizontalView = function PasteDisplayAsMenuHorizontalView(_ref8) {
|
|
182
|
-
var _smartCardContext$val,
|
|
193
|
+
var _smartCardContext$val, _cardAtPasteRange$app, _smartCardContext$val2;
|
|
183
194
|
var api = _ref8.api,
|
|
184
195
|
allowBlockCards = _ref8.allowBlockCards,
|
|
185
196
|
allowEmbeds = _ref8.allowEmbeds;
|
|
@@ -214,7 +225,11 @@ var PasteDisplayAsMenuHorizontalView = function PasteDisplayAsMenuHorizontalView
|
|
|
214
225
|
var pastedLinkUrl = pastedLinkUrlFromSlice !== null && pastedLinkUrlFromSlice !== void 0 ? pastedLinkUrlFromSlice : pastedLinkUrlFromCard;
|
|
215
226
|
var pastedLinkUrlState = pastedLinkUrl ? (_smartCardContext$val = smartCardContext.value) === null || _smartCardContext$val === void 0 || (_smartCardContext$val = _smartCardContext$val.store) === null || _smartCardContext$val === void 0 || (_smartCardContext$val = _smartCardContext$val.getState()) === null || _smartCardContext$val === void 0 ? void 0 : _smartCardContext$val[pastedLinkUrl] : undefined;
|
|
216
227
|
var hasResolvedSmartLinkData = Boolean(pastedLinkUrlState === null || pastedLinkUrlState === void 0 ? void 0 : pastedLinkUrlState.details);
|
|
217
|
-
var
|
|
228
|
+
var cardAtPasteRange = editorView && pasteRange ? getCardAtPasteRange(editorView.state, pasteRange.pasteStartPos, pasteRange.pasteEndPos, pastedLinkUrl) : undefined;
|
|
229
|
+
var currentAppearance = editorView && pasteRange ? (_cardAtPasteRange$app = cardAtPasteRange === null || cardAtPasteRange === void 0 ? void 0 : cardAtPasteRange.appearance) !== null && _cardAtPasteRange$app !== void 0 ? _cardAtPasteRange$app : 'url' : undefined;
|
|
230
|
+
// A 1P link that resolves to an embed is inserted as a native embed, not an embedCard.
|
|
231
|
+
// It displays as an embed, but the card commands cannot change its appearance.
|
|
232
|
+
var isNativeEmbedPaste = Boolean(cardAtPasteRange === null || cardAtPasteRange === void 0 ? void 0 : cardAtPasteRange.isNativeEmbed);
|
|
218
233
|
var handleClick = useCallback(function (appearance) {
|
|
219
234
|
return function () {
|
|
220
235
|
var _state$doc$nodeAt2, _targetNodeAttrs$url, _targetNodeAttrs$data;
|
|
@@ -226,17 +241,35 @@ var PasteDisplayAsMenuHorizontalView = function PasteDisplayAsMenuHorizontalView
|
|
|
226
241
|
dispatch = editorView.dispatch;
|
|
227
242
|
var pasteStartPos = pasteRange.pasteStartPos,
|
|
228
243
|
pasteEndPos = pasteRange.pasteEndPos;
|
|
229
|
-
var
|
|
244
|
+
var cardAtRange = getCardAtPasteRange(state, pasteStartPos, pasteEndPos, pastedLinkUrl);
|
|
245
|
+
if (cardAtRange !== null && cardAtRange !== void 0 && cardAtRange.isNativeEmbed) {
|
|
246
|
+
var nativeEmbedNode = state.doc.nodeAt(cardAtRange.pos);
|
|
247
|
+
// 'embed' is what a native embed already displays as, so there is nothing to apply.
|
|
248
|
+
if (nativeEmbedNode && appearance !== 'embed') {
|
|
249
|
+
var _api$analytics;
|
|
250
|
+
changeNativeEmbedAppearance({
|
|
251
|
+
editorView: editorView,
|
|
252
|
+
pos: cardAtRange.pos,
|
|
253
|
+
node: nativeEmbedNode,
|
|
254
|
+
appearance: appearance,
|
|
255
|
+
url: pastedLinkUrl,
|
|
256
|
+
editorAnalyticsApi: api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
toolbarDropdownMenu === null || toolbarDropdownMenu === void 0 || toolbarDropdownMenu.closeMenu(null);
|
|
260
|
+
isApplyingRef.current = false;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
230
263
|
if (appearance === 'url') {
|
|
231
|
-
if (
|
|
232
|
-
var _state$doc$nodeAt, _api$
|
|
233
|
-
changeSelectedCardToLink(pastedLinkUrl, pastedLinkUrl, true, (_state$doc$nodeAt = state.doc.nodeAt(
|
|
264
|
+
if (cardAtRange) {
|
|
265
|
+
var _state$doc$nodeAt, _api$analytics2;
|
|
266
|
+
changeSelectedCardToLink(pastedLinkUrl, pastedLinkUrl, true, (_state$doc$nodeAt = state.doc.nodeAt(cardAtRange.pos)) !== null && _state$doc$nodeAt !== void 0 ? _state$doc$nodeAt : undefined, cardAtRange.pos, api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions)(state, dispatch, editorView);
|
|
234
267
|
}
|
|
235
268
|
toolbarDropdownMenu === null || toolbarDropdownMenu === void 0 || toolbarDropdownMenu.closeMenu(null);
|
|
236
269
|
isApplyingRef.current = false;
|
|
237
270
|
return;
|
|
238
271
|
}
|
|
239
|
-
var targetPos =
|
|
272
|
+
var targetPos = cardAtRange === null || cardAtRange === void 0 ? void 0 : cardAtRange.pos;
|
|
240
273
|
var targetNodeAttrs = targetPos === undefined ? undefined : (_state$doc$nodeAt2 = state.doc.nodeAt(targetPos)) === null || _state$doc$nodeAt2 === void 0 ? void 0 : _state$doc$nodeAt2.attrs;
|
|
241
274
|
var targetNodeUrl = (_targetNodeAttrs$url = targetNodeAttrs === null || targetNodeAttrs === void 0 ? void 0 : targetNodeAttrs.url) !== null && _targetNodeAttrs$url !== void 0 ? _targetNodeAttrs$url : targetNodeAttrs === null || targetNodeAttrs === void 0 || (_targetNodeAttrs$data = targetNodeAttrs.data) === null || _targetNodeAttrs$data === void 0 ? void 0 : _targetNodeAttrs$data.url;
|
|
242
275
|
var isRecoveredAdjacentPastedCard = expValEqualsNoExposure('confluence_editor_paste_3p_link_actions_menu', 'isEnabled', true) && targetPos === pasteStartPos - 1 && pasteStartPos === pasteEndPos && targetNodeUrl === pastedLinkUrl;
|
|
@@ -260,9 +293,9 @@ var PasteDisplayAsMenuHorizontalView = function PasteDisplayAsMenuHorizontalView
|
|
|
260
293
|
targetPos: targetPos
|
|
261
294
|
});
|
|
262
295
|
frameRef.current = requestAnimationFrame(function () {
|
|
263
|
-
var _api$
|
|
296
|
+
var _api$analytics3;
|
|
264
297
|
frameRef.current = null;
|
|
265
|
-
setSelectedCardAppearance(appearance, api === null || api === void 0 || (_api$
|
|
298
|
+
setSelectedCardAppearance(appearance, api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions)(editorView.state, editorView.dispatch, editorView);
|
|
266
299
|
toolbarDropdownMenu === null || toolbarDropdownMenu === void 0 || toolbarDropdownMenu.closeMenu(null);
|
|
267
300
|
isApplyingRef.current = false;
|
|
268
301
|
});
|
|
@@ -287,26 +320,39 @@ var PasteDisplayAsMenuHorizontalView = function PasteDisplayAsMenuHorizontalView
|
|
|
287
320
|
var isSmartLinkConvertible = hasResolvedSmartLinkData;
|
|
288
321
|
var isBlockSupportedFromSelection = allowBlockCards && blockCardNodeType && isSupportedInParent(editorView.state, Fragment.from(blockCardNodeType.createChecked({})), undefined);
|
|
289
322
|
var isEmbedSupportedFromSelection = allowEmbeds && preview && embedCardNodeType && isSupportedInParent(editorView.state, Fragment.from(embedCardNodeType.createChecked({})), undefined);
|
|
290
|
-
|
|
291
|
-
|
|
323
|
+
// A native embed is replaced in place, so support depends on what its own position
|
|
324
|
+
// accepts rather than on the current selection or on an embed preview being available.
|
|
325
|
+
var nativeEmbedPos = isNativeEmbedPaste ? cardAtPasteRange === null || cardAtPasteRange === void 0 ? void 0 : cardAtPasteRange.pos : undefined;
|
|
326
|
+
var isSupportedForNativeEmbed = function isSupportedForNativeEmbed(appearance) {
|
|
327
|
+
return nativeEmbedPos !== undefined && isNativeEmbedAppearanceSupported({
|
|
328
|
+
editorView: editorView,
|
|
329
|
+
pos: nativeEmbedPos,
|
|
330
|
+
appearance: appearance
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
var isInlineSupported = isNativeEmbedPaste ? isSupportedForNativeEmbed('inline') : true;
|
|
334
|
+
var isBlockSupported = isNativeEmbedPaste ? allowBlockCards && isSupportedForNativeEmbed('block') : Boolean(isBlockSupportedFromAppearanceContext || isBlockSupportedFromSelection);
|
|
335
|
+
var isEmbedSupported =
|
|
336
|
+
// Whatever was pasted already displays this way, so the option must stay selectable.
|
|
337
|
+
currentAppearance === 'embed' || Boolean(isEmbedSupportedFromAppearanceContext || isEmbedSupportedFromSelection);
|
|
292
338
|
return /*#__PURE__*/React.createElement(Flex, {
|
|
293
339
|
xcss: styles.appearanceBox,
|
|
294
340
|
gap: "space.050"
|
|
295
341
|
}, /*#__PURE__*/React.createElement(AppearanceOptionIconButton, {
|
|
296
342
|
appearance: "url",
|
|
297
343
|
currentAppearance: currentAppearance,
|
|
298
|
-
isDisabled:
|
|
344
|
+
isDisabled: isNativeEmbedPaste && !isSupportedForNativeEmbed('url'),
|
|
299
345
|
label: intl.formatMessage(appearancePropsMap.url.title),
|
|
300
346
|
Icon: MinusIcon,
|
|
301
347
|
onClick: handleClick('url')
|
|
302
348
|
}), /*#__PURE__*/React.createElement(InlineAppearanceIconButton, {
|
|
303
349
|
currentAppearance: currentAppearance,
|
|
304
|
-
isDisabled: !isSmartLinkConvertible,
|
|
350
|
+
isDisabled: isNativeEmbedPaste ? !isInlineSupported : !isSmartLinkConvertible,
|
|
305
351
|
label: intl.formatMessage(appearancePropsMap.inline.title),
|
|
306
352
|
onClick: handleClick('inline')
|
|
307
353
|
}), /*#__PURE__*/React.createElement(BlockAppearanceIconButton, {
|
|
308
354
|
currentAppearance: currentAppearance,
|
|
309
|
-
isDisabled: !
|
|
355
|
+
isDisabled: !isBlockSupported || !isNativeEmbedPaste && !isSmartLinkConvertible,
|
|
310
356
|
label: intl.formatMessage(appearancePropsMap.block.title),
|
|
311
357
|
onClick: handleClick('block')
|
|
312
358
|
}), /*#__PURE__*/React.createElement(EmbedAppearanceIconButton, {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import _typeof from "@babel/runtime/helpers/typeof";
|
|
2
|
+
import { isSafeUrl } from '@atlaskit/adf-schema/url';
|
|
3
|
+
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
4
|
+
import { NATIVE_EMBED_EXTENSION_KEY, NATIVE_EMBED_EXTENSION_TYPE } from '@atlaskit/editor-common/extensions';
|
|
5
|
+
/**
|
|
6
|
+
* A native embed is an `extension` node that `editor-plugin-native-embeds` builds from
|
|
7
|
+
* embedCard ADF (via the `embedCardNodeTransformer` registered on the card plugin).
|
|
8
|
+
* A 1P link that resolves to an embed appearance — a whiteboard, page, slide or
|
|
9
|
+
* database — therefore lands in the document as one of these rather than as an
|
|
10
|
+
* `embedCard`, so card node types alone are not enough to describe what was pasted.
|
|
11
|
+
*
|
|
12
|
+
* `editor-plugin-native-embeds` owns the equivalent predicate, but the card plugin
|
|
13
|
+
* cannot depend on it: that would close the cycle
|
|
14
|
+
* editor-plugin-card -> editor-plugin-native-embeds -> editor-plugin-card.
|
|
15
|
+
*/
|
|
16
|
+
export var isNativeEmbedNode = function isNativeEmbedNode(node) {
|
|
17
|
+
var _node$attrs, _node$attrs2;
|
|
18
|
+
return (node === null || node === void 0 ? void 0 : node.type.name) === 'extension' && ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.extensionType) === NATIVE_EMBED_EXTENSION_TYPE && typeof ((_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.extensionKey) === 'string' && node.attrs.extensionKey.includes(NATIVE_EMBED_EXTENSION_KEY);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads the embedded URL from a native embed node. The URL is stored in
|
|
23
|
+
* `parameters.macroParams`, where values may be wrapped (`{ value }`) or bare, and is
|
|
24
|
+
* mirrored in `parameters.macroMetadata` so it survives a revert to a card.
|
|
25
|
+
*/
|
|
26
|
+
export var getNativeEmbedUrl = function getNativeEmbedUrl(node) {
|
|
27
|
+
var _node$attrs3, _parameters$macroPara, _ref, _parameters$macroMeta;
|
|
28
|
+
var parameters = (_node$attrs3 = node.attrs) === null || _node$attrs3 === void 0 ? void 0 : _node$attrs3.parameters;
|
|
29
|
+
var macroParamUrl = parameters === null || parameters === void 0 || (_parameters$macroPara = parameters.macroParams) === null || _parameters$macroPara === void 0 ? void 0 : _parameters$macroPara.url;
|
|
30
|
+
var url = (_ref = macroParamUrl && _typeof(macroParamUrl) === 'object' && 'value' in macroParamUrl ? macroParamUrl.value : macroParamUrl) !== null && _ref !== void 0 ? _ref : parameters === null || parameters === void 0 || (_parameters$macroMeta = parameters.macroMetadata) === null || _parameters$macroMeta === void 0 ? void 0 : _parameters$macroMeta.url;
|
|
31
|
+
return typeof url === 'string' ? url : undefined;
|
|
32
|
+
};
|
|
33
|
+
var canReplaceNodeWith = function canReplaceNodeWith(view, pos, nodeType) {
|
|
34
|
+
if (!nodeType) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
var $pos = view.state.doc.resolve(pos);
|
|
39
|
+
var index = $pos.index();
|
|
40
|
+
return $pos.parent.canReplaceWith(index, index + 1, nodeType);
|
|
41
|
+
} catch (_unused) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a native embed at `pos` can be replaced by the given appearance. `url` and
|
|
48
|
+
* `inline` both become a paragraph, `block` becomes a blockCard.
|
|
49
|
+
*/
|
|
50
|
+
export var isNativeEmbedAppearanceSupported = function isNativeEmbedAppearanceSupported(_ref2) {
|
|
51
|
+
var editorView = _ref2.editorView,
|
|
52
|
+
pos = _ref2.pos,
|
|
53
|
+
appearance = _ref2.appearance;
|
|
54
|
+
var _editorView$state$sch = editorView.state.schema.nodes,
|
|
55
|
+
paragraph = _editorView$state$sch.paragraph,
|
|
56
|
+
inlineCard = _editorView$state$sch.inlineCard,
|
|
57
|
+
blockCard = _editorView$state$sch.blockCard;
|
|
58
|
+
if (appearance === 'block') {
|
|
59
|
+
return canReplaceNodeWith(editorView, pos, blockCard);
|
|
60
|
+
}
|
|
61
|
+
if (appearance === 'inline' && !inlineCard) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (appearance === 'url' && !editorView.state.schema.marks.link) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return canReplaceNodeWith(editorView, pos, paragraph);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Replaces a native embed with a link, an inline card or a block card. Mirrors
|
|
72
|
+
* `setNativeEmbedAppearance` in `editor-plugin-native-embeds`, which cannot be reused
|
|
73
|
+
* here because of the package cycle described on `isNativeEmbedNode`.
|
|
74
|
+
*/
|
|
75
|
+
export var changeNativeEmbedAppearance = function changeNativeEmbedAppearance(_ref3) {
|
|
76
|
+
var editorView = _ref3.editorView,
|
|
77
|
+
pos = _ref3.pos,
|
|
78
|
+
node = _ref3.node,
|
|
79
|
+
appearance = _ref3.appearance,
|
|
80
|
+
url = _ref3.url,
|
|
81
|
+
editorAnalyticsApi = _ref3.editorAnalyticsApi;
|
|
82
|
+
var state = editorView.state,
|
|
83
|
+
dispatch = editorView.dispatch;
|
|
84
|
+
var _state$schema$nodes = state.schema.nodes,
|
|
85
|
+
paragraph = _state$schema$nodes.paragraph,
|
|
86
|
+
inlineCard = _state$schema$nodes.inlineCard,
|
|
87
|
+
blockCard = _state$schema$nodes.blockCard;
|
|
88
|
+
var link = state.schema.marks.link;
|
|
89
|
+
if (!isSafeUrl(url)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
var content;
|
|
93
|
+
if (appearance === 'url' && link && paragraph) {
|
|
94
|
+
content = paragraph.create(null, state.schema.text(url, [link.create({
|
|
95
|
+
href: url
|
|
96
|
+
})]));
|
|
97
|
+
} else if (appearance === 'inline' && inlineCard && paragraph) {
|
|
98
|
+
content = paragraph.create(null, inlineCard.create({
|
|
99
|
+
url: url
|
|
100
|
+
}));
|
|
101
|
+
} else if (appearance === 'block' && blockCard) {
|
|
102
|
+
content = blockCard.create({
|
|
103
|
+
url: url
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!content) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
var tr = state.tr.replaceWith(pos, pos + node.nodeSize, content).scrollIntoView();
|
|
111
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent({
|
|
112
|
+
action: ACTION.CHANGED_TYPE,
|
|
113
|
+
actionSubject: ACTION_SUBJECT.NATIVE_EMBED,
|
|
114
|
+
eventType: EVENT_TYPE.TRACK,
|
|
115
|
+
attributes: {
|
|
116
|
+
newType: appearance,
|
|
117
|
+
previousType: 'nativeEmbed'
|
|
118
|
+
}
|
|
119
|
+
})(tr);
|
|
120
|
+
dispatch(tr);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (_unused2) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -1,39 +1,101 @@
|
|
|
1
|
+
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
2
|
+
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
3
|
+
import { getNativeEmbedUrl, isNativeEmbedNode } from './nativeEmbedNode';
|
|
1
4
|
export var DISPLAY_AS_OPTIONS = ['url', 'inline', 'block', 'embed'];
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
var cardAtPos = function cardAtPos(node, pos) {
|
|
6
|
+
switch (node === null || node === void 0 ? void 0 : node.type.name) {
|
|
7
|
+
case 'inlineCard':
|
|
8
|
+
return {
|
|
9
|
+
appearance: 'inline',
|
|
10
|
+
pos: pos
|
|
11
|
+
};
|
|
12
|
+
case 'blockCard':
|
|
13
|
+
return {
|
|
14
|
+
appearance: 'block',
|
|
15
|
+
pos: pos
|
|
16
|
+
};
|
|
17
|
+
case 'embedCard':
|
|
18
|
+
return {
|
|
19
|
+
appearance: 'embed',
|
|
20
|
+
pos: pos
|
|
21
|
+
};
|
|
22
|
+
default:
|
|
23
|
+
return isNativeEmbedNode(node) ? {
|
|
24
|
+
appearance: 'embed',
|
|
25
|
+
isNativeEmbed: true,
|
|
26
|
+
pos: pos
|
|
27
|
+
} : undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var getNodeUrl = function getNodeUrl(node) {
|
|
31
|
+
var _attrs$url, _attrs$data;
|
|
32
|
+
if (isNativeEmbedNode(node)) {
|
|
33
|
+
return getNativeEmbedUrl(node);
|
|
34
|
+
}
|
|
35
|
+
var attrs = node.attrs;
|
|
36
|
+
var url = (_attrs$url = attrs === null || attrs === void 0 ? void 0 : attrs.url) !== null && _attrs$url !== void 0 ? _attrs$url : attrs === null || attrs === void 0 || (_attrs$data = attrs.data) === null || _attrs$data === void 0 ? void 0 : _attrs$data.url;
|
|
37
|
+
return typeof url === 'string' ? url : undefined;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Finds the card that a paste produced.
|
|
41
|
+
*
|
|
42
|
+
* The paste range is recorded when the paste happens and then mapped through every later
|
|
43
|
+
* transaction, so by the time the link has resolved into a card the range can point just
|
|
44
|
+
* past that card. `pastedUrl` resolves the ambiguity: when it is known, a candidate
|
|
45
|
+
* holding that URL wins over one that merely sits in the range, which stops a pre-existing
|
|
46
|
+
* neighbouring card from being reported as the pasted one.
|
|
47
|
+
*/
|
|
48
|
+
export var getCardAtPasteRange = function getCardAtPasteRange(state, pasteStartPos, pasteEndPos, pastedUrl) {
|
|
4
49
|
var docContentSize = state.doc.content.size;
|
|
5
50
|
var clampedStart = Math.max(0, Math.min(pasteStartPos, docContentSize));
|
|
6
51
|
var clampedEnd = Math.max(0, Math.min(pasteEndPos, docContentSize));
|
|
7
52
|
var from = Math.min(clampedStart, clampedEnd);
|
|
8
53
|
var to = Math.max(clampedStart, clampedEnd);
|
|
9
54
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
var node = state.doc.nodeAt(pos);
|
|
17
|
-
if ((node === null || node === void 0 ? void 0 : node.type.name) === 'inlineCard' || (node === null || node === void 0 ? void 0 : node.type.name) === 'blockCard' || (node === null || node === void 0 ? void 0 : node.type.name) === 'embedCard') {
|
|
18
|
-
return {
|
|
19
|
-
appearance: node.type.name === 'inlineCard' ? 'inline' : node.type.name === 'blockCard' ? 'block' : 'embed',
|
|
20
|
-
pos: pos
|
|
21
|
-
};
|
|
22
|
-
}
|
|
55
|
+
var _inRange;
|
|
56
|
+
var candidateAt = function candidateAt(pos) {
|
|
57
|
+
if (pos < 0 || pos >= docContentSize) {
|
|
58
|
+
return undefined;
|
|
23
59
|
}
|
|
24
|
-
|
|
60
|
+
var node = state.doc.nodeAt(pos);
|
|
61
|
+
var card = cardAtPos(node, pos);
|
|
62
|
+
return node && card ? {
|
|
63
|
+
card: card,
|
|
64
|
+
url: getNodeUrl(node)
|
|
65
|
+
} : undefined;
|
|
66
|
+
};
|
|
67
|
+
var adjacent = [candidateAt(from - 1), candidateAt(from)].filter(function (candidate) {
|
|
68
|
+
return Boolean(candidate);
|
|
69
|
+
});
|
|
70
|
+
var inRange = [];
|
|
25
71
|
state.doc.nodesBetween(from, to, function (node, pos) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
var card = cardAtPos(node, pos);
|
|
73
|
+
if (card) {
|
|
74
|
+
inRange.push({
|
|
75
|
+
card: card,
|
|
76
|
+
url: getNodeUrl(node)
|
|
77
|
+
});
|
|
31
78
|
return false;
|
|
32
79
|
}
|
|
33
80
|
return true;
|
|
34
81
|
});
|
|
82
|
+
if (pastedUrl) {
|
|
83
|
+
var match = [].concat(_toConsumableArray(adjacent), inRange).find(function (candidate) {
|
|
84
|
+
return candidate.url === pastedUrl;
|
|
85
|
+
});
|
|
86
|
+
if (match) {
|
|
87
|
+
return match.card;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (from === to) {
|
|
91
|
+
var _adjacent = _slicedToArray(adjacent, 1),
|
|
92
|
+
firstAdjacent = _adjacent[0];
|
|
93
|
+
if (firstAdjacent) {
|
|
94
|
+
return firstAdjacent.card;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return (_inRange = inRange[inRange.length - 1]) === null || _inRange === void 0 ? void 0 : _inRange.card;
|
|
35
98
|
} catch (_unused) {
|
|
36
|
-
return;
|
|
99
|
+
return undefined;
|
|
37
100
|
}
|
|
38
|
-
return result;
|
|
39
101
|
};
|