@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,453 @@
1
+ import uuid from 'uuid';
2
+ import { ACTION, INPUT_METHOD, PasteTypes } from '@atlaskit/editor-common/analytics';
3
+ import { addLinkMetadata } from '@atlaskit/editor-common/card';
4
+ import { insideTable } from '@atlaskit/editor-common/core-utils';
5
+ import { getExtensionAutoConvertersFromProvider } from '@atlaskit/editor-common/extensions';
6
+ import { isPastedFile as isPastedFileFromEvent, md } from '@atlaskit/editor-common/paste';
7
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
8
+ import { transformSingleLineCodeBlockToCodeMark, transformSliceNestedExpandToExpand, transformSliceToDecisionList, transformSliceToJoinAdjacentCodeBlocks } from '@atlaskit/editor-common/transforms';
9
+ import { containsAnyAnnotations, extractSliceFromStep, linkifyContent, mapChildren, measureRender } from '@atlaskit/editor-common/utils';
10
+ import { MarkdownTransformer } from '@atlaskit/editor-markdown-transformer';
11
+ import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
12
+ import { hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
13
+ import { handlePaste as handlePasteTable } from '@atlaskit/editor-tables/utils';
14
+ import { PastePluginActionTypes } from '../actions';
15
+ import { splitParagraphs, upgradeTextToLists } from '../commands';
16
+ import { handleMacroAutoConvert, handleMention, handleParagraphBlockMarks } from '../handlers';
17
+ import { transformSliceForMedia, transformSliceToCorrectMediaWrapper, transformSliceToMediaSingleWithNewExperience, unwrapNestedMediaElements } from '../plugins/media';
18
+ import { escapeLinks, getPasteSource, htmlContainsSingleFile, htmlHasInvalidLinkTags, isPastedFromExcel, isPastedFromWord, removeDuplicateInvalidLinks, transformUnsupportedBlockCardToInline } from '../util';
19
+ import { htmlHasIncompleteTable, isPastedFromTinyMCEConfluence, tryRebuildCompleteTableHtml } from '../util/tinyMCE';
20
+ import { createPasteMeasurePayload, getContentNodeTypes, handleCodeBlockWithAnalytics, handleExpandWithAnalytics, handleMarkdownWithAnalytics, handleMediaSingleWithAnalytics, handlePasteAsPlainTextWithAnalytics, handlePasteIntoCaptionWithAnalytics, handlePasteIntoTaskAndDecisionWithAnalytics, handlePasteLinkOnSelectedTextWithAnalytics, handlePasteNonNestableBlockNodesIntoListWithAnalytics, handlePastePanelOrDecisionIntoListWithAnalytics, handlePastePreservingMarksWithAnalytics, handleRichTextWithAnalytics, handleSelectedTableWithAnalytics, sendPasteAnalyticsEvent } from './analytics';
21
+ import { clipboardTextSerializer } from './clipboard-text-serializer';
22
+ import { createPluginState, pluginKey as stateKey } from './plugin-factory';
23
+ export { pluginKey as stateKey } from './plugin-factory';
24
+ export const isInsideBlockQuote = state => {
25
+ const {
26
+ blockquote
27
+ } = state.schema.nodes;
28
+ return hasParentNodeOfType(blockquote)(state.selection);
29
+ };
30
+ const PASTE = 'Editor Paste Plugin Paste Duration';
31
+ export function createPlugin(schema, dispatchAnalyticsEvent, dispatch, featureFlags, pluginInjectionApi, cardOptions, sanitizePrivateContent, providerFactory) {
32
+ var _pluginInjectionApi$a;
33
+ const editorAnalyticsAPI = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions;
34
+ const atlassianMarkDownParser = new MarkdownTransformer(schema, md);
35
+ function getMarkdownSlice(text, openStart, openEnd) {
36
+ let textInput = escapeBackslashExceptCodeblock(text);
37
+ const doc = atlassianMarkDownParser.parse(escapeLinks(textInput));
38
+ if (doc && doc.content) {
39
+ return new Slice(doc.content, openStart, openEnd);
40
+ }
41
+ return;
42
+ }
43
+ function escapeBackslashExceptCodeblock(textInput) {
44
+ const codeToken = '```';
45
+ if (!textInput.includes(codeToken)) {
46
+ return textInput.replace(/\\/g, '\\\\');
47
+ }
48
+ let isInsideCodeblock = false;
49
+ let textSplitByNewLine = textInput.split('\n');
50
+ // In the splitted array, we traverse through every line and check if it will be parsed as a codeblock.
51
+ textSplitByNewLine = textSplitByNewLine.map(text => {
52
+ if (text === codeToken) {
53
+ isInsideCodeblock = !isInsideCodeblock;
54
+ } else if (text.startsWith(codeToken) && isInsideCodeblock === false) {
55
+ // if there is some text after the ``` mark , it gets counted as language attribute only at the start of codeblock
56
+ isInsideCodeblock = true;
57
+ }
58
+ if (!isInsideCodeblock) {
59
+ // only escape text which is not inside a codeblock
60
+ text = text.replace(/\\/g, '\\\\');
61
+ }
62
+ return text;
63
+ });
64
+ textInput = textSplitByNewLine.join('\n');
65
+ return textInput;
66
+ }
67
+ let extensionAutoConverter;
68
+ async function setExtensionAutoConverter(name, extensionProviderPromise) {
69
+ if (name !== 'extensionProvider' || !extensionProviderPromise) {
70
+ return;
71
+ }
72
+ try {
73
+ extensionAutoConverter = await getExtensionAutoConvertersFromProvider(extensionProviderPromise);
74
+ } catch (e) {
75
+ // eslint-disable-next-line no-console
76
+ console.error(e);
77
+ }
78
+ }
79
+ if (providerFactory) {
80
+ providerFactory.subscribe('extensionProvider', setExtensionAutoConverter);
81
+ }
82
+ let mostRecentPasteEvent;
83
+ let pastedFromBitBucket = false;
84
+ return new SafePlugin({
85
+ key: stateKey,
86
+ state: createPluginState(dispatch, {
87
+ pastedMacroPositions: {},
88
+ lastContentPasted: null
89
+ }),
90
+ props: {
91
+ // For serialising to plain text
92
+ clipboardTextSerializer,
93
+ handleDOMEvents: {
94
+ paste: (view, event) => {
95
+ mostRecentPasteEvent = event;
96
+ return false;
97
+ }
98
+ },
99
+ handlePaste(view, rawEvent, slice) {
100
+ var _text, _pluginInjectionApi$a2, _pluginInjectionApi$a3, _analyticsPlugin$perf, _analyticsPlugin$perf2, _schema$nodes, _schema$nodes2, _schema$nodes3, _pluginInjectionApi$m;
101
+ const event = rawEvent;
102
+ if (!event.clipboardData) {
103
+ return false;
104
+ }
105
+ let text = event.clipboardData.getData('text/plain');
106
+ const html = event.clipboardData.getData('text/html');
107
+ const uriList = event.clipboardData.getData('text/uri-list');
108
+ // Links copied from iOS Safari share button only have the text/uri-list data type
109
+ // ProseMirror don't do anything with this type so we want to make our own open slice
110
+ // with url as text content so link is pasted inline
111
+ if (uriList && !text && !html) {
112
+ text = uriList;
113
+ slice = new Slice(Fragment.from(schema.text(text)), 1, 1);
114
+ }
115
+ if ((_text = text) !== null && _text !== void 0 && _text.includes('\r')) {
116
+ text = text.replace(/\r/g, '');
117
+ }
118
+ const isPastedFile = isPastedFileFromEvent(event);
119
+ const isPlainText = text && !html;
120
+ const isRichText = !!html;
121
+
122
+ // Bail if copied content has files
123
+ if (isPastedFile) {
124
+ if (!html) {
125
+ /**
126
+ * Microsoft Office, Number, Pages, etc. adds an image to clipboard
127
+ * with other mime-types so we don't let the event reach media.
128
+ * The detection ration here is that if the payload has both `html` and
129
+ * `files`, then it could be one of above or an image copied from web.
130
+ * Here, we don't have html, so we return true to allow default event behaviour
131
+ */
132
+ return true;
133
+ }
134
+
135
+ /**
136
+ * We want to return false for external copied image to allow
137
+ * it to be uploaded by the client.
138
+ */
139
+ if (htmlContainsSingleFile(html)) {
140
+ return true;
141
+ }
142
+ event.stopImmediatePropagation();
143
+ }
144
+ const {
145
+ state
146
+ } = view;
147
+ const analyticsPlugin = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a2 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a2 === void 0 ? void 0 : (_pluginInjectionApi$a3 = _pluginInjectionApi$a2.sharedState) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : _pluginInjectionApi$a3.currentState();
148
+ const pasteTrackingEnabled = analyticsPlugin === null || analyticsPlugin === void 0 ? void 0 : (_analyticsPlugin$perf = analyticsPlugin.performanceTracking) === null || _analyticsPlugin$perf === void 0 ? void 0 : (_analyticsPlugin$perf2 = _analyticsPlugin$perf.pasteTracking) === null || _analyticsPlugin$perf2 === void 0 ? void 0 : _analyticsPlugin$perf2.enabled;
149
+ if (pasteTrackingEnabled) {
150
+ const content = getContentNodeTypes(slice.content);
151
+ const pasteId = uuid();
152
+ const measureName = `${PASTE}_${pasteId}`;
153
+ measureRender(measureName, ({
154
+ duration,
155
+ distortedDuration
156
+ }) => {
157
+ const payload = createPasteMeasurePayload({
158
+ view,
159
+ duration,
160
+ content,
161
+ distortedDuration
162
+ });
163
+ if (payload) {
164
+ dispatchAnalyticsEvent(payload);
165
+ }
166
+ });
167
+ }
168
+ // creating a custom dispatch because we want to add a meta whenever we do a paste.
169
+ const dispatch = tr => {
170
+ var _state$doc$resolve$no, _input;
171
+ // https://product-fabric.atlassian.net/browse/ED-12633
172
+ // don't add closeHistory call if we're pasting a text inside placeholder text as we want the whole action
173
+ // to be atomic
174
+ const {
175
+ placeholder
176
+ } = state.schema.nodes;
177
+ const isPastingTextInsidePlaceholderText = ((_state$doc$resolve$no = state.doc.resolve(state.selection.$anchor.pos).nodeAfter) === null || _state$doc$resolve$no === void 0 ? void 0 : _state$doc$resolve$no.type) === placeholder;
178
+
179
+ // Don't add closeHistory if we're pasting over layout columns, as we will appendTransaction
180
+ // to cleanup the layout's structure and we want to keep the paste and re-structuring as
181
+ // one event.
182
+ const isPastingOverLayoutColumns = hasParentNodeOfType(state.schema.nodes.layoutColumn)(state.selection);
183
+
184
+ // don't add closeHistory call if we're pasting a table, as some tables may involve additional
185
+ // appendedTransactions to repair them (if they're partial or incomplete) and we don't want
186
+ // to split those repairing transactions in prosemirror-history when they're being added to the
187
+ // "done" stack
188
+ const isPastingTable = tr.steps.some(step => {
189
+ var _slice$content;
190
+ const slice = extractSliceFromStep(step);
191
+ let tableExists = false;
192
+ slice === null || slice === void 0 ? void 0 : (_slice$content = slice.content) === null || _slice$content === void 0 ? void 0 : _slice$content.forEach(node => {
193
+ if (node.type === state.schema.nodes.table) {
194
+ tableExists = true;
195
+ }
196
+ });
197
+ return tableExists;
198
+ });
199
+ if (!isPastingTextInsidePlaceholderText && !isPastingTable && !isPastingOverLayoutColumns && pluginInjectionApi !== null && pluginInjectionApi !== void 0 && pluginInjectionApi.betterTypeHistory) {
200
+ var _pluginInjectionApi$b;
201
+ tr = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$b = pluginInjectionApi.betterTypeHistory) === null || _pluginInjectionApi$b === void 0 ? void 0 : _pluginInjectionApi$b.actions.flagPasteEvent(tr);
202
+ }
203
+ addLinkMetadata(view.state.selection, tr, {
204
+ action: isPlainText ? ACTION.PASTED_AS_PLAIN : ACTION.PASTED,
205
+ inputMethod: INPUT_METHOD.CLIPBOARD
206
+ });
207
+ const pasteStartPos = Math.min(state.selection.anchor, state.selection.head);
208
+ const pasteEndPos = tr.selection.to;
209
+ const contentPasted = {
210
+ pasteStartPos,
211
+ pasteEndPos,
212
+ text,
213
+ isShiftPressed: Boolean(
214
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
+ view.shiftKey || ((_input = view.input) === null || _input === void 0 ? void 0 : _input.shiftKey)),
216
+ isPlainText: Boolean(isPlainText),
217
+ pastedSlice: tr.doc.slice(pasteStartPos, pasteEndPos),
218
+ pastedAt: Date.now(),
219
+ pasteSource: getPasteSource(event)
220
+ };
221
+ tr.setMeta(stateKey, {
222
+ type: PastePluginActionTypes.ON_PASTE,
223
+ contentPasted
224
+ });
225
+ view.dispatch(tr);
226
+ };
227
+ slice = handleParagraphBlockMarks(state, slice);
228
+ const plainTextPasteSlice = linkifyContent(state.schema)(slice);
229
+ if (handlePasteAsPlainTextWithAnalytics(editorAnalyticsAPI)(view, event, plainTextPasteSlice)(state, dispatch, view)) {
230
+ return true;
231
+ }
232
+
233
+ // transform slices based on destination
234
+ slice = transformSliceForMedia(slice, schema)(state.selection);
235
+ let markdownSlice;
236
+ if (isPlainText) {
237
+ var _markdownSlice;
238
+ markdownSlice = getMarkdownSlice(text, slice.openStart, slice.openEnd);
239
+
240
+ // https://product-fabric.atlassian.net/browse/ED-15134
241
+ // Lists are not allowed within Blockquotes at this time. Attempting to
242
+ // paste a markdown list ie. ">- foo" will yeild a markdownSlice of size 0.
243
+ // Rather then blocking the paste action with no UI feedback, this will instead
244
+ // force a "paste as plain text" action by clearing the markdownSlice.
245
+ markdownSlice = !((_markdownSlice = markdownSlice) !== null && _markdownSlice !== void 0 && _markdownSlice.size) ? undefined : markdownSlice;
246
+ if (markdownSlice) {
247
+ var _pluginInjectionApi$c, _pluginInjectionApi$c2, _pluginInjectionApi$e, _pluginInjectionApi$e2;
248
+ // linkify text prior to converting to macro
249
+ if (handlePasteLinkOnSelectedTextWithAnalytics(editorAnalyticsAPI)(view, event, markdownSlice, PasteTypes.markdown)(state, dispatch)) {
250
+ return true;
251
+ }
252
+
253
+ // run macro autoconvert prior to other conversions
254
+ if (handleMacroAutoConvert(text, markdownSlice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.card) === null || _pluginInjectionApi$c === void 0 ? void 0 : (_pluginInjectionApi$c2 = _pluginInjectionApi$c.actions) === null || _pluginInjectionApi$c2 === void 0 ? void 0 : _pluginInjectionApi$c2.queueCardsFromChangedTr, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e = pluginInjectionApi.extension) === null || _pluginInjectionApi$e === void 0 ? void 0 : (_pluginInjectionApi$e2 = _pluginInjectionApi$e.actions) === null || _pluginInjectionApi$e2 === void 0 ? void 0 : _pluginInjectionApi$e2.runMacroAutoConvert, cardOptions, extensionAutoConverter)(state, dispatch, view)) {
255
+ // TODO: handleMacroAutoConvert dispatch twice, so we can't use the helper
256
+ sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, markdownSlice, {
257
+ type: PasteTypes.markdown
258
+ });
259
+ return true;
260
+ }
261
+ }
262
+ }
263
+ slice = transformUnsupportedBlockCardToInline(slice, state, cardOptions);
264
+
265
+ // Handles edge case so that when copying text from the top level of the document
266
+ // it can be pasted into nodes like panels/actions/decisions without removing them.
267
+ // Overriding openStart to be 1 when only pasting a paragraph makes the preferred
268
+ // depth favour the text, rather than the paragraph node.
269
+ // https://github.com/ProseMirror/prosemirror-transform/blob/master/src/replace.js#:~:text=Transform.prototype.-,replaceRange,-%3D%20function(from%2C%20to
270
+ const selectionDepth = state.selection.$head.depth;
271
+ const selectionParentNode = state.selection.$head.node(selectionDepth - 1);
272
+ const selectionParentType = selectionParentNode === null || selectionParentNode === void 0 ? void 0 : selectionParentNode.type;
273
+ const edgeCaseNodeTypes = [(_schema$nodes = schema.nodes) === null || _schema$nodes === void 0 ? void 0 : _schema$nodes.panel, (_schema$nodes2 = schema.nodes) === null || _schema$nodes2 === void 0 ? void 0 : _schema$nodes2.taskList, (_schema$nodes3 = schema.nodes) === null || _schema$nodes3 === void 0 ? void 0 : _schema$nodes3.decisionList];
274
+ if (slice.openStart === 0 && slice.openEnd !== 1 && selectionParentNode && edgeCaseNodeTypes.includes(selectionParentType)) {
275
+ // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop
276
+ slice.openStart = 1;
277
+ }
278
+ if (handlePasteIntoTaskAndDecisionWithAnalytics(view, event, slice, isPlainText ? PasteTypes.plain : PasteTypes.richText, pluginInjectionApi)(state, dispatch)) {
279
+ return true;
280
+ }
281
+
282
+ // If we're in a code block, append the text contents of clipboard inside it
283
+ if (handleCodeBlockWithAnalytics(editorAnalyticsAPI)(view, event, slice, text)(state, dispatch)) {
284
+ return true;
285
+ }
286
+ if (handleMediaSingleWithAnalytics(editorAnalyticsAPI)(view, event, slice, isPastedFile ? PasteTypes.binary : PasteTypes.richText, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$m = pluginInjectionApi.media) === null || _pluginInjectionApi$m === void 0 ? void 0 : _pluginInjectionApi$m.actions.insertMediaAsMediaSingle)(state, dispatch, view)) {
287
+ return true;
288
+ }
289
+ if (handleSelectedTableWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) {
290
+ return true;
291
+ }
292
+
293
+ // If the clipboard only contains plain text, attempt to parse it as Markdown
294
+ if (isPlainText && markdownSlice) {
295
+ if (handlePastePreservingMarksWithAnalytics(view, event, markdownSlice, PasteTypes.markdown, pluginInjectionApi)(state, dispatch)) {
296
+ return true;
297
+ }
298
+ return handleMarkdownWithAnalytics(view, event, markdownSlice, pluginInjectionApi)(state, dispatch);
299
+ }
300
+ if (isRichText && isInsideBlockQuote(state)) {
301
+ //If pasting inside blockquote
302
+ //Skip the blockquote node and keep remaining nodes as they are
303
+ const {
304
+ blockquote
305
+ } = schema.nodes;
306
+ const children = [];
307
+ mapChildren(slice.content, node => {
308
+ if (node.type === blockquote) {
309
+ for (let i = 0; i < node.childCount; i++) {
310
+ children.push(node.child(i));
311
+ }
312
+ } else {
313
+ children.push(node);
314
+ }
315
+ });
316
+ slice = new Slice(Fragment.fromArray(children), slice.openStart, slice.openEnd);
317
+ }
318
+
319
+ // finally, handle rich-text copy-paste
320
+ if (isRichText) {
321
+ var _pluginInjectionApi$c3, _pluginInjectionApi$c4, _pluginInjectionApi$e3, _pluginInjectionApi$e4, _pluginInjectionApi$l;
322
+ // linkify the text where possible
323
+ slice = linkifyContent(state.schema)(slice);
324
+ if (handlePasteLinkOnSelectedTextWithAnalytics(editorAnalyticsAPI)(view, event, slice, PasteTypes.richText)(state, dispatch)) {
325
+ return true;
326
+ }
327
+
328
+ // run macro autoconvert prior to other conversions
329
+ if (handleMacroAutoConvert(text, slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c3 = pluginInjectionApi.card) === null || _pluginInjectionApi$c3 === void 0 ? void 0 : (_pluginInjectionApi$c4 = _pluginInjectionApi$c3.actions) === null || _pluginInjectionApi$c4 === void 0 ? void 0 : _pluginInjectionApi$c4.queueCardsFromChangedTr, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e3 = pluginInjectionApi.extension) === null || _pluginInjectionApi$e3 === void 0 ? void 0 : (_pluginInjectionApi$e4 = _pluginInjectionApi$e3.actions) === null || _pluginInjectionApi$e4 === void 0 ? void 0 : _pluginInjectionApi$e4.runMacroAutoConvert, cardOptions, extensionAutoConverter)(state, dispatch, view)) {
330
+ // TODO: handleMacroAutoConvert dispatch twice, so we can't use the helper
331
+ sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, slice, {
332
+ type: PasteTypes.richText
333
+ });
334
+ return true;
335
+ }
336
+
337
+ // get editor-tables to handle pasting tables if it can
338
+ // otherwise, just the replace the selection with the content
339
+ if (handlePasteTable(view, null, slice)) {
340
+ sendPasteAnalyticsEvent(editorAnalyticsAPI)(view, event, slice, {
341
+ type: PasteTypes.richText
342
+ });
343
+ return true;
344
+ }
345
+
346
+ // remove annotation marks from the pasted data if they are not present in the document
347
+ // for the cases when they are pasted from external pages
348
+ if (slice.content.size && containsAnyAnnotations(slice, state)) {
349
+ var _pluginInjectionApi$a4;
350
+ pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a4 = pluginInjectionApi.annotation) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.actions.stripNonExistingAnnotations(slice, state);
351
+ }
352
+
353
+ // ED-4732
354
+ if (handlePastePreservingMarksWithAnalytics(view, event, slice, PasteTypes.richText, pluginInjectionApi)(state, dispatch)) {
355
+ return true;
356
+ }
357
+
358
+ // Check that we are pasting in a location that does not accept
359
+ // breakout marks, if so we strip the mark and paste. Note that
360
+ // breakout marks are only valid in the root document.
361
+ if (selectionParentType !== state.schema.nodes.doc) {
362
+ const sliceCopy = Slice.fromJSON(state.schema, slice.toJSON() || {});
363
+ sliceCopy.content.descendants(node => {
364
+ // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop
365
+ node.marks = node.marks.filter(mark => mark.type.name !== 'breakout');
366
+ // as breakout marks should only be on top level nodes,
367
+ // we don't traverse the entire document
368
+ return false;
369
+ });
370
+ slice = sliceCopy;
371
+ }
372
+ if (handleExpandWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) {
373
+ return true;
374
+ }
375
+ if (!insideTable(state)) {
376
+ slice = transformSliceNestedExpandToExpand(slice, state.schema);
377
+ }
378
+
379
+ // Create a custom handler to avoid handling with handleRichText method
380
+ // As SafeInsert is used inside handleRichText which caused some bad UX like this:
381
+ // https://product-fabric.atlassian.net/browse/MEX-1520
382
+ if (handlePasteIntoCaptionWithAnalytics(editorAnalyticsAPI)(view, event, slice, PasteTypes.richText)(state, dispatch)) {
383
+ return true;
384
+ }
385
+ if (handlePastePanelOrDecisionIntoListWithAnalytics(editorAnalyticsAPI)(view, event, slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$l = pluginInjectionApi.list) === null || _pluginInjectionApi$l === void 0 ? void 0 : _pluginInjectionApi$l.actions.findRootParentListNode)(state, dispatch)) {
386
+ return true;
387
+ }
388
+ if (handlePasteNonNestableBlockNodesIntoListWithAnalytics(editorAnalyticsAPI)(view, event, slice)(state, dispatch)) {
389
+ return true;
390
+ }
391
+ return handleRichTextWithAnalytics(view, event, slice, pluginInjectionApi)(state, dispatch);
392
+ }
393
+ return false;
394
+ },
395
+ transformPasted(slice) {
396
+ if (sanitizePrivateContent) {
397
+ slice = handleMention(slice, schema);
398
+ }
399
+
400
+ /* Bitbucket copies diffs as multiple adjacent code blocks
401
+ * so we merge ALL adjacent code blocks to support paste here */
402
+ if (pastedFromBitBucket) {
403
+ slice = transformSliceToJoinAdjacentCodeBlocks(slice);
404
+ }
405
+ slice = transformSingleLineCodeBlockToCodeMark(slice, schema);
406
+ slice = transformSliceToCorrectMediaWrapper(slice, schema);
407
+ slice = transformSliceToMediaSingleWithNewExperience(slice, schema);
408
+ slice = transformSliceToDecisionList(slice, schema);
409
+
410
+ // splitting linebreaks into paragraphs must happen before upgrading text to lists
411
+ slice = splitParagraphs(slice, schema);
412
+ slice = upgradeTextToLists(slice, schema);
413
+ if (slice.content.childCount && slice.content.lastChild.type === schema.nodes.codeBlock) {
414
+ slice = new Slice(slice.content, 0, 0);
415
+ }
416
+ return slice;
417
+ },
418
+ transformPastedHTML(html) {
419
+ // Fix for issue ED-4438
420
+ // text from google docs should not be pasted as inline code
421
+ if (html.indexOf('id="docs-internal-guid-') >= 0) {
422
+ html = html.replace(/white-space:pre/g, '');
423
+ html = html.replace(/white-space:pre-wrap/g, '');
424
+ }
425
+
426
+ // Partial fix for ED-7331: During a copy/paste from the legacy tinyMCE
427
+ // confluence editor, if we encounter an incomplete table (e.g. table elements
428
+ // not wrapped in <table>), we try to rebuild a complete, valid table if possible.
429
+ if (mostRecentPasteEvent && isPastedFromTinyMCEConfluence(mostRecentPasteEvent, html) && htmlHasIncompleteTable(html)) {
430
+ const completeTableHtml = tryRebuildCompleteTableHtml(html);
431
+ if (completeTableHtml) {
432
+ html = completeTableHtml;
433
+ }
434
+ }
435
+ if (!isPastedFromWord(html) && !isPastedFromExcel(html) && html.indexOf('<img ') >= 0) {
436
+ html = unwrapNestedMediaElements(html);
437
+ }
438
+
439
+ // https://product-fabric.atlassian.net/browse/ED-11714
440
+ // Checking for edge case when copying a list item containing links from Notion
441
+ // The html from this case is invalid with duplicate nested links
442
+ if (htmlHasInvalidLinkTags(html)) {
443
+ html = removeDuplicateInvalidLinks(html);
444
+ }
445
+
446
+ // Fix for ED-13568: Code blocks being copied/pasted when next to each other get merged
447
+ pastedFromBitBucket = html.indexOf('data-qa="code-line"') >= 0;
448
+ mostRecentPasteEvent = null;
449
+ return html;
450
+ }
451
+ }
452
+ });
453
+ }
@@ -0,0 +1,30 @@
1
+ import { pluginFactory } from '@atlaskit/editor-common/utils';
2
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
3
+ import { reducer } from '../reducer';
4
+ export const pluginKey = new PluginKey('pastePlugin');
5
+ export const {
6
+ createPluginState,
7
+ createCommand,
8
+ getPluginState
9
+ } = pluginFactory(pluginKey, reducer, {
10
+ mapping: (tr, pluginState) => {
11
+ if (tr.docChanged) {
12
+ let atLeastOnePositionChanged = false;
13
+ const positionsMappedThroughChanges = Object.entries(pluginState.pastedMacroPositions).reduce((acc, [key, position]) => {
14
+ const mappedPosition = tr.mapping.map(position);
15
+ if (position !== mappedPosition) {
16
+ atLeastOnePositionChanged = true;
17
+ }
18
+ acc[key] = tr.mapping.map(position);
19
+ return acc;
20
+ }, {});
21
+ if (atLeastOnePositionChanged) {
22
+ return {
23
+ ...pluginState,
24
+ pastedMacroPositions: positionsMappedThroughChanges
25
+ };
26
+ }
27
+ }
28
+ return pluginState;
29
+ }
30
+ });
@@ -0,0 +1,32 @@
1
+ import { PastePluginActionTypes as ActionTypes } from './actions';
2
+ export const reducer = (state, action) => {
3
+ switch (action.type) {
4
+ case ActionTypes.START_TRACKING_PASTED_MACRO_POSITIONS:
5
+ {
6
+ return {
7
+ ...state,
8
+ pastedMacroPositions: {
9
+ ...state.pastedMacroPositions,
10
+ ...action.pastedMacroPositions
11
+ }
12
+ };
13
+ }
14
+ case ActionTypes.STOP_TRACKING_PASTED_MACRO_POSITIONS:
15
+ {
16
+ const filteredMacroPositions = Object.fromEntries(Object.entries(state.pastedMacroPositions).filter(([key]) => !action.pastedMacroPositionKeys.includes(key)));
17
+ return {
18
+ ...state,
19
+ pastedMacroPositions: filteredMacroPositions
20
+ };
21
+ }
22
+ case ActionTypes.ON_PASTE:
23
+ {
24
+ return {
25
+ ...state,
26
+ lastContentPasted: action.contentPasted
27
+ };
28
+ }
29
+ default:
30
+ return state;
31
+ }
32
+ };