@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.
@@ -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
- export const getCardAtPasteRange = (state, pasteStartPos, pasteEndPos) => {
3
- let result;
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
- if (from === to) {
11
- for (const pos of [from - 1, from]) {
12
- if (pos < 0 || pos >= docContentSize) {
13
- continue;
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
- if (node.type.name === 'inlineCard' || node.type.name === 'blockCard' || node.type.name === 'embedCard') {
26
- result = {
27
- appearance: node.type.name === 'inlineCard' ? 'inline' : node.type.name === 'blockCard' ? 'block' : 'embed',
28
- pos
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 _editorView$state$doc, _maybeAttrs$url, _maybeAttrs$data;
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
- var maybeAttrs = cardAtPasteRange ? (_editorView$state$doc = editorView.state.doc.nodeAt(cardAtPasteRange.pos)) === null || _editorView$state$doc === void 0 ? void 0 : _editorView$state$doc.attrs : undefined;
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, _getCardAtPasteRange$, _getCardAtPasteRange, _smartCardContext$val2;
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 currentAppearance = editorView && pasteRange ? (_getCardAtPasteRange$ = (_getCardAtPasteRange = getCardAtPasteRange(editorView.state, pasteRange.pasteStartPos, pasteRange.pasteEndPos)) === null || _getCardAtPasteRange === void 0 ? void 0 : _getCardAtPasteRange.appearance) !== null && _getCardAtPasteRange$ !== void 0 ? _getCardAtPasteRange$ : 'url' : undefined;
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 cardAtPasteRange = getCardAtPasteRange(state, pasteStartPos, pasteEndPos);
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 (cardAtPasteRange) {
232
- var _state$doc$nodeAt, _api$analytics;
233
- changeSelectedCardToLink(pastedLinkUrl, pastedLinkUrl, true, (_state$doc$nodeAt = state.doc.nodeAt(cardAtPasteRange.pos)) !== null && _state$doc$nodeAt !== void 0 ? _state$doc$nodeAt : undefined, cardAtPasteRange.pos, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(state, dispatch, editorView);
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 = cardAtPasteRange === null || cardAtPasteRange === void 0 ? void 0 : cardAtPasteRange.pos;
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$analytics2;
296
+ var _api$analytics3;
264
297
  frameRef.current = null;
265
- setSelectedCardAppearance(appearance, api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions)(editorView.state, editorView.dispatch, editorView);
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
- var isBlockSupported = Boolean(isBlockSupportedFromAppearanceContext || isBlockSupportedFromSelection);
291
- var isEmbedSupported = Boolean(isEmbedSupportedFromAppearanceContext || isEmbedSupportedFromSelection);
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: false,
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: !isSmartLinkConvertible || !isBlockSupported,
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
- export var getCardAtPasteRange = function getCardAtPasteRange(state, pasteStartPos, pasteEndPos) {
3
- var result;
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
- if (from === to) {
11
- for (var _i = 0, _arr = [from - 1, from]; _i < _arr.length; _i++) {
12
- var pos = _arr[_i];
13
- if (pos < 0 || pos >= docContentSize) {
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
- if (node.type.name === 'inlineCard' || node.type.name === 'blockCard' || node.type.name === 'embedCard') {
27
- result = {
28
- appearance: node.type.name === 'inlineCard' ? 'inline' : node.type.name === 'blockCard' ? 'block' : 'embed',
29
- pos: pos
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
  };