@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
package/dist/es2019/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { pastePlugin } from './plugin';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createPlugin } from './pm-plugins/main';
|
|
2
|
+
import { pluginKey } from './pm-plugins/plugin-factory';
|
|
3
|
+
export const pastePlugin = ({
|
|
4
|
+
config,
|
|
5
|
+
api
|
|
6
|
+
}) => {
|
|
7
|
+
var _api$featureFlags;
|
|
8
|
+
const {
|
|
9
|
+
cardOptions,
|
|
10
|
+
sanitizePrivateContent
|
|
11
|
+
} = config !== null && config !== void 0 ? config : {};
|
|
12
|
+
const featureFlags = (api === null || api === void 0 ? void 0 : (_api$featureFlags = api.featureFlags) === null || _api$featureFlags === void 0 ? void 0 : _api$featureFlags.sharedState.currentState()) || {};
|
|
13
|
+
return {
|
|
14
|
+
name: 'paste',
|
|
15
|
+
pmPlugins() {
|
|
16
|
+
return [{
|
|
17
|
+
name: 'paste',
|
|
18
|
+
plugin: ({
|
|
19
|
+
schema,
|
|
20
|
+
providerFactory,
|
|
21
|
+
dispatchAnalyticsEvent,
|
|
22
|
+
dispatch
|
|
23
|
+
}) => createPlugin(schema, dispatchAnalyticsEvent, dispatch, featureFlags, api, cardOptions, sanitizePrivateContent, providerFactory)
|
|
24
|
+
}];
|
|
25
|
+
},
|
|
26
|
+
getSharedState: editorState => {
|
|
27
|
+
if (!editorState) {
|
|
28
|
+
return {
|
|
29
|
+
lastContentPasted: null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const pluginState = pluginKey.getState(editorState);
|
|
33
|
+
return {
|
|
34
|
+
lastContentPasted: pluginState.lastContentPasted
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DEFAULT_IMAGE_WIDTH } from '@atlaskit/editor-common/media-single';
|
|
2
|
+
import { mapSlice, removeNestedEmptyEls, unwrap, walkUpTreeUntil } from '@atlaskit/editor-common/utils';
|
|
3
|
+
import { hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
4
|
+
import { getRandomHex } from '@atlaskit/media-common';
|
|
5
|
+
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensure correct layout in nested mode
|
|
9
|
+
*
|
|
10
|
+
* TODO: this func is only used in handlePaste, so layout update won't work for drop event
|
|
11
|
+
*/
|
|
12
|
+
export function transformSliceForMedia(slice, schema) {
|
|
13
|
+
const {
|
|
14
|
+
mediaSingle,
|
|
15
|
+
layoutSection,
|
|
16
|
+
table,
|
|
17
|
+
bulletList,
|
|
18
|
+
orderedList,
|
|
19
|
+
expand,
|
|
20
|
+
nestedExpand
|
|
21
|
+
} = schema.nodes;
|
|
22
|
+
return selection => {
|
|
23
|
+
let newSlice = slice;
|
|
24
|
+
if (hasParentNodeOfType([layoutSection, table, bulletList, orderedList, expand, nestedExpand])(selection)) {
|
|
25
|
+
newSlice = mapSlice(newSlice, node => {
|
|
26
|
+
const extendedOrLegacyAttrs = getBooleanFF('platform.editor.media.extended-resize-experience') ? {
|
|
27
|
+
layout: node.attrs.layout,
|
|
28
|
+
widthType: node.attrs.widthType,
|
|
29
|
+
width: node.attrs.width
|
|
30
|
+
} : {
|
|
31
|
+
layout: node.attrs.layout
|
|
32
|
+
};
|
|
33
|
+
let attrs = {};
|
|
34
|
+
if (hasParentNodeOfType([layoutSection, table])(selection)) {
|
|
35
|
+
// Supports layouts
|
|
36
|
+
attrs = {
|
|
37
|
+
...extendedOrLegacyAttrs
|
|
38
|
+
};
|
|
39
|
+
} else if (hasParentNodeOfType([bulletList, orderedList, expand, nestedExpand])(selection)) {
|
|
40
|
+
// does not support other layouts
|
|
41
|
+
attrs = {
|
|
42
|
+
...extendedOrLegacyAttrs,
|
|
43
|
+
layout: 'center'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return node.type.name === 'mediaSingle' ? mediaSingle.createChecked(attrs, node.content, node.marks) : node;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return newSlice;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TODO move this to editor-common
|
|
54
|
+
export const isImage = fileType => {
|
|
55
|
+
return !!fileType && (fileType.indexOf('image/') > -1 || fileType.indexOf('video/') > -1);
|
|
56
|
+
};
|
|
57
|
+
export const transformSliceToCorrectMediaWrapper = (slice, schema) => {
|
|
58
|
+
const {
|
|
59
|
+
mediaGroup,
|
|
60
|
+
mediaSingle,
|
|
61
|
+
media
|
|
62
|
+
} = schema.nodes;
|
|
63
|
+
return mapSlice(slice, (node, parent) => {
|
|
64
|
+
if (!parent && node.type === media) {
|
|
65
|
+
if (mediaSingle && (isImage(node.attrs.__fileMimeType) || node.attrs.type === 'external')) {
|
|
66
|
+
return mediaSingle.createChecked({}, node);
|
|
67
|
+
} else {
|
|
68
|
+
return mediaGroup.createChecked({}, [node]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return node;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* This func will be called when copy & paste, drag & drop external html with media, media files, and slices from editor
|
|
77
|
+
* Because width may not be available when transform, DEFAULT_IMAGE_WIDTH is used as a fallback
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
export const transformSliceToMediaSingleWithNewExperience = (slice, schema) => {
|
|
81
|
+
const {
|
|
82
|
+
mediaInline,
|
|
83
|
+
mediaSingle,
|
|
84
|
+
media
|
|
85
|
+
} = schema.nodes;
|
|
86
|
+
const newSlice = mapSlice(slice, node => {
|
|
87
|
+
// This logic is duplicated in editor-plugin-ai where external images can be inserted
|
|
88
|
+
// from external sources through the use of AI. The editor-plugin-ai package is avoiding
|
|
89
|
+
// sharing dependencies with editor-core to support products using it with various versions
|
|
90
|
+
// of editor packages.
|
|
91
|
+
// The duplication is in the following file:
|
|
92
|
+
// packages/editor/editor-plugin-ai/src/prebuilt/content-transformers/markdown-to-pm/markdown-transformer.ts
|
|
93
|
+
if (node.type === mediaSingle) {
|
|
94
|
+
return getBooleanFF('platform.editor.media.extended-resize-experience') ? mediaSingle.createChecked({
|
|
95
|
+
width: node.attrs.width || DEFAULT_IMAGE_WIDTH,
|
|
96
|
+
widthType: node.attrs.widthType || 'pixel',
|
|
97
|
+
layout: node.attrs.layout
|
|
98
|
+
}, node.content, node.marks) : node;
|
|
99
|
+
}
|
|
100
|
+
return node;
|
|
101
|
+
});
|
|
102
|
+
return mapSlice(newSlice, node => {
|
|
103
|
+
const __mediaTraceId = getRandomHex(8);
|
|
104
|
+
if (node.type === media) {
|
|
105
|
+
return media.createChecked({
|
|
106
|
+
...node.attrs,
|
|
107
|
+
__external: node.attrs.type === 'external',
|
|
108
|
+
__mediaTraceId: node.attrs.type === 'external' ? null : __mediaTraceId
|
|
109
|
+
}, node.content, node.marks);
|
|
110
|
+
}
|
|
111
|
+
if (node.type.name === 'mediaInline') {
|
|
112
|
+
return mediaInline.createChecked({
|
|
113
|
+
...node.attrs,
|
|
114
|
+
__mediaTraceId
|
|
115
|
+
}, node.content, node.marks);
|
|
116
|
+
}
|
|
117
|
+
return node;
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check base styles to see if an element will be invisible when rendered in a document.
|
|
123
|
+
* @param element
|
|
124
|
+
*/
|
|
125
|
+
const isElementInvisible = element => {
|
|
126
|
+
return element.style.opacity === '0' || element.style.display === 'none' || element.style.visibility === 'hidden';
|
|
127
|
+
};
|
|
128
|
+
const VALID_TAGS_CONTAINER = ['DIV', 'TD'];
|
|
129
|
+
function canContainImage(element) {
|
|
130
|
+
if (!element) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return VALID_TAGS_CONTAINER.indexOf(element.tagName) !== -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Given a html string, we attempt to hoist any nested `<img>` tags,
|
|
138
|
+
* not directly wrapped by a `<div>` as ProseMirror no-op's
|
|
139
|
+
* on those scenarios.
|
|
140
|
+
* @param html
|
|
141
|
+
*/
|
|
142
|
+
export const unwrapNestedMediaElements = html => {
|
|
143
|
+
const parser = new DOMParser();
|
|
144
|
+
let doc = parser.parseFromString(html, 'text/html');
|
|
145
|
+
const wrapper = doc.body;
|
|
146
|
+
|
|
147
|
+
// Remove Google Doc's wrapper <b> el
|
|
148
|
+
const docsWrapper = wrapper.querySelector('b[id^="docs-internal-guid-"]');
|
|
149
|
+
if (docsWrapper) {
|
|
150
|
+
unwrap(wrapper, docsWrapper);
|
|
151
|
+
}
|
|
152
|
+
const imageTags = wrapper.querySelectorAll('img');
|
|
153
|
+
if (!imageTags.length) {
|
|
154
|
+
return html;
|
|
155
|
+
}
|
|
156
|
+
imageTags.forEach(imageTag => {
|
|
157
|
+
// Capture the immediate parent, we may remove the media from here later.
|
|
158
|
+
const mediaParent = imageTag.parentElement;
|
|
159
|
+
if (!mediaParent) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If either the parent or the image itself contains styles that would make
|
|
164
|
+
// them invisible on copy, dont paste them.
|
|
165
|
+
if (isElementInvisible(mediaParent) || isElementInvisible(imageTag)) {
|
|
166
|
+
mediaParent.removeChild(imageTag);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If its wrapped by a valid container we assume its safe to bypass.
|
|
171
|
+
// ProseMirror should handle these cases properly.
|
|
172
|
+
if (canContainImage(mediaParent) || mediaParent instanceof HTMLSpanElement && mediaParent.closest('[class*="emoji-common"]')) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Find the top most element that the parent has a valid container for the image.
|
|
177
|
+
// Stop just before found the wrapper
|
|
178
|
+
const insertBeforeElement = walkUpTreeUntil(mediaParent, element => {
|
|
179
|
+
// If is at the top just use this element as reference
|
|
180
|
+
if (element.parentElement === wrapper) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return canContainImage(element.parentElement);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Here we try to insert the media right after its top most valid parent element
|
|
187
|
+
// Unless its the last element in our structure then we will insert above it.
|
|
188
|
+
if (insertBeforeElement && insertBeforeElement.parentElement) {
|
|
189
|
+
// Insert as close as possible to the most closest valid element index in the tree.
|
|
190
|
+
insertBeforeElement.parentElement.insertBefore(imageTag, insertBeforeElement.nextElementSibling || insertBeforeElement);
|
|
191
|
+
|
|
192
|
+
// Attempt to clean up lines left behind by the image
|
|
193
|
+
mediaParent.innerText = mediaParent.innerText.trim();
|
|
194
|
+
// Walk up and delete empty elements left over after removing the image tag
|
|
195
|
+
removeNestedEmptyEls(mediaParent);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// If last child is a hardbreak we don't want it
|
|
200
|
+
if (wrapper.lastElementChild && wrapper.lastElementChild.tagName === 'BR') {
|
|
201
|
+
wrapper.removeChild(wrapper.lastElementChild);
|
|
202
|
+
}
|
|
203
|
+
return wrapper.innerHTML;
|
|
204
|
+
};
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD, PasteContents, PasteTypes } from '@atlaskit/editor-common/analytics';
|
|
2
|
+
import { getLinkDomain, mapSlice } from '@atlaskit/editor-common/utils';
|
|
3
|
+
import { findParentNode } from '@atlaskit/editor-prosemirror/utils';
|
|
4
|
+
import { handleCodeBlock, handleExpandPasteInTable, handleMarkdown, handleMediaSingle, handlePasteAsPlainText, handlePasteIntoCaption, handlePasteIntoTaskOrDecisionOrPanel, handlePasteLinkOnSelectedText, handlePasteNonNestableBlockNodesIntoList, handlePastePanelOrDecisionContentIntoList, handlePastePreservingMarks, handleRichText, handleSelectedTable } from '../handlers';
|
|
5
|
+
import { getPasteSource } from '../util';
|
|
6
|
+
const contentToPasteContent = {
|
|
7
|
+
url: PasteContents.url,
|
|
8
|
+
paragraph: PasteContents.text,
|
|
9
|
+
bulletList: PasteContents.bulletList,
|
|
10
|
+
orderedList: PasteContents.orderedList,
|
|
11
|
+
heading: PasteContents.heading,
|
|
12
|
+
blockquote: PasteContents.blockquote,
|
|
13
|
+
codeBlock: PasteContents.codeBlock,
|
|
14
|
+
panel: PasteContents.panel,
|
|
15
|
+
rule: PasteContents.rule,
|
|
16
|
+
mediaSingle: PasteContents.mediaSingle,
|
|
17
|
+
mediaCard: PasteContents.mediaCard,
|
|
18
|
+
mediaGroup: PasteContents.mediaGroup,
|
|
19
|
+
table: PasteContents.table,
|
|
20
|
+
tableCells: PasteContents.tableCells,
|
|
21
|
+
tableHeader: PasteContents.tableHeader,
|
|
22
|
+
tableRow: PasteContents.tableRow,
|
|
23
|
+
decisionList: PasteContents.decisionList,
|
|
24
|
+
decisionItem: PasteContents.decisionItem,
|
|
25
|
+
taskList: PasteContents.taskItem,
|
|
26
|
+
extension: PasteContents.extension,
|
|
27
|
+
bodiedExtension: PasteContents.bodiedExtension,
|
|
28
|
+
blockCard: PasteContents.blockCard,
|
|
29
|
+
layoutSection: PasteContents.layoutSection
|
|
30
|
+
};
|
|
31
|
+
const nodeToActionSubjectId = {
|
|
32
|
+
blockquote: ACTION_SUBJECT_ID.PASTE_BLOCKQUOTE,
|
|
33
|
+
blockCard: ACTION_SUBJECT_ID.PASTE_BLOCK_CARD,
|
|
34
|
+
bodiedExtension: ACTION_SUBJECT_ID.PASTE_BODIED_EXTENSION,
|
|
35
|
+
bulletList: ACTION_SUBJECT_ID.PASTE_BULLET_LIST,
|
|
36
|
+
codeBlock: ACTION_SUBJECT_ID.PASTE_CODE_BLOCK,
|
|
37
|
+
decisionList: ACTION_SUBJECT_ID.PASTE_DECISION_LIST,
|
|
38
|
+
extension: ACTION_SUBJECT_ID.PASTE_EXTENSION,
|
|
39
|
+
heading: ACTION_SUBJECT_ID.PASTE_HEADING,
|
|
40
|
+
mediaGroup: ACTION_SUBJECT_ID.PASTE_MEDIA_GROUP,
|
|
41
|
+
mediaSingle: ACTION_SUBJECT_ID.PASTE_MEDIA_SINGLE,
|
|
42
|
+
orderedList: ACTION_SUBJECT_ID.PASTE_ORDERED_LIST,
|
|
43
|
+
panel: ACTION_SUBJECT_ID.PASTE_PANEL,
|
|
44
|
+
rule: ACTION_SUBJECT_ID.PASTE_RULE,
|
|
45
|
+
table: ACTION_SUBJECT_ID.PASTE_TABLE,
|
|
46
|
+
tableCell: ACTION_SUBJECT_ID.PASTE_TABLE_CELL,
|
|
47
|
+
tableHeader: ACTION_SUBJECT_ID.PASTE_TABLE_HEADER,
|
|
48
|
+
tableRow: ACTION_SUBJECT_ID.PASTE_TABLE_ROW,
|
|
49
|
+
taskList: ACTION_SUBJECT_ID.PASTE_TASK_LIST
|
|
50
|
+
};
|
|
51
|
+
export function getContent({
|
|
52
|
+
schema,
|
|
53
|
+
slice
|
|
54
|
+
}) {
|
|
55
|
+
const {
|
|
56
|
+
nodes: {
|
|
57
|
+
paragraph
|
|
58
|
+
},
|
|
59
|
+
marks: {
|
|
60
|
+
link
|
|
61
|
+
}
|
|
62
|
+
} = schema;
|
|
63
|
+
const nodeOrMarkName = new Set();
|
|
64
|
+
slice.content.forEach(node => {
|
|
65
|
+
if (node.type === paragraph && node.content.size === 0) {
|
|
66
|
+
// Skip empty paragraph
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (node.type.name === 'text' && link.isInSet(node.marks)) {
|
|
70
|
+
nodeOrMarkName.add('url');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check node contain link
|
|
75
|
+
if (node.type === paragraph && node.rangeHasMark(0, node.nodeSize - 2, link)) {
|
|
76
|
+
nodeOrMarkName.add('url');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
nodeOrMarkName.add(node.type.name);
|
|
80
|
+
});
|
|
81
|
+
if (nodeOrMarkName.size > 1) {
|
|
82
|
+
return PasteContents.mixed;
|
|
83
|
+
}
|
|
84
|
+
if (nodeOrMarkName.size === 0) {
|
|
85
|
+
return PasteContents.uncategorized;
|
|
86
|
+
}
|
|
87
|
+
const type = nodeOrMarkName.values().next().value;
|
|
88
|
+
const pasteContent = contentToPasteContent[type];
|
|
89
|
+
return pasteContent ? pasteContent : PasteContents.uncategorized;
|
|
90
|
+
}
|
|
91
|
+
export function getMediaTraceId(slice) {
|
|
92
|
+
let traceId;
|
|
93
|
+
mapSlice(slice, node => {
|
|
94
|
+
if (node.type.name === 'media' || node.type.name === 'mediaInline') {
|
|
95
|
+
traceId = node.attrs.__mediaTraceId;
|
|
96
|
+
}
|
|
97
|
+
return node;
|
|
98
|
+
});
|
|
99
|
+
return traceId;
|
|
100
|
+
}
|
|
101
|
+
function getActionSubjectId({
|
|
102
|
+
selection,
|
|
103
|
+
schema
|
|
104
|
+
}) {
|
|
105
|
+
const {
|
|
106
|
+
nodes: {
|
|
107
|
+
paragraph,
|
|
108
|
+
listItem,
|
|
109
|
+
taskItem,
|
|
110
|
+
decisionItem
|
|
111
|
+
}
|
|
112
|
+
} = schema;
|
|
113
|
+
const parent = findParentNode(node => {
|
|
114
|
+
if (node.type !== paragraph && node.type !== listItem && node.type !== taskItem && node.type !== decisionItem) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
})(selection);
|
|
119
|
+
if (!parent) {
|
|
120
|
+
return ACTION_SUBJECT_ID.PASTE_PARAGRAPH;
|
|
121
|
+
}
|
|
122
|
+
const parentType = parent.node.type;
|
|
123
|
+
const actionSubjectId = nodeToActionSubjectId[parentType.name];
|
|
124
|
+
return actionSubjectId ? actionSubjectId : ACTION_SUBJECT_ID.PASTE_PARAGRAPH;
|
|
125
|
+
}
|
|
126
|
+
function createPasteAsPlainPayload(actionSubjectId, text, linksInPasteCount) {
|
|
127
|
+
return {
|
|
128
|
+
action: ACTION.PASTED_AS_PLAIN,
|
|
129
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
130
|
+
actionSubjectId,
|
|
131
|
+
eventType: EVENT_TYPE.TRACK,
|
|
132
|
+
attributes: {
|
|
133
|
+
inputMethod: INPUT_METHOD.KEYBOARD,
|
|
134
|
+
pasteSize: text.length,
|
|
135
|
+
linksInPasteCount
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function createPastePayload(actionSubjectId, attributes, linkDomain) {
|
|
140
|
+
return {
|
|
141
|
+
action: ACTION.PASTED,
|
|
142
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
143
|
+
actionSubjectId,
|
|
144
|
+
eventType: EVENT_TYPE.TRACK,
|
|
145
|
+
attributes: {
|
|
146
|
+
inputMethod: INPUT_METHOD.KEYBOARD,
|
|
147
|
+
...attributes
|
|
148
|
+
},
|
|
149
|
+
...(linkDomain && linkDomain.length > 0 ? {
|
|
150
|
+
nonPrivacySafeAttributes: {
|
|
151
|
+
linkDomain
|
|
152
|
+
}
|
|
153
|
+
} : {})
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function createPasteAnalyticsPayloadBySelection(event, slice, pasteContext) {
|
|
157
|
+
return selection => {
|
|
158
|
+
const text = event.clipboardData ? event.clipboardData.getData('text/plain') || event.clipboardData.getData('text/uri-list') : '';
|
|
159
|
+
const actionSubjectId = getActionSubjectId({
|
|
160
|
+
selection: selection,
|
|
161
|
+
schema: selection.$from.doc.type.schema
|
|
162
|
+
});
|
|
163
|
+
const pasteSize = slice.size;
|
|
164
|
+
const content = getContent({
|
|
165
|
+
schema: selection.$from.doc.type.schema,
|
|
166
|
+
slice
|
|
167
|
+
});
|
|
168
|
+
const linkUrls = [];
|
|
169
|
+
const mediaTraceId = getMediaTraceId(slice);
|
|
170
|
+
|
|
171
|
+
// If we have a link among the pasted content, grab the
|
|
172
|
+
// domain and send it up with the analytics event
|
|
173
|
+
if (content === PasteContents.url || content === PasteContents.mixed) {
|
|
174
|
+
mapSlice(slice, node => {
|
|
175
|
+
const linkMark = node.marks.find(mark => mark.type.name === 'link');
|
|
176
|
+
if (linkMark) {
|
|
177
|
+
linkUrls.push(linkMark.attrs.href);
|
|
178
|
+
}
|
|
179
|
+
return node;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (pasteContext.asPlain) {
|
|
183
|
+
return createPasteAsPlainPayload(actionSubjectId, text, linkUrls.length);
|
|
184
|
+
}
|
|
185
|
+
const source = getPasteSource(event);
|
|
186
|
+
if (pasteContext.type === PasteTypes.plain) {
|
|
187
|
+
return createPastePayload(actionSubjectId, {
|
|
188
|
+
pasteSize: text.length,
|
|
189
|
+
type: pasteContext.type,
|
|
190
|
+
content: PasteContents.text,
|
|
191
|
+
source,
|
|
192
|
+
hyperlinkPasteOnText: false,
|
|
193
|
+
linksInPasteCount: linkUrls.length,
|
|
194
|
+
pasteSplitList: pasteContext.pasteSplitList
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const linkDomains = linkUrls.map(getLinkDomain);
|
|
198
|
+
return createPastePayload(actionSubjectId, {
|
|
199
|
+
type: pasteContext.type,
|
|
200
|
+
pasteSize,
|
|
201
|
+
content,
|
|
202
|
+
source,
|
|
203
|
+
hyperlinkPasteOnText: !!pasteContext.hyperlinkPasteOnText,
|
|
204
|
+
linksInPasteCount: linkUrls.length,
|
|
205
|
+
mediaTraceId,
|
|
206
|
+
pasteSplitList: pasteContext.pasteSplitList
|
|
207
|
+
}, linkDomains);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function createPasteAnalyticsPayload(view, event, slice, pasteContext) {
|
|
211
|
+
return createPasteAnalyticsPayloadBySelection(event, slice, pasteContext)(view.state.selection);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// TODO: ED-6612 We should not dispatch only analytics, it's preferred to wrap each command with his own analytics.
|
|
215
|
+
// However, handlers like handleMacroAutoConvert dispatch multiple time,
|
|
216
|
+
// so pasteCommandWithAnalytics is useless in this case.
|
|
217
|
+
export const sendPasteAnalyticsEvent = editorAnalyticsAPI => (view, event, slice, pasteContext) => {
|
|
218
|
+
const tr = view.state.tr;
|
|
219
|
+
const payload = createPasteAnalyticsPayload(view, event, slice, pasteContext);
|
|
220
|
+
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(tr);
|
|
221
|
+
view.dispatch(tr);
|
|
222
|
+
};
|
|
223
|
+
export const handlePasteAsPlainTextWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
224
|
+
type: PasteTypes.plain,
|
|
225
|
+
asPlain: true
|
|
226
|
+
}))(handlePasteAsPlainText(slice, event, editorAnalyticsAPI));
|
|
227
|
+
export const handlePasteIntoTaskAndDecisionWithAnalytics = (view, event, slice, type, pluginInjectionApi) => {
|
|
228
|
+
var _pluginInjectionApi$a, _pluginInjectionApi$c, _pluginInjectionApi$c2;
|
|
229
|
+
return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
230
|
+
type
|
|
231
|
+
}))(handlePasteIntoTaskOrDecisionOrPanel(slice, 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));
|
|
232
|
+
};
|
|
233
|
+
export const handlePasteIntoCaptionWithAnalytics = editorAnalyticsAPI => (view, event, slice, type) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
234
|
+
type
|
|
235
|
+
}))(handlePasteIntoCaption(slice));
|
|
236
|
+
export const handleCodeBlockWithAnalytics = editorAnalyticsAPI => (view, event, slice, text) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
237
|
+
type: PasteTypes.plain
|
|
238
|
+
}))(handleCodeBlock(text));
|
|
239
|
+
export const handleMediaSingleWithAnalytics = editorAnalyticsAPI => (view, event, slice, type, insertMediaAsMediaSingle) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
240
|
+
type
|
|
241
|
+
}))(handleMediaSingle(INPUT_METHOD.CLIPBOARD, insertMediaAsMediaSingle)(slice));
|
|
242
|
+
export const handlePastePreservingMarksWithAnalytics = (view, event, slice, type, pluginInjectionApi) => {
|
|
243
|
+
var _pluginInjectionApi$a2, _pluginInjectionApi$c3, _pluginInjectionApi$c4;
|
|
244
|
+
return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a2 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a2 === void 0 ? void 0 : _pluginInjectionApi$a2.actions)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
245
|
+
type
|
|
246
|
+
}))(handlePastePreservingMarks(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));
|
|
247
|
+
};
|
|
248
|
+
export const handleMarkdownWithAnalytics = (view, event, slice, pluginInjectionApi) => {
|
|
249
|
+
var _pluginInjectionApi$a3, _pluginInjectionApi$c5, _pluginInjectionApi$c6;
|
|
250
|
+
return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a3 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : _pluginInjectionApi$a3.actions)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
251
|
+
type: PasteTypes.markdown
|
|
252
|
+
}))(handleMarkdown(slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c5 = pluginInjectionApi.card) === null || _pluginInjectionApi$c5 === void 0 ? void 0 : (_pluginInjectionApi$c6 = _pluginInjectionApi$c5.actions) === null || _pluginInjectionApi$c6 === void 0 ? void 0 : _pluginInjectionApi$c6.queueCardsFromChangedTr));
|
|
253
|
+
};
|
|
254
|
+
export const handleRichTextWithAnalytics = (view, event, slice, pluginInjectionApi) => {
|
|
255
|
+
var _pluginInjectionApi$a4, _pluginInjectionApi$c7, _pluginInjectionApi$c8;
|
|
256
|
+
return injectAnalyticsPayloadBeforeCommand(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a4 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.actions)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
257
|
+
type: PasteTypes.richText
|
|
258
|
+
}))(handleRichText(slice, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c7 = pluginInjectionApi.card) === null || _pluginInjectionApi$c7 === void 0 ? void 0 : (_pluginInjectionApi$c8 = _pluginInjectionApi$c7.actions) === null || _pluginInjectionApi$c8 === void 0 ? void 0 : _pluginInjectionApi$c8.queueCardsFromChangedTr));
|
|
259
|
+
};
|
|
260
|
+
const injectAnalyticsPayloadBeforeCommand = editorAnalyticsAPI => createPayloadByTransaction => {
|
|
261
|
+
return mainCommand => {
|
|
262
|
+
return (state, dispatch, view) => {
|
|
263
|
+
let originalTransaction = state.tr;
|
|
264
|
+
const fakeDispatch = tr => {
|
|
265
|
+
originalTransaction = tr;
|
|
266
|
+
};
|
|
267
|
+
const result = mainCommand(state, fakeDispatch, view);
|
|
268
|
+
if (!result) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (dispatch && originalTransaction.docChanged) {
|
|
272
|
+
// it needs to know the selection before the changes
|
|
273
|
+
const payload = createPayloadByTransaction(state.selection);
|
|
274
|
+
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(originalTransaction);
|
|
275
|
+
dispatch(originalTransaction);
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
export const handlePastePanelOrDecisionIntoListWithAnalytics = editorAnalyticsAPI => (view, event, slice, findRootParentListNode) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
282
|
+
type: PasteTypes.richText
|
|
283
|
+
}))(handlePastePanelOrDecisionContentIntoList(slice, findRootParentListNode));
|
|
284
|
+
export const handlePasteNonNestableBlockNodesIntoListWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
285
|
+
type: PasteTypes.richText,
|
|
286
|
+
pasteSplitList: true
|
|
287
|
+
}))(handlePasteNonNestableBlockNodesIntoList(slice));
|
|
288
|
+
export const handleExpandWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
289
|
+
type: PasteTypes.richText,
|
|
290
|
+
pasteSplitList: true
|
|
291
|
+
}))(handleExpandPasteInTable(slice));
|
|
292
|
+
export const handleSelectedTableWithAnalytics = editorAnalyticsAPI => (view, event, slice) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
293
|
+
type: PasteTypes.richText
|
|
294
|
+
}))(handleSelectedTable(editorAnalyticsAPI)(slice));
|
|
295
|
+
export const handlePasteLinkOnSelectedTextWithAnalytics = editorAnalyticsAPI => (view, event, slice, type) => injectAnalyticsPayloadBeforeCommand(editorAnalyticsAPI)(createPasteAnalyticsPayloadBySelection(event, slice, {
|
|
296
|
+
type,
|
|
297
|
+
hyperlinkPasteOnText: true
|
|
298
|
+
}))(handlePasteLinkOnSelectedText(slice));
|
|
299
|
+
export const createPasteMeasurePayload = ({
|
|
300
|
+
view,
|
|
301
|
+
duration,
|
|
302
|
+
content,
|
|
303
|
+
distortedDuration
|
|
304
|
+
}) => {
|
|
305
|
+
const pasteIntoNode = getActionSubjectId({
|
|
306
|
+
selection: view.state.selection,
|
|
307
|
+
schema: view.state.schema
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
action: ACTION.PASTED_TIMED,
|
|
311
|
+
actionSubject: ACTION_SUBJECT.EDITOR,
|
|
312
|
+
eventType: EVENT_TYPE.OPERATIONAL,
|
|
313
|
+
attributes: {
|
|
314
|
+
pasteIntoNode,
|
|
315
|
+
content,
|
|
316
|
+
time: duration,
|
|
317
|
+
distortedDuration
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
export const getContentNodeTypes = content => {
|
|
322
|
+
let nodeTypes = new Set();
|
|
323
|
+
if (content.size) {
|
|
324
|
+
content.forEach(node => {
|
|
325
|
+
if (node.content && node.content.size) {
|
|
326
|
+
nodeTypes = new Set([...nodeTypes, ...getContentNodeTypes(node.content)]);
|
|
327
|
+
}
|
|
328
|
+
nodeTypes.add(node.type.name);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return Array.from(nodeTypes);
|
|
332
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a plain text serialization of a given slice. This is used for populating the plain text
|
|
3
|
+
* section of the clipboard on copy.
|
|
4
|
+
* The current implementation is bare bones - only inlineCards, blockCards and mentions are tested (they
|
|
5
|
+
* previously were empty on plain text copy).
|
|
6
|
+
* Unknown nodes are passed to node.textBetween().
|
|
7
|
+
*
|
|
8
|
+
* By default (without this function passed to the editor), the editor uses
|
|
9
|
+
* `slice.content.textBetween(0, slice.content.size, "\n\n")`
|
|
10
|
+
* (see https://prosemirror.net/docs/ref/#view.EditorProps.clipboardTextSerializer)
|
|
11
|
+
*/
|
|
12
|
+
export function clipboardTextSerializer(slice) {
|
|
13
|
+
let text = '';
|
|
14
|
+
const blockSeparater = '\n\n';
|
|
15
|
+
slice.content.nodesBetween(0, slice.content.size, node => {
|
|
16
|
+
if (node.type.isBlock) {
|
|
17
|
+
text += blockSeparater;
|
|
18
|
+
}
|
|
19
|
+
if (node.type.name === 'paragraph') {
|
|
20
|
+
return true;
|
|
21
|
+
} else if (node.type.name === 'hardBreak') {
|
|
22
|
+
text += '\n';
|
|
23
|
+
} else if (node.type.name === 'text') {
|
|
24
|
+
text += node.text;
|
|
25
|
+
} else if (node.type.name === 'inlineCard') {
|
|
26
|
+
text += node.attrs.url;
|
|
27
|
+
} else if (node.type.name === 'blockCard') {
|
|
28
|
+
text += node.attrs.url;
|
|
29
|
+
} else if (node.type.name === 'mention') {
|
|
30
|
+
text += node.attrs.text;
|
|
31
|
+
} else {
|
|
32
|
+
text += node.textBetween(0, node.content.size, '\n\n');
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
return text.trim();
|
|
37
|
+
}
|