@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,124 @@
|
|
|
1
|
+
import { CAPTION_PLACEHOLDER_ID } from '@atlaskit/editor-common/media-single';
|
|
2
|
+
import { TableSharedCssClassName, UnsupportedSharedCssClassName } from '@atlaskit/editor-common/styles';
|
|
3
|
+
import { Side } from './selection';
|
|
4
|
+
export const isLeftCursor = side => side === Side.LEFT;
|
|
5
|
+
export function getMediaNearPos(doc, $pos, schema, dir = -1) {
|
|
6
|
+
let $currentPos = $pos;
|
|
7
|
+
let currentNode = null;
|
|
8
|
+
const {
|
|
9
|
+
mediaSingle,
|
|
10
|
+
media,
|
|
11
|
+
mediaGroup
|
|
12
|
+
} = schema.nodes;
|
|
13
|
+
do {
|
|
14
|
+
$currentPos = doc.resolve(dir === -1 ? $currentPos.before() : $currentPos.after());
|
|
15
|
+
if (!$currentPos) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
currentNode = (dir === -1 ? $currentPos.nodeBefore : $currentPos.nodeAfter) || $currentPos.parent;
|
|
19
|
+
if (!currentNode || currentNode.type === schema.nodes.doc) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (currentNode.type === mediaSingle || currentNode.type === media || currentNode.type === mediaGroup) {
|
|
23
|
+
return currentNode;
|
|
24
|
+
}
|
|
25
|
+
} while ($currentPos.depth > 0);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export const isTextBlockNearPos = (doc, schema, $pos, dir) => {
|
|
29
|
+
let $currentPos = $pos;
|
|
30
|
+
let currentNode = dir === -1 ? $currentPos.nodeBefore : $currentPos.nodeAfter;
|
|
31
|
+
|
|
32
|
+
// If next node is a text or a text block bail out early.
|
|
33
|
+
if (currentNode && (currentNode.isTextblock || currentNode.isText)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
while ($currentPos.depth > 0) {
|
|
37
|
+
$currentPos = doc.resolve(dir === -1 ? $currentPos.before() : $currentPos.after());
|
|
38
|
+
if (!$currentPos) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
currentNode = (dir === -1 ? $currentPos.nodeBefore : $currentPos.nodeAfter) || $currentPos.parent;
|
|
42
|
+
if (!currentNode || currentNode.type === schema.nodes.doc) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (currentNode.isTextblock) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let childNode = currentNode;
|
|
50
|
+
while (childNode && childNode.firstChild) {
|
|
51
|
+
childNode = childNode.firstChild;
|
|
52
|
+
if (childNode && (childNode.isTextblock || childNode.isText)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
export function getLayoutModeFromTargetNode(node) {
|
|
59
|
+
let layout;
|
|
60
|
+
if (node.attrs.layout) {
|
|
61
|
+
layout = node.attrs.layout;
|
|
62
|
+
}
|
|
63
|
+
if (node.marks && node.marks.length) {
|
|
64
|
+
layout = (node.marks.find(mark => mark.type.name === 'breakout') || {
|
|
65
|
+
attrs: {
|
|
66
|
+
mode: ''
|
|
67
|
+
}
|
|
68
|
+
}).attrs.mode;
|
|
69
|
+
}
|
|
70
|
+
if (node.type.name === 'table' && node.attrs.width) {
|
|
71
|
+
layout = 'fixed-width';
|
|
72
|
+
}
|
|
73
|
+
if (['wide', 'full-width', 'fixed-width'].indexOf(layout) === -1) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
return layout;
|
|
77
|
+
}
|
|
78
|
+
export const isIgnoredClick = elem => {
|
|
79
|
+
if (elem.nodeName === 'BUTTON' || elem.closest('button')) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// check if we're clicking an image caption placeholder
|
|
84
|
+
if (elem.closest(`[data-id="${CAPTION_PLACEHOLDER_ID}"]`)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// check if target node has a parent table node
|
|
89
|
+
let tableWrap;
|
|
90
|
+
let node = elem;
|
|
91
|
+
while (node) {
|
|
92
|
+
if (node.className && (node.getAttribute('class') || '').indexOf(TableSharedCssClassName.TABLE_CONTAINER) > -1) {
|
|
93
|
+
tableWrap = node;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
node = node.parentNode;
|
|
97
|
+
}
|
|
98
|
+
if (tableWrap) {
|
|
99
|
+
const rowControls = tableWrap.querySelector(`.${TableSharedCssClassName.TABLE_ROW_CONTROLS_WRAPPER}`);
|
|
100
|
+
const isColumnControlsDecoration = elem && elem.classList && elem.classList.contains(TableSharedCssClassName.TABLE_COLUMN_CONTROLS_DECORATIONS);
|
|
101
|
+
return rowControls && rowControls.contains(elem) || isColumnControlsDecoration;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if unsupported node selection
|
|
105
|
+
// (without this, selection requires double clicking in FF due to posAtCoords differences)
|
|
106
|
+
if (elem.closest(`.${UnsupportedSharedCssClassName.BLOCK_CONTAINER}`)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
Calculates custom style for breakout mode
|
|
114
|
+
Mainly to handle table width to include the numbered column width as well
|
|
115
|
+
*/
|
|
116
|
+
export const getComputedStyleForLayoutMode = (dom, node, style) => {
|
|
117
|
+
if (node && node.type.name === 'table') {
|
|
118
|
+
const tableContainer = dom.querySelector('.pm-table-container');
|
|
119
|
+
if (tableContainer) {
|
|
120
|
+
return window.getComputedStyle(tableContainer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return style;
|
|
124
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { selectionPlugin } from './plugin';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { pluginFactory } from '@atlaskit/editor-common/utils';
|
|
2
|
+
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
|
|
3
|
+
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
4
|
+
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
|
|
5
|
+
import { reducer } from './reducer';
|
|
6
|
+
import { selectionPluginKey } from './types';
|
|
7
|
+
import { getDecorations, isSelectableContainerNode } from './utils';
|
|
8
|
+
const handleDocChanged = (tr, pluginState) => {
|
|
9
|
+
// in some collab edge cases mapping decorations could throw an error
|
|
10
|
+
try {
|
|
11
|
+
if (pluginState.decorationSet.find().length === 0 && (!tr.selectionSet || getDecorations(tr).find().length === 0)) {
|
|
12
|
+
return pluginState;
|
|
13
|
+
}
|
|
14
|
+
const decorationSet = pluginState.decorationSet.map(tr.mapping, tr.doc);
|
|
15
|
+
return {
|
|
16
|
+
...pluginState,
|
|
17
|
+
decorationSet
|
|
18
|
+
};
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return {
|
|
21
|
+
...pluginState,
|
|
22
|
+
decorationSet: DecorationSet.empty
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const handleSelectionChanged = (tr, pluginState) => {
|
|
27
|
+
// Reset relative selection pos when user clicks to select a node
|
|
28
|
+
if ((tr.selection instanceof NodeSelection && isSelectableContainerNode(tr.selection.node) || tr.selection instanceof CellSelection) && !tr.getMeta(selectionPluginKey)) {
|
|
29
|
+
return {
|
|
30
|
+
...pluginState,
|
|
31
|
+
selectionRelativeToNode: undefined
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return pluginState;
|
|
35
|
+
};
|
|
36
|
+
export const {
|
|
37
|
+
createCommand,
|
|
38
|
+
getPluginState,
|
|
39
|
+
createPluginState
|
|
40
|
+
} = pluginFactory(selectionPluginKey, reducer, {
|
|
41
|
+
onDocChanged: handleDocChanged,
|
|
42
|
+
onSelectionChanged: handleSelectionChanged
|
|
43
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { selectNearNode } from './commands';
|
|
2
|
+
import gapCursorKeymapPlugin from './pm-plugins/gap-cursor-keymap';
|
|
3
|
+
import gapCursorPlugin from './pm-plugins/gap-cursor-main';
|
|
4
|
+
import { gapCursorPluginKey } from './pm-plugins/gap-cursor-plugin-key';
|
|
5
|
+
import selectionKeymapPlugin from './pm-plugins/keymap';
|
|
6
|
+
import { createPlugin } from './pm-plugins/selection-main';
|
|
7
|
+
import { selectionPluginKey } from './types';
|
|
8
|
+
const displayGapCursor = toggle => ({
|
|
9
|
+
tr
|
|
10
|
+
}) => {
|
|
11
|
+
return tr.setMeta(gapCursorPluginKey, {
|
|
12
|
+
displayGapCursor: toggle
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
export const selectionPlugin = ({
|
|
16
|
+
config: options
|
|
17
|
+
}) => ({
|
|
18
|
+
name: 'selection',
|
|
19
|
+
commands: {
|
|
20
|
+
displayGapCursor
|
|
21
|
+
},
|
|
22
|
+
actions: {
|
|
23
|
+
selectNearNode: ({
|
|
24
|
+
selectionRelativeToNode,
|
|
25
|
+
selection
|
|
26
|
+
}) => state => {
|
|
27
|
+
return selectNearNode(selectionRelativeToNode, selection)({
|
|
28
|
+
tr: state.tr
|
|
29
|
+
}) || state.tr;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
getSharedState(editorState) {
|
|
33
|
+
if (!editorState) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const pluginState = selectionPluginKey.getState(editorState);
|
|
37
|
+
return {
|
|
38
|
+
selectionRelativeToNode: pluginState === null || pluginState === void 0 ? void 0 : pluginState.selectionRelativeToNode
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
pmPlugins() {
|
|
42
|
+
return [{
|
|
43
|
+
name: 'selection',
|
|
44
|
+
plugin: ({
|
|
45
|
+
dispatch,
|
|
46
|
+
dispatchAnalyticsEvent
|
|
47
|
+
}) => createPlugin(dispatch, dispatchAnalyticsEvent, options)
|
|
48
|
+
}, {
|
|
49
|
+
name: 'selectionKeymap',
|
|
50
|
+
plugin: selectionKeymapPlugin
|
|
51
|
+
}, {
|
|
52
|
+
name: 'gapCursorKeymap',
|
|
53
|
+
plugin: () => gapCursorKeymapPlugin()
|
|
54
|
+
}, {
|
|
55
|
+
name: 'gapCursor',
|
|
56
|
+
plugin: () => gapCursorPlugin
|
|
57
|
+
}];
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
export default selectionPlugin;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
2
|
+
function isNodeContentEmpty(maybeNode) {
|
|
3
|
+
return (maybeNode === null || maybeNode === void 0 ? void 0 : maybeNode.content.size) === 0 || (maybeNode === null || maybeNode === void 0 ? void 0 : maybeNode.textContent) === '';
|
|
4
|
+
}
|
|
5
|
+
function findEmptySelectableParentNodePosition($pos, isValidPosition) {
|
|
6
|
+
const {
|
|
7
|
+
doc
|
|
8
|
+
} = $pos;
|
|
9
|
+
if ($pos.pos + 1 > doc.content.size) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if ($pos.nodeBefore !== null) {
|
|
13
|
+
if (isValidPosition($pos)) {
|
|
14
|
+
return $pos;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// We can not use `$pos.before()` because ProseMirror throws an error when depth is zero.
|
|
18
|
+
const currentPosIndex = $pos.index();
|
|
19
|
+
if (currentPosIndex === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const previousIndex = currentPosIndex - 1;
|
|
23
|
+
const $previousPos = $pos.doc.resolve($pos.posAtIndex(previousIndex));
|
|
24
|
+
if (isValidPosition($previousPos)) {
|
|
25
|
+
return $previousPos;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (isValidPosition($pos)) {
|
|
30
|
+
return $pos;
|
|
31
|
+
}
|
|
32
|
+
const positionLevelUp = $pos.before();
|
|
33
|
+
const resolvedPositionLevelUp = doc.resolve(positionLevelUp);
|
|
34
|
+
return findEmptySelectableParentNodePosition(resolvedPositionLevelUp, isValidPosition);
|
|
35
|
+
}
|
|
36
|
+
const checkPositionNode = $pos => {
|
|
37
|
+
const maybeNode = $pos.nodeAfter;
|
|
38
|
+
if (!maybeNode || !maybeNode.isBlock) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (maybeNode.isAtom) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return isNodeContentEmpty(maybeNode) && NodeSelection.isSelectable(maybeNode);
|
|
45
|
+
};
|
|
46
|
+
function findNextSelectionPosition({
|
|
47
|
+
$targetHead,
|
|
48
|
+
$anchor,
|
|
49
|
+
doc
|
|
50
|
+
}) {
|
|
51
|
+
const direction = $anchor.pos < $targetHead.pos ? 'down' : 'up';
|
|
52
|
+
const maybeNextPosition = findEmptySelectableParentNodePosition($targetHead, checkPositionNode);
|
|
53
|
+
if (maybeNextPosition && maybeNextPosition.nodeAfter) {
|
|
54
|
+
const nodeAfter = maybeNextPosition.nodeAfter;
|
|
55
|
+
const pos = maybeNextPosition.pos;
|
|
56
|
+
const nextPositionToSelect = direction === 'down' ? Math.min(nodeAfter.nodeSize + pos, doc.content.size) : Math.max(pos, 0);
|
|
57
|
+
return doc.resolve(nextPositionToSelect);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
export const onCreateSelectionBetween = (view, $anchor, $head) => {
|
|
62
|
+
var _$head$parent, _$head$parent2;
|
|
63
|
+
if ($anchor.pos === $head.pos) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if ($anchor.depth === $head.depth && $anchor.sameParent($head)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If the head is targeting a paragraph on root, then let ProseMirror handle the text selection
|
|
71
|
+
if ($head.depth === 1 && ((_$head$parent = $head.parent) === null || _$head$parent === void 0 ? void 0 : _$head$parent.type.name) === 'paragraph') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If head is at the beginning of a non-empty textblock, let ProseMirror handle the text selection
|
|
76
|
+
if ((_$head$parent2 = $head.parent) !== null && _$head$parent2 !== void 0 && _$head$parent2.isTextblock && !isNodeContentEmpty($head.parent) && $head.parentOffset === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const $nextHeadPosition = findNextSelectionPosition({
|
|
80
|
+
$targetHead: $head,
|
|
81
|
+
$anchor,
|
|
82
|
+
doc: view.state.doc
|
|
83
|
+
});
|
|
84
|
+
if (!$nextHeadPosition) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const forcedTextSelection = TextSelection.create(view.state.doc, $anchor.pos, $nextHeadPosition.pos);
|
|
88
|
+
return forcedTextSelection;
|
|
89
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
2
|
+
/*
|
|
3
|
+
* The way expand was built, no browser reconize selection on it.
|
|
4
|
+
* For instance, when a selection going to a "collapsed" expand
|
|
5
|
+
* the browser will try to send the cursor to inside the expand content (wrong),
|
|
6
|
+
* this behavior is caused because the expand content is never true hidden
|
|
7
|
+
* we just set the height to 1px.
|
|
8
|
+
*
|
|
9
|
+
* So, we need to capture a possible selection event
|
|
10
|
+
* when a collapsed exxpand is the next node in the common depth.
|
|
11
|
+
* If that is true, we create a new TextSelection and stop the event bubble
|
|
12
|
+
*/
|
|
13
|
+
const isCollpasedExpand = node => {
|
|
14
|
+
return Boolean(node && ['expand', 'nestedExpand'].includes(node.type.name) && !node.attrs.__expanded);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ED-18072 - Cannot shift + arrow past bodied extension if it is not empty
|
|
19
|
+
*/
|
|
20
|
+
const isBodiedExtension = node => {
|
|
21
|
+
return Boolean(node && ['bodiedExtension'].includes(node.type.name));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ED-19861 - [Regression] keyboard selections within action items are unpredicatable
|
|
26
|
+
* Table was added to the list of problematic nodes because the desired behaviour when Shift+Up from outside the
|
|
27
|
+
* table is to select the table node itself, rather than the table cell content. Previously this behaviour was handled
|
|
28
|
+
* in `packages/editor/editor-core/src/plugins/selection/pm-plugins/events/create-selection-between.ts` but there was
|
|
29
|
+
* a bug in `create-selection-between` which after fixing the bug that code was no longer handling table selection
|
|
30
|
+
* correctly, so to fix that table was added here.
|
|
31
|
+
*/
|
|
32
|
+
const isTable = node => {
|
|
33
|
+
return Boolean(node && ['table'].includes(node.type.name));
|
|
34
|
+
};
|
|
35
|
+
const isProblematicNode = node => {
|
|
36
|
+
return isCollpasedExpand(node) || isBodiedExtension(node) || isTable(node);
|
|
37
|
+
};
|
|
38
|
+
const findFixedProblematicNodePosition = (doc, $head, direction) => {
|
|
39
|
+
if ($head.pos === 0 || $head.depth === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (direction === 'up') {
|
|
43
|
+
const pos = $head.before();
|
|
44
|
+
const $posResolved = $head.doc.resolve(pos);
|
|
45
|
+
const maybeProblematicNode = $posResolved.nodeBefore;
|
|
46
|
+
if (maybeProblematicNode && isProblematicNode(maybeProblematicNode)) {
|
|
47
|
+
const nodeSize = maybeProblematicNode.nodeSize;
|
|
48
|
+
const nodeStartPosition = pos - nodeSize;
|
|
49
|
+
|
|
50
|
+
// ($head.pos - 1) will correspond to (nodeStartPosition + nodeSize) when we are at the start of the text node
|
|
51
|
+
const isAtEndOfProblematicNode = $head.pos - 1 === nodeStartPosition + nodeSize;
|
|
52
|
+
if (isAtEndOfProblematicNode) {
|
|
53
|
+
const startPosNode = Math.max(nodeStartPosition, 0);
|
|
54
|
+
const $startPosNode = $head.doc.resolve(Math.min(startPosNode, $head.doc.content.size));
|
|
55
|
+
return $startPosNode;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (direction === 'down') {
|
|
60
|
+
const pos = $head.after();
|
|
61
|
+
const maybeProblematicNode = doc.nodeAt(pos);
|
|
62
|
+
if (maybeProblematicNode && isProblematicNode(maybeProblematicNode) && $head.pos + 1 === pos) {
|
|
63
|
+
const nodeSize = maybeProblematicNode.nodeSize;
|
|
64
|
+
const nodePosition = pos + nodeSize;
|
|
65
|
+
const startPosNode = Math.max(nodePosition, 0);
|
|
66
|
+
const $startPosNode = $head.doc.resolve(Math.min(startPosNode, $head.doc.content.size));
|
|
67
|
+
return $startPosNode;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
export const onKeydown = (view, event) => {
|
|
73
|
+
/*
|
|
74
|
+
* This workaround is needed for some specific situations.
|
|
75
|
+
* - expand collapse
|
|
76
|
+
* - bodied extension
|
|
77
|
+
*/
|
|
78
|
+
if (!(event instanceof KeyboardEvent)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!event.shiftKey || event.ctrlKey || event.metaKey) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const {
|
|
88
|
+
doc,
|
|
89
|
+
selection: {
|
|
90
|
+
$head,
|
|
91
|
+
$anchor
|
|
92
|
+
}
|
|
93
|
+
} = view.state;
|
|
94
|
+
if (event.key === 'ArrowRight' && $head.nodeAfter || event.key === 'ArrowLeft' && $head.nodeBefore) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const direction = ['ArrowLeft', 'ArrowUp', 'Home'].includes(event.key) ? 'up' : 'down';
|
|
98
|
+
const $fixedProblematicNodePosition = findFixedProblematicNodePosition(doc, $head, direction);
|
|
99
|
+
if ($fixedProblematicNodePosition) {
|
|
100
|
+
// an offset is used here so that left arrow selects the first character before the node (consistent with arrow right)
|
|
101
|
+
const headOffset = event.key === 'ArrowLeft' ? -1 : 0;
|
|
102
|
+
const head = $fixedProblematicNodePosition.pos + headOffset;
|
|
103
|
+
const forcedTextSelection = TextSelection.create(view.state.doc, $anchor.pos, head);
|
|
104
|
+
const tr = view.state.tr;
|
|
105
|
+
tr.setSelection(forcedTextSelection);
|
|
106
|
+
view.dispatch(tr);
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { backspace, bindKeymapWithCommand, deleteKey, insertNewLine, moveDown, moveLeft, moveRight, moveUp } from '@atlaskit/editor-common/keymaps';
|
|
2
|
+
import { createParagraphNear } from '@atlaskit/editor-prosemirror/commands';
|
|
3
|
+
import { keymap } from '@atlaskit/editor-prosemirror/keymap';
|
|
4
|
+
import { arrow, deleteNode } from '../gap-cursor/actions';
|
|
5
|
+
import { Direction } from '../gap-cursor/direction';
|
|
6
|
+
import { GapCursorSelection } from '../gap-cursor/selection';
|
|
7
|
+
export default function keymapPlugin() {
|
|
8
|
+
const map = {};
|
|
9
|
+
bindKeymapWithCommand(insertNewLine.common, (state, dispatch, view) => {
|
|
10
|
+
const isInGapCursor = state.selection instanceof GapCursorSelection;
|
|
11
|
+
// Only operate in gap cursor
|
|
12
|
+
if (!isInGapCursor) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return createParagraphNear(state, dispatch);
|
|
16
|
+
}, map);
|
|
17
|
+
bindKeymapWithCommand(moveLeft.common, (state, dispatch, view) => {
|
|
18
|
+
const endOfTextblock = view ? view.endOfTextblock.bind(view) : undefined;
|
|
19
|
+
return arrow(Direction.LEFT, endOfTextblock)(state, dispatch, view);
|
|
20
|
+
}, map);
|
|
21
|
+
bindKeymapWithCommand(moveRight.common, (state, dispatch, view) => {
|
|
22
|
+
const endOfTextblock = view ? view.endOfTextblock.bind(view) : undefined;
|
|
23
|
+
return arrow(Direction.RIGHT, endOfTextblock)(state, dispatch);
|
|
24
|
+
}, map);
|
|
25
|
+
bindKeymapWithCommand(moveUp.common, (state, dispatch, view) => {
|
|
26
|
+
const endOfTextblock = view ? view.endOfTextblock.bind(view) : undefined;
|
|
27
|
+
return arrow(Direction.UP, endOfTextblock)(state, dispatch);
|
|
28
|
+
}, map);
|
|
29
|
+
bindKeymapWithCommand(moveDown.common, (state, dispatch, view) => {
|
|
30
|
+
const endOfTextblock = view ? view.endOfTextblock.bind(view) : undefined;
|
|
31
|
+
return arrow(Direction.DOWN, endOfTextblock)(state, dispatch);
|
|
32
|
+
}, map);
|
|
33
|
+
|
|
34
|
+
// default PM's Backspace doesn't handle removing block nodes when cursor is after it
|
|
35
|
+
bindKeymapWithCommand(backspace.common, deleteNode(Direction.BACKWARD), map);
|
|
36
|
+
|
|
37
|
+
// handle Delete key (remove node after the cursor)
|
|
38
|
+
bindKeymapWithCommand(deleteKey.common, deleteNode(Direction.FORWARD), map);
|
|
39
|
+
return keymap(map);
|
|
40
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
|
+
import { GapCursorSelection, Side as GapCursorSide, hideCaretModifier, JSON_ID, setGapCursorAtPos, Side } from '@atlaskit/editor-common/selection';
|
|
3
|
+
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
|
|
4
|
+
import { findPositionOfNodeBefore } from '@atlaskit/editor-prosemirror/utils';
|
|
5
|
+
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
6
|
+
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
|
|
7
|
+
import { deleteNode } from '../gap-cursor/actions';
|
|
8
|
+
import { Direction } from '../gap-cursor/direction';
|
|
9
|
+
import { getLayoutModeFromTargetNode, isIgnoredClick } from '../gap-cursor/utils';
|
|
10
|
+
import { toDOM } from '../gap-cursor/utils/place-gap-cursor';
|
|
11
|
+
import { gapCursorPluginKey } from './gap-cursor-plugin-key';
|
|
12
|
+
const plugin = new SafePlugin({
|
|
13
|
+
key: gapCursorPluginKey,
|
|
14
|
+
state: {
|
|
15
|
+
init: () => ({
|
|
16
|
+
selectionIsGapCursor: false,
|
|
17
|
+
displayGapCursor: true
|
|
18
|
+
}),
|
|
19
|
+
apply: (tr, pluginState, _oldState, newState) => {
|
|
20
|
+
var _meta$displayGapCurso;
|
|
21
|
+
const meta = tr.getMeta(gapCursorPluginKey);
|
|
22
|
+
const selectionIsGapCursor = newState.selection instanceof GapCursorSelection;
|
|
23
|
+
return {
|
|
24
|
+
selectionIsGapCursor,
|
|
25
|
+
// only attempt to hide gap cursor if selection is gap cursor
|
|
26
|
+
displayGapCursor: selectionIsGapCursor ? (_meta$displayGapCurso = meta === null || meta === void 0 ? void 0 : meta.displayGapCursor) !== null && _meta$displayGapCurso !== void 0 ? _meta$displayGapCurso : pluginState.displayGapCursor : true
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
view: view => {
|
|
31
|
+
/**
|
|
32
|
+
* If the selection is at the beginning of a document and is a NodeSelection,
|
|
33
|
+
* convert to a GapCursor selection. This is to stop users accidentally replacing
|
|
34
|
+
* the first node of a document by accident.
|
|
35
|
+
*/
|
|
36
|
+
if (view.state.selection.anchor === 0 && view.state.selection instanceof NodeSelection) {
|
|
37
|
+
// This is required otherwise the dispatch doesn't trigger in the correct place
|
|
38
|
+
window.requestAnimationFrame(() => {
|
|
39
|
+
view.dispatch(view.state.tr.setSelection(new GapCursorSelection(view.state.doc.resolve(0), GapCursorSide.LEFT)));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
update(view) {
|
|
44
|
+
const {
|
|
45
|
+
selectionIsGapCursor
|
|
46
|
+
} = gapCursorPluginKey.getState(view.state);
|
|
47
|
+
/**
|
|
48
|
+
* Starting with prosemirror-view 1.19.4, cursor wrapper that previously was hiding cursor doesn't exist:
|
|
49
|
+
* https://github.com/ProseMirror/prosemirror-view/commit/4a56bc7b7e61e96ef879d1dae1014ede0fc09e43
|
|
50
|
+
*
|
|
51
|
+
* Because it was causing issues with RTL: https://github.com/ProseMirror/prosemirror/issues/948
|
|
52
|
+
*
|
|
53
|
+
* This is the work around which uses `caret-color: transparent` in order to hide regular caret,
|
|
54
|
+
* when gap cursor is visible.
|
|
55
|
+
*
|
|
56
|
+
* Browser support is pretty good: https://caniuse.com/#feat=css-caret-color
|
|
57
|
+
*/
|
|
58
|
+
view.dom.classList.toggle(hideCaretModifier, selectionIsGapCursor);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
props: {
|
|
63
|
+
decorations: editorState => {
|
|
64
|
+
const {
|
|
65
|
+
doc,
|
|
66
|
+
selection
|
|
67
|
+
} = editorState;
|
|
68
|
+
const {
|
|
69
|
+
displayGapCursor
|
|
70
|
+
} = gapCursorPluginKey.getState(editorState);
|
|
71
|
+
if (selection instanceof GapCursorSelection && displayGapCursor) {
|
|
72
|
+
const {
|
|
73
|
+
$from,
|
|
74
|
+
side
|
|
75
|
+
} = selection;
|
|
76
|
+
|
|
77
|
+
// render decoration DOM node always to the left of the target node even if selection points to the right
|
|
78
|
+
// otherwise positioning of the right gap cursor is a nightmare when the target node has a nodeView with vertical margins
|
|
79
|
+
let position = selection.head;
|
|
80
|
+
const isRightCursor = side === Side.RIGHT;
|
|
81
|
+
if (isRightCursor && $from.nodeBefore) {
|
|
82
|
+
const nodeBeforeStart = findPositionOfNodeBefore(selection);
|
|
83
|
+
if (typeof nodeBeforeStart === 'number') {
|
|
84
|
+
position = nodeBeforeStart;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const node = isRightCursor ? $from.nodeBefore : $from.nodeAfter;
|
|
88
|
+
const layoutMode = node && getLayoutModeFromTargetNode(node);
|
|
89
|
+
return DecorationSet.create(doc, [Decoration.widget(position, toDOM, {
|
|
90
|
+
key: `${JSON_ID}-${side}-${layoutMode}`,
|
|
91
|
+
side: layoutMode ? -1 : 0
|
|
92
|
+
})]);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
},
|
|
96
|
+
// render gap cursor only when its valid
|
|
97
|
+
createSelectionBetween(view, $anchor, $head) {
|
|
98
|
+
if (view && view.state && view.state.selection instanceof CellSelection) {
|
|
99
|
+
// Do not show GapCursor when there is a CellSection happening
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if ($anchor.pos === $head.pos && GapCursorSelection.valid($head)) {
|
|
103
|
+
return new GapCursorSelection($head);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
},
|
|
107
|
+
handleClick(view, nodePos, event) {
|
|
108
|
+
var _$pos$parent;
|
|
109
|
+
const posAtCoords = view.posAtCoords({
|
|
110
|
+
left: event.clientX,
|
|
111
|
+
top: event.clientY
|
|
112
|
+
});
|
|
113
|
+
if (!posAtCoords || isIgnoredClick(event.target)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const isInsideTheTarget = posAtCoords.pos === posAtCoords.inside;
|
|
117
|
+
if (isInsideTheTarget) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const leftSideOffsetX = 20;
|
|
121
|
+
const side = event.offsetX > leftSideOffsetX ? Side.RIGHT : Side.LEFT;
|
|
122
|
+
const $pos = view.state.doc.resolve(nodePos);
|
|
123
|
+
// In the new prosemirror-view posAtCoords is not returning a precise value for our media nodes
|
|
124
|
+
if (((_$pos$parent = $pos.parent) === null || _$pos$parent === void 0 ? void 0 : _$pos$parent.type.name) === 'mediaSingle') {
|
|
125
|
+
const $insidePos = view.state.doc.resolve(Math.max(posAtCoords.inside, 0));
|
|
126
|
+
// We don't have GapCursors problems when the node target is inside the root level
|
|
127
|
+
if ($insidePos.depth <= 1) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const mediaGapCursor = !$pos.nodeBefore ? $pos.before() : $pos.after();
|
|
131
|
+
return setGapCursorAtPos(mediaGapCursor, side)(view.state, view.dispatch);
|
|
132
|
+
}
|
|
133
|
+
const docSize = view.state.doc.content.size;
|
|
134
|
+
const nodeInside = posAtCoords.inside < 0 || posAtCoords.inside > docSize ? null : view.state.doc.nodeAt(posAtCoords.inside);
|
|
135
|
+
if (nodeInside !== null && nodeInside !== void 0 && nodeInside.isAtom) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return setGapCursorAtPos(nodePos, side)(view.state, view.dispatch);
|
|
139
|
+
},
|
|
140
|
+
handleDOMEvents: {
|
|
141
|
+
/**
|
|
142
|
+
* Android composition events aren't handled well by Prosemirror
|
|
143
|
+
* We've added a couple of beforeinput hooks to help PM out when trying to delete
|
|
144
|
+
* certain nodes. We can remove these when PM has better composition support.
|
|
145
|
+
* @see https://github.com/ProseMirror/prosemirror/issues/543
|
|
146
|
+
*/
|
|
147
|
+
beforeinput: (view, event) => {
|
|
148
|
+
if (event.inputType === 'deleteContentBackward' && view.state.selection instanceof GapCursorSelection) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
return deleteNode(Direction.BACKWARD)(view.state, view.dispatch);
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
export default plugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { bindKeymapWithCommand, moveLeft, moveRight } from '@atlaskit/editor-common/keymaps';
|
|
2
|
+
import { keymap } from '@atlaskit/editor-prosemirror/keymap';
|
|
3
|
+
import { arrowLeft, arrowRight } from '../commands';
|
|
4
|
+
function keymapPlugin() {
|
|
5
|
+
const list = {};
|
|
6
|
+
bindKeymapWithCommand(moveRight.common, arrowRight, list);
|
|
7
|
+
bindKeymapWithCommand(moveLeft.common, arrowLeft, list);
|
|
8
|
+
return keymap(list);
|
|
9
|
+
}
|
|
10
|
+
export default keymapPlugin;
|