@atlaskit/editor-plugin-selection
Version:
Selection plugin for @atlaskit/editor-core
252 lines • 12.6 kB
JavaScript
/* eslint-disable import/no-extraneous-dependencies */
import { isIgnored as isIgnoredByGapCursor, RelativeSelectionPos, GapCursorSelection, Side } from '@atlaskit/editor-common/selection';
import { isEmptyParagraph, isNodeEmpty } from '@atlaskit/editor-common/utils';
import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { SelectionDirection, selectionPluginKey } from '../types';
import { SelectionActionTypes } from './actions';
import { createCommand, getPluginState } from './plugin-factory';
import { findFirstChildNodeToSelect, findLastChildNodeToSelect, findSelectableContainerAfter, findSelectableContainerBefore, findSelectableContainerParent, isSelectableContainerNode, isSelectionAtEndOfParentNode, isSelectionAtStartOfParentNode } from './utils';
export const selectNearNode = (selectionRelativeToNode, selection) => ({
tr
}) => {
tr.setMeta(selectionPluginKey, {
type: SelectionActionTypes.SET_RELATIVE_SELECTION,
selectionRelativeToNode
});
if (selection) {
return tr.setSelection(selection);
}
return tr;
};
export const setSelectionRelativeToNode = (selectionRelativeToNode, selection) => createCommand({
type: SelectionActionTypes.SET_RELATIVE_SELECTION,
selectionRelativeToNode
}, tr => {
return selectNearNode(selectionRelativeToNode, selection)({
tr
}) || tr;
});
export const arrowRight = (state, dispatch) => {
const {
selection
} = state;
if (selection instanceof GapCursorSelection) {
return arrowRightFromGapCursor(selection)(state, dispatch);
} else if (selection instanceof NodeSelection) {
return arrowRightFromNode(selection)(state, dispatch);
} else if (selection instanceof TextSelection) {
return arrowRightFromText(selection)(state, dispatch);
}
return false;
};
export const arrowLeft = (state, dispatch) => {
const {
selection
} = state;
if (selection instanceof GapCursorSelection) {
return arrowLeftFromGapCursor(selection)(state, dispatch);
} else if (selection instanceof NodeSelection) {
return arrowLeftFromNode(selection)(state, dispatch);
} else if (selection instanceof TextSelection) {
return arrowLeftFromText(selection)(state, dispatch);
}
return false;
};
const arrowRightFromGapCursor = selection => (state, dispatch) => {
const {
$from,
$to,
side
} = selection;
if (side === Side.LEFT) {
const selectableNode = findSelectableContainerAfter($to, state.doc);
if (selectableNode) {
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
} else if (side === Side.RIGHT && isSelectionAtEndOfParentNode($from, selection)) {
const selectableNode = findSelectableContainerParent(selection);
if (selectableNode) {
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
return false;
};
const arrowLeftFromGapCursor = selection => (state, dispatch) => {
const {
$from,
side
} = selection;
const {
selectionRelativeToNode
} = getPluginState(state);
if (side === Side.RIGHT) {
const selectableNode = findSelectableContainerBefore($from, state.doc);
if (selectableNode) {
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
} else if (side === Side.LEFT && isSelectionAtStartOfParentNode($from, selection)) {
if (selectionRelativeToNode === RelativeSelectionPos.Before) {
const $parent = state.doc.resolve(selection.$from.before(selection.$from.depth));
if ($parent) {
const selectableNode = findSelectableContainerBefore($parent, state.doc);
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
// selection is inside node without gap cursor preceeded by another node without gap cursor - set node selection for previous node
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
// we don't return this as we want to reset the relative pos, but not block other plugins
// from responding to arrow left key
setSelectionRelativeToNode()(state, dispatch);
} else {
const selectableNode = findSelectableContainerParent(selection);
if (selectableNode) {
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
}
return false;
};
const arrowRightFromNode = selection => (state, dispatch) => {
const {
node,
from,
$to
} = selection;
const {
selectionRelativeToNode
} = getPluginState(state);
if (node.isAtom) {
if (isSelectionAtEndOfParentNode($to, selection) && (node.isInline || isIgnoredByGapCursor(node))) {
// selection is for inline node or atom node which is ignored by gap-cursor and that is the last child of its parent node - set text selection after it
return findAndSetTextSelection(RelativeSelectionPos.End, state.doc.resolve(from + 1), SelectionDirection.After)(state, dispatch);
}
return false;
} else if (selectionRelativeToNode === RelativeSelectionPos.Start) {
// selection is for container node - set selection inside it at the start
return setSelectionInsideAtNodeStart(RelativeSelectionPos.Inside, node, from)(state, dispatch);
} else if (isIgnoredByGapCursor(node) && (!selectionRelativeToNode || selectionRelativeToNode === RelativeSelectionPos.End)) {
const selectableNode = findSelectableContainerAfter($to, state.doc);
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
// selection is for node without gap cursor followed by another node without gap cursor - set node selection for next node
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
return false;
};
const arrowLeftFromNode = selection => (state, dispatch) => {
const {
node,
from,
to,
$from
} = selection;
const {
selectionRelativeToNode
} = getPluginState(state);
if (node.isAtom) {
if (isSelectionAtStartOfParentNode($from, selection) && (node.isInline || isIgnoredByGapCursor(node))) {
// selection is for inline node or atom node which is ignored by gap-cursor and that is the first child of its parent node - set text selection before it
return findAndSetTextSelection(RelativeSelectionPos.Start, state.doc.resolve(from), SelectionDirection.Before)(state, dispatch);
}
return false;
} else if (selectionRelativeToNode === RelativeSelectionPos.End) {
// selection is for container node - set selection inside it at the end
return setSelectionInsideAtNodeEnd(RelativeSelectionPos.Inside, node, from, to)(state, dispatch);
} else if (!selectionRelativeToNode || selectionRelativeToNode === RelativeSelectionPos.Inside) {
// selection is for container node - set selection inside it at the start
// (this is a special case when the user selects by clicking node)
return setSelectionInsideAtNodeStart(RelativeSelectionPos.Before, node, from)(state, dispatch);
} else if (isIgnoredByGapCursor(node) && selectionRelativeToNode === RelativeSelectionPos.Start) {
// selection is for node without gap cursor preceeded by another node without gap cursor - set node selection for previous node
const selectableNode = findSelectableContainerBefore($from, state.doc);
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
return false;
};
const arrowRightFromText = selection => (state, dispatch) => {
if (isSelectionAtEndOfParentNode(selection.$to, selection)) {
const selectableNode = findSelectableContainerParent(selection);
if (selectableNode) {
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
return false;
};
const arrowLeftFromText = selection => (state, dispatch) => {
const {
selectionRelativeToNode
} = getPluginState(state);
if (selectionRelativeToNode === RelativeSelectionPos.Before) {
const selectableNode = findSelectableContainerBefore(selection.$from, state.doc);
if (selectableNode && isIgnoredByGapCursor(selectableNode.node)) {
// selection is inside node without gap cursor preceeded by another node without gap cursor - set node selection for previous node
return setSelectionRelativeToNode(RelativeSelectionPos.End, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
// we don't return this as we want to reset the relative pos, but not block other plugins
// from responding to arrow left key
setSelectionRelativeToNode(undefined)(state, dispatch);
} else if (isSelectionAtStartOfParentNode(selection.$from, selection)) {
const selectableNode = findSelectableContainerParent(selection);
if (selectableNode) {
return setSelectionRelativeToNode(RelativeSelectionPos.Start, NodeSelection.create(state.doc, selectableNode.pos))(state, dispatch);
}
}
return false;
};
const findAndSetTextSelection = (selectionRelativeToNode, $pos, dir) => (state, dispatch) => {
const sel = Selection.findFrom($pos, dir, true);
if (sel) {
return setSelectionRelativeToNode(selectionRelativeToNode, sel)(state, dispatch);
}
return false;
};
const setSelectionInsideAtNodeStart = (selectionRelativeToNode, node, pos) => (state, dispatch) => {
if (isNodeEmpty(node)) {
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(pos), SelectionDirection.After)(state, dispatch);
}
const selectableNode = findFirstChildNodeToSelect(node);
if (selectableNode) {
const {
node: childNode,
pos: childPos
} = selectableNode;
const selectionPos = pos + childPos + 1;
if (childNode.isText || childNode.isAtom && isIgnoredByGapCursor(childNode) || childNode.isInline) {
//selection is for text node, inline node or atom node which is ignored by gap-cursor. set selection before it.
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos), SelectionDirection.Before)(state, dispatch);
} else if (isEmptyParagraph(childNode)) {
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos + 1), SelectionDirection.Before)(state, dispatch);
} else if (!isIgnoredByGapCursor(node)) {
return setSelectionRelativeToNode(selectionRelativeToNode, new GapCursorSelection(state.doc.resolve(selectionPos), Side.LEFT))(state, dispatch);
} else if (isSelectableContainerNode(node)) {
return setSelectionRelativeToNode(selectionRelativeToNode, NodeSelection.create(state.doc, selectionPos))(state, dispatch);
}
}
return false;
};
export const setSelectionInsideAtNodeEnd = (selectionRelativeToNode, node, from, to) => (state, dispatch) => {
if (isNodeEmpty(node)) {
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(to), SelectionDirection.Before)(state, dispatch);
}
const selectableNode = findLastChildNodeToSelect(node);
if (selectableNode) {
const {
node: childNode,
pos: childPos
} = selectableNode;
const selectionPos = from + childPos + childNode.nodeSize;
if (childNode.isText || childNode.isAtom && isIgnoredByGapCursor(childNode) || childNode.isInline) {
//selection is for text node, inline node or atom node which is ignored by gap-cursor. set selection after it.
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos + 1), SelectionDirection.After)(state, dispatch);
} else if (isEmptyParagraph(childNode)) {
return findAndSetTextSelection(selectionRelativeToNode, state.doc.resolve(selectionPos), SelectionDirection.After)(state, dispatch);
} else if (!isIgnoredByGapCursor(node)) {
return setSelectionRelativeToNode(selectionRelativeToNode, new GapCursorSelection(state.doc.resolve(selectionPos + 1), Side.RIGHT))(state, dispatch);
} else if (isSelectableContainerNode(node)) {
return setSelectionRelativeToNode(selectionRelativeToNode, NodeSelection.create(state.doc, selectionPos))(state, dispatch);
}
}
return false;
};