mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
refactor(editor): highlight selected cards of TOC based on signal (#9807)
Close [BS-2314](https://linear.app/affine-design/issue/BS-2314/添加打开toc时,将note-block-高亮), [BS-1868](https://linear.app/affine-design/issue/BS-1868/toc-里面-note之间顺序可拖动性,在page和edgeless里面是不同的,这个是设计的行为么?) This PR refactor the highlight logic of note cards of TOC panel: - notes block selected in edgeless note - notes block covered by text or block selection in page mode - note cards selected in TOC for dragging Other changes: - remove not used codes - add tests for highlight note cards
This commit is contained in:
@@ -245,10 +245,12 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
}
|
||||
}
|
||||
|
||||
private _isFirstNote() {
|
||||
private _isFirstVisibleNote() {
|
||||
return (
|
||||
this.model.parent?.children.find(child =>
|
||||
matchFlavours(child, ['affine:note'])
|
||||
this.model.parent?.children.find(
|
||||
child =>
|
||||
matchFlavours(child, ['affine:note']) &&
|
||||
child.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
) === this.model
|
||||
);
|
||||
}
|
||||
@@ -509,7 +511,8 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
.editing=${this._editing}
|
||||
></edgeless-note-mask>
|
||||
|
||||
${isCollapsable && (!this._isFirstNote() || !this._enablePageHeader)
|
||||
${isCollapsable &&
|
||||
(!this._isFirstVisibleNote() || !this._enablePageHeader)
|
||||
? html`<div
|
||||
class="${classMap({
|
||||
'edgeless-note-collapse-button': true,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { SurfaceSelection } from '@blocksuite/block-std';
|
||||
import type {
|
||||
EdgelessRootBlockComponent,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/blocks';
|
||||
import {
|
||||
BlocksUtils,
|
||||
matchFlavours,
|
||||
NoteDisplayMode,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/blocks';
|
||||
@@ -122,8 +124,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
|
||||
private readonly _activeHeadingId$ = signal<string | null>(null);
|
||||
|
||||
private _changedFlag = false;
|
||||
|
||||
private _clearHighlightMask = () => {};
|
||||
|
||||
private _docDisposables: DisposableGroup | null = null;
|
||||
@@ -132,14 +132,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
|
||||
private _lockActiveHeadingId = false;
|
||||
|
||||
private _oldViewport?: {
|
||||
zoom: number;
|
||||
center: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
get viewportPadding(): [number, number, number, number] {
|
||||
return this.fitPadding
|
||||
? ([0, 0, 0, 0].map((val, idx) =>
|
||||
@@ -153,28 +145,8 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
this._docDisposables = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Click at blank area to clear selection
|
||||
*/
|
||||
private _clickHandler(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
// check if click at outline-card, if so, do nothing
|
||||
if (
|
||||
(e.target as HTMLElement).closest('outline-note-card') ||
|
||||
this._selected.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selected = [];
|
||||
this.edgeless?.service.selection.set({
|
||||
elements: this._selected,
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _deSelectNoteInEdgelessMode(note: NoteBlockModel) {
|
||||
if (!this._isEdgelessMode() || !this.edgeless) return;
|
||||
if (!this.edgeless) return;
|
||||
|
||||
const { selection } = this.edgeless.service;
|
||||
if (!selection.has(note.id)) return;
|
||||
@@ -188,7 +160,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
/*
|
||||
* Double click at blank area to disable notes sorting option
|
||||
*/
|
||||
private _doubleClickHandler(e: MouseEvent) {
|
||||
private readonly _doubleClickHandler = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// check if click at outline-card, if so, do nothing
|
||||
if (
|
||||
@@ -199,16 +171,30 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
}
|
||||
|
||||
this.toggleNotesSorting();
|
||||
}
|
||||
};
|
||||
|
||||
private _drag() {
|
||||
const selectedVisibleNotes = this._selectedNotes$.peek().filter(id => {
|
||||
const model = this.doc.getBlock(id)?.model;
|
||||
return (
|
||||
model &&
|
||||
matchFlavours(model, ['affine:note']) &&
|
||||
model.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
!this._selected.length ||
|
||||
selectedVisibleNotes.length === 0 ||
|
||||
!this._pageVisibleNotes.length ||
|
||||
!this.doc.root
|
||||
)
|
||||
return;
|
||||
|
||||
this.edgeless?.service.selection.set({
|
||||
elements: selectedVisibleNotes,
|
||||
editing: false,
|
||||
});
|
||||
|
||||
this._dragging = true;
|
||||
|
||||
// cache the notes in case it is changed by other peers
|
||||
@@ -221,7 +207,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
});
|
||||
return map;
|
||||
}, new Map<string, OutlineNoteItem>());
|
||||
const selected = this._selected.slice();
|
||||
|
||||
startDragging({
|
||||
container: this,
|
||||
@@ -235,7 +220,13 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
|
||||
if (insertIdx === undefined) return;
|
||||
|
||||
this._moveNotes(insertIdx, selected, notesMap, notes, children);
|
||||
this._moveNotes(
|
||||
insertIdx,
|
||||
selectedVisibleNotes,
|
||||
notesMap,
|
||||
notes,
|
||||
children
|
||||
);
|
||||
},
|
||||
onDragMove: (idx, indicatorTranslateY) => {
|
||||
this.insertIndex = idx;
|
||||
@@ -306,10 +297,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
}
|
||||
}
|
||||
|
||||
private _isEdgelessMode() {
|
||||
return this.editor.mode === 'edgeless';
|
||||
}
|
||||
|
||||
private _moveNotes(
|
||||
index: number,
|
||||
selected: string[],
|
||||
@@ -334,14 +321,13 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
.filter(block => !draggingBlocks.has(block));
|
||||
const newChildren = [...leftPart, ...blocks, ...rightPart];
|
||||
|
||||
this._changedFlag = true;
|
||||
this.doc.updateBlock(this.doc.root, {
|
||||
children: newChildren,
|
||||
});
|
||||
}
|
||||
|
||||
private _PanelList(withEdgelessOnlyNotes: boolean) {
|
||||
const selectedNotesSet = new Set(this._selected);
|
||||
const selectedNotesSet = new Set(this._selectedNotes$.value);
|
||||
const theme = this.editor.std.get(ThemeProvider).theme;
|
||||
|
||||
return html`<div class="panel-list">
|
||||
@@ -354,17 +340,17 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
${this._pageVisibleNotes.length
|
||||
? repeat(
|
||||
this._pageVisibleNotes,
|
||||
note => note.note.id,
|
||||
(note, idx) => html`
|
||||
item => item.note.id,
|
||||
(item, idx) => html`
|
||||
<affine-outline-note-card
|
||||
data-note-id=${note.note.id}
|
||||
.note=${note.note}
|
||||
data-note-id=${item.note.id}
|
||||
.note=${item.note}
|
||||
.theme=${theme}
|
||||
.number=${idx + 1}
|
||||
.index=${note.index}
|
||||
.index=${item.index}
|
||||
.doc=${this.doc}
|
||||
.activeHeadingId=${this._activeHeadingId$.value}
|
||||
.status=${selectedNotesSet.has(note.note.id)
|
||||
.status=${selectedNotesSet.has(item.note.id)
|
||||
? this._dragging
|
||||
? 'placeholder'
|
||||
: 'selected'
|
||||
@@ -387,19 +373,23 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
${repeat(
|
||||
this._edgelessOnlyNotes,
|
||||
note => note.note.id,
|
||||
(note, idx) =>
|
||||
(item, idx) =>
|
||||
html`<affine-outline-note-card
|
||||
data-note-id=${note.note.id}
|
||||
.note=${note.note}
|
||||
data-note-id=${item.note.id}
|
||||
.note=${item.note}
|
||||
.theme=${theme}
|
||||
.number=${idx + 1}
|
||||
.index=${note.index}
|
||||
.index=${item.index}
|
||||
.doc=${this.doc}
|
||||
.activeHeadingId=${this._activeHeadingId$.value}
|
||||
.invisible=${true}
|
||||
.showPreviewIcon=${this.showPreviewIcon}
|
||||
.enableNotesSorting=${this.enableNotesSorting}
|
||||
.status=${selectedNotesSet.has(item.note.id)
|
||||
? 'selected'
|
||||
: undefined}
|
||||
@fitview=${this._fitToElement}
|
||||
@select=${this._selectNote}
|
||||
@displaymodechange=${this._handleDisplayModeChange}
|
||||
></affine-outline-note-card>`
|
||||
)} `
|
||||
@@ -447,31 +437,23 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
this._lockActiveHeadingId = false;
|
||||
}
|
||||
|
||||
private readonly _selectedNotes$ = signal<string[]>([]);
|
||||
|
||||
private _selectNote(e: SelectEvent) {
|
||||
const { selected, id, multiselect } = e.detail;
|
||||
|
||||
let selectedNotes = this._selectedNotes$.peek();
|
||||
|
||||
if (!selected) {
|
||||
this._selected = this._selected.filter(noteId => noteId !== id);
|
||||
selectedNotes = selectedNotes.filter(noteId => noteId !== id);
|
||||
} else if (multiselect) {
|
||||
this._selected = [...this._selected, id];
|
||||
selectedNotes = [...selectedNotes, id];
|
||||
} else {
|
||||
this._selected = [id];
|
||||
selectedNotes = [id];
|
||||
}
|
||||
|
||||
// When edgeless mode, should select notes which display in both mode
|
||||
const selectedIds = this._pageVisibleNotes.reduce((ids, item) => {
|
||||
const note = item.note;
|
||||
if (
|
||||
this._selected.includes(note.id) &&
|
||||
(!note.displayMode ||
|
||||
note.displayMode === NoteDisplayMode.DocAndEdgeless)
|
||||
) {
|
||||
ids.push(note.id);
|
||||
}
|
||||
return ids;
|
||||
}, [] as string[]);
|
||||
this.edgeless?.service.selection.set({
|
||||
elements: selectedIds,
|
||||
elements: selectedNotes,
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
@@ -537,25 +519,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
return;
|
||||
}
|
||||
|
||||
const oldSelectedSet = this._selected.reduce((pre, id) => {
|
||||
pre.add(id);
|
||||
return pre;
|
||||
}, new Set<string>());
|
||||
const newSelected: string[] = [];
|
||||
|
||||
rootModel.children.forEach(block => {
|
||||
if (!BlocksUtils.matchFlavours(block, ['affine:note'])) return;
|
||||
|
||||
const blockModel = block as NoteBlockModel;
|
||||
|
||||
if (
|
||||
blockModel.displayMode !== NoteDisplayMode.EdgelessOnly &&
|
||||
oldSelectedSet.has(block.id)
|
||||
) {
|
||||
newSelected.push(block.id);
|
||||
}
|
||||
});
|
||||
|
||||
this._pageVisibleNotes = getNotesFromDoc(this.doc, [
|
||||
NoteDisplayMode.DocAndEdgeless,
|
||||
NoteDisplayMode.DocOnly,
|
||||
@@ -563,7 +526,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
this._edgelessOnlyNotes = getNotesFromDoc(this.doc, [
|
||||
NoteDisplayMode.EdgelessOnly,
|
||||
]);
|
||||
this._selected = newSelected;
|
||||
}
|
||||
|
||||
private _updateNoticeVisibility() {
|
||||
@@ -583,24 +545,38 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
}
|
||||
}
|
||||
|
||||
private _zoomToFit() {
|
||||
const edgeless = this.edgeless;
|
||||
private _watchSelectedNotes() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const { std, doc, mode } = this.editor;
|
||||
|
||||
if (!edgeless) return;
|
||||
const currSelectedNotes =
|
||||
mode === 'edgeless'
|
||||
? std.selection
|
||||
.filter(SurfaceSelection)
|
||||
.filter(({ blockId }) => {
|
||||
const model = doc.getBlock(blockId)?.model;
|
||||
return !!model && matchFlavours(model, ['affine:note']);
|
||||
})
|
||||
.map(({ blockId }) => blockId)
|
||||
: (std.command.exec('getSelectedModels').selectedModels ?? [])
|
||||
.map(model => {
|
||||
let parent = model.parent;
|
||||
while (parent && !matchFlavours(parent, ['affine:note'])) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent ? [parent.id] : [];
|
||||
})
|
||||
.flat();
|
||||
|
||||
const bound = edgeless.gfx.elementsBound;
|
||||
|
||||
this._oldViewport = {
|
||||
zoom: edgeless.service.viewport.zoom,
|
||||
center: {
|
||||
x: edgeless.service.viewport.center.x,
|
||||
y: edgeless.service.viewport.center.y,
|
||||
},
|
||||
};
|
||||
edgeless.service.viewport.setViewportByBound(
|
||||
new Bound(bound.x, bound.y, bound.w, bound.h),
|
||||
this.viewportPadding,
|
||||
true
|
||||
const preSelected = this._selectedNotes$.peek();
|
||||
if (
|
||||
preSelected.length !== currSelectedNotes.length ||
|
||||
preSelected.some(id => !currSelectedNotes.includes(id))
|
||||
) {
|
||||
this._selectedNotes$.value = currSelectedNotes;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -615,29 +591,16 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
}
|
||||
)
|
||||
);
|
||||
this._watchSelectedNotes();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (!this._changedFlag && this._oldViewport) {
|
||||
const edgeless = this.edgeless;
|
||||
|
||||
if (!edgeless) return;
|
||||
|
||||
edgeless.service.viewport.setViewport(
|
||||
this._oldViewport.zoom,
|
||||
[this._oldViewport.center.x, this._oldViewport.center.y],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
this._clearDocDisposables();
|
||||
this._clearHighlightMask();
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
this.disposables.addFromEvent(this, 'click', this._clickHandler);
|
||||
this.disposables.addFromEvent(this, 'dblclick', this._doubleClickHandler);
|
||||
}
|
||||
|
||||
@@ -667,15 +630,8 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
this._setDocDisposables();
|
||||
}
|
||||
|
||||
if (
|
||||
_changedProperties.has('mode') &&
|
||||
this.edgeless &&
|
||||
this._isEdgelessMode()
|
||||
) {
|
||||
if (_changedProperties.has('edgeless')) {
|
||||
this._clearHighlightMask();
|
||||
if (_changedProperties.get('mode') === undefined) return;
|
||||
|
||||
requestAnimationFrame(() => this._zoomToFit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,12 +644,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
@state()
|
||||
private accessor _pageVisibleNotes: OutlineNoteItem[] = [];
|
||||
|
||||
/**
|
||||
* store the id of selected notes
|
||||
*/
|
||||
@state()
|
||||
private accessor _selected: string[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
|
||||
@@ -707,7 +657,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
accessor editor!: AffineEditorContainer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor enableNotesSorting!: boolean;
|
||||
accessor enableNotesSorting: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor fitPadding!: number[];
|
||||
|
||||
@@ -143,11 +143,13 @@ const styles = css`
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.card-preview.edgeless .card-content:hover {
|
||||
.card-preview .card-content:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-preview.edgeless .card-header-container:hover {
|
||||
.card-container[data-invisible='false']
|
||||
.card-preview
|
||||
.card-header-container:hover {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
@@ -156,11 +158,11 @@ const styles = css`
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.card-container.selected .card-preview.edgeless {
|
||||
.card-container.selected .card-preview {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.card-container.placeholder .card-preview.edgeless {
|
||||
.card-container.placeholder .card-preview {
|
||||
background: var(--affine-hover-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
@@ -178,7 +180,7 @@ const styles = css`
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-preview.page outline-block-preview:hover {
|
||||
.card-preview outline-block-preview:hover {
|
||||
color: var(--affine-brand-color);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, unsafeCSS } from 'lit';
|
||||
import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
||||
@@ -112,7 +113,25 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadSettingsFromLocalStorage();
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
if (this.editor.mode === 'edgeless') {
|
||||
this._enableNotesSorting = true;
|
||||
} else {
|
||||
this._loadSettingsFromLocalStorage();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (_changedProperties.has('editor')) {
|
||||
if (this.editor.mode === 'edgeless') {
|
||||
this._enableNotesSorting = true;
|
||||
} else {
|
||||
this._loadSettingsFromLocalStorage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -44,20 +44,14 @@ test.describe('toc-panel', () => {
|
||||
return panel.locator(`affine-outline-panel-body .title`);
|
||||
}
|
||||
|
||||
async function toggleNoteSorting(page: Page) {
|
||||
const enableSortingButton = page.locator(
|
||||
'.outline-panel-header-container .note-sorting-button'
|
||||
);
|
||||
await enableSortingButton.click();
|
||||
}
|
||||
|
||||
async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) {
|
||||
const fromRect = await fromCard.boundingBox();
|
||||
const toRect = await toCard.boundingBox();
|
||||
|
||||
await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10);
|
||||
await page.mouse.click(fromRect!.x + 10, fromRect!.y + 10);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 10 });
|
||||
await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 20 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
@@ -267,7 +261,6 @@ test.describe('toc-panel', () => {
|
||||
await page.mouse.click(100, 100);
|
||||
|
||||
await toggleTocPanel(page);
|
||||
await toggleNoteSorting(page);
|
||||
const docVisibleCard = page.locator(
|
||||
'.card-container[data-invisible="false"]'
|
||||
);
|
||||
@@ -311,7 +304,6 @@ test.describe('toc-panel', () => {
|
||||
);
|
||||
|
||||
await toggleTocPanel(page);
|
||||
await toggleNoteSorting(page);
|
||||
const docVisibleCard = page.locator(
|
||||
'.card-container[data-invisible="false"]'
|
||||
);
|
||||
@@ -345,7 +337,6 @@ test.describe('toc-panel', () => {
|
||||
);
|
||||
|
||||
await toggleTocPanel(page);
|
||||
await toggleNoteSorting(page);
|
||||
const docVisibleCard = page.locator(
|
||||
'.card-container[data-invisible="false"]'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user