@atlaskit/editor-plugin-paste 0.1.22 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.eslintrc.js +18 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/cjs/actions.js +12 -0
  4. package/dist/cjs/commands.js +255 -0
  5. package/dist/cjs/edge-cases/index.js +88 -0
  6. package/dist/cjs/edge-cases/lists.js +107 -0
  7. package/dist/cjs/handlers.js +939 -0
  8. package/dist/cjs/index.js +8 -1
  9. package/dist/cjs/plugin.js +43 -0
  10. package/dist/cjs/plugins/media.js +207 -0
  11. package/dist/cjs/pm-plugins/analytics.js +376 -0
  12. package/dist/cjs/pm-plugins/clipboard-text-serializer.js +43 -0
  13. package/dist/cjs/pm-plugins/main.js +484 -0
  14. package/dist/cjs/pm-plugins/plugin-factory.js +42 -0
  15. package/dist/cjs/reducer.js +41 -0
  16. package/dist/cjs/util/index.js +214 -0
  17. package/dist/cjs/util/tinyMCE.js +183 -0
  18. package/dist/es2019/actions.js +6 -0
  19. package/dist/es2019/commands.js +236 -0
  20. package/dist/es2019/edge-cases/index.js +87 -0
  21. package/dist/es2019/edge-cases/lists.js +113 -0
  22. package/dist/es2019/handlers.js +919 -0
  23. package/dist/es2019/index.js +1 -1
  24. package/dist/es2019/plugin.js +38 -0
  25. package/dist/es2019/plugins/media.js +204 -0
  26. package/dist/es2019/pm-plugins/analytics.js +332 -0
  27. package/dist/es2019/pm-plugins/clipboard-text-serializer.js +37 -0
  28. package/dist/es2019/pm-plugins/main.js +453 -0
  29. package/dist/es2019/pm-plugins/plugin-factory.js +30 -0
  30. package/dist/es2019/reducer.js +32 -0
  31. package/dist/es2019/util/index.js +209 -0
  32. package/dist/es2019/util/tinyMCE.js +168 -0
  33. package/dist/esm/actions.js +6 -0
  34. package/dist/esm/commands.js +249 -0
  35. package/dist/esm/edge-cases/index.js +81 -0
  36. package/dist/esm/edge-cases/lists.js +98 -0
  37. package/dist/esm/handlers.js +918 -0
  38. package/dist/esm/index.js +1 -1
  39. package/dist/esm/plugin.js +37 -0
  40. package/dist/esm/plugins/media.js +199 -0
  41. package/dist/esm/pm-plugins/analytics.js +364 -0
  42. package/dist/esm/pm-plugins/clipboard-text-serializer.js +37 -0
  43. package/dist/esm/pm-plugins/main.js +471 -0
  44. package/dist/esm/pm-plugins/plugin-factory.js +36 -0
  45. package/dist/esm/reducer.js +34 -0
  46. package/dist/esm/util/index.js +194 -0
  47. package/dist/esm/util/tinyMCE.js +176 -0
  48. package/dist/types/actions.d.ts +21 -0
  49. package/dist/types/commands.d.ts +29 -0
  50. package/dist/types/edge-cases/index.d.ts +11 -0
  51. package/dist/types/edge-cases/lists.d.ts +18 -0
  52. package/dist/types/handlers.d.ts +55 -0
  53. package/dist/types/index.d.ts +1 -0
  54. package/dist/types/plugin.d.ts +2 -0
  55. package/dist/types/plugins/media.d.ts +23 -0
  56. package/dist/types/pm-plugins/analytics.d.ts +44 -0
  57. package/dist/types/pm-plugins/clipboard-text-serializer.d.ts +13 -0
  58. package/dist/types/pm-plugins/main.d.ts +12 -0
  59. package/dist/types/pm-plugins/plugin-factory.d.ts +3 -0
  60. package/dist/types/reducer.d.ts +3 -0
  61. package/dist/types/util/index.d.ts +21 -0
  62. package/dist/types/util/tinyMCE.d.ts +32 -0
  63. package/dist/types-ts4.5/actions.d.ts +21 -0
  64. package/dist/types-ts4.5/commands.d.ts +29 -0
  65. package/dist/types-ts4.5/edge-cases/index.d.ts +11 -0
  66. package/dist/types-ts4.5/edge-cases/lists.d.ts +18 -0
  67. package/dist/types-ts4.5/handlers.d.ts +55 -0
  68. package/dist/types-ts4.5/index.d.ts +1 -0
  69. package/dist/types-ts4.5/plugin.d.ts +2 -0
  70. package/dist/types-ts4.5/plugins/media.d.ts +23 -0
  71. package/dist/types-ts4.5/pm-plugins/analytics.d.ts +44 -0
  72. package/dist/types-ts4.5/pm-plugins/clipboard-text-serializer.d.ts +13 -0
  73. package/dist/types-ts4.5/pm-plugins/main.d.ts +12 -0
  74. package/dist/types-ts4.5/pm-plugins/plugin-factory.d.ts +3 -0
  75. package/dist/types-ts4.5/reducer.d.ts +3 -0
  76. package/dist/types-ts4.5/util/index.d.ts +21 -0
  77. package/dist/types-ts4.5/util/tinyMCE.d.ts +32 -0
  78. package/package.json +18 -6
