mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 15:26:59 +08:00
224 lines
6.3 KiB
TypeScript
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,
|
|
});
|