@atlaskit/editor-plugin-selection 0.1.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/CHANGELOG.md +1 -0
- package/LICENSE.md +13 -0
- package/README.md +30 -0
- package/dist/cjs/actions.js +11 -0
- package/dist/cjs/commands.js +257 -0
- package/dist/cjs/gap-cursor/actions.js +255 -0
- package/dist/cjs/gap-cursor/direction.js +23 -0
- package/dist/cjs/gap-cursor/selection.js +30 -0
- package/dist/cjs/gap-cursor/utils/is-ignored.js +12 -0
- package/dist/cjs/gap-cursor/utils/is-valid-target-node.js +12 -0
- package/dist/cjs/gap-cursor/utils/place-gap-cursor.js +103 -0
- package/dist/cjs/gap-cursor/utils.js +137 -0
- package/dist/cjs/gap-cursor-selection.js +37 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/plugin-factory.js +49 -0
- package/dist/cjs/plugin.js +75 -0
- package/dist/cjs/pm-plugins/events/create-selection-between.js +92 -0
- package/dist/cjs/pm-plugins/events/keydown.js +115 -0
- package/dist/cjs/pm-plugins/gap-cursor-keymap.js +46 -0
- package/dist/cjs/pm-plugins/gap-cursor-main.js +159 -0
- package/dist/cjs/pm-plugins/gap-cursor-plugin-key.js +8 -0
- package/dist/cjs/pm-plugins/keymap.js +16 -0
- package/dist/cjs/pm-plugins/selection-main.js +104 -0
- package/dist/cjs/reducer.js +26 -0
- package/dist/cjs/types.js +20 -0
- package/dist/cjs/utils.js +280 -0
- package/dist/es2019/actions.js +5 -0
- package/dist/es2019/commands.js +250 -0
- package/dist/es2019/gap-cursor/actions.js +256 -0
- package/dist/es2019/gap-cursor/direction.js +15 -0
- package/dist/es2019/gap-cursor/selection.js +1 -0
- package/dist/es2019/gap-cursor/utils/is-ignored.js +1 -0
- package/dist/es2019/gap-cursor/utils/is-valid-target-node.js +1 -0
- package/dist/es2019/gap-cursor/utils/place-gap-cursor.js +94 -0
- package/dist/es2019/gap-cursor/utils.js +124 -0
- package/dist/es2019/gap-cursor-selection.js +2 -0
- package/dist/es2019/index.js +1 -0
- package/dist/es2019/plugin-factory.js +43 -0
- package/dist/es2019/plugin.js +60 -0
- package/dist/es2019/pm-plugins/events/create-selection-between.js +89 -0
- package/dist/es2019/pm-plugins/events/keydown.js +111 -0
- package/dist/es2019/pm-plugins/gap-cursor-keymap.js +40 -0
- package/dist/es2019/pm-plugins/gap-cursor-main.js +157 -0
- package/dist/es2019/pm-plugins/gap-cursor-plugin-key.js +2 -0
- package/dist/es2019/pm-plugins/keymap.js +10 -0
- package/dist/es2019/pm-plugins/selection-main.js +97 -0
- package/dist/es2019/reducer.js +18 -0
- package/dist/es2019/types.js +9 -0
- package/dist/es2019/utils.js +233 -0
- package/dist/esm/actions.js +5 -0
- package/dist/esm/commands.js +251 -0
- package/dist/esm/gap-cursor/actions.js +249 -0
- package/dist/esm/gap-cursor/direction.js +15 -0
- package/dist/esm/gap-cursor/selection.js +1 -0
- package/dist/esm/gap-cursor/utils/is-ignored.js +1 -0
- package/dist/esm/gap-cursor/utils/is-valid-target-node.js +1 -0
- package/dist/esm/gap-cursor/utils/place-gap-cursor.js +97 -0
- package/dist/esm/gap-cursor/utils.js +128 -0
- package/dist/esm/gap-cursor-selection.js +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/plugin-factory.js +43 -0
- package/dist/esm/plugin.js +68 -0
- package/dist/esm/pm-plugins/events/create-selection-between.js +86 -0
- package/dist/esm/pm-plugins/events/keydown.js +109 -0
- package/dist/esm/pm-plugins/gap-cursor-keymap.js +40 -0
- package/dist/esm/pm-plugins/gap-cursor-main.js +153 -0
- package/dist/esm/pm-plugins/gap-cursor-plugin-key.js +2 -0
- package/dist/esm/pm-plugins/keymap.js +10 -0
- package/dist/esm/pm-plugins/selection-main.js +98 -0
- package/dist/esm/reducer.js +19 -0
- package/dist/esm/types.js +9 -0
- package/dist/esm/utils.js +241 -0
- package/dist/types/actions.d.ts +17 -0
- package/dist/types/commands.d.ts +9 -0
- package/dist/types/gap-cursor/actions.d.ts +23 -0
- package/dist/types/gap-cursor/direction.d.ts +10 -0
- package/dist/types/gap-cursor/selection.d.ts +1 -0
- package/dist/types/gap-cursor/utils/is-ignored.d.ts +1 -0
- package/dist/types/gap-cursor/utils/is-valid-target-node.d.ts +1 -0
- package/dist/types/gap-cursor/utils/place-gap-cursor.d.ts +2 -0
- package/dist/types/gap-cursor/utils.d.ts +8 -0
- package/dist/types/gap-cursor-selection.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/plugin-factory.d.ts +2 -0
- package/dist/types/plugin.d.ts +13 -0
- package/dist/types/pm-plugins/events/create-selection-between.d.ts +4 -0
- package/dist/types/pm-plugins/events/keydown.d.ts +2 -0
- package/dist/types/pm-plugins/gap-cursor-keymap.d.ts +2 -0
- package/dist/types/pm-plugins/gap-cursor-main.d.ts +6 -0
- package/dist/types/pm-plugins/gap-cursor-plugin-key.d.ts +2 -0
- package/dist/types/pm-plugins/keymap.d.ts +3 -0
- package/dist/types/pm-plugins/selection-main.d.ts +7 -0
- package/dist/types/reducer.d.ts +3 -0
- package/dist/types/types.d.ts +20 -0
- package/dist/types/utils.d.ts +58 -0
- package/package.json +93 -0
- package/types/package.json +15 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
|
+
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
3
|
+
import { SelectionActionTypes } from '../actions';
|
|
4
|
+
import { createPluginState, getPluginState } from '../plugin-factory';
|
|
5
|
+
import { selectionPluginKey } from '../types';
|
|
6
|
+
import { getAllSelectionAnalyticsPayload, getCellSelectionAnalyticsPayload, getDecorations, getNodeSelectionAnalyticsPayload, getRangeSelectionAnalyticsPayload, shouldRecalcDecorations } from '../utils';
|
|
7
|
+
import { onCreateSelectionBetween } from './events/create-selection-between';
|
|
8
|
+
import { onKeydown } from './events/keydown';
|
|
9
|
+
export const getInitialState = state => ({
|
|
10
|
+
decorationSet: getDecorations(state.tr),
|
|
11
|
+
selection: state.selection
|
|
12
|
+
});
|
|
13
|
+
export const createPlugin = (dispatch, dispatchAnalyticsEvent, options = {}) => {
|
|
14
|
+
return new SafePlugin({
|
|
15
|
+
key: selectionPluginKey,
|
|
16
|
+
state: createPluginState(dispatch, getInitialState),
|
|
17
|
+
view: () => ({
|
|
18
|
+
update: (editorView, oldEditorState) => {
|
|
19
|
+
const {
|
|
20
|
+
state
|
|
21
|
+
} = editorView;
|
|
22
|
+
if (!shouldRecalcDecorations({
|
|
23
|
+
oldEditorState,
|
|
24
|
+
newEditorState: state
|
|
25
|
+
})) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const analyticsPayload = getNodeSelectionAnalyticsPayload(state.selection) || getAllSelectionAnalyticsPayload(state.selection) ||
|
|
29
|
+
// We handle all range/cell selections except click and drag here, which is
|
|
30
|
+
// handled in mouseup handler below
|
|
31
|
+
!editorView.mouseDown && (getRangeSelectionAnalyticsPayload(state.selection, state.doc) || getCellSelectionAnalyticsPayload(state));
|
|
32
|
+
|
|
33
|
+
// We have to use dispatchAnalyticsEvent over any of the analytics plugin helpers
|
|
34
|
+
// as there were several issues caused by the fact that adding analytics through
|
|
35
|
+
// the plugin adds a new step to the transaction
|
|
36
|
+
// This causes prosemirror to run through some different code paths, eg. attempting
|
|
37
|
+
// to map selection
|
|
38
|
+
if (analyticsPayload) {
|
|
39
|
+
dispatchAnalyticsEvent(analyticsPayload);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
appendTransaction(_transactions, oldEditorState, newEditorState) {
|
|
44
|
+
if (!shouldRecalcDecorations({
|
|
45
|
+
oldEditorState,
|
|
46
|
+
newEditorState
|
|
47
|
+
})) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const {
|
|
51
|
+
tr
|
|
52
|
+
} = newEditorState;
|
|
53
|
+
tr.setMeta(selectionPluginKey, {
|
|
54
|
+
type: SelectionActionTypes.SET_DECORATIONS,
|
|
55
|
+
selection: tr.selection,
|
|
56
|
+
decorationSet: getDecorations(tr)
|
|
57
|
+
});
|
|
58
|
+
return tr;
|
|
59
|
+
},
|
|
60
|
+
filterTransaction(tr, state) {
|
|
61
|
+
// Prevent single click selecting atom nodes on mobile (we want to select with long press gesture instead)
|
|
62
|
+
if (options.useLongPressSelection && tr.selectionSet && tr.selection instanceof NodeSelection && !tr.getMeta(selectionPluginKey)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Prevent prosemirror's mutation observer overriding a node selection with a text selection
|
|
67
|
+
// for exact same range - this was cause of being unable to change dates in collab:
|
|
68
|
+
// https://product-fabric.atlassian.net/browse/ED-10645
|
|
69
|
+
if (state.selection instanceof NodeSelection && tr.selection instanceof TextSelection && state.selection.from === tr.selection.from && state.selection.to === tr.selection.to) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
props: {
|
|
75
|
+
createSelectionBetween: onCreateSelectionBetween,
|
|
76
|
+
decorations(state) {
|
|
77
|
+
return getPluginState(state).decorationSet;
|
|
78
|
+
},
|
|
79
|
+
handleDOMEvents: {
|
|
80
|
+
keydown: onKeydown,
|
|
81
|
+
// We only want to fire analytics for a click and drag range/cell selection when
|
|
82
|
+
// the user has finished, otherwise we will get an event almost every time they move
|
|
83
|
+
// their mouse which is too much
|
|
84
|
+
mouseup: (editorView, event) => {
|
|
85
|
+
const mouseEvent = event;
|
|
86
|
+
if (!mouseEvent.shiftKey) {
|
|
87
|
+
const analyticsPayload = getRangeSelectionAnalyticsPayload(editorView.state.selection, editorView.state.doc) || getCellSelectionAnalyticsPayload(editorView.state);
|
|
88
|
+
if (analyticsPayload) {
|
|
89
|
+
dispatchAnalyticsEvent(analyticsPayload);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SelectionActionTypes } from './actions';
|
|
2
|
+
export function reducer(pluginState, action) {
|
|
3
|
+
switch (action.type) {
|
|
4
|
+
case SelectionActionTypes.SET_DECORATIONS:
|
|
5
|
+
return {
|
|
6
|
+
...pluginState,
|
|
7
|
+
decorationSet: action.decorationSet,
|
|
8
|
+
selection: action.selection
|
|
9
|
+
};
|
|
10
|
+
case SelectionActionTypes.SET_RELATIVE_SELECTION:
|
|
11
|
+
return {
|
|
12
|
+
...pluginState,
|
|
13
|
+
selectionRelativeToNode: action.selectionRelativeToNode
|
|
14
|
+
};
|
|
15
|
+
default:
|
|
16
|
+
return pluginState;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RelativeSelectionPos } from '@atlaskit/editor-common/selection';
|
|
2
|
+
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
3
|
+
export const selectionPluginKey = new PluginKey('selection');
|
|
4
|
+
export { RelativeSelectionPos };
|
|
5
|
+
export let SelectionDirection = /*#__PURE__*/function (SelectionDirection) {
|
|
6
|
+
SelectionDirection[SelectionDirection["Before"] = -1] = "Before";
|
|
7
|
+
SelectionDirection[SelectionDirection["After"] = 1] = "After";
|
|
8
|
+
return SelectionDirection;
|
|
9
|
+
}({});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { createSelectionClickHandler, getAllSelectionAnalyticsPayload, getCellSelectionAnalyticsPayload, getNodeSelectionAnalyticsPayload, getRangeSelectionAnalyticsPayload, isIgnored as isIgnoredByGapCursor, isSelectionAtEndOfNode, isSelectionAtStartOfNode } from '@atlaskit/editor-common/selection';
|
|
2
|
+
import { isEmptyParagraph } from '@atlaskit/editor-common/utils';
|
|
3
|
+
import { AllSelection, NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
4
|
+
import { findParentNode, flatten } from '@atlaskit/editor-prosemirror/utils';
|
|
5
|
+
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
6
|
+
import { akEditorSelectedNodeClassName } from '@atlaskit/editor-shared-styles';
|
|
7
|
+
import { selectionPluginKey } from './types';
|
|
8
|
+
export const getDecorations = tr => {
|
|
9
|
+
if (tr.selection instanceof NodeSelection) {
|
|
10
|
+
return DecorationSet.create(tr.doc, [Decoration.node(tr.selection.from, tr.selection.to, {
|
|
11
|
+
class: akEditorSelectedNodeClassName
|
|
12
|
+
})]);
|
|
13
|
+
}
|
|
14
|
+
if (tr.selection instanceof TextSelection || tr.selection instanceof AllSelection) {
|
|
15
|
+
const decorations = getNodesToDecorateFromSelection(tr.selection, tr.doc).map(({
|
|
16
|
+
node,
|
|
17
|
+
pos
|
|
18
|
+
}) => {
|
|
19
|
+
return Decoration.node(pos, pos + node.nodeSize, {
|
|
20
|
+
class: akEditorSelectedNodeClassName
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return DecorationSet.create(tr.doc, decorations);
|
|
24
|
+
}
|
|
25
|
+
return DecorationSet.empty;
|
|
26
|
+
};
|
|
27
|
+
const topLevelBlockNodesThatHaveSelectionStyles = ['table', 'panel', 'expand', 'layoutSection', 'decisionList', 'decisionItem', 'codeBlock'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Use `getNodesToDecorateFromSelection` to collect and return
|
|
31
|
+
* a list of nodes within the Selection that should have Selection
|
|
32
|
+
* decorations applied. This allows selection styles to be added to
|
|
33
|
+
* nested nodes. It will ignore text nodes as decorations are
|
|
34
|
+
* applied natively and also ignore nodes that don't completely
|
|
35
|
+
* sit within the given `Selection`.
|
|
36
|
+
*/
|
|
37
|
+
export const getNodesToDecorateFromSelection = (selection, doc) => {
|
|
38
|
+
const nodes = [];
|
|
39
|
+
if (selection.from !== selection.to) {
|
|
40
|
+
const {
|
|
41
|
+
from,
|
|
42
|
+
to
|
|
43
|
+
} = selection;
|
|
44
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
45
|
+
const withinSelection = from <= pos && pos + node.nodeSize <= to;
|
|
46
|
+
// The reason we need to check for these nodes is to stop
|
|
47
|
+
// traversing their children if they are within a selection -
|
|
48
|
+
// this is to prevent selection styles from being added to
|
|
49
|
+
// the children as well as the parent node.
|
|
50
|
+
// Example scenario is if an entire table has been selected
|
|
51
|
+
// we should not traverse its children so we can apply the
|
|
52
|
+
// selection styles to the table. But if an entire tableRow
|
|
53
|
+
// has been selected (but the parent table has not) we should
|
|
54
|
+
// traverse it as it could contain other nodes that need
|
|
55
|
+
// selection styles. I couldn’t see a clear way to differentiate
|
|
56
|
+
// without explicitly stating which nodes should be traversed
|
|
57
|
+
// and which shouldn’t.
|
|
58
|
+
const isTopLevelNodeThatHasSelectionStyles = topLevelBlockNodesThatHaveSelectionStyles.includes(node.type.name);
|
|
59
|
+
// If the node is a top-level block node and completely sits within
|
|
60
|
+
// the selection, we do not recurse it's children to prevent selection
|
|
61
|
+
// styles being added to its child nodes. The expected behaviour
|
|
62
|
+
// is that selection styles are only added to the parent.
|
|
63
|
+
if (node && withinSelection && isTopLevelNodeThatHasSelectionStyles) {
|
|
64
|
+
nodes.push({
|
|
65
|
+
node,
|
|
66
|
+
pos
|
|
67
|
+
});
|
|
68
|
+
return false;
|
|
69
|
+
// Otherwise we recurse the children and return them so we can apply
|
|
70
|
+
// selection styles. Text is handled by the browser.
|
|
71
|
+
} else if (node && withinSelection && !node.isText) {
|
|
72
|
+
nodes.push({
|
|
73
|
+
node,
|
|
74
|
+
pos
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return nodes;
|
|
81
|
+
};
|
|
82
|
+
export function shouldRecalcDecorations({
|
|
83
|
+
oldEditorState,
|
|
84
|
+
newEditorState
|
|
85
|
+
}) {
|
|
86
|
+
const oldSelection = oldEditorState.selection;
|
|
87
|
+
const newSelection = newEditorState.selection;
|
|
88
|
+
const oldPluginState = selectionPluginKey.getState(oldEditorState);
|
|
89
|
+
const newPluginState = selectionPluginKey.getState(newEditorState);
|
|
90
|
+
if (!oldPluginState || !newPluginState) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If selection is unchanged, no need to recalculate
|
|
95
|
+
if (oldSelection.eq(newSelection)) {
|
|
96
|
+
// We need this special case for NodeSelection, as Prosemirror still thinks the
|
|
97
|
+
// selections are equal when the node has changed
|
|
98
|
+
if (oldSelection instanceof NodeSelection && newSelection instanceof NodeSelection) {
|
|
99
|
+
const oldDecorations = oldPluginState.decorationSet.find();
|
|
100
|
+
const newDecorations = newPluginState.decorationSet.find();
|
|
101
|
+
// There might not be old or new decorations if the node selection is for a text node
|
|
102
|
+
// This wouldn't have happened intentionally, but we need to handle this case regardless
|
|
103
|
+
if (oldDecorations.length > 0 && newDecorations.length > 0) {
|
|
104
|
+
return !oldDecorations[0].eq(newDecorations[0]);
|
|
105
|
+
}
|
|
106
|
+
return !(oldDecorations.length === 0 && newDecorations.length === 0);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// There's no point updating decorations if going from one standard TextSelection to another
|
|
112
|
+
if (oldSelection instanceof TextSelection && newSelection instanceof TextSelection && oldSelection.from === oldSelection.to && newSelection.from === newSelection.to) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
export const isSelectableContainerNode = node => !!(node && !node.isAtom && NodeSelection.isSelectable(node));
|
|
118
|
+
export const isSelectableChildNode = node => !!(node && (node.isText || isEmptyParagraph(node) || NodeSelection.isSelectable(node)));
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Finds closest parent node that is a selectable block container node
|
|
122
|
+
* If it finds a parent that is not selectable but supports gap cursor, will
|
|
123
|
+
* return undefined
|
|
124
|
+
*/
|
|
125
|
+
export const findSelectableContainerParent = selection => {
|
|
126
|
+
let foundNodeThatSupportsGapCursor = false;
|
|
127
|
+
const selectableNode = findParentNode(node => {
|
|
128
|
+
const isSelectable = isSelectableContainerNode(node);
|
|
129
|
+
if (!isSelectable && !isIgnoredByGapCursor(node)) {
|
|
130
|
+
foundNodeThatSupportsGapCursor = true;
|
|
131
|
+
}
|
|
132
|
+
return isSelectable;
|
|
133
|
+
})(selection);
|
|
134
|
+
if (!foundNodeThatSupportsGapCursor) {
|
|
135
|
+
return selectableNode;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Finds node before that is a selectable block container node, starting
|
|
141
|
+
* from $pos.depth + 1 and working in
|
|
142
|
+
* If it finds a node that is not selectable but supports gap cursor, will
|
|
143
|
+
* return undefined
|
|
144
|
+
*/
|
|
145
|
+
export const findSelectableContainerBefore = ($pos, doc) => {
|
|
146
|
+
// prosemirror just returns the same pos from Selection.findFrom when
|
|
147
|
+
// parent.inlineContent is true, so we move position back one here
|
|
148
|
+
// to counteract that
|
|
149
|
+
if ($pos.parent.inlineContent && isSelectableContainerNode($pos.parent)) {
|
|
150
|
+
$pos = doc.resolve($pos.start() - 1);
|
|
151
|
+
}
|
|
152
|
+
const selectionBefore = Selection.findFrom($pos, -1);
|
|
153
|
+
if (selectionBefore) {
|
|
154
|
+
const $selectionBefore = doc.resolve(selectionBefore.from);
|
|
155
|
+
for (let i = $pos.depth + 1; i <= $selectionBefore.depth; i++) {
|
|
156
|
+
const node = $selectionBefore.node(i);
|
|
157
|
+
if (isSelectableContainerNode(node)) {
|
|
158
|
+
return {
|
|
159
|
+
node,
|
|
160
|
+
pos: $selectionBefore.start(i) - 1
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (i > $pos.depth + 1 && !isIgnoredByGapCursor(node)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Stick to the default left selection behaviour,
|
|
170
|
+
* useful for mediaSingleWithCaption
|
|
171
|
+
*/
|
|
172
|
+
if (selectionBefore instanceof NodeSelection && NodeSelection.isSelectable(selectionBefore.node)) {
|
|
173
|
+
return {
|
|
174
|
+
node: selectionBefore.node,
|
|
175
|
+
pos: selectionBefore.from
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Finds node after that is a selectable block container node, starting
|
|
183
|
+
* from $pos.depth + 1 and working in
|
|
184
|
+
* If it finds a node that is not selectable but supports gap cursor, will
|
|
185
|
+
* return undefined
|
|
186
|
+
*/
|
|
187
|
+
export const findSelectableContainerAfter = ($pos, doc) => {
|
|
188
|
+
const selectionAfter = Selection.findFrom($pos, 1);
|
|
189
|
+
if (selectionAfter) {
|
|
190
|
+
const $selectionAfter = doc.resolve(selectionAfter.from);
|
|
191
|
+
for (let i = $pos.depth + 1; i <= $selectionAfter.depth; i++) {
|
|
192
|
+
const node = $selectionAfter.node(i);
|
|
193
|
+
if (isSelectableContainerNode(node)) {
|
|
194
|
+
return {
|
|
195
|
+
node,
|
|
196
|
+
pos: $selectionAfter.start(i) - 1
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (i > $pos.depth + 1 && !isIgnoredByGapCursor(node)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Finds first child node that is a selectable block container node OR that
|
|
208
|
+
* supports gap cursor
|
|
209
|
+
*/
|
|
210
|
+
export const findFirstChildNodeToSelect = parent => flatten(parent).find(child => isSelectableChildNode(child.node) || !isIgnoredByGapCursor(child.node));
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Finds last child node that is a selectable block container node OR that
|
|
214
|
+
* supports gap cursor
|
|
215
|
+
*/
|
|
216
|
+
export const findLastChildNodeToSelect = parent => {
|
|
217
|
+
let child;
|
|
218
|
+
parent.descendants((node, pos) => {
|
|
219
|
+
if (isSelectableChildNode(node) || !isIgnoredByGapCursor(node)) {
|
|
220
|
+
child = {
|
|
221
|
+
node,
|
|
222
|
+
pos
|
|
223
|
+
};
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (child) {
|
|
228
|
+
return child;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
export const isSelectionAtStartOfParentNode = ($pos, selection) => isSelectionAtStartOfNode($pos, findSelectableContainerParent(selection));
|
|
232
|
+
export const isSelectionAtEndOfParentNode = ($pos, selection) => isSelectionAtEndOfNode($pos, findSelectableContainerParent(selection));
|
|
233
|
+
export { getNodeSelectionAnalyticsPayload, getAllSelectionAnalyticsPayload, getRangeSelectionAnalyticsPayload, getCellSelectionAnalyticsPayload, createSelectionClickHandler };
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { isIgnored as isIgnoredByGapCursor } from '@atlaskit/editor-common/selection';
|
|
2
|
+
import { isEmptyParagraph, isNodeEmpty } from '@atlaskit/editor-common/utils';
|
|
3
|
+
import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
4
|
+
import { SelectionActionTypes } from './actions';
|
|
5
|
+
import { GapCursorSelection, Side } from './gap-cursor-selection';
|
|
6
|
+
import { createCommand, getPluginState } from './plugin-factory';
|
|
7
|
+
import { RelativeSelectionPos, SelectionDirection, selectionPluginKey } from './types';
|
|
8
|
+
import { findFirstChildNodeToSelect, findLastChildNodeToSelect, findSelectableContainerAfter, findSelectableContainerBefore, findSelectableContainerParent, isSelectableContainerNode, isSelectionAtEndOfParentNode, isSelectionAtStartOfParentNode } from './utils';
|
|
9
|
+
export var selectNearNode = function selectNearNode(selectionRelativeToNode, selection) {
|
|
10
|
+
return function (_ref) {
|
|
11
|
+
var tr = _ref.tr;
|
|
12
|
+
tr.setMeta(selectionPluginKey, {
|
|
13
|
+
type: SelectionActionTypes.SET_RELATIVE_SELECTION,
|
|
14
|
+
selectionRelativeToNode: selectionRelativeToNode
|
|
15
|
+
});
|
|
16
|
+
if (selection) {
|
|
17
|
+
return tr.setSelection(selection);
|
|
18
|
+
}
|
|
19
|
+
return tr;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export var setSelectionRelativeToNode = function setSelectionRelativeToNode(selectionRelativeToNode, selection) {
|
|
23
|
+
return createCommand({
|
|
24
|
+
type: SelectionActionTypes.SET_RELATIVE_SELECTION,
|
|
25
|
+
selectionRelativeToNode: selectionRelativeToNode
|
|
26
|
+
}, function (tr) {
|
|
27
|
+
return selectNearNode(selectionRelativeToNode, selection)({
|
|
28
|
+
tr: tr
|
|
29
|
+
}) || tr;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
export var arrowRight = function arrowRight(state, dispatch) {
|
|
33
|
+
var selection = state.selection;
|
|
34
|
+
if (selection instanceof GapCursorSelection) {
|
|
35
|
+
return arrowRightFromGapCursor(selection)(state, dispatch);
|
|
36
|
+
} else if (selection instanceof NodeSelection) {
|
|
37
|
+
return arrowRightFromNode(selection)(state, dispatch);
|
|
38
|
+
} else if (selection instanceof TextSelection) {
|
|
39
|
+
return arrowRightFromText(selection)(state, dispatch);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
export var arrowLeft = function arrowLeft(state, dispatch) {
|
|
44
|
+
var selection = state.selection;
|
|
45
|
+
if (selection instanceof GapCursorSelection) {
|
|
46
|
+
return arrowLeftFromGapCursor(selection)(state, dispatch);
|
|
47
|
+
} else if (selection instanceof NodeSelection) {
|
|
48
|
+
return arrowLeftFromNode(selection)(state, dispatch);
|
|
49
|
+
} else if (selection instanceof TextSelection) {
|
|
50
|
+
return arrowLeftFromText(selection)(state, dispatch);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
var arrowRightFromGapCursor = function arrowRightFromGapCursor(selection) {
|
|
55
|
+
return function (state, dispatch) {
|
|
56
|
+
var $from = selection.$from,
|
|
57
|
+
$to = selection.$to,
|
|
58
|
+
side = selection.side;
|
|
59
|
+
if (side === Side.LEFT) {
|
|
60
|
+
var selectableNode = findSelectableContainerAfter($to, state.doc);
|
|
61
|
+
if (selectableNode) {
|
|
62
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
|
|
63
|
+
}
|
|
64
|
+
} else if (side === Side.RIGHT && isSelectionAtEndOfParentNode($from, selection)) {
|
|
65
|
+
var _selectableNode = findSelectableContainerParent(selection);
|
|
66
|
+
if (_selectableNode) {
|
|
67
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, _selectableNode.pos))(state, dispatch);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
var arrowLeftFromGapCursor = function arrowLeftFromGapCursor(selection) {
|
|
74
|
+
return function (state, dispatch) {
|
|
75
|
+
var $from = selection.$from,
|
|
76
|
+
side = selection.side;
|
|
77
|
+
var _getPluginState = getPluginState(state),
|
|
78
|
+
selectionRelativeToNode = _getPluginState.selectionRelativeToNode;
|
|
79
|
+
if (side === Side.RIGHT) {
|
|
80
|
+
var selectableNode = findSelectableContainerBefore($from, state.doc);
|
|
81
|
+
if (selectableNode) {
|
|
82
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
|
|
83
|
+
}
|
|
84
|
+
} else if (side === Side.LEFT && isSelectionAtStartOfParentNode($from, selection)) {
|
|
85
|
+
if (selectionRelativeToNode === RelativeSelectionPos.Before) {
|
|
86
|
+
var $parent = state.doc.resolve(selection.$from.before(selection.$from.depth));
|
|
87
|
+
if ($parent) {
|
|
88
|
+
var _selectableNode2 = findSelectableContainerBefore($parent, state.doc);
|
|
89
|
+
if (_selectableNode2 && isIgnoredByGapCursor(_selectableNode2.node)) {
|
|
90
|
+
// selection is inside node without gap cursor preceeded by another node without gap cursor - set node selection for previous node
|
|
91
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, _selectableNode2.pos))(state, dispatch);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// we don't return this as we want to reset the relative pos, but not block other plugins
|
|
95
|
+
// from responding to arrow left key
|
|
96
|
+
setSelectionRelativeToNode()(state, dispatch);
|
|
97
|
+
} else {
|
|
98
|
+
var _selectableNode3 = findSelectableContainerParent(selection);
|
|
99
|
+
if (_selectableNode3) {
|
|
100
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, _selectableNode3.pos))(state, dispatch);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
var arrowRightFromNode = function arrowRightFromNode(selection) {
|
|
108
|
+
return function (state, dispatch) {
|
|
109
|
+
var node = selection.node,
|
|
110
|
+
from = selection.from,
|
|
111
|
+
$to = selection.$to;
|
|
112
|
+
var _getPluginState2 = getPluginState(state),
|
|
113
|
+
selectionRelativeToNode = _getPluginState2.selectionRelativeToNode;
|
|
114
|
+
if (node.isAtom) {
|
|
115
|
+
if (isSelectionAtEndOfParentNode($to, selection)) {
|
|
116
|
+
// selection is for inline node that is the last child of its parent node - set text selection after it
|
|
117
|
+
return findAndSetTextSelection(RelativeSelectionPos.End, state.doc.resolve(from + 1), SelectionDirection.After)(state, dispatch);
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
} else if (selectionRelativeToNode === RelativeSelectionPos.Start) {
|
|
121
|
+
// selection is for container node - set selection inside it at the start
|
|
122
|
+
return setSelectionInsideAtNodeStart(RelativeSelectionPos.Inside, node, from)(state, dispatch);
|
|
123
|
+
} else if (isIgnoredByGapCursor(node) && (!selectionRelativeToNode || selectionRelativeToNode === RelativeSelectionPos.End)) {
|
|
124
|
+
var selectableNode = findSelectableContainerAfter($to, state.doc);
|
|
125
|
+
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
|
|
126
|
+
// selection is for node without gap cursor followed by another node without gap cursor - set node selection for next node
|
|
127
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
var arrowLeftFromNode = function arrowLeftFromNode(selection) {
|
|
134
|
+
return function (state, dispatch) {
|
|
135
|
+
var node = selection.node,
|
|
136
|
+
from = selection.from,
|
|
137
|
+
to = selection.to,
|
|
138
|
+
$from = selection.$from;
|
|
139
|
+
var _getPluginState3 = getPluginState(state),
|
|
140
|
+
selectionRelativeToNode = _getPluginState3.selectionRelativeToNode;
|
|
141
|
+
if (node.isAtom) {
|
|
142
|
+
if (isSelectionAtStartOfParentNode($from, selection)) {
|
|
143
|
+
// selection is for inline node that is the first child of its parent node - set text selection before it
|
|
144
|
+
return findAndSetTextSelection(RelativeSelectionPos.Start, state.doc.resolve(from), SelectionDirection.Before)(state, dispatch);
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
} else if (selectionRelativeToNode === RelativeSelectionPos.End) {
|
|
148
|
+
// selection is for container node - set selection inside it at the end
|
|
149
|
+
return setSelectionInsideAtNodeEnd(RelativeSelectionPos.Inside, node, from, to)(state, dispatch);
|
|
150
|
+
} else if (!selectionRelativeToNode || selectionRelativeToNode === RelativeSelectionPos.Inside) {
|
|
151
|
+
// selection is for container node - set selection inside it at the start
|
|
152
|
+
// (this is a special case when the user selects by clicking node)
|
|
153
|
+
return setSelectionInsideAtNodeStart(RelativeSelectionPos.Before, node, from)(state, dispatch);
|
|
154
|
+
} else if (isIgnoredByGapCursor(node) && selectionRelativeToNode === RelativeSelectionPos.Start) {
|
|
155
|
+
// selection is for node without gap cursor preceeded by another node without gap cursor - set node selection for previous node
|
|
156
|
+
var selectableNode = findSelectableContainerBefore($from, state.doc);
|
|
157
|
+
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
|
|
158
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
var arrowRightFromText = function arrowRightFromText(selection) {
|
|
165
|
+
return function (state, dispatch) {
|
|
166
|
+
if (isSelectionAtEndOfParentNode(selection.$to, selection)) {
|
|
167
|
+
var selectableNode = findSelectableContainerParent(selection);
|
|
168
|
+
if (selectableNode) {
|
|
169
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
var arrowLeftFromText = function arrowLeftFromText(selection) {
|
|
176
|
+
return function (state, dispatch) {
|
|
177
|
+
var _getPluginState4 = getPluginState(state),
|
|
178
|
+
selectionRelativeToNode = _getPluginState4.selectionRelativeToNode;
|
|
179
|
+
if (selectionRelativeToNode === RelativeSelectionPos.Before) {
|
|
180
|
+
var selectableNode = findSelectableContainerBefore(selection.$from, state.doc);
|
|
181
|
+
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
|
|
182
|
+
// selection is inside node without gap cursor preceeded by another node without gap cursor - set node selection for previous node
|
|
183
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
|
|
184
|
+
}
|
|
185
|
+
// we don't return this as we want to reset the relative pos, but not block other plugins
|
|
186
|
+
// from responding to arrow left key
|
|
187
|
+
setSelectionRelativeToNode(undefined)(state, dispatch);
|
|
188
|
+
} else if (isSelectionAtStartOfParentNode(selection.$from, selection)) {
|
|
189
|
+
var _selectableNode4 = findSelectableContainerParent(selection);
|
|
190
|
+
if (_selectableNode4) {
|
|
191
|
+
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, _selectableNode4.pos))(state, dispatch);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
var findAndSetTextSelection = function findAndSetTextSelection(selectionRelativeToNode, $pos, dir) {
|
|
198
|
+
return function (state, dispatch) {
|
|
199
|
+
var sel = Selection.findFrom($pos, dir, true);
|
|
200
|
+
if (sel) {
|
|
201
|
+
return setSelectionRelativeToNode(selectionRelativeToNode, sel)(state, dispatch);
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
var setSelectionInsideAtNodeStart = function setSelectionInsideAtNodeStart(selectionRelativeToNode, node, pos) {
|
|
207
|
+
return function (state, dispatch) {
|
|
208
|
+
if (isNodeEmpty(node)) {
|
|
209
|
+
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(pos), SelectionDirection.After)(state, dispatch);
|
|
210
|
+
}
|
|
211
|
+
var selectableNode = findFirstChildNodeToSelect(node);
|
|
212
|
+
if (selectableNode) {
|
|
213
|
+
var childNode = selectableNode.node,
|
|
214
|
+
childPos = selectableNode.pos;
|
|
215
|
+
var selectionPos = pos + childPos + 1;
|
|
216
|
+
if (childNode.isText || childNode.isAtom) {
|
|
217
|
+
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos), SelectionDirection.Before)(state, dispatch);
|
|
218
|
+
} else if (isEmptyParagraph(childNode)) {
|
|
219
|
+
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos + 1), SelectionDirection.Before)(state, dispatch);
|
|
220
|
+
} else if (!isIgnoredByGapCursor(node)) {
|
|
221
|
+
return setSelectionRelativeToNode(selectionRelativeToNode, new GapCursorSelection(state.doc.resolve(selectionPos), Side.LEFT))(state, dispatch);
|
|
222
|
+
} else if (isSelectableContainerNode(node)) {
|
|
223
|
+
return setSelectionRelativeToNode(selectionRelativeToNode, NodeSelection.create(state.doc, selectionPos))(state, dispatch);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
export var setSelectionInsideAtNodeEnd = function setSelectionInsideAtNodeEnd(selectionRelativeToNode, node, from, to) {
|
|
230
|
+
return function (state, dispatch) {
|
|
231
|
+
if (isNodeEmpty(node)) {
|
|
232
|
+
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(to), SelectionDirection.Before)(state, dispatch);
|
|
233
|
+
}
|
|
234
|
+
var selectableNode = findLastChildNodeToSelect(node);
|
|
235
|
+
if (selectableNode) {
|
|
236
|
+
var childNode = selectableNode.node,
|
|
237
|
+
childPos = selectableNode.pos;
|
|
238
|
+
var selectionPos = from + childPos + childNode.nodeSize;
|
|
239
|
+
if (childNode.isText || childNode.isAtom) {
|
|
240
|
+
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos + 1), SelectionDirection.After)(state, dispatch);
|
|
241
|
+
} else if (isEmptyParagraph(childNode)) {
|
|
242
|
+
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos), SelectionDirection.After)(state, dispatch);
|
|
243
|
+
} else if (!isIgnoredByGapCursor(node)) {
|
|
244
|
+
return setSelectionRelativeToNode(selectionRelativeToNode, new GapCursorSelection(state.doc.resolve(selectionPos + 1), Side.RIGHT))(state, dispatch);
|
|
245
|
+
} else if (isSelectableContainerNode(node)) {
|
|
246
|
+
return setSelectionRelativeToNode(selectionRelativeToNode, NodeSelection.create(state.doc, selectionPos))(state, dispatch);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
};
|
|
251
|
+
};
|