@@ -0,0 +1,919 @@
1
+ import uuid from 'uuid/v4';
2
+ import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
3
+ import { insideTable } from '@atlaskit/editor-common/core-utils';
4
+ import { anyMarkActive } from '@atlaskit/editor-common/mark';
5
+ import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection';
6
+ import { canLinkBeCreatedInRange, insideTableCell, isInListItem, isLinkMark, isListItemNode, isListNode, isParagraph, isText, linkifyContent, mapSlice } from '@atlaskit/editor-common/utils';
7
+ import { closeHistory } from '@atlaskit/editor-prosemirror/history';
8
+ import { Fragment, Node as PMNode, Slice } from '@atlaskit/editor-prosemirror/model';
9
+ import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
10
+ import { canInsert, findParentNodeOfType, findParentNodeOfTypeClosestToPos, hasParentNodeOfType, safeInsert } from '@atlaskit/editor-prosemirror/utils';
11
+ import { replaceSelectedTable } from '@atlaskit/editor-tables/utils';
12
+ // TODO: ED-20519 Needs Macro extraction
13
+
14
+ import { startTrackingPastedMacroPositions, stopTrackingPastedMacroPositions } from './commands';
15
+ import { insertSliceForLists, insertSliceForListsInsideBlockquote } from './edge-cases';
16
+ import { getPluginState as getPastePluginState } from './pm-plugins/plugin-factory';
17
+ import { addReplaceSelectedTableAnalytics, applyTextMarksToSlice, hasOnlyNodesOfType } from './util';
18
+
19
+ /** Helper type for single arg function */
20
+
21
+ /* eslint-disable @typescript-eslint/no-explicit-any */
22
+ /**
23
+ * Compose 1 to n functions.
24
+ * @param func first function
25
+ * @param funcs additional functions
26
+ */
27
+ function compose(func, ...funcs) {
28
+ const allFuncs = [func, ...funcs];
29
+ return function composed(raw) {
30
+ return allFuncs.reduceRight((memo, func) => func(memo), raw);
31
+ };
32
+ }
33
+ /* eslint-enable @typescript-eslint/no-explicit-any */
34
+
35
+ // remove text attribute from mention for copy/paste (GDPR)
36
+ export function handleMention(slice, schema) {
37
+ return mapSlice(slice, node => {
38
+ if (node.type.name === schema.nodes.mention.name) {
39
+ const mention = node.attrs;
40
+ const newMention = {
41
+ ...mention,
42
+ text: ''
43
+ };
44
+ return schema.nodes.mention.create(newMention, node.content, node.marks);
45
+ }
46
+ return node;
47
+ });
48
+ }
49
+ export function handlePasteIntoTaskOrDecisionOrPanel(slice, queueCardsFromChangedTr) {
50
+ return (state, dispatch) => {
51
+ var _slice$content$firstC, _transformedSlice$con;
52
+ const {
53
+ schema,
54
+ tr: {
55
+ selection
56
+ }
57
+ } = state;
58
+ const {
59
+ marks: {
60
+ code: codeMark
61
+ },
62
+ nodes: {
63
+ decisionItem,
64
+ emoji,
65
+ hardBreak,
66
+ mention,
67
+ paragraph,
68
+ taskItem,
69
+ text,
70
+ panel,
71
+ bulletList,
72
+ orderedList,
73
+ taskList,
74
+ listItem,
75
+ expand,
76
+ heading
77
+ }
78
+ } = schema;
79
+ const selectionIsValidNode = state.selection instanceof NodeSelection && ['decisionList', 'decisionItem', 'taskList', 'taskItem'].includes(state.selection.node.type.name);
80
+ const selectionHasValidParentNode = hasParentNodeOfType([decisionItem, taskItem, panel])(state.selection);
81
+ const selectionIsPanel = hasParentNodeOfType([panel])(state.selection);
82
+
83
+ // Some types of content should be handled by the default handler, not this function.
84
+ // Check through slice content to see if it contains an invalid node.
85
+ let sliceIsInvalid = false;
86
+ slice.content.nodesBetween(0, slice.content.size, node => {
87
+ if (node.type === bulletList || node.type === orderedList || node.type === expand || node.type === heading || node.type === listItem) {
88
+ sliceIsInvalid = true;
89
+ }
90
+ if (selectionIsPanel && node.type === taskList) {
91
+ sliceIsInvalid = true;
92
+ }
93
+ });
94
+
95
+ // If the selection is a panel,
96
+ // and the slice's first node is a paragraph
97
+ // and it is not from a depth that would indicate it being from inside from another node (e.g. text from a decision)
98
+ // then we can rely on the default behaviour.
99
+ const sliceIsAPanelReceivingLowDepthText = selectionIsPanel && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type) === paragraph && slice.openEnd < 2;
100
+ if (sliceIsInvalid || sliceIsAPanelReceivingLowDepthText || !selectionIsValidNode && !selectionHasValidParentNode) {
101
+ return false;
102
+ }
103
+ const filters = [linkifyContent(schema)];
104
+ const selectionMarks = selection.$head.marks();
105
+ if (selection instanceof TextSelection && Array.isArray(selectionMarks) && selectionMarks.length > 0 && hasOnlyNodesOfType(paragraph, text, emoji, mention, hardBreak)(slice) && (!codeMark.isInSet(selectionMarks) || anyMarkActive(state, codeMark)) // check if there is a code mark anywhere in the selection
106
+ ) {
107
+ filters.push(applyTextMarksToSlice(schema, selection.$head.marks()));
108
+ }
109
+ const transformedSlice = compose.apply(null, filters)(slice);
110
+ const tr = closeHistory(state.tr);
111
+ const transformedSliceIsValidNode = transformedSlice.content.firstChild.type.inlineContent || ['decisionList', 'decisionItem', 'taskList', 'taskItem', 'panel'].includes(transformedSlice.content.firstChild.type.name) && !isInListItem(state);
112
+ // If the slice or the selection are valid nodes to handle,
113
+ // and the slice is not a whole node (i.e. openStart is 1 and openEnd is 0)
114
+ // or the slice's first node is a paragraph,
115
+ // then we can replace the selection with our slice.
116
+ if ((transformedSliceIsValidNode || selectionIsValidNode) && !(transformedSlice.openStart === 1 && transformedSlice.openEnd === 0 ||
117
+ // Whole codeblock node has reverse slice depths.
118
+ transformedSlice.openStart === 0 && transformedSlice.openEnd === 1) || ((_transformedSlice$con = transformedSlice.content.firstChild) === null || _transformedSlice$con === void 0 ? void 0 : _transformedSlice$con.type) === paragraph) {
119
+ tr.replaceSelection(transformedSlice).scrollIntoView();
120
+ } else {
121
+ // This maintains both the selection (destination) and the slice (paste content).
122
+ safeInsert(transformedSlice.content)(tr).scrollIntoView();
123
+ }
124
+ queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
125
+ if (dispatch) {
126
+ dispatch(tr);
127
+ }
128
+ return true;
129
+ };
130
+ }
131
+ export function handlePasteNonNestableBlockNodesIntoList(slice) {
132
+ return (state, dispatch) => {
133
+ var _tr$doc$nodeAt, _sliceContent$firstCh, _findParentNodeOfType;
134
+ const {
135
+ tr
136
+ } = state;
137
+ const {
138
+ selection
139
+ } = tr;
140
+ const {
141
+ $from,
142
+ $to,
143
+ from,
144
+ to
145
+ } = selection;
146
+ const {
147
+ orderedList,
148
+ bulletList,
149
+ listItem
150
+ } = state.schema.nodes;
151
+
152
+ // Selected nodes
153
+ const selectionParentListItemNode = findParentNodeOfType(listItem)(selection);
154
+ const selectionParentListNodeWithPos = findParentNodeOfType([bulletList, orderedList])(selection);
155
+ const selectionParentListNode = selectionParentListNodeWithPos === null || selectionParentListNodeWithPos === void 0 ? void 0 : selectionParentListNodeWithPos.node;
156
+
157
+ // Slice info
158
+ const sliceContent = slice.content;
159
+ const sliceIsListItems = isListNode(sliceContent.firstChild) && isListNode(sliceContent.lastChild);
160
+
161
+ // Find case of slices that can be inserted into a list item
162
+ // (eg. paragraphs, list items, code blocks, media single)
163
+ // These scenarios already get handled elsewhere and don't need to split the list
164
+ let sliceContainsBlockNodesOtherThanThoseAllowedInListItem = false;
165
+ slice.content.forEach(child => {
166
+ var _listItem$spec$conten;
167
+ if (!listItem || child.isBlock && !((_listItem$spec$conten = listItem.spec.content) !== null && _listItem$spec$conten !== void 0 && _listItem$spec$conten.includes(child.type.name))) {
168
+ sliceContainsBlockNodesOtherThanThoseAllowedInListItem = true;
169
+ }
170
+ });
171
+ if (!selectionParentListItemNode || !sliceContent || canInsert($from, sliceContent) ||
172
+ // eg. inline nodes that can be inserted in a list item
173
+ !sliceContainsBlockNodesOtherThanThoseAllowedInListItem || sliceIsListItems || !selectionParentListNodeWithPos) {
174
+ return false;
175
+ }
176
+
177
+ // Offsets
178
+ const listWrappingOffset = $to.depth - selectionParentListNodeWithPos.depth + 1; // difference in depth between to position and list node
179
+ const listItemWrappingOffset = $to.depth - selectionParentListNodeWithPos.depth; // difference in depth between to position and list item node
180
+
181
+ // Anything to do with nested lists should safeInsert and not be handled here
182
+ const grandParentListNode = findParentNodeOfTypeClosestToPos(tr.doc.resolve(selectionParentListNodeWithPos.pos), [bulletList, orderedList]);
183
+ const selectionIsInNestedList = !!grandParentListNode;
184
+ let selectedListItemHasNestedList = false;
185
+ selectionParentListItemNode.node.content.forEach(child => {
186
+ if (isListNode(child)) {
187
+ selectedListItemHasNestedList = true;
188
+ }
189
+ });
190
+ if (selectedListItemHasNestedList || selectionIsInNestedList) {
191
+ return false;
192
+ }
193
+
194
+ // Node after the insert position
195
+ const nodeAfterInsertPositionIsListItem = ((_tr$doc$nodeAt = tr.doc.nodeAt(to + listItemWrappingOffset)) === null || _tr$doc$nodeAt === void 0 ? void 0 : _tr$doc$nodeAt.type.name) === 'listItem';
196
+
197
+ // Get the next list items position (used later to find the split out ordered list)
198
+ const indexOfNextListItem = $to.indexAfter($to.depth - listItemWrappingOffset);
199
+ const positionOfNextListItem = tr.doc.resolve(selectionParentListNodeWithPos.pos + 1).posAtIndex(indexOfNextListItem);
200
+
201
+ // These nodes paste as plain text by default so need to be handled differently
202
+ const sliceContainsNodeThatPastesAsPlainText = sliceContent.firstChild && ['taskItem', 'taskList', 'heading', 'blockquote'].includes(sliceContent.firstChild.type.name);
203
+
204
+ // Work out position to replace up to
205
+ let replaceTo;
206
+ if (sliceContainsNodeThatPastesAsPlainText && nodeAfterInsertPositionIsListItem) {
207
+ replaceTo = to + listItemWrappingOffset;
208
+ } else if (sliceContainsNodeThatPastesAsPlainText || !nodeAfterInsertPositionIsListItem) {
209
+ replaceTo = to;
210
+ } else {
211
+ replaceTo = to + listWrappingOffset;
212
+ }
213
+
214
+ // handle the insertion of the slice
215
+ if (sliceContainsNodeThatPastesAsPlainText || nodeAfterInsertPositionIsListItem || sliceContent.childCount > 1 && ((_sliceContent$firstCh = sliceContent.firstChild) === null || _sliceContent$firstCh === void 0 ? void 0 : _sliceContent$firstCh.type.name) !== 'paragraph') {
216
+ tr.replaceWith(from, replaceTo, sliceContent).scrollIntoView();
217
+ } else {
218
+ // When the selection is not at the end of a list item
219
+ // eg. middle of list item, start of list item
220
+ tr.replaceSelection(slice).scrollIntoView();
221
+ }
222
+
223
+ // Find the ordered list node after the pasted content so we can set it's order
224
+ const mappedPositionOfNextListItem = tr.mapping.map(positionOfNextListItem);
225
+ if (mappedPositionOfNextListItem > tr.doc.nodeSize) {
226
+ return false;
227
+ }
228
+ const nodeAfterPastedContentResolvedPos = findParentNodeOfTypeClosestToPos(tr.doc.resolve(mappedPositionOfNextListItem), [orderedList]);
229
+
230
+ // Work out the new split out lists 'order' (the number it starts from)
231
+ const originalParentOrderedListNodeOrder = selectionParentListNode === null || selectionParentListNode === void 0 ? void 0 : selectionParentListNode.attrs.order;
232
+ const numOfListItemsInOriginalList = (_findParentNodeOfType = findParentNodeOfTypeClosestToPos(tr.doc.resolve(from - 1), [orderedList])) === null || _findParentNodeOfType === void 0 ? void 0 : _findParentNodeOfType.node.childCount;
233
+
234
+ // Set the new split out lists order attribute
235
+ if (typeof originalParentOrderedListNodeOrder === 'number' && numOfListItemsInOriginalList && nodeAfterPastedContentResolvedPos) {
236
+ tr.setNodeMarkup(nodeAfterPastedContentResolvedPos.pos, orderedList, {
237
+ ...nodeAfterPastedContentResolvedPos.node.attrs,
238
+ order: originalParentOrderedListNodeOrder + numOfListItemsInOriginalList
239
+ });
240
+ }
241
+
242
+ // dispatch transaction
243
+ if (tr.docChanged) {
244
+ if (dispatch) {
245
+ dispatch(tr);
246
+ }
247
+ return true;
248
+ }
249
+ return false;
250
+ };
251
+ }
252
+ export const doesSelectionWhichStartsOrEndsInListContainEntireList = (selection, findRootParentListNode) => {
253
+ const {
254
+ $from,
255
+ $to,
256
+ from,
257
+ to
258
+ } = selection;
259
+ const selectionParentListItemNodeResolvedPos = findRootParentListNode ? findRootParentListNode($from) || findRootParentListNode($to) : null;
260
+ const selectionParentListNode = selectionParentListItemNodeResolvedPos === null || selectionParentListItemNodeResolvedPos === void 0 ? void 0 : selectionParentListItemNodeResolvedPos.parent;
261
+ if (!selectionParentListItemNodeResolvedPos || !selectionParentListNode) {
262
+ return false;
263
+ }
264
+ const startOfEntireList = $from.pos < $to.pos ? selectionParentListItemNodeResolvedPos.pos + $from.depth - 1 : selectionParentListItemNodeResolvedPos.pos + $to.depth - 1;
265
+ const endOfEntireList = $from.pos < $to.pos ? selectionParentListItemNodeResolvedPos.pos + selectionParentListNode.nodeSize - $to.depth - 1 : selectionParentListItemNodeResolvedPos.pos + selectionParentListNode.nodeSize - $from.depth - 1;
266
+ if (!startOfEntireList || !endOfEntireList) {
267
+ return false;
268
+ }
269
+ if (from < to) {
270
+ return startOfEntireList >= $from.pos && endOfEntireList <= $to.pos;
271
+ } else if (from > to) {
272
+ return startOfEntireList >= $to.pos && endOfEntireList <= $from.pos;
273
+ } else {
274
+ return false;
275
+ }
276
+ };
277
+ export function handlePastePanelOrDecisionContentIntoList(slice, findRootParentListNode) {
278
+ return (state, dispatch) => {
279
+ const {
280
+ schema,
281
+ tr
282
+ } = state;
283
+ const {
284
+ selection
285
+ } = tr;
286
+ // Check this pasting action is related to copy content from panel node into a selected the list node
287
+ const blockNode = slice.content.firstChild;
288
+ const isSliceWholeNode = slice.openStart === 0 && slice.openEnd === 0;
289
+ const selectionParentListItemNode = selection.$to.node(selection.$to.depth - 1);
290
+ const sliceIsWholeNodeButShouldNotReplaceSelection = isSliceWholeNode && !doesSelectionWhichStartsOrEndsInListContainEntireList(selection, findRootParentListNode);
291
+ if (!selectionParentListItemNode || (selectionParentListItemNode === null || selectionParentListItemNode === void 0 ? void 0 : selectionParentListItemNode.type) !== schema.nodes.listItem || !blockNode || !['panel', 'decisionList'].includes(blockNode === null || blockNode === void 0 ? void 0 : blockNode.type.name) || slice.content.childCount > 1 || (blockNode === null || blockNode === void 0 ? void 0 : blockNode.content.firstChild) === undefined || sliceIsWholeNodeButShouldNotReplaceSelection) {
292
+ return false;
293
+ }
294
+
295
+ // Paste the panel node contents extracted instead of pasting the entire panel node
296
+ tr.replaceSelection(slice).scrollIntoView();
297
+ if (dispatch) {
298
+ dispatch(tr);
299
+ }
300
+ return true;
301
+ };
302
+ }
303
+
304
+ // If we paste a link onto some selected text, apply the link as a mark
305
+ export function handlePasteLinkOnSelectedText(slice) {
306
+ return (state, dispatch) => {
307
+ const {
308
+ schema,
309
+ selection,
310
+ selection: {
311
+ from,
312
+ to
313
+ },
314
+ tr
315
+ } = state;
316
+ let linkMark;
317
+
318
+ // check if we have a link on the clipboard
319
+ if (slice.content.childCount === 1 && isParagraph(slice.content.child(0), schema)) {
320
+ const paragraph = slice.content.child(0);
321
+ if (paragraph.content.childCount === 1 && isText(paragraph.content.child(0), schema)) {
322
+ const text = paragraph.content.child(0);
323
+
324
+ // If pasteType is plain text, then
325
+ // @atlaskit/editor-markdown-transformer in getMarkdownSlice decode
326
+ // url before setting text property of text node.
327
+ // However href of marks will be without decoding.
328
+ // So, if there is character (e.g space) in url eligible escaping then
329
+ // mark.attrs.href will not be equal to text.text.
330
+ // That's why decoding mark.attrs.href before comparing.
331
+ // However, if pasteType is richText, that means url in text.text
332
+ // and href in marks, both won't be decoded.
333
+ linkMark = text.marks.find(mark => isLinkMark(mark, schema) && (mark.attrs.href === text.text || decodeURI(mark.attrs.href) === text.text));
334
+ }
335
+ }
336
+
337
+ // if we have a link, apply it to the selected text if we have any and it's allowed
338
+ if (linkMark && selection instanceof TextSelection && !selection.empty && canLinkBeCreatedInRange(from, to)(state)) {
339
+ tr.addMark(from, to, linkMark);
340
+ if (dispatch) {
341
+ dispatch(tr);
342
+ }
343
+ return true;
344
+ }
345
+ return false;
346
+ };
347
+ }
348
+ export function handlePasteAsPlainText(slice, _event, editorAnalyticsAPI) {
349
+ return (state, dispatch, view) => {
350
+ var _input;
351
+ if (!view) {
352
+ return false;
353
+ }
354
+
355
+ // prosemirror-bump-fix
356
+ // Yes, this is wrong by default. But, we need to keep the private PAI usage to unblock the prosemirror bump
357
+ // So, this code will make sure we are checking for both version (current and the newest prosemirror-view version
358
+ const isShiftKeyPressed =
359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
+ view.shiftKey || ((_input = view.input) === null || _input === void 0 ? void 0 : _input.shiftKey);
361
+ // In case of SHIFT+CMD+V ("Paste and Match Style") we don't want to run the usual
362
+ // fuzzy matching of content. ProseMirror already handles this scenario and will
363
+ // provide us with slice containing paragraphs with plain text, which we decorate
364
+ // with "stored marks".
365
+ // @see prosemirror-view/src/clipboard.js:parseFromClipboard()).
366
+ // @see prosemirror-view/src/input.js:doPaste().
367
+ if (isShiftKeyPressed) {
368
+ let tr = closeHistory(state.tr);
369
+ const {
370
+ selection
371
+ } = tr;
372
+
373
+ // <- using the same internal flag that prosemirror-view is using
374
+
375
+ // if user has selected table we need custom logic to replace the table
376
+ tr = replaceSelectedTable(state, slice);
377
+
378
+ // add analytics after replacing selected table
379
+ tr = addReplaceSelectedTableAnalytics(state, tr, editorAnalyticsAPI);
380
+
381
+ // otherwise just replace the selection
382
+ if (!tr.docChanged) {
383
+ tr.replaceSelection(slice);
384
+ }
385
+ (state.storedMarks || []).forEach(mark => {
386
+ tr.addMark(selection.from, selection.from + slice.size, mark);
387
+ });
388
+ tr.scrollIntoView();
389
+ if (dispatch) {
390
+ dispatch(tr);
391
+ }
392
+ return true;
393
+ }
394
+ return false;
395
+ };
396
+ }
397
+ export function handlePastePreservingMarks(slice, queueCardsFromChangedTr) {
398
+ return (state, dispatch) => {
399
+ const {
400
+ schema,
401
+ tr: {
402
+ selection
403
+ }
404
+ } = state;
405
+ const {
406
+ marks: {
407
+ code: codeMark,
408
+ annotation: annotationMark
409
+ },
410
+ nodes: {
411
+ bulletList,
412
+ emoji,
413
+ hardBreak,
414
+ heading,
415
+ listItem,
416
+ mention,
417
+ orderedList,
418
+ text
419
+ }
420
+ } = schema;
421
+ if (!(selection instanceof TextSelection)) {
422
+ return false;
423
+ }
424
+ const selectionMarks = selection.$head.marks();
425
+ if (selectionMarks.length === 0) {
426
+ return false;
427
+ }
428
+
429
+ // special case for codeMark: will preserve mark only if codeMark is currently active
430
+ // won't preserve mark if cursor is on the edge on the mark (namely inactive)
431
+ const hasActiveCodeMark = codeMark && codeMark.isInSet(selectionMarks) && anyMarkActive(state, codeMark);
432
+ const hasAnnotationMark = annotationMark && annotationMark.isInSet(selectionMarks);
433
+ const selectionIsHeading = hasParentNodeOfType([heading])(state.selection);
434
+
435
+ // if the pasted data is one of the node types below
436
+ // we apply current selection marks to the pasted slice
437
+ if (hasOnlyNodesOfType(bulletList, hardBreak, heading, listItem, text, emoji, mention, orderedList)(slice) || selectionIsHeading || hasActiveCodeMark || hasAnnotationMark) {
438
+ const transformedSlice = applyTextMarksToSlice(schema, selectionMarks)(slice);
439
+ const tr = closeHistory(state.tr).replaceSelection(transformedSlice).setStoredMarks(selectionMarks).scrollIntoView();
440
+ queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
441
+ if (dispatch) {
442
+ dispatch(tr);
443
+ }
444
+ return true;
445
+ }
446
+ return false;
447
+ };
448
+ }
449
+ async function getSmartLinkAdf(text, type, cardOptions) {
450
+ if (!cardOptions.provider) {
451
+ throw Error('No card provider found');
452
+ }
453
+ const provider = await cardOptions.provider;
454
+ return await provider.resolve(text, type);
455
+ }
456
+ function insertAutoMacro(slice, macro, view, from, to) {
457
+ if (view) {
458
+ // insert the text or linkified/md-converted clipboard data
459
+ const selection = view.state.tr.selection;
460
+ let tr;
461
+ let before;
462
+ if (typeof from === 'number' && typeof to === 'number') {
463
+ tr = view.state.tr.replaceRange(from, to, slice);
464
+ before = tr.mapping.map(from, -1);
465
+ } else {
466
+ tr = view.state.tr.replaceSelection(slice);
467
+ before = tr.mapping.map(selection.from, -1);
468
+ }
469
+ view.dispatch(tr);
470
+
471
+ // replace the text with the macro as a separate transaction
472
+ // so the autoconversion generates 2 undo steps
473
+ view.dispatch(closeHistory(view.state.tr).replaceRangeWith(before, before + slice.size, macro).scrollIntoView());
474
+ return true;
475
+ }
476
+ return false;
477
+ }
478
+ export function handleMacroAutoConvert(text, slice, queueCardsFromChangedTr, runMacroAutoConvert, cardsOptions, extensionAutoConverter) {
479
+ return (state, dispatch, view) => {
480
+ let macro = null;
481
+
482
+ // try to use auto convert from extension provider first
483
+ if (extensionAutoConverter) {
484
+ const extension = extensionAutoConverter(text);
485
+ if (extension) {
486
+ macro = PMNode.fromJSON(state.schema, extension);
487
+ }
488
+ }
489
+
490
+ // then try from macro provider (which will be removed some time in the future)
491
+ if (!macro) {
492
+ var _runMacroAutoConvert;
493
+ macro = (_runMacroAutoConvert = runMacroAutoConvert === null || runMacroAutoConvert === void 0 ? void 0 : runMacroAutoConvert(state, text)) !== null && _runMacroAutoConvert !== void 0 ? _runMacroAutoConvert : null;
494
+ }
495
+ if (macro) {
496
+ /**
497
+ * if FF enabled, run through smart links and check for result
498
+ */
499
+ if (cardsOptions && cardsOptions.resolveBeforeMacros && cardsOptions.resolveBeforeMacros.length) {
500
+ if (cardsOptions.resolveBeforeMacros.indexOf(macro.attrs.extensionKey) < 0) {
501
+ return insertAutoMacro(slice, macro, view);
502
+ }
503
+ if (!view) {
504
+ throw new Error('View is missing');
505
+ }
506
+ const trackingId = uuid();
507
+ const trackingFrom = `handleMacroAutoConvert-from-${trackingId}`;
508
+ const trackingTo = `handleMacroAutoConvert-to-${trackingId}`;
509
+ startTrackingPastedMacroPositions({
510
+ [trackingFrom]: state.selection.from,
511
+ [trackingTo]: state.selection.to
512
+ })(state, dispatch);
513
+ getSmartLinkAdf(text, 'inline', cardsOptions).then(() => {
514
+ // we use view.state rather than state because state becomes a stale
515
+ // state reference after getSmartLinkAdf's async work
516
+ const {
517
+ pastedMacroPositions
518
+ } = getPastePluginState(view.state);
519
+ if (dispatch) {
520
+ handleMarkdown(slice, queueCardsFromChangedTr, pastedMacroPositions[trackingFrom], pastedMacroPositions[trackingTo])(view.state, dispatch);
521
+ }
522
+ }).catch(() => {
523
+ const {
524
+ pastedMacroPositions
525
+ } = getPastePluginState(view.state);
526
+ insertAutoMacro(slice, macro, view, pastedMacroPositions[trackingFrom], pastedMacroPositions[trackingTo]);
527
+ }).finally(() => {
528
+ stopTrackingPastedMacroPositions([trackingFrom, trackingTo])(view.state, dispatch);
529
+ });
530
+ return true;
531
+ }
532
+ return insertAutoMacro(slice, macro, view);
533
+ }
534
+ return !!macro;
535
+ };
536
+ }
537
+ export function handleCodeBlock(text) {
538
+ return (state, dispatch) => {
539
+ const {
540
+ codeBlock
541
+ } = state.schema.nodes;
542
+ if (text && hasParentNodeOfType(codeBlock)(state.selection)) {
543
+ const tr = closeHistory(state.tr);
544
+ tr.scrollIntoView();
545
+ if (dispatch) {
546
+ dispatch(tr.insertText(text));
547
+ }
548
+ return true;
549
+ }
550
+ return false;
551
+ };
552
+ }
553
+ function isOnlyMedia(state, slice) {
554
+ const {
555
+ media
556
+ } = state.schema.nodes;
557
+ return slice.content.childCount === 1 && slice.content.firstChild.type === media;
558
+ }
559
+ function isOnlyMediaSingle(state, slice) {
560
+ const {
561
+ mediaSingle
562
+ } = state.schema.nodes;
563
+ return mediaSingle && slice.content.childCount === 1 && slice.content.firstChild.type === mediaSingle;
564
+ }
565
+ export function handleMediaSingle(inputMethod, insertMediaAsMediaSingle) {
566
+ return function (slice) {
567
+ return (state, dispatch, view) => {
568
+ if (view) {
569
+ if (isOnlyMedia(state, slice)) {
570
+ var _insertMediaAsMediaSi;
571
+ return (_insertMediaAsMediaSi = insertMediaAsMediaSingle === null || insertMediaAsMediaSingle === void 0 ? void 0 : insertMediaAsMediaSingle(view, slice.content.firstChild, inputMethod)) !== null && _insertMediaAsMediaSi !== void 0 ? _insertMediaAsMediaSi : false;
572
+ }
573
+ if (insideTable(state) && isOnlyMediaSingle(state, slice)) {
574
+ const tr = state.tr.replaceSelection(slice);
575
+ const nextPos = tr.doc.resolve(tr.mapping.map(state.selection.$from.pos));
576
+ if (dispatch) {
577
+ dispatch(tr.setSelection(new GapCursorSelection(nextPos, Side.RIGHT)));
578
+ }
579
+ return true;
580
+ }
581
+ }
582
+ return false;
583
+ };
584
+ };
585
+ }
586
+ const checkExpand = slice => {
587
+ let hasExpand = false;
588
+ slice.content.forEach(node => {
589
+ if (node.type.name === 'expand') {
590
+ hasExpand = true;
591
+ }
592
+ });
593
+ return hasExpand;
594
+ };
595
+ export function handleExpandPasteInTable(slice) {
596
+ return (state, dispatch) => {
597
+ // Do not handle expand if it's not being pasted into a table
598
+ // OR if it's nested within another node when being pasted into a table
599
+ if (!insideTable(state) || !checkExpand(slice)) {
600
+ return false;
601
+ }
602
+ const {
603
+ expand,
604
+ nestedExpand
605
+ } = state.schema.nodes;
606
+ let {
607
+ tr
608
+ } = state;
609
+ let hasExpand = false;
610
+ const newSlice = mapSlice(slice, maybeNode => {
611
+ if (maybeNode.type === expand) {
612
+ hasExpand = true;
613
+ try {
614
+ return nestedExpand.createChecked(maybeNode.attrs, maybeNode.content, maybeNode.marks);
615
+ } catch (e) {
616
+ tr = safeInsert(maybeNode, tr.selection.$to.pos)(tr);
617
+ return Fragment.empty;
618
+ }
619
+ }
620
+ return maybeNode;
621
+ });
622
+ if (hasExpand && dispatch) {
623
+ // If the slice is a subset, we can let PM replace the selection
624
+ // it will insert as text where it can't place the node.
625
+ // Otherwise we use safeInsert to insert below instead of
626
+ // replacing/splitting the current node.
627
+ if (slice.openStart > 1 && slice.openEnd > 1) {
628
+ dispatch(tr.replaceSelection(newSlice));
629
+ } else {
630
+ dispatch(safeInsert(newSlice.content)(tr));
631
+ }
632
+ return true;
633
+ }
634
+ return false;
635
+ };
636
+ }
637
+ export function handleMarkdown(markdownSlice, queueCardsFromChangedTr, from, to) {
638
+ return (state, dispatch) => {
639
+ const tr = closeHistory(state.tr);
640
+ const pastesFrom = typeof from === 'number' ? from : tr.selection.from;
641
+ if (typeof from === 'number' && typeof to === 'number') {
642
+ tr.replaceRange(from, to, markdownSlice);
643
+ } else {
644
+ tr.replaceSelection(markdownSlice);
645
+ }
646
+ const textPosition = tr.doc.resolve(Math.min(pastesFrom + markdownSlice.size, tr.doc.content.size));
647
+ tr.setSelection(TextSelection.near(textPosition, -1));
648
+ queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
649
+ if (dispatch) {
650
+ dispatch(tr.scrollIntoView());
651
+ }
652
+ return true;
653
+ };
654
+ }
655
+ function removePrecedingBackTick(tr) {
656
+ const {
657
+ $from: {
658
+ nodeBefore
659
+ },
660
+ from
661
+ } = tr.selection;
662
+ if (nodeBefore && nodeBefore.isText && nodeBefore.text.endsWith('`')) {
663
+ tr.delete(from - 1, from);
664
+ }
665
+ }
666
+ function hasInlineCode(state, slice) {
667
+ return slice.content.firstChild && slice.content.firstChild.marks.some(m => m.type === state.schema.marks.code);
668
+ }
669
+ function rollupLeafListItems(list, leafListItems) {
670
+ list.content.forEach(child => {
671
+ if (isListNode(child) || isListItemNode(child) && isListNode(child.firstChild)) {
672
+ rollupLeafListItems(child, leafListItems);
673
+ } else {
674
+ leafListItems.push(child);
675
+ }
676
+ });
677
+ }
678
+ function shouldFlattenList(state, slice) {
679
+ const node = slice.content.firstChild;
680
+ return node && insideTable(state) && isListNode(node) && slice.openStart > slice.openEnd;
681
+ }
682
+ function sliceHasTopLevelMarks(slice) {
683
+ let hasTopLevelMarks = false;
684
+ slice.content.descendants(node => {
685
+ if (node.marks.length > 0) {
686
+ hasTopLevelMarks = true;
687
+ }
688
+ return false;
689
+ });
690
+ return hasTopLevelMarks;
691
+ }
692
+ function getTopLevelMarkTypesInSlice(slice) {
693
+ const markTypes = new Set();
694
+ slice.content.descendants(node => {
695
+ node.marks.map(mark => mark.type).forEach(markType => markTypes.add(markType));
696
+ return false;
697
+ });
698
+ return markTypes;
699
+ }
700
+ export function handleParagraphBlockMarks(state, slice) {
701
+ if (slice.content.size === 0) {
702
+ return slice;
703
+ }
704
+ const {
705
+ schema,
706
+ selection: {
707
+ $from
708
+ }
709
+ } = state;
710
+
711
+ // If no paragraph in the slice contains marks, there's no need for special handling
712
+ // Note: this doesn't check for marks applied to lower level nodes such as text
713
+ if (!sliceHasTopLevelMarks(slice)) {
714
+ return slice;
715
+ }
716
+
717
+ // If pasting a single paragraph into pre-existing content, match destination formatting
718
+ const destinationHasContent = $from.parent.textContent.length > 0;
719
+ if (slice.content.childCount === 1 && destinationHasContent) {
720
+ return slice;
721
+ }
722
+
723
+ // Check the parent of (paragraph -> text) because block marks are assigned to a wrapper
724
+ // element around the paragraph node
725
+ const grandparent = $from.node(Math.max(0, $from.depth - 1));
726
+ const markTypesInSlice = getTopLevelMarkTypesInSlice(slice);
727
+ let forbiddenMarkTypes = [];
728
+ for (let markType of markTypesInSlice) {
729
+ if (!grandparent.type.allowsMarkType(markType)) {
730
+ forbiddenMarkTypes.push(markType);
731
+ }
732
+ }
733
+ if (forbiddenMarkTypes.length === 0) {
734
+ // In a slice containing one or more paragraphs at the document level (not wrapped in
735
+ // another node), the first paragraph will only have its text content captured and pasted
736
+ // since openStart is 1. We decrement the open depth of the slice so it retains any block
737
+ // marks applied to it. We only care about the depth at the start of the selection so
738
+ // there's no need to change openEnd - the rest of the slice gets pasted correctly.
739
+ const openStart = Math.max(0, slice.openStart - 1);
740
+ return new Slice(slice.content, openStart, slice.openEnd);
741
+ }
742
+
743
+ // If the paragraph contains marks forbidden by the parent node (e.g. alignment/indentation),
744
+ // drop those marks from the slice
745
+ return mapSlice(slice, node => {
746
+ if (node.type === schema.nodes.paragraph) {
747
+ return schema.nodes.paragraph.createChecked(undefined, node.content, node.marks.filter(mark => !forbiddenMarkTypes.includes(mark.type)));
748
+ }
749
+ return node;
750
+ });
751
+ }
752
+
753
+ /**
754
+ * ED-6300: When a nested list is pasted in a table cell and the slice has openStart > openEnd,
755
+ * it splits the table. As a workaround, we flatten the list to even openStart and openEnd.
756
+ *
757
+ * Note: this only happens if the first child is a list
758
+ *
759
+ * Example: copying "one" and "two"
760
+ * - zero
761
+ * - one
762
+ * - two
763
+ *
764
+ * Before:
765
+ * ul
766
+ * ┗━ li
767
+ * ┗━ ul
768
+ * ┗━ li
769
+ * ┗━ p -> "one"
770
+ * ┗━ li
771
+ * ┗━ p -> "two"
772
+ *
773
+ * After:
774
+ * ul
775
+ * ┗━ li
776
+ * ┗━ p -> "one"
777
+ * ┗━ li
778
+ * ┗━p -> "two"
779
+ */
780
+ export function flattenNestedListInSlice(slice) {
781
+ if (!slice.content.firstChild) {
782
+ return slice;
783
+ }
784
+ const listToFlatten = slice.content.firstChild;
785
+ const leafListItems = [];
786
+ rollupLeafListItems(listToFlatten, leafListItems);
787
+ const contentWithFlattenedList = slice.content.replaceChild(0, listToFlatten.type.createChecked(listToFlatten.attrs, leafListItems));
788
+ return new Slice(contentWithFlattenedList, slice.openEnd, slice.openEnd);
789
+ }
790
+ export function handleRichText(slice, queueCardsFromChangedTr) {
791
+ return (state, dispatch) => {
792
+ var _slice$content, _slice$content2, _firstChildOfSlice$ty, _lastChildOfSlice$typ, _panelParentOverCurre;
793
+ const {
794
+ codeBlock,
795
+ heading,
796
+ paragraph,
797
+ panel
798
+ } = state.schema.nodes;
799
+ const {
800
+ selection,
801
+ schema
802
+ } = state;
803
+ const firstChildOfSlice = (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.firstChild;
804
+ const lastChildOfSlice = (_slice$content2 = slice.content) === null || _slice$content2 === void 0 ? void 0 : _slice$content2.lastChild;
805
+
806
+ // In case user is pasting inline code,
807
+ // any backtick ` immediately preceding it should be removed.
808
+ let tr = state.tr;
809
+ if (hasInlineCode(state, slice)) {
810
+ removePrecedingBackTick(tr);
811
+ }
812
+ if (shouldFlattenList(state, slice)) {
813
+ slice = flattenNestedListInSlice(slice);
814
+ }
815
+ closeHistory(tr);
816
+ const isFirstChildListNode = isListNode(firstChildOfSlice);
817
+ const isLastChildListNode = isListNode(lastChildOfSlice);
818
+ const isSliceContentListNodes = isFirstChildListNode || isLastChildListNode;
819
+ const isFirstChildTaskListNode = (firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$ty = firstChildOfSlice.type) === null || _firstChildOfSlice$ty === void 0 ? void 0 : _firstChildOfSlice$ty.name) === 'taskList';
820
+ const isLastChildTaskListNode = (lastChildOfSlice === null || lastChildOfSlice === void 0 ? void 0 : (_lastChildOfSlice$typ = lastChildOfSlice.type) === null || _lastChildOfSlice$typ === void 0 ? void 0 : _lastChildOfSlice$typ.name) === 'taskList';
821
+ const isSliceContentTaskListNodes = isFirstChildTaskListNode || isLastChildTaskListNode;
822
+
823
+ // We want to use safeInsert to insert invalid content, as it inserts at the closest non schema violating position
824
+ // rather than spliting the selection parent node in half (which is what replaceSelection does)
825
+ // Exception is paragraph and heading nodes, these should be split, provided their parent supports the pasted content
826
+ const textNodes = [heading, paragraph];
827
+ const selectionParent = selection.$to.node(selection.$to.depth - 1);
828
+ const noNeedForSafeInsert = selection.$to.node().type.validContent(slice.content) || textNodes.includes(selection.$to.node().type) && selectionParent.type.validContent(slice.content);
829
+ let panelParentOverCurrentSelection = findParentNodeOfType(panel)(tr.selection);
830
+ const isTargetPanelEmpty = panelParentOverCurrentSelection && ((_panelParentOverCurre = panelParentOverCurrentSelection.node) === null || _panelParentOverCurre === void 0 ? void 0 : _panelParentOverCurre.content.size) === 2;
831
+ if (!isSliceContentTaskListNodes && (isSliceContentListNodes || isTargetPanelEmpty)) {
832
+ insertSliceForLists({
833
+ tr,
834
+ slice,
835
+ schema
836
+ });
837
+ } else if (noNeedForSafeInsert) {
838
+ var _firstChildOfSlice$ty2, _firstChildOfSlice$co, _firstChildOfSlice$co2;
839
+ if ((firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$ty2 = firstChildOfSlice.type) === null || _firstChildOfSlice$ty2 === void 0 ? void 0 : _firstChildOfSlice$ty2.name) === 'blockquote' && firstChildOfSlice !== null && firstChildOfSlice !== void 0 && (_firstChildOfSlice$co = firstChildOfSlice.content.firstChild) !== null && _firstChildOfSlice$co !== void 0 && _firstChildOfSlice$co.type.name && ['bulletList', 'orderedList'].includes(firstChildOfSlice === null || firstChildOfSlice === void 0 ? void 0 : (_firstChildOfSlice$co2 = firstChildOfSlice.content.firstChild) === null || _firstChildOfSlice$co2 === void 0 ? void 0 : _firstChildOfSlice$co2.type.name)) {
840
+ // checks if parent node is a blockquote and child node is either a bulletlist or orderedlist
841
+ insertSliceForListsInsideBlockquote({
842
+ tr,
843
+ slice
844
+ });
845
+ } else {
846
+ var _slice$content$lastCh, _slice$content$lastCh2;
847
+ tr.replaceSelection(slice);
848
+ // when cursor is inside a table cell, and slice.content.lastChild is a panel, expand, or decisionList
849
+ // need to make sure the cursor position is is right after the panel, expand, or decisionList
850
+ // still in the same table cell, see issue: https://product-fabric.atlassian.net/browse/ED-17862
851
+ const shouldUpdateCursorPosAfterPaste = ['panel', 'nestedExpand', 'decisionList', 'codeBlock'].includes(((_slice$content$lastCh = slice.content.lastChild) === null || _slice$content$lastCh === void 0 ? void 0 : (_slice$content$lastCh2 = _slice$content$lastCh.type) === null || _slice$content$lastCh2 === void 0 ? void 0 : _slice$content$lastCh2.name) || '');
852
+ if (insideTableCell(state) && shouldUpdateCursorPosAfterPaste) {
853
+ const nextPos = tr.doc.resolve(tr.mapping.map(selection.$from.pos));
854
+ tr.setSelection(new GapCursorSelection(nextPos, Side.RIGHT));
855
+ }
856
+ }
857
+ } else {
858
+ // need to scan the slice if there's a block node or list items inside it
859
+ let sliceHasList = false;
860
+ slice.content.nodesBetween(0, slice.content.size, (node, start) => {
861
+ if (node.type === state.schema.nodes.listItem) {
862
+ sliceHasList = true;
863
+ return false;
864
+ }
865
+ });
866
+ if (insideTableCell(state) && isInListItem(state) && canInsert(selection.$from, slice.content) && canInsert(selection.$to, slice.content) || sliceHasList) {
867
+ tr.replaceSelection(slice);
868
+ } else {
869
+ // need safeInsert rather than replaceSelection, so that nodes aren't split in half
870
+ // e.g. when pasting a layout into a table, replaceSelection splits the table in half and adds the layout in the middle
871
+ tr = safeInsert(slice.content, tr.selection.$to.pos)(tr);
872
+ }
873
+ }
874
+ tr.setStoredMarks([]);
875
+ if (tr.selection.empty && tr.selection.$from.parent.type === codeBlock) {
876
+ tr.setSelection(TextSelection.near(tr.selection.$from, 1));
877
+ }
878
+ tr.scrollIntoView();
879
+
880
+ // queue link cards, ignoring any errors
881
+ queueCardsFromChangedTr === null || queueCardsFromChangedTr === void 0 ? void 0 : queueCardsFromChangedTr(state, tr, INPUT_METHOD.CLIPBOARD);
882
+ if (dispatch) {
883
+ dispatch(tr);
884
+ }
885
+ return true;
886
+ };
887
+ }
888
+ export function handlePasteIntoCaption(slice) {
889
+ return (state, dispatch) => {
890
+ const {
891
+ caption
892
+ } = state.schema.nodes;
893
+ const tr = state.tr;
894
+ if (hasParentNodeOfType(caption)(state.selection)) {
895
+ // We let PM replace the selection and it will insert as text where it can't place the node
896
+ // This is totally fine as caption is just a simple block that only contains inline contents
897
+ // And it is more in line with WYSIWYG expectations
898
+ tr.replaceSelection(slice).scrollIntoView();
899
+ if (dispatch) {
900
+ dispatch(tr);
901
+ }
902
+ return true;
903
+ }
904
+ return false;
905
+ };
906
+ }
907
+ export const handleSelectedTable = editorAnalyticsAPI => slice => (state, dispatch) => {
908
+ let tr = replaceSelectedTable(state, slice);
909
+
910
+ // add analytics after replacing selected table
911
+ tr = addReplaceSelectedTableAnalytics(state, tr, editorAnalyticsAPI);
912
+ if (tr.docChanged) {
913
+ if (dispatch) {
914
+ dispatch(tr);
915
+ }
916
+ return true;
917
+ }
918
+ return false;
919
+ };