@atlaskit/editor-plugin-paste 1.3.0 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/plugin.js +11 -2
  3. package/dist/cjs/pm-plugins/move-analytics/actions.js +11 -0
  4. package/dist/cjs/pm-plugins/move-analytics/commands.js +45 -0
  5. package/dist/cjs/pm-plugins/move-analytics/plugin-factory.js +13 -0
  6. package/dist/cjs/pm-plugins/move-analytics/plugin-key.js +8 -0
  7. package/dist/cjs/pm-plugins/move-analytics/plugin.js +138 -0
  8. package/dist/cjs/pm-plugins/move-analytics/reducer.js +26 -0
  9. package/dist/cjs/pm-plugins/move-analytics/types.js +19 -0
  10. package/dist/cjs/pm-plugins/move-analytics/utils.js +120 -0
  11. package/dist/es2019/plugin.js +12 -2
  12. package/dist/es2019/pm-plugins/move-analytics/actions.js +5 -0
  13. package/dist/es2019/pm-plugins/move-analytics/commands.js +28 -0
  14. package/dist/es2019/pm-plugins/move-analytics/plugin-factory.js +8 -0
  15. package/dist/es2019/pm-plugins/move-analytics/plugin-key.js +2 -0
  16. package/dist/es2019/pm-plugins/move-analytics/plugin.js +144 -0
  17. package/dist/es2019/pm-plugins/move-analytics/reducer.js +21 -0
  18. package/dist/es2019/pm-plugins/move-analytics/types.js +13 -0
  19. package/dist/es2019/pm-plugins/move-analytics/utils.js +124 -0
  20. package/dist/esm/plugin.js +11 -2
  21. package/dist/esm/pm-plugins/move-analytics/actions.js +5 -0
  22. package/dist/esm/pm-plugins/move-analytics/commands.js +38 -0
  23. package/dist/esm/pm-plugins/move-analytics/plugin-factory.js +8 -0
  24. package/dist/esm/pm-plugins/move-analytics/plugin-key.js +2 -0
  25. package/dist/esm/pm-plugins/move-analytics/plugin.js +133 -0
  26. package/dist/esm/pm-plugins/move-analytics/reducer.js +19 -0
  27. package/dist/esm/pm-plugins/move-analytics/types.js +13 -0
  28. package/dist/esm/pm-plugins/move-analytics/utils.js +114 -0
  29. package/dist/types/pm-plugins/move-analytics/actions.d.ts +14 -0
  30. package/dist/types/pm-plugins/move-analytics/commands.d.ts +5 -0
  31. package/dist/types/pm-plugins/move-analytics/plugin-factory.d.ts +1 -0
  32. package/dist/types/pm-plugins/move-analytics/plugin-key.d.ts +3 -0
  33. package/dist/types/pm-plugins/move-analytics/plugin.d.ts +4 -0
  34. package/dist/types/pm-plugins/move-analytics/reducer.d.ts +3 -0
  35. package/dist/types/pm-plugins/move-analytics/types.d.ts +11 -0
  36. package/dist/types/pm-plugins/move-analytics/utils.d.ts +12 -0
  37. package/dist/types/types.d.ts +1 -0
  38. package/dist/types-ts4.5/pm-plugins/move-analytics/actions.d.ts +14 -0
  39. package/dist/types-ts4.5/pm-plugins/move-analytics/commands.d.ts +5 -0
  40. package/dist/types-ts4.5/pm-plugins/move-analytics/plugin-factory.d.ts +1 -0
  41. package/dist/types-ts4.5/pm-plugins/move-analytics/plugin-key.d.ts +3 -0
  42. package/dist/types-ts4.5/pm-plugins/move-analytics/plugin.d.ts +4 -0
  43. package/dist/types-ts4.5/pm-plugins/move-analytics/reducer.d.ts +3 -0
  44. package/dist/types-ts4.5/pm-plugins/move-analytics/types.d.ts +11 -0
  45. package/dist/types-ts4.5/pm-plugins/move-analytics/utils.d.ts +12 -0
  46. package/dist/types-ts4.5/types.d.ts +1 -0
  47. package/package.json +2 -2
@@ -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
+ };
@@ -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,2 @@
1
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
2
+ export var pluginKey = new PluginKey('moveAnalyticsPlugin');
@@ -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
+ };