Skip to content

Commit

Permalink
feat(blocks): optimize the experience of switching selected blocks us…
Browse files Browse the repository at this point in the history
…ing ArrowUp and ArrowDown (#6489)
  • Loading branch information
Flrande authored Mar 18, 2024
1 parent 05a3c47 commit 393e558
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -648,12 +648,10 @@ function handleParagraphDeleteActions(
'affine:bookmark',
'affine:code',
'affine:image',
'affine:divider',
...EMBED_BLOCK_FLAVOUR_LIST,
])
) {
doc.deleteBlock(model, {
bringChildrenTo: parent,
});
const previousSiblingElement = getBlockComponentByModel(
editorHost,
previousSibling
Expand All @@ -664,6 +662,12 @@ function handleParagraphDeleteActions(
});
editorHost.selection.setGroup('note', [selection]);

if (model.text?.length === 0) {
doc.deleteBlock(model, {
bringChildrenTo: parent,
});
}

return true;
}

Expand Down Expand Up @@ -835,7 +839,16 @@ function handleParagraphBlockForwardDelete(
}
function handleEmbedDividerCodeSibling(nextSibling: ExtendedModel | null) {
if (matchFlavours(nextSibling, ['affine:divider'])) {
doc.deleteBlock(nextSibling);
const nextSiblingComponent = getBlockComponentByModel(
editorHost,
nextSibling
);
assertExists(nextSiblingComponent);
editorHost.selection.setGroup('note', [
editorHost.selection.create('block', {
path: nextSiblingComponent.path,
}),
]);
return true;
}

Expand Down
29 changes: 29 additions & 0 deletions packages/blocks/src/note-block/commands/focus-block-end.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Command } from '@blocksuite/block-std';

export const focusBlockEnd: Command<'focusBlock'> = (ctx, next) => {
const { focusBlock, std } = ctx;
if (!focusBlock || !focusBlock.model.text) return;

const { selection } = std;

selection.setGroup('note', [
selection.create('text', {
from: {
path: focusBlock.path,
index: focusBlock.model.text.length,
length: 0,
},
to: null,
}),
]);

return next();
};

declare global {
namespace BlockSuite {
interface Commands {
focusBlockEnd: typeof focusBlockEnd;
}
}
}
25 changes: 25 additions & 0 deletions packages/blocks/src/note-block/commands/focus-block-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Command } from '@blocksuite/block-std';

export const focusBlockStart: Command<'focusBlock'> = (ctx, next) => {
const { focusBlock, std } = ctx;
if (!focusBlock || !focusBlock.model.text) return;

const { selection } = std;

selection.setGroup('note', [
selection.create('text', {
from: { path: focusBlock.path, index: 0, length: 0 },
to: null,
}),
]);

return next();
};

declare global {
namespace BlockSuite {
interface Commands {
focusBlockStart: typeof focusBlockStart;
}
}
}
208 changes: 148 additions & 60 deletions packages/blocks/src/note-block/keymap-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { ReactiveControllerHost } from 'lit';
import { moveBlockConfigs } from '../_common/configs/move-block.js';
import { quickActionConfig } from '../_common/configs/quick-action/config.js';
import { textConversionConfigs } from '../_common/configs/text-conversion.js';
import { buildPath } from '../_common/utils/index.js';
import { buildPath, matchFlavours } from '../_common/utils/index.js';
import { onModelElementUpdated } from '../root-block/utils/callback.js';
import { ensureBlockInContainer } from './utils.js';

Expand Down Expand Up @@ -67,94 +67,182 @@ export class KeymapController implements ReactiveController {
this._bindMoveBlockHotKey();
};

private _onArrowDown = () => {
private _onArrowDown = (ctx: UIEventStateContext) => {
const event = ctx.get('defaultState').event;

const [result] = this._std.command
.chain()
.inline((_, 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
.getTextSelection()
.inline<'currentSelectionPath'>((ctx, next) => {
const currentTextSelection = ctx.currentTextSelection;
assertExists(currentTextSelection);
return next({ currentSelectionPath: currentTextSelection.path });
})
.getNextBlock({
filter: block => ensureBlockInContainer(block, this.host),
})
.inline<'focusBlock'>((ctx, next) => {
const { nextBlock } = ctx;
assertExists(nextBlock);

if (
matchFlavours(nextBlock.model, [
'affine:paragraph',
'affine:list',
'affine:code',
])
)
return;

return next({
focusBlock: nextBlock,
});
})
.selectBlock(),
// block selection - select the next block
this._onBlockDown(cmd),
// 1. is paragraph, list, code block - focus it
// 2. is not - select it using block selection
cmd
.getBlockSelections()
.inline<'currentSelectionPath'>((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
assertExists(currentBlockSelections);
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) {
return;
}
return next({ currentSelectionPath: blockSelection.path });
})
.getNextBlock({
filter: block => ensureBlockInContainer(block, this.host),
})
.inline<'focusBlock'>((ctx, next) => {
const { nextBlock } = ctx;
assertExists(nextBlock);

if (
matchFlavours(nextBlock.model, [
'affine:paragraph',
'affine:list',
'affine:code',
])
) {
this._std.command
.chain()
.focusBlockStart({ focusBlock: nextBlock })
.run();
event.preventDefault();
return;
}

return next({
focusBlock: nextBlock,
});
})
.selectBlock(),
])
.run();

return result;
};

