feat(editor): improve select perf (#15353)

maybe fix #12675


#### PR Dependency Tree


* **PR #15353** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved block selection updates so selected states refresh reliably.
  - Corrected selected-block ordering and duplicate handling.
- Improved toolbar positioning accuracy and reduced unnecessary layout
recalculations.
  - Adjusted toolbar animation behavior for surface-based tools.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-28 11:38:00 +08:00
committed by GitHub
parent cfc7bbb90f
commit e7ec8a1032
4 changed files with 39 additions and 31 deletions
@@ -1,4 +1,5 @@
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { watch } from '@blocksuite/global/lit';
import { BlockComponent, type BlockService } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
import { html, nothing } from 'lit';
@@ -59,7 +60,7 @@ export class CaptionedBlockComponent<
: nothing}
${this.selectedStyle === SelectedStyle.Background
? html`<affine-block-selection
.selected=${this.selected$.value}
.selected=${watch(this.selected$)}
></affine-block-selection>`
: null}
${this.useZeroWidth && !this.store.readonly
@@ -5,7 +5,7 @@ import type {
TextSelection,
} from '@blocksuite/std';
import { BlockComponent } from '@blocksuite/std';
import type { RoleType } from '@blocksuite/store';
import type { BlockModel, RoleType } from '@blocksuite/store';
import type { ImageSelection } from '../../selection/index.js';
@@ -129,35 +129,33 @@ export const getSelectedBlocksCommand: Command<
dirtyResult = dirtyResult.filter(ctx.filter);
}
const getModelPath = (el: BlockComponent) => {
const path: number[] = [];
let model = el.model;
while (model) {
const parent = ctx.std.store.getParent(model.id);
if (!parent) break;
path.unshift(parent.children.findIndex(child => child.id === model.id));
model = parent;
}
return path;
};
const compareByModelPath = (a: BlockComponent, b: BlockComponent) => {
if (a === b) return 0;
const aPath = getModelPath(a);
const bPath = getModelPath(b);
const length = Math.min(aPath.length, bPath.length);
for (let i = 0; i < length; i++) {
const diff = aPath[i] - bPath[i];
if (diff !== 0) return diff;
}
return aPath.length - bPath.length;
};
const seen = new Set<BlockComponent>();
// remove duplicate elements
const result: BlockComponent[] = dirtyResult
.filter((el, index) => dirtyResult.indexOf(el) === index)
const result: BlockComponent[] = dirtyResult.filter(el => {
if (seen.has(el)) return false;
seen.add(el);
return true;
});
if (result.length > 1) {
const modelOrder = new Map<string, number>();
const visit = (model: BlockModel) => {
modelOrder.set(model.id, modelOrder.size);
model.children.forEach(visit);
};
const root = ctx.std.store.root;
if (root) {
visit(root);
}
// sort by model tree position, which is the order used for paste/export
.sort(compareByModelPath);
result.sort(
(a, b) =>
(modelOrder.get(a.blockId) ?? Number.MAX_SAFE_INTEGER) -
(modelOrder.get(b.blockId) ?? Number.MAX_SAFE_INTEGER)
);
}
if (result.length === 0) return;
@@ -146,7 +146,16 @@ export class AffineToolbarWidget extends WidgetComponent {
}
setReferenceElementWithBlocks(blocks: BlockComponent[]) {
const getClientRects = () => blocks.map(e => e.getBoundingClientRect());
let cachedClientRects: DOMRect[] | null = null;
const getClientRects = () => {
if (!cachedClientRects) {
cachedClientRects = blocks.map(e => e.getBoundingClientRect());
requestAnimationFrame(() => {
cachedClientRects = null;
});
}
return cachedClientRects;
};
this.referenceElement$.value = blocks.length
? () => ({
@@ -56,7 +56,7 @@ export function autoUpdatePosition(
flavour: string,
placement: ToolbarPlacement,
sideOptions: Partial<SideObject> | null,
options: AutoUpdateOptions = { elementResize: false, animationFrame: true }
options: AutoUpdateOptions = { elementResize: false }
) {
const isInline = flavour === 'affine:note';
const hasSurfaceScope = flavour.includes('surface');
@@ -147,7 +147,7 @@ export function autoUpdatePosition(
() => {
update().catch(console.error);
},
options
{ animationFrame: hasSurfaceScope, ...options }
);
}