Files
AFFiNE-Mirror/blocksuite/affine/block-paragraph/src/paragraph-keymap.ts
2024-12-20 15:38:06 +08:00

224 lines
6.3 KiB
TypeScript

import {
focusTextModel,
getInlineEditorByModel,
markdownInput,
textKeymap,
} from '@blocksuite/affine-components/rich-text';
import { ParagraphBlockSchema } from '@blocksuite/affine-model';
import {
calculateCollapsedSiblings,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import { KeymapExtension } from '@blocksuite/block-std';
import { IS_MAC } from '@blocksuite/global/env';
import { forwardDelete } from './utils/forward-delete.js';
import { mergeWithPrev } from './utils/merge-with-prev.js';
export const ParagraphKeymapExtension = KeymapExtension(
std => {
return {
Backspace: ctx => {
const text = std.selection.find('text');
if (!text) return;
const isCollapsed = text.isCollapsed();
const isStart = isCollapsed && text.from.index === 0;
if (!isStart) return;
const { doc } = std;
const model = doc.getBlock(text.from.blockId)?.model;
if (!model || !matchFlavours(model, ['affine:paragraph'])) return;
// const { model, doc } = this;
const event = ctx.get('keyboardState').raw;
event.preventDefault();
// When deleting at line start of a paragraph block,
// firstly switch it to normal text, then delete this empty block.
if (model.type !== 'text') {
// Try to switch to normal text
doc.captureSync();
doc.updateBlock(model, { type: 'text' });
return true;
}
const merged = mergeWithPrev(std.host, model);
if (merged) {
return true;
}
std.command.chain().canDedentParagraph().dedentParagraph().run();
return true;
},
'Mod-Enter': ctx => {
const { doc } = std;
const text = std.selection.find('text');
if (!text) return;
const model = doc.getBlock(text.from.blockId)?.model;
if (!model || !matchFlavours(model, ['affine:paragraph'])) return;
const inlineEditor = getInlineEditorByModel(
std.host,
text.from.blockId
);
const inlineRange = inlineEditor?.getInlineRange();
if (!inlineRange || !inlineEditor) return;
const raw = ctx.get('keyboardState').raw;
raw.preventDefault();
if (model.type === 'quote') {
doc.captureSync();
inlineEditor.insertText(inlineRange, '\n');
inlineEditor.setInlineRange({
index: inlineRange.index + 1,
length: 0,
});
return true;
}
std.command.exec('addParagraph');
return true;
},
Enter: ctx => {
const { doc } = std;
const text = std.selection.find('text');
if (!text) return;
const model = doc.getBlock(text.from.blockId)?.model;
if (!model || !matchFlavours(model, ['affine:paragraph'])) return;
const inlineEditor = getInlineEditorByModel(
std.host,
text.from.blockId
);
const range = inlineEditor?.getInlineRange();
if (!range || !inlineEditor) return;
const raw = ctx.get('keyboardState').raw;
const isEnd = model.text.length === range.index;
if (model.type === 'quote') {
const textStr = model.text.toString();
/**
* If quote block ends with two blank lines, split the block
* ---
* before:
* > \n
* > \n|
*
* after:
* > \n
* |
* ---
*/
const endWithTwoBlankLines =
textStr === '\n' || textStr.endsWith('\n');
if (isEnd && endWithTwoBlankLines) {
raw.preventDefault();
doc.captureSync();
model.text.delete(range.index - 1, 1);
std.command.exec('addParagraph');
return true;
}
return true;
}
raw.preventDefault();
if (markdownInput(std, model.id)) {
return true;
}
if (model.type.startsWith('h') && model.collapsed) {
const parent = doc.getParent(model);
if (!parent) return true;
const index = parent.children.indexOf(model);
if (index === -1) return true;
const collapsedSiblings = calculateCollapsedSiblings(model);
const rightText = model.text.split(range.index);
const newId = doc.addBlock(
model.flavour,
{ type: model.type, text: rightText },
parent,
index + collapsedSiblings.length + 1
);
focusTextModel(std, newId);
return true;
}
if (isEnd) {
std.command.exec('addParagraph');
return true;
}
std.command.exec('splitParagraph');
return true;
},
Delete: ctx => {
const deleted = forwardDelete(std);
if (!deleted) {
return;
}
const event = ctx.get('keyboardState').raw;
event.preventDefault();
return true;
},
'Control-d': ctx => {
if (!IS_MAC) return;
const deleted = forwardDelete(std);
if (!deleted) {
return;
}
const event = ctx.get('keyboardState').raw;
event.preventDefault();
return true;
},
Space: ctx => {
if (!markdownInput(std)) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
'Shift-Space': ctx => {
if (!markdownInput(std)) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
Tab: ctx => {
const [success] = std.command
.chain()
.canIndentParagraph()
.indentParagraph()
.run();
if (!success) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
'Shift-Tab': ctx => {
const [success] = std.command
.chain()
.canDedentParagraph()
.dedentParagraph()
.run();
if (!success) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
};
},
{
flavour: ParagraphBlockSchema.model.flavour,
}
);
export const ParagraphTextKeymapExtension = KeymapExtension(textKeymap, {
flavour: ParagraphBlockSchema.model.flavour,
});