@atlaskit/editor-plugin-paste 0.1.22 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +18 -0
- package/CHANGELOG.md +6 -0
- package/dist/cjs/actions.js +12 -0
- package/dist/cjs/commands.js +255 -0
- package/dist/cjs/edge-cases/index.js +88 -0
- package/dist/cjs/edge-cases/lists.js +107 -0
- package/dist/cjs/handlers.js +939 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/plugin.js +43 -0
- package/dist/cjs/plugins/media.js +207 -0
- package/dist/cjs/pm-plugins/analytics.js +376 -0
- package/dist/cjs/pm-plugins/clipboard-text-serializer.js +43 -0
- package/dist/cjs/pm-plugins/main.js +484 -0
- package/dist/cjs/pm-plugins/plugin-factory.js +42 -0
- package/dist/cjs/reducer.js +41 -0
- package/dist/cjs/util/index.js +214 -0
- package/dist/cjs/util/tinyMCE.js +183 -0
- package/dist/es2019/actions.js +6 -0
- package/dist/es2019/commands.js +236 -0
- package/dist/es2019/edge-cases/index.js +87 -0
- package/dist/es2019/edge-cases/lists.js +113 -0
- package/dist/es2019/handlers.js +919 -0
- package/dist/es2019/index.js +1 -1
- package/dist/es2019/plugin.js +38 -0
- package/dist/es2019/plugins/media.js +204 -0
- package/dist/es2019/pm-plugins/analytics.js +332 -0
- package/dist/es2019/pm-plugins/clipboard-text-serializer.js +37 -0
- package/dist/es2019/pm-plugins/main.js +453 -0
- package/dist/es2019/pm-plugins/plugin-factory.js +30 -0
- package/dist/es2019/reducer.js +32 -0
- package/dist/es2019/util/index.js +209 -0
- package/dist/es2019/util/tinyMCE.js +168 -0
- package/dist/esm/actions.js +6 -0
- package/dist/esm/commands.js +249 -0
- package/dist/esm/edge-cases/index.js +81 -0
- package/dist/esm/edge-cases/lists.js +98 -0
- package/dist/esm/handlers.js +918 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/plugin.js +37 -0
- package/dist/esm/plugins/media.js +199 -0
- package/dist/esm/pm-plugins/analytics.js +364 -0
- package/dist/esm/pm-plugins/clipboard-text-serializer.js +37 -0
- package/dist/esm/pm-plugins/main.js +471 -0
- package/dist/esm/pm-plugins/plugin-factory.js +36 -0
- package/dist/esm/reducer.js +34 -0
- package/dist/esm/util/index.js +194 -0
- package/dist/esm/util/tinyMCE.js +176 -0
- package/dist/types/actions.d.ts +21 -0
- package/dist/types/commands.d.ts +29 -0
- package/dist/types/edge-cases/index.d.ts +11 -0
- package/dist/types/edge-cases/lists.d.ts +18 -0
- package/dist/types/handlers.d.ts +55 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/plugin.d.ts +2 -0
- package/dist/types/plugins/media.d.ts +23 -0
- package/dist/types/pm-plugins/analytics.d.ts +44 -0
- package/dist/types/pm-plugins/clipboard-text-serializer.d.ts +13 -0
- package/dist/types/pm-plugins/main.d.ts +12 -0
- package/dist/types/pm-plugins/plugin-factory.d.ts +3 -0
- package/dist/types/reducer.d.ts +3 -0
- package/dist/types/util/index.d.ts +21 -0
- package/dist/types/util/tinyMCE.d.ts +32 -0
- package/dist/types-ts4.5/actions.d.ts +21 -0
- package/dist/types-ts4.5/commands.d.ts +29 -0
- package/dist/types-ts4.5/edge-cases/index.d.ts +11 -0
- package/dist/types-ts4.5/edge-cases/lists.d.ts +18 -0
- package/dist/types-ts4.5/handlers.d.ts +55 -0
- package/dist/types-ts4.5/index.d.ts +1 -0
- package/dist/types-ts4.5/plugin.d.ts +2 -0
- package/dist/types-ts4.5/plugins/media.d.ts +23 -0
- package/dist/types-ts4.5/pm-plugins/analytics.d.ts +44 -0
- package/dist/types-ts4.5/pm-plugins/clipboard-text-serializer.d.ts +13 -0
- package/dist/types-ts4.5/pm-plugins/main.d.ts +12 -0
- package/dist/types-ts4.5/pm-plugins/plugin-factory.d.ts +3 -0
- package/dist/types-ts4.5/reducer.d.ts +3 -0
- package/dist/types-ts4.5/util/index.d.ts +21 -0
- package/dist/types-ts4.5/util/tinyMCE.d.ts +32 -0
- package/package.json +17 -5
|
@@ -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
|
+
};
|