@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.
- package/.eslintrc.js +18 -0
- package/CHANGELOG.md +12 -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 +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
|
+
};
|