@atlaskit/editor-plugin-paste 1.3.1 → 1.3.2
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/CHANGELOG.md +8 -0
- package/dist/cjs/plugin.js +11 -2
- package/dist/cjs/pm-plugins/move-analytics/actions.js +11 -0
- package/dist/cjs/pm-plugins/move-analytics/commands.js +45 -0
- package/dist/cjs/pm-plugins/move-analytics/plugin-factory.js +13 -0
- package/dist/cjs/pm-plugins/move-analytics/plugin-key.js +8 -0
- package/dist/cjs/pm-plugins/move-analytics/plugin.js +138 -0
- package/dist/cjs/pm-plugins/move-analytics/reducer.js +26 -0
- package/dist/cjs/pm-plugins/move-analytics/types.js +19 -0
- package/dist/cjs/pm-plugins/move-analytics/utils.js +120 -0
- package/dist/es2019/plugin.js +12 -2
- package/dist/es2019/pm-plugins/move-analytics/actions.js +5 -0
- package/dist/es2019/pm-plugins/move-analytics/commands.js +28 -0
- package/dist/es2019/pm-plugins/move-analytics/plugin-factory.js +8 -0
- package/dist/es2019/pm-plugins/move-analytics/plugin-key.js +2 -0
- package/dist/es2019/pm-plugins/move-analytics/plugin.js +144 -0
- package/dist/es2019/pm-plugins/move-analytics/reducer.js +21 -0
- package/dist/es2019/pm-plugins/move-analytics/types.js +13 -0
- package/dist/es2019/pm-plugins/move-analytics/utils.js +124 -0
- package/dist/esm/plugin.js +11 -2
- package/dist/esm/pm-plugins/move-analytics/actions.js +5 -0
- package/dist/esm/pm-plugins/move-analytics/commands.js +38 -0
- package/dist/esm/pm-plugins/move-analytics/plugin-factory.js +8 -0
- package/dist/esm/pm-plugins/move-analytics/plugin-key.js +2 -0
- package/dist/esm/pm-plugins/move-analytics/plugin.js +133 -0
- package/dist/esm/pm-plugins/move-analytics/reducer.js +19 -0
- package/dist/esm/pm-plugins/move-analytics/types.js +13 -0
- package/dist/esm/pm-plugins/move-analytics/utils.js +114 -0
- package/dist/types/pm-plugins/move-analytics/actions.d.ts +14 -0
- package/dist/types/pm-plugins/move-analytics/commands.d.ts +5 -0
- package/dist/types/pm-plugins/move-analytics/plugin-factory.d.ts +1 -0
- package/dist/types/pm-plugins/move-analytics/plugin-key.d.ts +3 -0
- package/dist/types/pm-plugins/move-analytics/plugin.d.ts +4 -0
- package/dist/types/pm-plugins/move-analytics/reducer.d.ts +3 -0
- package/dist/types/pm-plugins/move-analytics/types.d.ts +11 -0
- package/dist/types/pm-plugins/move-analytics/utils.d.ts +12 -0
- package/dist/types/types.d.ts +1 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/actions.d.ts +14 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/commands.d.ts +5 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/plugin-factory.d.ts +1 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/plugin-key.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/plugin.d.ts +4 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/reducer.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/types.d.ts +11 -0
- package/dist/types-ts4.5/pm-plugins/move-analytics/utils.d.ts +12 -0
- package/dist/types-ts4.5/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
2
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
|
+
import { resetContentMoved, resetContentMovedTransform, updateContentMoved } from './commands';
|
|
4
|
+
import { createPluginState, getPluginState } from './plugin-factory';
|
|
5
|
+
import { pluginKey } from './plugin-key';
|
|
6
|
+
import { defaultState } from './types';
|
|
7
|
+
import { isBlockNodeWithoutTable, isCursorSelectionAndInsideTopLevelNode, isEntireTopLevelBlockquoteSelected, isEntireTopLevelHeadingOrParagraphSelected, isExcludedNode, isInlineNode, isNestedInlineNode, isNestedTable, isNodeSelection, isTextSelection, isValidNodeName } from './utils';
|
|
8
|
+
|
|
9
|
+
// This plugin exists only in FullPage/FullWidth Editor and is used to register an event that tells us
|
|
10
|
+
// that a user cut and than pasted a node. This order of actions could be considered an alternative
|
|
11
|
+
// to new Drag and Drop functionality. The event (document moved) is not accurate, but should be enough to be
|
|
12
|
+
// used during DnD roll out. After DnD release this plugin must be removed.
|
|
13
|
+
export const createPlugin = (dispatch, editorAnalyticsAPI) => {
|
|
14
|
+
// This variable is used to distinguish between copy and cut events in transformCopied.
|
|
15
|
+
let isCutEvent = false;
|
|
16
|
+
return new SafePlugin({
|
|
17
|
+
key: pluginKey,
|
|
18
|
+
state: createPluginState(dispatch, defaultState),
|
|
19
|
+
props: {
|
|
20
|
+
handleDOMEvents: {
|
|
21
|
+
cut: () => {
|
|
22
|
+
isCutEvent = true;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handlePaste: ({
|
|
26
|
+
state,
|
|
27
|
+
dispatch
|
|
28
|
+
}, event, slice) => {
|
|
29
|
+
var _content$firstChild;
|
|
30
|
+
// The state was cleaned after previous paste. We don't need to update plugin state
|
|
31
|
+
// with 'contentPasted' if currentActions array doesn't have 'copiedOrCut'.
|
|
32
|
+
const {
|
|
33
|
+
contentMoved
|
|
34
|
+
} = getPluginState(state);
|
|
35
|
+
const hasCutAction = contentMoved.currentActions.includes('contentCut');
|
|
36
|
+
if (!hasCutAction) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const {
|
|
40
|
+
content,
|
|
41
|
+
size
|
|
42
|
+
} = slice;
|
|
43
|
+
const nodeName = (_content$firstChild = content.firstChild) === null || _content$firstChild === void 0 ? void 0 : _content$firstChild.type.name;
|
|
44
|
+
// We should not account for pastes that go inside another node and create nested content as DnD can't do it.
|
|
45
|
+
if (!nodeName || !(contentMoved !== null && contentMoved !== void 0 && contentMoved.nodeName) || !isValidNodeName(contentMoved === null || contentMoved === void 0 ? void 0 : contentMoved.nodeName, nodeName) || size !== (contentMoved === null || contentMoved === void 0 ? void 0 : contentMoved.size) || !isCursorSelectionAndInsideTopLevelNode(state.selection)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const {
|
|
49
|
+
tr
|
|
50
|
+
} = state;
|
|
51
|
+
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
|
|
52
|
+
action: ACTION.MOVED,
|
|
53
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
54
|
+
actionSubjectId: ACTION_SUBJECT_ID.NODE,
|
|
55
|
+
eventType: EVENT_TYPE.TRACK,
|
|
56
|
+
attributes: {
|
|
57
|
+
nodeType: contentMoved === null || contentMoved === void 0 ? void 0 : contentMoved.nodeName // keep nodeName from copied slice
|
|
58
|
+
}
|
|
59
|
+
})(tr);
|
|
60
|
+
|
|
61
|
+
// reset to default state
|
|
62
|
+
const updatedTr = resetContentMovedTransform()(tr);
|
|
63
|
+
dispatch(updatedTr);
|
|
64
|
+
},
|
|
65
|
+
transformCopied: (slice, {
|
|
66
|
+
state,
|
|
67
|
+
dispatch
|
|
68
|
+
}) => {
|
|
69
|
+
var _content$firstChild2;
|
|
70
|
+
// We want to listen only to 'cut' events
|
|
71
|
+
if (!isCutEvent) {
|
|
72
|
+
return slice;
|
|
73
|
+
}
|
|
74
|
+
let resetState = false;
|
|
75
|
+
const {
|
|
76
|
+
content,
|
|
77
|
+
size
|
|
78
|
+
} = slice;
|
|
79
|
+
// Content should be just one node, so we added a check for slice.content.childCount === 1;
|
|
80
|
+
// 1. It is possible to select a table by dragging the mouse over the table's rows.
|
|
81
|
+
// As a result, slice will contain rows without tableNode itself and the childCount will be the number of rows.
|
|
82
|
+
// From a user's perspective the whole table is selected and copied and on paste a table will indeed be created.
|
|
83
|
+
// 2. Some block nodes can get selected when a user drags the mouse from the paragraph above the node to
|
|
84
|
+
// the paragraph below the node. Visually only the node in between is selected, in reality, three nodes are
|
|
85
|
+
// in the slice.
|
|
86
|
+
// These cases are ignored and moveContent event won't be counted.
|
|
87
|
+
if (content.childCount !== 1) {
|
|
88
|
+
resetState = true;
|
|
89
|
+
}
|
|
90
|
+
const nodeName = ((_content$firstChild2 = content.firstChild) === null || _content$firstChild2 === void 0 ? void 0 : _content$firstChild2.type.name) || '';
|
|
91
|
+
// Some nodes are not relevant as they are parts of nodes, not whole nodes (like tableCell, tableHeader instead of table node)
|
|
92
|
+
// Some nodes like lists, taskList(item), decisionList(item) requires tricky checks that we want to avoid doing.
|
|
93
|
+
// These nodes were added to excludedNodes array.
|
|
94
|
+
if (!resetState && isExcludedNode(nodeName)) {
|
|
95
|
+
resetState = true;
|
|
96
|
+
}
|
|
97
|
+
const {
|
|
98
|
+
selection
|
|
99
|
+
} = state;
|
|
100
|
+
// DnD can't drag part of text in a paragraph/heading. DnD will select the whole node with all the text inside.
|
|
101
|
+
// So we are only interested in cut slices that contain the entire node, not just a part of it.
|
|
102
|
+
if (!resetState && nodeName === 'paragraph' && !isEntireTopLevelHeadingOrParagraphSelected(selection, nodeName)) {
|
|
103
|
+
resetState = true;
|
|
104
|
+
}
|
|
105
|
+
if (!resetState && nodeName === 'heading' && !isEntireTopLevelHeadingOrParagraphSelected(selection, nodeName)) {
|
|
106
|
+
resetState = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// DnD can't drag just one paragraph (when blockquote contains multiple paragraphs) or just a part of a paragraph in the blockquote.
|
|
110
|
+
// DnD will select and drag the whole blockquote. So we need to register cut events that have the entire blockquote too.
|
|
111
|
+
if (!resetState && nodeName === 'blockquote' && !isEntireTopLevelBlockquoteSelected(state)) {
|
|
112
|
+
resetState = true;
|
|
113
|
+
}
|
|
114
|
+
if (!resetState && isInlineNode(nodeName) && isNestedInlineNode(selection)) {
|
|
115
|
+
resetState = true;
|
|
116
|
+
}
|
|
117
|
+
if (!resetState && nodeName === 'table' && isNestedTable(selection)) {
|
|
118
|
+
resetState = true;
|
|
119
|
+
}
|
|
120
|
+
const isBlockNode = isBlockNodeWithoutTable(nodeName);
|
|
121
|
+
if (!resetState && isNodeSelection(selection) && isBlockNode && selection.$anchor.node().type.name !== 'doc' // checks that the block node is not a topLevel node
|
|
122
|
+
) {
|
|
123
|
+
resetState = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Some blockNodes can have text inside of them cut, in that case TextSelection occurs, we don't need to track
|
|
127
|
+
// these cut events.
|
|
128
|
+
if (!resetState && isTextSelection(selection) && isBlockNode) {
|
|
129
|
+
resetState = true;
|
|
130
|
+
}
|
|
131
|
+
if (resetState) {
|
|
132
|
+
resetContentMoved()(state, dispatch);
|
|
133
|
+
} else {
|
|
134
|
+
updateContentMoved({
|
|
135
|
+
size: size,
|
|
136
|
+
nodeName: nodeName
|
|
137
|
+
}, 'contentCut')(state, dispatch);
|
|
138
|
+
}
|
|
139
|
+
isCutEvent = false;
|
|
140
|
+
return slice;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MoveAnalyticPluginTypes } from './actions';
|
|
2
|
+
import { defaultState } from './types';
|
|
3
|
+
export const reducer = (state, action) => {
|
|
4
|
+
switch (action.type) {
|
|
5
|
+
case MoveAnalyticPluginTypes.UpdateMovedAction:
|
|
6
|
+
return {
|
|
7
|
+
...state,
|
|
8
|
+
contentMoved: {
|
|
9
|
+
...state.contentMoved,
|
|
10
|
+
...action.data
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
case MoveAnalyticPluginTypes.RemoveMovedAction:
|
|
14
|
+
return {
|
|
15
|
+
...state,
|
|
16
|
+
contentMoved: defaultState.contentMoved
|
|
17
|
+
};
|
|
18
|
+
default:
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// For an event to be considered a move content event we want to ensure that
|
|
2
|
+
// cut had happened and was followed by paste. So our currentActions array should
|
|
3
|
+
// look like this ['contentCut', 'contentPasted'];
|
|
4
|
+
// After removing appendTransaction we don't update currentActions with 'contentPasted'
|
|
5
|
+
// but leaving it as array in case we decide to change it back or add 'contentCopied'.
|
|
6
|
+
|
|
7
|
+
export const defaultState = {
|
|
8
|
+
contentMoved: {
|
|
9
|
+
nodeName: undefined,
|
|
10
|
+
size: undefined,
|
|
11
|
+
currentActions: []
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
2
|
+
import { findParentNodeOfTypeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
|
|
3
|
+
const excludedNodes = ['caption', 'layoutColumn', 'listItem', 'nestedExpand', 'tableHeader', 'tableCell', 'tableRow', 'text', 'placeholder', 'unsupportedBlock', 'unsupportedInline', 'hardBreak', 'media', 'confluenceUnsupportedBlock', 'confluenceUnsupportedInline', 'bulletList', 'orderedList', 'taskList', 'taskItem', 'decisionList', 'decisionItem'];
|
|
4
|
+
export const isExcludedNode = nodeName => excludedNodes.includes(nodeName);
|
|
5
|
+
export const isCursorSelectionAndInsideTopLevelNode = selection => {
|
|
6
|
+
const {
|
|
7
|
+
$from,
|
|
8
|
+
from,
|
|
9
|
+
to
|
|
10
|
+
} = selection;
|
|
11
|
+
if (from !== to || $from.depth > 1) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
const inlineNodes = ['emoji', 'date', 'status', 'mention', 'mediaInline', 'inlineCard', 'inlineExtension'];
|
|
17
|
+
export const isInlineNode = nodeName => {
|
|
18
|
+
return inlineNodes.includes(nodeName);
|
|
19
|
+
};
|
|
20
|
+
export const isNestedInlineNode = selection => {
|
|
21
|
+
if (selection.$from.depth !== 1) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// check if the node is a part of a larger paragraph or heading
|
|
26
|
+
const parentSize = selection.$from.parent.content.size;
|
|
27
|
+
const contentSize = selection.content().size;
|
|
28
|
+
const parentChildCount = selection.$from.parent.childCount;
|
|
29
|
+
// when the node was copied and pasted, it won't have extra space the parent has only one child
|
|
30
|
+
if (parentChildCount === 1) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// some inline nodes (like date and emoji) when inserted have extra space that is stores as a child
|
|
35
|
+
if (parentChildCount === 2 && parentSize - contentSize === 1) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
const blockNodes = ['bodiedExtension', 'blockCard', 'codeBlock', 'embedCard', 'expand', 'extension', 'layoutSection', 'mediaGroup', 'mediaSingle', 'panel', 'rule'];
|
|
41
|
+
export const isBlockNodeWithoutTable = nodeName => {
|
|
42
|
+
return blockNodes.includes(nodeName);
|
|
43
|
+
};
|
|
44
|
+
const parentNodes = ['expand', 'extension', 'bodiedExtension', 'layoutSection'];
|
|
45
|
+
export const isNestedTable = selection => {
|
|
46
|
+
const parentNode = selection.$anchor.node(1);
|
|
47
|
+
return parentNode && parentNodes.includes(parentNode.type.name);
|
|
48
|
+
};
|
|
49
|
+
const getPastedNameOfInlineNode = nodeName => {
|
|
50
|
+
if (inlineNodes.includes(nodeName)) {
|
|
51
|
+
return 'paragraph';
|
|
52
|
+
}
|
|
53
|
+
return nodeName;
|
|
54
|
+
};
|
|
55
|
+
export const isValidNodeName = (copiedNodeName, pastedNodeName) => {
|
|
56
|
+
if (copiedNodeName === pastedNodeName) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (getPastedNameOfInlineNode(copiedNodeName) === pastedNodeName) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
export const isTextSelection = selection => {
|
|
65
|
+
return selection instanceof TextSelection && selection.from !== selection.to;
|
|
66
|
+
};
|
|
67
|
+
export const isNodeSelection = selection => {
|
|
68
|
+
return selection instanceof NodeSelection;
|
|
69
|
+
};
|
|
70
|
+
const isEntireHeadingOrParagraphSelected = (selection, nodeName) => {
|
|
71
|
+
if (!isTextSelection(selection)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const {
|
|
75
|
+
$from,
|
|
76
|
+
$to
|
|
77
|
+
} = selection;
|
|
78
|
+
if (!($from.parent.type.name === nodeName)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if ($from.parent === $to.parent) {
|
|
82
|
+
const node = $from.parent;
|
|
83
|
+
return $from.parentOffset === 0 && $to.parentOffset === node.content.size;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export const isEntireTopLevelHeadingOrParagraphSelected = (selection, nodeName) => {
|
|
87
|
+
if (selection.$from.depth !== 1) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return isEntireHeadingOrParagraphSelected(selection, nodeName);
|
|
91
|
+
};
|
|
92
|
+
export const isEntireTopLevelBlockquoteSelected = state => {
|
|
93
|
+
const {
|
|
94
|
+
schema,
|
|
95
|
+
selection
|
|
96
|
+
} = state;
|
|
97
|
+
const {
|
|
98
|
+
blockquote
|
|
99
|
+
} = schema.nodes;
|
|
100
|
+
const blockquoteNode = findParentNodeOfTypeClosestToPos(selection.$from, blockquote);
|
|
101
|
+
if (!blockquoteNode) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// checks if it is a top level blockquote
|
|
106
|
+
const {
|
|
107
|
+
depth
|
|
108
|
+
} = blockquoteNode;
|
|
109
|
+
if (depth !== 1) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const {
|
|
113
|
+
from,
|
|
114
|
+
to
|
|
115
|
+
} = selection;
|
|
116
|
+
let selectedNodesCount = 0;
|
|
117
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
118
|
+
if (pos >= from && pos + node.nodeSize <= to) {
|
|
119
|
+
selectedNodesCount++;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return selectedNodesCount === blockquoteNode.node.childCount;
|
|
124
|
+
};
|
package/dist/esm/plugin.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { createPlugin } from './pm-plugins/main';
|
|
2
|
+
import { createPlugin as createMoveAnalyticsPlugin } from './pm-plugins/move-analytics/plugin';
|
|
2
3
|
import { pluginKey } from './pm-plugins/plugin-factory';
|
|
3
4
|
export var pastePlugin = function pastePlugin(_ref) {
|
|
4
|
-
var _api$featureFlags;
|
|
5
|
+
var _api$featureFlags, _api$analytics;
|
|
5
6
|
var config = _ref.config,
|
|
6
7
|
api = _ref.api;
|
|
7
8
|
var _ref2 = config !== null && config !== void 0 ? config : {},
|
|
8
9
|
cardOptions = _ref2.cardOptions,
|
|
9
|
-
sanitizePrivateContent = _ref2.sanitizePrivateContent
|
|
10
|
+
sanitizePrivateContent = _ref2.sanitizePrivateContent,
|
|
11
|
+
isFullPage = _ref2.isFullPage;
|
|
10
12
|
var featureFlags = (api === null || api === void 0 || (_api$featureFlags = api.featureFlags) === null || _api$featureFlags === void 0 ? void 0 : _api$featureFlags.sharedState.currentState()) || {};
|
|
13
|
+
var editorAnalyticsAPI = api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
|
|
11
14
|
return {
|
|
12
15
|
name: 'paste',
|
|
13
16
|
pmPlugins: function pmPlugins() {
|
|
@@ -20,6 +23,12 @@ export var pastePlugin = function pastePlugin(_ref) {
|
|
|
20
23
|
dispatch = _ref3.dispatch;
|
|
21
24
|
return createPlugin(schema, dispatchAnalyticsEvent, dispatch, featureFlags, api, cardOptions, sanitizePrivateContent, providerFactory);
|
|
22
25
|
}
|
|
26
|
+
}, {
|
|
27
|
+
name: 'moveAnalyticsPlugin',
|
|
28
|
+
plugin: function plugin(_ref4) {
|
|
29
|
+
var dispatch = _ref4.dispatch;
|
|
30
|
+
return isFullPage ? createMoveAnalyticsPlugin(dispatch, editorAnalyticsAPI) : undefined;
|
|
31
|
+
}
|
|
23
32
|
}];
|
|
24
33
|
},
|
|
25
34
|
getSharedState: function getSharedState(editorState) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export var MoveAnalyticPluginTypes = /*#__PURE__*/function (MoveAnalyticPluginTypes) {
|
|
2
|
+
MoveAnalyticPluginTypes[MoveAnalyticPluginTypes["UpdateMovedAction"] = 0] = "UpdateMovedAction";
|
|
3
|
+
MoveAnalyticPluginTypes[MoveAnalyticPluginTypes["RemoveMovedAction"] = 1] = "RemoveMovedAction";
|
|
4
|
+
return MoveAnalyticPluginTypes;
|
|
5
|
+
}({});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
2
|
+
import { MoveAnalyticPluginTypes } from './actions';
|
|
3
|
+
import { createCommand, getPluginState } from './plugin-factory';
|
|
4
|
+
import { pluginKey } from './plugin-key';
|
|
5
|
+
export var updateContentMoved = function updateContentMoved(nextState, nextAction) {
|
|
6
|
+
return createCommand(function (state) {
|
|
7
|
+
var _getPluginState = getPluginState(state),
|
|
8
|
+
contentMoved = _getPluginState.contentMoved;
|
|
9
|
+
var data = {
|
|
10
|
+
currentActions: [].concat(_toConsumableArray(contentMoved.currentActions), [nextAction]),
|
|
11
|
+
size: (nextState === null || nextState === void 0 ? void 0 : nextState.size) || contentMoved.size,
|
|
12
|
+
nodeName: nextState === null || nextState === void 0 ? void 0 : nextState.nodeName
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
type: MoveAnalyticPluginTypes.UpdateMovedAction,
|
|
16
|
+
data: data
|
|
17
|
+
};
|
|
18
|
+
}, function (tr) {
|
|
19
|
+
return tr.setMeta('addToHistory', false);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
export var resetContentMoved = function resetContentMoved() {
|
|
23
|
+
return createCommand(function () {
|
|
24
|
+
return {
|
|
25
|
+
type: MoveAnalyticPluginTypes.RemoveMovedAction
|
|
26
|
+
};
|
|
27
|
+
}, function (tr) {
|
|
28
|
+
return tr.setMeta('addToHistory', false);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
export var resetContentMovedTransform = function resetContentMovedTransform() {
|
|
32
|
+
return function (tr) {
|
|
33
|
+
var payload = {
|
|
34
|
+
type: MoveAnalyticPluginTypes.RemoveMovedAction
|
|
35
|
+
};
|
|
36
|
+
return tr.setMeta(pluginKey, payload);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { pluginFactory } from '@atlaskit/editor-common/utils';
|
|
2
|
+
import { pluginKey } from './plugin-key';
|
|
3
|
+
import { reducer } from './reducer';
|
|
4
|
+
var _pluginFactory = pluginFactory(pluginKey, reducer),
|
|
5
|
+
createPluginState = _pluginFactory.createPluginState,
|
|
6
|
+
createCommand = _pluginFactory.createCommand,
|
|
7
|
+
getPluginState = _pluginFactory.getPluginState;
|
|
8
|
+
export { createPluginState, createCommand, getPluginState };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
2
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
|
+
import { resetContentMoved, resetContentMovedTransform, updateContentMoved } from './commands';
|
|
4
|
+
import { createPluginState, getPluginState } from './plugin-factory';
|
|
5
|
+
import { pluginKey } from './plugin-key';
|
|
6
|
+
import { defaultState } from './types';
|
|
7
|
+
import { isBlockNodeWithoutTable, isCursorSelectionAndInsideTopLevelNode, isEntireTopLevelBlockquoteSelected, isEntireTopLevelHeadingOrParagraphSelected, isExcludedNode, isInlineNode, isNestedInlineNode, isNestedTable, isNodeSelection, isTextSelection, isValidNodeName } from './utils';
|
|
8
|
+
|
|
9
|
+
// This plugin exists only in FullPage/FullWidth Editor and is used to register an event that tells us
|
|
10
|
+
// that a user cut and than pasted a node. This order of actions could be considered an alternative
|
|
11
|
+
// to new Drag and Drop functionality. The event (document moved) is not accurate, but should be enough to be
|
|
12
|
+
// used during DnD roll out. After DnD release this plugin must be removed.
|
|
13
|
+
export var createPlugin = function createPlugin(dispatch, editorAnalyticsAPI) {
|
|
14
|
+
// This variable is used to distinguish between copy and cut events in transformCopied.
|
|
15
|
+
var isCutEvent = false;
|
|
16
|
+
return new SafePlugin({
|
|
17
|
+
key: pluginKey,
|
|
18
|
+
state: createPluginState(dispatch, defaultState),
|
|
19
|
+
props: {
|
|
20
|
+
handleDOMEvents: {
|
|
21
|
+
cut: function cut() {
|
|
22
|
+
isCutEvent = true;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handlePaste: function handlePaste(_ref, event, slice) {
|
|
26
|
+
var _content$firstChild;
|
|
27
|
+
var state = _ref.state,
|
|
28
|
+
dispatch = _ref.dispatch;
|
|
29
|
+
// The state was cleaned after previous paste. We don't need to update plugin state
|
|
30
|
+
// with 'contentPasted' if currentActions array doesn't have 'copiedOrCut'.
|
|
31
|
+
var _getPluginState = getPluginState(state),
|
|
32
|
+
contentMoved = _getPluginState.contentMoved;
|
|
33
|
+
var hasCutAction = contentMoved.currentActions.includes('contentCut');
|
|
34
|
+
if (!hasCutAction) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
var content = slice.content,
|
|
38
|
+
size = slice.size;
|
|
39
|
+
var nodeName = (_content$firstChild = content.firstChild) === null || _content$firstChild === void 0 ? void 0 : _content$firstChild.type.name;
|
|
40
|
+
// We should not account for pastes that go inside another node and create nested content as DnD can't do it.
|
|
41
|
+
if (!nodeName || !(contentMoved !== null && contentMoved !== void 0 && contentMoved.nodeName) || !isValidNodeName(contentMoved === null || contentMoved === void 0 ? void 0 : contentMoved.nodeName, nodeName) || size !== (contentMoved === null || contentMoved === void 0 ? void 0 : contentMoved.size) || !isCursorSelectionAndInsideTopLevelNode(state.selection)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
var tr = state.tr;
|
|
45
|
+
editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
|
|
46
|
+
action: ACTION.MOVED,
|
|
47
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
48
|
+
actionSubjectId: ACTION_SUBJECT_ID.NODE,
|
|
49
|
+
eventType: EVENT_TYPE.TRACK,
|
|
50
|
+
attributes: {
|
|
51
|
+
nodeType: contentMoved === null || contentMoved === void 0 ? void 0 : contentMoved.nodeName // keep nodeName from copied slice
|
|
52
|
+
}
|
|
53
|
+
})(tr);
|
|
54
|
+
|
|
55
|
+
// reset to default state
|
|
56
|
+
var updatedTr = resetContentMovedTransform()(tr);
|
|
57
|
+
dispatch(updatedTr);
|
|
58
|
+
},
|
|
59
|
+
transformCopied: function transformCopied(slice, _ref2) {
|
|
60
|
+
var _content$firstChild2;
|
|
61
|
+
var state = _ref2.state,
|
|
62
|
+
dispatch = _ref2.dispatch;
|
|
63
|
+
// We want to listen only to 'cut' events
|
|
64
|
+
if (!isCutEvent) {
|
|
65
|
+
return slice;
|
|
66
|
+
}
|
|
67
|
+
var resetState = false;
|
|
68
|
+
var content = slice.content,
|
|
69
|
+
size = slice.size;
|
|
70
|
+
// Content should be just one node, so we added a check for slice.content.childCount === 1;
|
|
71
|
+
// 1. It is possible to select a table by dragging the mouse over the table's rows.
|
|
72
|
+
// As a result, slice will contain rows without tableNode itself and the childCount will be the number of rows.
|
|
73
|
+
// From a user's perspective the whole table is selected and copied and on paste a table will indeed be created.
|
|
74
|
+
// 2. Some block nodes can get selected when a user drags the mouse from the paragraph above the node to
|
|
75
|
+
// the paragraph below the node. Visually only the node in between is selected, in reality, three nodes are
|
|
76
|
+
// in the slice.
|
|
77
|
+
// These cases are ignored and moveContent event won't be counted.
|
|
78
|
+
if (content.childCount !== 1) {
|
|
79
|
+
resetState = true;
|
|
80
|
+
}
|
|
81
|
+
var nodeName = ((_content$firstChild2 = content.firstChild) === null || _content$firstChild2 === void 0 ? void 0 : _content$firstChild2.type.name) || '';
|
|
82
|
+
// Some nodes are not relevant as they are parts of nodes, not whole nodes (like tableCell, tableHeader instead of table node)
|
|
83
|
+
// Some nodes like lists, taskList(item), decisionList(item) requires tricky checks that we want to avoid doing.
|
|
84
|
+
// These nodes were added to excludedNodes array.
|
|
85
|
+
if (!resetState && isExcludedNode(nodeName)) {
|
|
86
|
+
resetState = true;
|
|
87
|
+
}
|
|
88
|
+
var selection = state.selection;
|
|
89
|
+
// DnD can't drag part of text in a paragraph/heading. DnD will select the whole node with all the text inside.
|
|
90
|
+
// So we are only interested in cut slices that contain the entire node, not just a part of it.
|
|
91
|
+
if (!resetState && nodeName === 'paragraph' && !isEntireTopLevelHeadingOrParagraphSelected(selection, nodeName)) {
|
|
92
|
+
resetState = true;
|
|
93
|
+
}
|
|
94
|
+
if (!resetState && nodeName === 'heading' && !isEntireTopLevelHeadingOrParagraphSelected(selection, nodeName)) {
|
|
95
|
+
resetState = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// DnD can't drag just one paragraph (when blockquote contains multiple paragraphs) or just a part of a paragraph in the blockquote.
|
|
99
|
+
// DnD will select and drag the whole blockquote. So we need to register cut events that have the entire blockquote too.
|
|
100
|
+
if (!resetState && nodeName === 'blockquote' && !isEntireTopLevelBlockquoteSelected(state)) {
|
|
101
|
+
resetState = true;
|
|
102
|
+
}
|
|
103
|
+
if (!resetState && isInlineNode(nodeName) && isNestedInlineNode(selection)) {
|
|
104
|
+
resetState = true;
|
|
105
|
+
}
|
|
106
|
+
if (!resetState && nodeName === 'table' && isNestedTable(selection)) {
|
|
107
|
+
resetState = true;
|
|
108
|
+
}
|
|
109
|
+
var isBlockNode = isBlockNodeWithoutTable(nodeName);
|
|
110
|
+
if (!resetState && isNodeSelection(selection) && isBlockNode && selection.$anchor.node().type.name !== 'doc' // checks that the block node is not a topLevel node
|
|
111
|
+
) {
|
|
112
|
+
resetState = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Some blockNodes can have text inside of them cut, in that case TextSelection occurs, we don't need to track
|
|
116
|
+
// these cut events.
|
|
117
|
+
if (!resetState && isTextSelection(selection) && isBlockNode) {
|
|
118
|
+
resetState = true;
|
|
119
|
+
}
|
|
120
|
+
if (resetState) {
|
|
121
|
+
resetContentMoved()(state, dispatch);
|
|
122
|
+
} else {
|
|
123
|
+
updateContentMoved({
|
|
124
|
+
size: size,
|
|
125
|
+
nodeName: nodeName
|
|
126
|
+
}, 'contentCut')(state, dispatch);
|
|
127
|
+
}
|
|
128
|
+
isCutEvent = false;
|
|
129
|
+
return slice;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
|
+
import { MoveAnalyticPluginTypes } from './actions';
|
|
5
|
+
import { defaultState } from './types';
|
|
6
|
+
export var reducer = function reducer(state, action) {
|
|
7
|
+
switch (action.type) {
|
|
8
|
+
case MoveAnalyticPluginTypes.UpdateMovedAction:
|
|
9
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
10
|
+
contentMoved: _objectSpread(_objectSpread({}, state.contentMoved), action.data)
|
|
11
|
+
});
|
|
12
|
+
case MoveAnalyticPluginTypes.RemoveMovedAction:
|
|
13
|
+
return _objectSpread(_objectSpread({}, state), {}, {
|
|
14
|
+
contentMoved: defaultState.contentMoved
|
|
15
|
+
});
|
|
16
|
+
default:
|
|
17
|
+
return state;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// For an event to be considered a move content event we want to ensure that
|
|
2
|
+
// cut had happened and was followed by paste. So our currentActions array should
|
|
3
|
+
// look like this ['contentCut', 'contentPasted'];
|
|
4
|
+
// After removing appendTransaction we don't update currentActions with 'contentPasted'
|
|
5
|
+
// but leaving it as array in case we decide to change it back or add 'contentCopied'.
|
|
6
|
+
|
|
7
|
+
export var defaultState = {
|
|
8
|
+
contentMoved: {
|
|
9
|
+
nodeName: undefined,
|
|
10
|
+
size: undefined,
|
|
11
|
+
currentActions: []
|
|
12
|
+
}
|
|
13
|
+
};
|