mirror of
https://github.com/laurent22/joplin.git
synced 2026-10-25 02:47:28 +00:00
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
133 lines
4.5 KiB
TypeScript
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;
|