chore(editor): improve selection of doc in canvas (#11314)

Close [BS-2705](https://linear.app/affine-design/issue/BS-2705/[improvement]-通过-viability-control-选择-hide-in-edgeless)

This PR disabled selecting operation of notes that are only shown in page mode.
This commit is contained in:
L-Sun
2025-03-31 12:35:02 +00:00
parent 587fea02b8
commit fec698fd8b
4 changed files with 107 additions and 28 deletions

View File

@@ -10,6 +10,8 @@ import {
type ConnectorElementModel, type ConnectorElementModel,
GroupElementModel, GroupElementModel,
MindmapElementModel, MindmapElementModel,
NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { resetNativeSelection } from '@blocksuite/affine-shared/utils'; import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable'; import { DisposableGroup } from '@blocksuite/global/disposable';
@@ -141,6 +143,12 @@ export class DefaultTool extends BaseTool {
if (el instanceof MindmapElementModel) { if (el instanceof MindmapElementModel) {
return bound.contains(el.elementBound); return bound.contains(el.elementBound);
} }
if (
el instanceof NoteBlockModel &&
el.props.displayMode === NoteDisplayMode.DocOnly
) {
return false;
}
return true; return true;
}); });

View File

@@ -12,7 +12,13 @@ import {
import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store'; import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context'; import { consume } from '@lit/context';
import { effect, signal } from '@preact/signals-core'; import {
batch,
computed,
effect,
type Signal,
signal,
} from '@preact/signals-core';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
import { query } from 'lit/decorators.js'; import { query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
@@ -53,7 +59,22 @@ export class OutlinePanelBody extends SignalWatcher(
private readonly _edgelessOnlyNotes$ = signal<NoteBlockModel[]>([]); private readonly _edgelessOnlyNotes$ = signal<NoteBlockModel[]>([]);
private readonly _selectedNotes$ = signal<NoteBlockModel[]>([]); private readonly _selectedNotes$: Record<
NoteDisplayMode,
Signal<NoteBlockModel[]>
> = {
[NoteDisplayMode.DocOnly]: signal<NoteBlockModel[]>([]),
[NoteDisplayMode.DocAndEdgeless]: signal<NoteBlockModel[]>([]),
[NoteDisplayMode.EdgelessOnly]: signal<NoteBlockModel[]>([]),
};
private readonly _allSelectedNotes$ = computed(() =>
[
NoteDisplayMode.DocAndEdgeless,
NoteDisplayMode.DocOnly,
NoteDisplayMode.EdgelessOnly,
].flatMap(mode => this._selectedNotes$[mode].value)
);
private _clearHighlightMask = () => {}; private _clearHighlightMask = () => {};
@@ -136,7 +157,7 @@ export class OutlinePanelBody extends SignalWatcher(
if (!this.doc.root) return; if (!this.doc.root) return;
const pageVisibleNotes = this._pageVisibleNotes$.peek(); const pageVisibleNotes = this._pageVisibleNotes$.peek();
const selected = this._selectedNotes$.peek(); const selected = this._allSelectedNotes$.peek();
const children = this.doc.root.children.slice(); const children = this.doc.root.children.slice();
const noteIndex = new Map<NoteBlockModel, number>(); const noteIndex = new Map<NoteBlockModel, number>();
@@ -203,23 +224,39 @@ export class OutlinePanelBody extends SignalWatcher(
const note = this.doc.getBlock(id)?.model; const note = this.doc.getBlock(id)?.model;
if (!note || !matchModels(note, [NoteBlockModel])) return; if (!note || !matchModels(note, [NoteBlockModel])) return;
let selectedNotes = this._selectedNotes$.peek(); // map from signal to value
const selectedNotes = Object.fromEntries(
Object.entries(this._selectedNotes$).map(([k, v]) => [k, v.peek()])
) as Record<NoteDisplayMode, NoteBlockModel[]>;
if (!selected) { if (multiselect) {
selectedNotes = selectedNotes.filter(_note => _note !== note); selectedNotes[note.props.displayMode] = selected
} else if (multiselect) { ? [...selectedNotes[note.props.displayMode], note]
selectedNotes = [...selectedNotes, note]; : selectedNotes[note.props.displayMode].filter(_note => _note !== note);
} else { } else {
selectedNotes = [note]; selectedNotes[note.props.displayMode] = selected ? [note] : [];
Object.keys(this._selectedNotes$).forEach(mode => {
if (mode !== note.props.displayMode) {
selectedNotes[mode as NoteDisplayMode] = [];
}
});
} }
// We use gfx.selection and effect to keep sync between canvas and outline panel
if (editorMode === 'edgeless') { if (editorMode === 'edgeless') {
gfx.selection.set({ gfx.selection.set({
elements: selectedNotes.map(({ id }) => id), elements: [...selectedNotes.both, ...selectedNotes.edgeless].map(
({ id }) => id
),
editing: false, editing: false,
}); });
this._selectedNotes$.doc.value = selectedNotes.doc;
} else { } else {
this._selectedNotes$.value = selectedNotes; [NoteDisplayMode.DocOnly, NoteDisplayMode.DocAndEdgeless].forEach(
mode => {
this._selectedNotes$[mode].value = selectedNotes[mode];
}
);
} }
} }
@@ -237,13 +274,16 @@ export class OutlinePanelBody extends SignalWatcher(
return !!model && matchModels(model, [NoteBlockModel]); return !!model && matchModels(model, [NoteBlockModel]);
}); });
const preSelected = this._selectedNotes$.peek(); // update selected notes from edgeless selection
if ( batch(() => {
preSelected.length !== currSelectedNotes.length || [NoteDisplayMode.DocAndEdgeless, NoteDisplayMode.EdgelessOnly].forEach(
preSelected.some(note => !currSelectedNotes.includes(note)) mode => {
) { this._selectedNotes$[mode].value = currSelectedNotes.filter(
this._selectedNotes$.value = currSelectedNotes; note => note.props.displayMode === mode
} );
}
);
});
}); });
} }
@@ -280,11 +320,6 @@ export class OutlinePanelBody extends SignalWatcher(
std.dnd.monitor<NoteCardEntity, NoteDropPayload>({ std.dnd.monitor<NoteCardEntity, NoteDropPayload>({
onDragStart: () => { onDragStart: () => {
this._dragging$.value = true; this._dragging$.value = true;
this._selectedNotes$.value = this._selectedNotes$
.peek()
.filter(note => {
return this._pageVisibleNotes$.value.includes(note);
});
}, },
onDrag: data => { onDrag: data => {
const target = data.location.current.dropTargets[0]; const target = data.location.current.dropTargets[0];
@@ -377,7 +412,7 @@ export class OutlinePanelBody extends SignalWatcher(
index=${index} index=${index}
.note=${note} .note=${note}
.activeHeadingId=${this._activeHeadingId$.value} .activeHeadingId=${this._activeHeadingId$.value}
.status=${this._selectedNotes$.value.includes(note) .status=${this._allSelectedNotes$.value.includes(note)
? this._dragging$.value ? this._dragging$.value
? 'dragging' ? 'dragging'
: 'selected' : 'selected'

View File

@@ -320,9 +320,10 @@ test.describe('TOC and edgeless selection', () => {
expect(await getEdgelessSelectedIds(page)).toHaveLength(0); expect(await getEdgelessSelectedIds(page)).toHaveLength(0);
await cards.nth(1).click(); await cards.nth(1).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(1); expect(
await cards.nth(1).click(); await getEdgelessSelectedIds(page),
expect(await getEdgelessSelectedIds(page)).toHaveLength(0); 'should not select doc only note'
).toHaveLength(0);
await cards.nth(2).click(); await cards.nth(2).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(1); expect(await getEdgelessSelectedIds(page)).toHaveLength(1);
@@ -333,7 +334,7 @@ test.describe('TOC and edgeless selection', () => {
await cards.nth(1).click({ modifiers: ['Shift'] }); await cards.nth(1).click({ modifiers: ['Shift'] });
await cards.nth(2).click({ modifiers: ['Shift'] }); await cards.nth(2).click({ modifiers: ['Shift'] });
expect(await getEdgelessSelectedIds(page)).toHaveLength(3); expect(await getEdgelessSelectedIds(page)).toHaveLength(2);
}); });
test('should select note cards when select note blocks in canvas', async ({ test('should select note cards when select note blocks in canvas', async ({

View File

@@ -7,6 +7,9 @@ import {
assertEdgelessTool, assertEdgelessTool,
changeEdgelessNoteBackground, changeEdgelessNoteBackground,
changeNoteDisplayMode, changeNoteDisplayMode,
dragBetweenViewCoords,
getSelectedBound,
getSelectedBoundCount,
locatorComponentToolbar, locatorComponentToolbar,
locatorEdgelessZoomToolButton, locatorEdgelessZoomToolButton,
selectNoteInEdgeless, selectNoteInEdgeless,
@@ -19,6 +22,7 @@ import {
import { import {
click, click,
clickBlockById, clickBlockById,
clickView,
dragBetweenCoords, dragBetweenCoords,
dragBetweenIndices, dragBetweenIndices,
enterPlaygroundRoom, enterPlaygroundRoom,
@@ -31,6 +35,7 @@ import {
pressBackspace, pressBackspace,
pressEnter, pressEnter,
pressTab, pressTab,
selectAllByKeyboard,
type, type,
undoByKeyboard, undoByKeyboard,
waitForInlineEditorStateUpdated, waitForInlineEditorStateUpdated,
@@ -541,3 +546,33 @@ test('select text cross blocks in edgeless note', async ({ page }) => {
await pressBackspace(page); await pressBackspace(page);
await assertRichTexts(page, ['ac']); await assertRichTexts(page, ['ac']);
}); });
test('should not select doc only note', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await clickView(page, [0, 0]);
await selectAllByKeyboard(page);
const noteBound = await getSelectedBound(page);
await triggerComponentToolbarAction(page, 'changeNoteDisplayMode');
await waitNextFrame(page);
await changeNoteDisplayMode(page, NoteDisplayMode.DocOnly);
await selectAllByKeyboard(page);
expect(await getSelectedBoundCount(page)).toBe(0);
await clickView(page, [
0.5 * (noteBound[0] + noteBound[2]),
0.5 * (noteBound[1] + noteBound[3]),
]);
expect(await getSelectedBoundCount(page)).toBe(0);
await dragBetweenViewCoords(
page,
[noteBound[0] - 10, noteBound[1] - 10],
[noteBound[0] + noteBound[2] + 10, noteBound[1] + noteBound[3] + 10]
);
expect(await getSelectedBoundCount(page)).toBe(0);
});