private _onBlockDown = (cmd: BlockSuite.CommandChain) => {
return cmd
.getBlockSelections()
.inline<'currentSelectionPath'>((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
assertExists(currentBlockSelections);
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) {
return;
}
return next({ currentSelectionPath: blockSelection.path });
})
.getNextBlock()
.inline<'focusBlock'>((ctx, next) => {
const { nextBlock } = ctx;
assertExists(nextBlock);

if (!ensureBlockInContainer(nextBlock, this.host)) {
return;
}

return next({
focusBlock: nextBlock,
});
})
.selectBlock();
};
private _onArrowUp = (ctx: UIEventStateContext) => {
const event = ctx.get('defaultState').event;

private _onArrowUp = () => {
const [result] = this._std.command
.chain()
.inline((_, 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
.getTextSelection()
.inline<'currentSelectionPath'>((ctx, next) => {
const currentTextSelection = ctx.currentTextSelection;
assertExists(currentTextSelection);
return next({ currentSelectionPath: currentTextSelection.path });
})
.getPrevBlock({
filter: block => ensureBlockInContainer(block, this.host),
})
.inline<'focusBlock'>((ctx, next) => {
const { prevBlock } = ctx;
assertExists(prevBlock);

if (
matchFlavours(prevBlock.model, [
'affine:paragraph',
'affine:list',
'affine:code',
])
)
return;

return next({
focusBlock: prevBlock,
});
})
.selectBlock(),
// block selection - select the previous block
this._onBlockUp(cmd),
// 1. is paragraph, list, code block - focus it
// 2. is not - select it using block selection
cmd
.getBlockSelections()
.inline<'currentSelectionPath'>((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
assertExists(currentBlockSelections);
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) {
return;
}
return next({ currentSelectionPath: blockSelection.path });
})
.getPrevBlock({
filter: block => ensureBlockInContainer(block, this.host),
})
.inline<'focusBlock'>((ctx, next) => {
const { prevBlock } = ctx;
assertExists(prevBlock);

if (
matchFlavours(prevBlock.model, [
'affine:paragraph',
'affine:list',
'affine:code',
])
) {
this._std.command
.chain()
.focusBlockEnd({ focusBlock: prevBlock })
.run();
event.preventDefault();
return;
}

return next({
focusBlock: prevBlock,
});
})
.selectBlock(),
])
.run();

return result;
};

private _onBlockUp = (cmd: BlockSuite.CommandChain) => {
return cmd
.getBlockSelections()
.inline<'currentSelectionPath'>((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
assertExists(currentBlockSelections);
const blockSelection = currentBlockSelections.at(0);
if (!blockSelection) {
return;
}
return next({ currentSelectionPath: blockSelection.path });
})
.getPrevBlock()
.inline((ctx, next) => {
const { prevBlock } = ctx;
assertExists(prevBlock);

if (!ensureBlockInContainer(prevBlock, this.host)) {
return;
}

return next({
focusBlock: prevBlock,
});
})
.selectBlock();
};

private _onShiftArrowDown = () => {
const [result] = this._std.command
.chain()
Expand Down
4 changes: 4 additions & 0 deletions packages/blocks/src/note-block/note-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
captureEventTarget,
getDuplicateBlocks,
} from '../root-block/widgets/drag-handle/utils.js';
import { focusBlockEnd } from './commands/focus-block-end.js';
import { focusBlockStart } from './commands/focus-block-start.js';
import {
registerTextStyleCommands,
selectBlock,
Expand Down Expand Up @@ -131,6 +133,8 @@ export class NoteService extends BlockService<NoteBlockModel> {
this.std.command
.add('selectBlocksBetween', selectBlocksBetween)
.add('selectBlock', selectBlock)
.add('focusBlockStart', focusBlockStart)
.add('focusBlockEnd', focusBlockEnd)
.add('updateBlockType', updateBlockType);

registerTextStyleCommands(this.std);
Expand Down
Loading

0 comments on commit 393e558

Please sign in to comment.