snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
This commit is contained in:
646
blocksuite/affine/blocks/note/src/note-keymap.ts
Normal file
646
blocksuite/affine/blocks/note/src/note-keymap.ts
Normal file
@@ -0,0 +1,646 @@
|
||||
import {
|
||||
CodeBlockModel,
|
||||
ListBlockModel,
|
||||
NoteBlockModel,
|
||||
NoteBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
textAlignConfigs,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
focusBlockEnd,
|
||||
focusBlockStart,
|
||||
getBlockSelectionsCommand,
|
||||
getNextBlockCommand,
|
||||
getPrevBlockCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
asyncGetBlockComponent,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type BlockStdScope,
|
||||
type Chain,
|
||||
KeymapExtension,
|
||||
TextSelection,
|
||||
type UIEventHandler,
|
||||
type UIEventStateContext,
|
||||
} from '@blocksuite/std';
|
||||
import type { BaseSelection } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
dedentBlocks,
|
||||
dedentBlocksToRoot,
|
||||
indentBlocks,
|
||||
selectBlock,
|
||||
selectBlocksBetween,
|
||||
updateBlockAlign,
|
||||
updateBlockType,
|
||||
} from './commands';
|
||||
import { moveBlockConfigs } from './move-block';
|
||||
import { quickActionConfig } from './quick-action';
|
||||
|
||||
class NoteKeymap {
|
||||
constructor(readonly std: BlockStdScope) {}
|
||||
|
||||
private _anchorSel: BlockSelection | null = null;
|
||||
|
||||
private readonly _bindMoveBlockHotKey = () => {
|
||||
return moveBlockConfigs.reduce(
|
||||
(acc, config) => {
|
||||
const keys = config.hotkey.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
return config.action(this.std);
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
return {
|
||||
...acc,
|
||||
...keys,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindQuickActionHotKey = () => {
|
||||
return quickActionConfig
|
||||
.filter(config => config.hotkey)
|
||||
.reduce(
|
||||
(acc, config) => {
|
||||
return {
|
||||
...acc,
|
||||
[config.hotkey!]: ctx => {
|
||||
if (!config.showWhen(this.std)) return;
|
||||
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
config.action(this.std);
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindTextConversionHotKey = () => {
|
||||
return textConversionConfigs
|
||||
.filter(item => item.hotkey)
|
||||
.reduce(
|
||||
(acc, item) => {
|
||||
const keymap = item.hotkey!.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(updateBlockType, {
|
||||
flavour: item.flavour,
|
||||
props: {
|
||||
type: item.type,
|
||||
},
|
||||
})
|
||||
.pipe((ctx, next) => {
|
||||
const newModels = ctx.updatedBlocks;
|
||||
if (!newModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.flavour !== 'affine:code') {
|
||||
return;
|
||||
}
|
||||
|
||||
const [codeModel] = newModels;
|
||||
asyncGetBlockComponent(ctx.std, codeModel.id)
|
||||
.then(codeElement => {
|
||||
if (!codeElement) {
|
||||
return;
|
||||
}
|
||||
this._std.selection.setGroup('note', [
|
||||
this._std.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: codeElement.blockId,
|
||||
index: 0,
|
||||
length: codeModel.text?.length ?? 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...keymap,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindTextAlignHotKey = () => {
|
||||
return textAlignConfigs.reduce(
|
||||
(acc, item) => {
|
||||
const keymap = item.hotkey!.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(updateBlockAlign, { textAlign: item.textAlign })
|
||||
.run();
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...keymap,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private _focusBlock: BlockComponent | null = null;
|
||||
|
||||
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
||||
const doc = this._std.store;
|
||||
let parent = doc.getBlock(blockId)?.model ?? null;
|
||||
while (parent) {
|
||||
if (matchModels(parent, [NoteBlockModel])) {
|
||||
return parent;
|
||||
}
|
||||
parent = doc.getParent(parent);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private readonly _onArrowDown = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe((_, next) => {
|
||||
this._reset();
|
||||
return next();
|
||||
})
|
||||
.try(cmd => [
|
||||
// text selection - select the next block
|
||||
// 1. is paragraph, list, code block - follow the default behavior
|
||||
// 2. is not - select the next block (use block selection instead of text selection)
|
||||
cmd
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentTextSelection = ctx.currentTextSelection;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: currentTextSelection.blockId });
|
||||
})
|
||||
.pipe(getNextBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const { nextBlock } = ctx;
|
||||
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchModels(nextBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: nextBlock,
|
||||
});
|
||||
}
|
||||
|
||||
return next({});
|
||||
}),
|
||||
|
||||
// block selection - select the next block
|
||||
// 1. is paragraph, list, code block - focus it
|
||||
// 2. is not - select it using block selection
|
||||
cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
const blockSelection = currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: blockSelection.blockId });
|
||||
})
|
||||
.pipe(getNextBlockCommand)
|
||||
.pipe<{ focusBlock: BlockComponent }>((ctx, next) => {
|
||||
const { nextBlock } = ctx;
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if (
|
||||
matchModels(nextBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
this._std.command.exec(focusBlockStart, {
|
||||
focusBlock: nextBlock,
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: nextBlock,
|
||||
});
|
||||
return next();
|
||||
}),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onArrowUp = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe((_, next) => {
|
||||
this._reset();
|
||||
return next();
|
||||
})
|
||||
.try(cmd => [
|
||||
// text selection - select the previous block
|
||||
// 1. is paragraph, list, code block - follow the default behavior
|
||||
// 2. is not - select the previous block (use block selection instead of text selection)
|
||||
cmd
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentTextSelection = ctx.currentTextSelection;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: currentTextSelection.blockId });
|
||||
})
|
||||
.pipe(getPrevBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const { prevBlock } = ctx;
|
||||
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchModels(prevBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: prevBlock,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
// block selection - select the previous block
|
||||
// 1. is paragraph, list, code block - focus it
|
||||
// 2. is not - select it using block selection
|
||||
cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
const blockSelection = currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: blockSelection.blockId });
|
||||
})
|
||||
.pipe(getPrevBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const { prevBlock } = ctx;
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
matchModels(prevBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
event.preventDefault();
|
||||
this._std.command.exec(focusBlockEnd, {
|
||||
focusBlock: prevBlock,
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: prevBlock,
|
||||
});
|
||||
return next();
|
||||
}),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onBlockShiftDown = (cmd: Chain) => {
|
||||
return cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string; anchorBlock: BlockComponent }>(
|
||||
(ctx, next) => {
|
||||
const blockSelections = ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._anchorSel) {
|
||||
this._anchorSel = blockSelections.at(-1) ?? null;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorBlock = ctx.std.view.getBlock(this._anchorSel.blockId);
|
||||
if (!anchorBlock) {
|
||||
return;
|
||||
}
|
||||
return next({
|
||||
anchorBlock,
|
||||
currentSelectionPath:
|
||||
this._focusBlock?.blockId ?? anchorBlock?.blockId,
|
||||
});
|
||||
}
|
||||
)
|
||||
.pipe(getNextBlockCommand)
|
||||
.pipe<{ focusBlock: BlockComponent }>((ctx, next) => {
|
||||
const nextBlock = ctx.nextBlock;
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
this._focusBlock = nextBlock;
|
||||
return next({
|
||||
focusBlock: this._focusBlock,
|
||||
});
|
||||
})
|
||||
.pipe(selectBlocksBetween, { tail: true });
|
||||
};
|
||||
|
||||
private readonly _onBlockShiftUp = (cmd: Chain) => {
|
||||
return cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string; anchorBlock: BlockComponent }>(
|
||||
(ctx, next) => {
|
||||
const blockSelections = ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
return;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
this._anchorSel = blockSelections.at(0) ?? null;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
return;
|
||||
}
|
||||
const anchorBlock = ctx.std.view.getBlock(this._anchorSel.blockId);
|
||||
if (!anchorBlock) {
|
||||
return;
|
||||
}
|
||||
return next({
|
||||
anchorBlock,
|
||||
currentSelectionPath:
|
||||
this._focusBlock?.blockId ?? anchorBlock?.blockId,
|
||||
});
|
||||
}
|
||||
)
|
||||
.pipe(getPrevBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const prevBlock = ctx.prevBlock;
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
this._focusBlock = prevBlock;
|
||||
return next({
|
||||
focusBlock: this._focusBlock,
|
||||
});
|
||||
})
|
||||
.pipe(selectBlocksBetween, { tail: false });
|
||||
};
|
||||
|
||||
private readonly _onEnter = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const blockSelection = ctx.currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { view, store, selection } = ctx.std;
|
||||
|
||||
const element = view.getBlock(blockSelection.blockId);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { model } = element;
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parent.children.indexOf(model) ?? undefined;
|
||||
|
||||
const blockId = store.addBlock(
|
||||
'affine:paragraph',
|
||||
{},
|
||||
parent,
|
||||
index + 1
|
||||
);
|
||||
|
||||
const sel = selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
selection.setGroup('note', [sel]);
|
||||
this._reset();
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onEsc = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const blockSelection = ctx.currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.std.selection.update(selList => {
|
||||
return selList.filter(sel => !sel.is(BlockSelection));
|
||||
});
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onSelectAll: UIEventHandler = ctx => {
|
||||
const selection = this._std.selection;
|
||||
const block = selection.find(BlockSelection);
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
const note = this._getClosestNoteByBlockId(block.blockId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const children = note.children;
|
||||
const blocks: BlockSelection[] = children.map(child => {
|
||||
return selection.create(BlockSelection, {
|
||||
blockId: child.id,
|
||||
});
|
||||
});
|
||||
selection.update(selList => {
|
||||
return selList
|
||||
.filter<BaseSelection>(sel => !sel.is(BlockSelection))
|
||||
.concat(blocks);
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _onShiftArrowDown = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.try(cmd => [
|
||||
// block selection
|
||||
this._onBlockShiftDown(cmd),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onShiftArrowUp = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.try(cmd => [
|
||||
// block selection
|
||||
this._onBlockShiftUp(cmd),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _reset = () => {
|
||||
this._anchorSel = null;
|
||||
this._focusBlock = null;
|
||||
};
|
||||
|
||||
private get _std() {
|
||||
return this.std;
|
||||
}
|
||||
|
||||
get hotKeys(): Record<string, UIEventHandler> {
|
||||
return {
|
||||
...this._bindMoveBlockHotKey(),
|
||||
...this._bindQuickActionHotKey(),
|
||||
...this._bindTextConversionHotKey(),
|
||||
...this._bindTextAlignHotKey(),
|
||||
Tab: ctx => {
|
||||
const [success] = this.std.command.exec(indentBlocks);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Tab': ctx => {
|
||||
const [success] = this.std.command.exec(dedentBlocks);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Mod-Backspace': ctx => {
|
||||
const [success] = this.std.command.exec(dedentBlocksToRoot);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowDown: this._onArrowDown,
|
||||
ArrowUp: this._onArrowUp,
|
||||
'Shift-ArrowDown': this._onShiftArrowDown,
|
||||
'Shift-ArrowUp': this._onShiftArrowUp,
|
||||
Escape: this._onEsc,
|
||||
Enter: this._onEnter,
|
||||
'Mod-a': this._onSelectAll,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const NoteKeymapExtension = KeymapExtension(
|
||||
std => new NoteKeymap(std).hotKeys,
|
||||
{
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user