Files

133 lines
4.5 KiB
TypeScript

import { buildKeymap as buildBaseKeymap } from 'prosemirror-example-setup';
import schema from '../schema';
import { keymap } from 'prosemirror-keymap';
import { baseKeymap, chainCommands, exitCode, liftEmptyBlock, newlineInCode } from 'prosemirror-commands';
import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
import commands from '../commands/commands';
import { EditorCommandType } from '../../types';
import { Command, EditorState, TextSelection, Plugin } from 'prosemirror-state';
import splitBlockAs from '../vendor/splitBlockAs';
import canReplaceSelectionWith from '../utils/canReplaceSelectionWith';
const splitBlockAsDefault = splitBlockAs();
const splitBlockAsParagraph = splitBlockAs(() => ({ type: schema.nodes.paragraph }));
const convertDoubleHardBreakToNewParagraph: Command = (state, dispatch) => {
const { from, to } = state.selection;
let foundHardBreak = false;
let hardBreakFrom = -1;
state.doc.nodesBetween(from - 1, to, (node, pos) => {
if (node.type === schema.nodes.hard_break) {
foundHardBreak = true;
hardBreakFrom = pos;
}
return !foundHardBreak;
});
if (foundHardBreak) {
const updatedSelection = TextSelection.create(state.doc, hardBreakFrom, to);
const tr = state.tr.setSelection(updatedSelection);
const splitBlockTransaction = splitBlockAsParagraph(updatedSelection, tr) || splitBlockAsDefault(updatedSelection, tr);
if (splitBlockTransaction && dispatch) {
dispatch(splitBlockTransaction);
}
return !!splitBlockTransaction;
}
return false;
};
const listItemTypes = [
// Apply the list item keymap to all list item types
// Ref: Default keymap in prosemirror-example-setup.
schema.nodes.list_item, schema.nodes.task_list_item,
];
const isInEmptyListItem = (state: EditorState) => {
const anchor = state.selection.$anchor;
const selectionGrandparent = anchor.node(Math.max(0, anchor.depth - 1));
const inList = listItemTypes.includes(selectionGrandparent?.type);
return inList && anchor.parent.content.size === 0;
};
const isInEmptyParagraph = (state: EditorState) => {
const selectionParent = state.selection.$anchor.parent;
return state.selection.empty &&
state.selection.$anchor.parent.type === schema.nodes.paragraph &&
selectionParent.content.size === 0;
};
// Handle double-hard-break -> paragraph conversion with a Plugin to work around
// a bug on Android. If convertDoubleHardBreakToNewParagraph is handled with the
// main keymap logic (with a keymap() extension), then it's possible for the cursor
// to get stuck in some cases.
// See https://github.com/laurent22/joplin/issues/12960.
const replaceDoubleHardBreaksOnEnter = new Plugin({
props: {
handleDOMEvents: {
keydown: (view, event) => {
if (event.key === 'Enter') {
const commandResult = convertDoubleHardBreakToNewParagraph(view.state, view.dispatch);
if (commandResult) {
event.preventDefault();
return true;
}
}
return false;
},
},
},
});
const insertHardBreak: Command = (state, dispatch) => {
// Avoid adding hard breaks at the beginning of list items
if (isInEmptyListItem(state)) return false;
// Avoid adding hard breaks at the beginning of paragraphs -- certain input rules
// only work when the cursor is at the beginning of a paragraph. If a paragraph
// starts with a hard break, it may incorrectly appear to the user that the cursor is at the
// start of a paragraph, leading to unexpected behavior related to input rules.
if (isInEmptyParagraph(state)) return false;
if (!canReplaceSelectionWith(state.selection, schema.nodes.hard_break)) return false;
if (dispatch) {
const hardBreak = schema.nodes.hard_break.create();
// Default to inserting a hard break. See https://github.com/ProseMirror/prosemirror-example-setup/blob/8c11be6850604081dceda8f36e08d2426875e19a/src/keymap.ts#L77C26-L77C39
dispatch(
state.tr.replaceSelectionWith(hardBreak)
.scrollIntoView(),
);
}
return true;
};
const keymapExtension = [
listItemTypes.map(itemType => keymap({
'Enter': splitListItem(itemType),
'Mod-[': liftListItem(itemType),
'Mod-]': sinkListItem(itemType),
})),
replaceDoubleHardBreaksOnEnter,
keymap({
'Enter': chainCommands(
newlineInCode,
exitCode,
liftEmptyBlock,
insertHardBreak,
),
}),
keymap({
'Mod-k': commands[EditorCommandType.EditLink],
'Mod-i': commands[EditorCommandType.ToggleItalicized],
'Mod-`': commands[EditorCommandType.ToggleCode],
'Mod-f': commands[EditorCommandType.ToggleSearch],
}),
keymap(buildBaseKeymap(schema)),
keymap(baseKeymap),
].flat();
export default keymapExtension;