mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +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 (
|
return (
|
||||||
this.model.parent?.children.find(child =>
|
this.model.parent?.children.find(
|
||||||
matchFlavours(child, ['affine:note'])
|
child =>
|
||||||
|
matchFlavours(child, ['affine:note']) &&
|
||||||
|
child.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||||
) === this.model
|
) === this.model
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -509,7 +511,8 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
|||||||
.editing=${this._editing}
|
.editing=${this._editing}
|
||||||
></edgeless-note-mask>
|
></edgeless-note-mask>
|
||||||
|
|
||||||
${isCollapsable && (!this._isFirstNote() || !this._enablePageHeader)
|
${isCollapsable &&
|
||||||
|
(!this._isFirstVisibleNote() || !this._enablePageHeader)
|
||||||
? html`<div
|
? html`<div
|
||||||
class="${classMap({
|
class="${classMap({
|
||||||
'edgeless-note-collapse-button': true,
|
'edgeless-note-collapse-button': true,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { SurfaceSelection } from '@blocksuite/block-std';
|
||||||
import type {
|
import type {
|
||||||
EdgelessRootBlockComponent,
|
EdgelessRootBlockComponent,
|
||||||
NoteBlockModel,
|
NoteBlockModel,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
import {
|
import {
|
||||||
BlocksUtils,
|
BlocksUtils,
|
||||||
|
matchFlavours,
|
||||||
NoteDisplayMode,
|
NoteDisplayMode,
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
@@ -122,8 +124,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _activeHeadingId$ = signal<string | null>(null);
|
private readonly _activeHeadingId$ = signal<string | null>(null);
|
||||||
|
|
||||||
private _changedFlag = false;
|
|
||||||
|
|
||||||
private _clearHighlightMask = () => {};
|
private _clearHighlightMask = () => {};
|
||||||
|
|
||||||
private _docDisposables: DisposableGroup | null = null;
|
private _docDisposables: DisposableGroup | null = null;
|
||||||
@@ -132,14 +132,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
|
|
||||||
private _lockActiveHeadingId = false;
|
private _lockActiveHeadingId = false;
|
||||||
|
|
||||||
private _oldViewport?: {
|
|
||||||
zoom: number;
|
|
||||||
center: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
get viewportPadding(): [number, number, number, number] {
|
get viewportPadding(): [number, number, number, number] {
|
||||||
return this.fitPadding
|
return this.fitPadding
|
||||||
? ([0, 0, 0, 0].map((val, idx) =>
|
? ([0, 0, 0, 0].map((val, idx) =>
|
||||||
@@ -153,28 +145,8 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
this._docDisposables = null;
|
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) {
|
private _deSelectNoteInEdgelessMode(note: NoteBlockModel) {
|
||||||
if (!this._isEdgelessMode() || !this.edgeless) return;
|
if (!this.edgeless) return;
|
||||||
|
|
||||||
const { selection } = this.edgeless.service;
|
const { selection } = this.edgeless.service;
|
||||||
if (!selection.has(note.id)) return;
|
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
|
* Double click at blank area to disable notes sorting option
|
||||||
*/
|
*/
|
||||||
private _doubleClickHandler(e: MouseEvent) {
|
private readonly _doubleClickHandler = (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// check if click at outline-card, if so, do nothing
|
// check if click at outline-card, if so, do nothing
|
||||||
if (
|
if (
|
||||||
@@ -199,16 +171,30 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.toggleNotesSorting();
|
this.toggleNotesSorting();
|
||||||
}
|
};
|
||||||
|
|
||||||
private _drag() {
|
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 (
|
if (
|
||||||
!this._selected.length ||
|
selectedVisibleNotes.length === 0 ||
|
||||||
!this._pageVisibleNotes.length ||
|
!this._pageVisibleNotes.length ||
|
||||||
!this.doc.root
|
!this.doc.root
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.edgeless?.service.selection.set({
|
||||||
|
elements: selectedVisibleNotes,
|
||||||
|
editing: false,
|
||||||
|
});
|
||||||
|
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
|
|
||||||
// cache the notes in case it is changed by other peers
|
// cache the notes in case it is changed by other peers
|
||||||
@@ -221,7 +207,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, new Map<string, OutlineNoteItem>());
|
}, new Map<string, OutlineNoteItem>());
|
||||||
const selected = this._selected.slice();
|
|
||||||
|
|
||||||
startDragging({
|
startDragging({
|
||||||
container: this,
|
container: this,
|
||||||
@@ -235,7 +220,13 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
|
|
||||||
if (insertIdx === undefined) return;
|
if (insertIdx === undefined) return;
|
||||||
|
|
||||||
this._moveNotes(insertIdx, selected, notesMap, notes, children);
|
this._moveNotes(
|
||||||
|
insertIdx,
|
||||||
|
selectedVisibleNotes,
|
||||||
|
notesMap,
|
||||||
|
notes,
|
||||||
|
children
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onDragMove: (idx, indicatorTranslateY) => {
|
onDragMove: (idx, indicatorTranslateY) => {
|
||||||
this.insertIndex = idx;
|
this.insertIndex = idx;
|
||||||
@@ -306,10 +297,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isEdgelessMode() {
|
|
||||||
return this.editor.mode === 'edgeless';
|
|
||||||
}
|
|
||||||
|
|
||||||
private _moveNotes(
|
private _moveNotes(
|
||||||
index: number,
|
index: number,
|
||||||
selected: string[],
|
selected: string[],
|
||||||
@@ -334,14 +321,13 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
.filter(block => !draggingBlocks.has(block));
|
.filter(block => !draggingBlocks.has(block));
|
||||||
const newChildren = [...leftPart, ...blocks, ...rightPart];
|
const newChildren = [...leftPart, ...blocks, ...rightPart];
|
||||||
|
|
||||||
this._changedFlag = true;
|
|
||||||
this.doc.updateBlock(this.doc.root, {
|
this.doc.updateBlock(this.doc.root, {
|
||||||
children: newChildren,
|
children: newChildren,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _PanelList(withEdgelessOnlyNotes: boolean) {
|
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;
|
const theme = this.editor.std.get(ThemeProvider).theme;
|
||||||
|
|
||||||
return html`<div class="panel-list">
|
return html`<div class="panel-list">
|
||||||
@@ -354,17 +340,17 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
${this._pageVisibleNotes.length
|
${this._pageVisibleNotes.length
|
||||||
? repeat(
|
? repeat(
|
||||||
this._pageVisibleNotes,
|
this._pageVisibleNotes,
|
||||||
note => note.note.id,
|
item => item.note.id,
|
||||||
(note, idx) => html`
|
(item, idx) => html`
|
||||||
<affine-outline-note-card
|
<affine-outline-note-card
|
||||||
data-note-id=${note.note.id}
|
data-note-id=${item.note.id}
|
||||||
.note=${note.note}
|
.note=${item.note}
|
||||||
.theme=${theme}
|
.theme=${theme}
|
||||||
.number=${idx + 1}
|
.number=${idx + 1}
|
||||||
.index=${note.index}
|
.index=${item.index}
|
||||||
.doc=${this.doc}
|
.doc=${this.doc}
|
||||||
.activeHeadingId=${this._activeHeadingId$.value}
|
.activeHeadingId=${this._activeHeadingId$.value}
|
||||||
.status=${selectedNotesSet.has(note.note.id)
|
.status=${selectedNotesSet.has(item.note.id)
|
||||||
? this._dragging
|
? this._dragging
|
||||||
? 'placeholder'
|
? 'placeholder'
|
||||||
: 'selected'
|
: 'selected'
|
||||||
@@ -387,19 +373,23 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
${repeat(
|
${repeat(
|
||||||
this._edgelessOnlyNotes,
|
this._edgelessOnlyNotes,
|
||||||
note => note.note.id,
|
note => note.note.id,
|
||||||
(note, idx) =>
|
(item, idx) =>
|
||||||
html`<affine-outline-note-card
|
html`<affine-outline-note-card
|
||||||
data-note-id=${note.note.id}
|
data-note-id=${item.note.id}
|
||||||
.note=${note.note}
|
.note=${item.note}
|
||||||
.theme=${theme}
|
.theme=${theme}
|
||||||
.number=${idx + 1}
|
.number=${idx + 1}
|
||||||
.index=${note.index}
|
.index=${item.index}
|
||||||
.doc=${this.doc}
|
.doc=${this.doc}
|
||||||
.activeHeadingId=${this._activeHeadingId$.value}
|
.activeHeadingId=${this._activeHeadingId$.value}
|
||||||
.invisible=${true}
|
.invisible=${true}
|
||||||
.showPreviewIcon=${this.showPreviewIcon}
|
.showPreviewIcon=${this.showPreviewIcon}
|
||||||
.enableNotesSorting=${this.enableNotesSorting}
|
.enableNotesSorting=${this.enableNotesSorting}
|
||||||
|
.status=${selectedNotesSet.has(item.note.id)
|
||||||
|
? 'selected'
|
||||||
|
: undefined}
|
||||||
@fitview=${this._fitToElement}
|
@fitview=${this._fitToElement}
|
||||||
|
@select=${this._selectNote}
|
||||||
@displaymodechange=${this._handleDisplayModeChange}
|
@displaymodechange=${this._handleDisplayModeChange}
|
||||||
></affine-outline-note-card>`
|
></affine-outline-note-card>`
|
||||||
)} `
|
)} `
|
||||||
@@ -447,31 +437,23 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
this._lockActiveHeadingId = false;
|
this._lockActiveHeadingId = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly _selectedNotes$ = signal<string[]>([]);
|
||||||
|
|
||||||
private _selectNote(e: SelectEvent) {
|
private _selectNote(e: SelectEvent) {
|
||||||
const { selected, id, multiselect } = e.detail;
|
const { selected, id, multiselect } = e.detail;
|
||||||
|
|
||||||
|
let selectedNotes = this._selectedNotes$.peek();
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
this._selected = this._selected.filter(noteId => noteId !== id);
|
selectedNotes = selectedNotes.filter(noteId => noteId !== id);
|
||||||
} else if (multiselect) {
|
} else if (multiselect) {
|
||||||
this._selected = [...this._selected, id];
|
selectedNotes = [...selectedNotes, id];
|
||||||
} else {
|
} 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({
|
this.edgeless?.service.selection.set({
|
||||||
elements: selectedIds,
|
elements: selectedNotes,
|
||||||
editing: false,
|
editing: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -537,25 +519,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
return;
|
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, [
|
this._pageVisibleNotes = getNotesFromDoc(this.doc, [
|
||||||
NoteDisplayMode.DocAndEdgeless,
|
NoteDisplayMode.DocAndEdgeless,
|
||||||
NoteDisplayMode.DocOnly,
|
NoteDisplayMode.DocOnly,
|
||||||
@@ -563,7 +526,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
this._edgelessOnlyNotes = getNotesFromDoc(this.doc, [
|
this._edgelessOnlyNotes = getNotesFromDoc(this.doc, [
|
||||||
NoteDisplayMode.EdgelessOnly,
|
NoteDisplayMode.EdgelessOnly,
|
||||||
]);
|
]);
|
||||||
this._selected = newSelected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateNoticeVisibility() {
|
private _updateNoticeVisibility() {
|
||||||
@@ -583,24 +545,38 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _zoomToFit() {
|
private _watchSelectedNotes() {
|
||||||
const edgeless = this.edgeless;
|
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;
|
const preSelected = this._selectedNotes$.peek();
|
||||||
|
if (
|
||||||
this._oldViewport = {
|
preSelected.length !== currSelectedNotes.length ||
|
||||||
zoom: edgeless.service.viewport.zoom,
|
preSelected.some(id => !currSelectedNotes.includes(id))
|
||||||
center: {
|
) {
|
||||||
x: edgeless.service.viewport.center.x,
|
this._selectedNotes$.value = currSelectedNotes;
|
||||||
y: edgeless.service.viewport.center.y,
|
}
|
||||||
},
|
})
|
||||||
};
|
|
||||||
edgeless.service.viewport.setViewportByBound(
|
|
||||||
new Bound(bound.x, bound.y, bound.w, bound.h),
|
|
||||||
this.viewportPadding,
|
|
||||||
true
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,29 +591,16 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
this._watchSelectedNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback(): void {
|
override disconnectedCallback(): void {
|
||||||
super.disconnectedCallback();
|
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._clearDocDisposables();
|
||||||
this._clearHighlightMask();
|
this._clearHighlightMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
override firstUpdated(): void {
|
override firstUpdated(): void {
|
||||||
this.disposables.addFromEvent(this, 'click', this._clickHandler);
|
|
||||||
this.disposables.addFromEvent(this, 'dblclick', this._doubleClickHandler);
|
this.disposables.addFromEvent(this, 'dblclick', this._doubleClickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,15 +630,8 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
this._setDocDisposables();
|
this._setDocDisposables();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (_changedProperties.has('edgeless')) {
|
||||||
_changedProperties.has('mode') &&
|
|
||||||
this.edgeless &&
|
|
||||||
this._isEdgelessMode()
|
|
||||||
) {
|
|
||||||
this._clearHighlightMask();
|
this._clearHighlightMask();
|
||||||
if (_changedProperties.get('mode') === undefined) return;
|
|
||||||
|
|
||||||
requestAnimationFrame(() => this._zoomToFit());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,12 +644,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
@state()
|
@state()
|
||||||
private accessor _pageVisibleNotes: OutlineNoteItem[] = [];
|
private accessor _pageVisibleNotes: OutlineNoteItem[] = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* store the id of selected notes
|
|
||||||
*/
|
|
||||||
@state()
|
|
||||||
private accessor _selected: string[] = [];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor doc!: Store;
|
accessor doc!: Store;
|
||||||
|
|
||||||
@@ -707,7 +657,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
accessor editor!: AffineEditorContainer;
|
accessor editor!: AffineEditorContainer;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor enableNotesSorting!: boolean;
|
accessor enableNotesSorting: boolean = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor fitPadding!: number[];
|
accessor fitPadding!: number[];
|
||||||
|
|||||||
@@ -143,11 +143,13 @@ const styles = css`
|
|||||||
color: var(--affine-text-primary-color);
|
color: var(--affine-text-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-preview.edgeless .card-content:hover {
|
.card-preview .card-content:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-preview.edgeless .card-header-container:hover {
|
.card-container[data-invisible='false']
|
||||||
|
.card-preview
|
||||||
|
.card-header-container:hover {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +158,11 @@ const styles = css`
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-container.selected .card-preview.edgeless {
|
.card-container.selected .card-preview {
|
||||||
background: var(--affine-hover-color);
|
background: var(--affine-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-container.placeholder .card-preview.edgeless {
|
.card-container.placeholder .card-preview {
|
||||||
background: var(--affine-hover-color);
|
background: var(--affine-hover-color);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
@@ -178,7 +180,7 @@ const styles = css`
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-preview.page outline-block-preview:hover {
|
.card-preview outline-block-preview:hover {
|
||||||
color: var(--affine-brand-color);
|
color: var(--affine-brand-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
|
import { effect } from '@preact/signals-core';
|
||||||
import { baseTheme } from '@toeverything/theme';
|
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 { property, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
||||||
@@ -112,7 +113,25 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.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() {
|
override render() {
|
||||||
|
|||||||
@@ -44,20 +44,14 @@ test.describe('toc-panel', () => {
|
|||||||
return panel.locator(`affine-outline-panel-body .title`);
|
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) {
|
async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) {
|
||||||
const fromRect = await fromCard.boundingBox();
|
const fromRect = await fromCard.boundingBox();
|
||||||
const toRect = await toCard.boundingBox();
|
const toRect = await toCard.boundingBox();
|
||||||
|
|
||||||
await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10);
|
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.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();
|
await page.mouse.up();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +261,6 @@ test.describe('toc-panel', () => {
|
|||||||
await page.mouse.click(100, 100);
|
await page.mouse.click(100, 100);
|
||||||
|
|
||||||
await toggleTocPanel(page);
|
await toggleTocPanel(page);
|
||||||
await toggleNoteSorting(page);
|
|
||||||
const docVisibleCard = page.locator(
|
const docVisibleCard = page.locator(
|
||||||
'.card-container[data-invisible="false"]'
|
'.card-container[data-invisible="false"]'
|
||||||
);
|
);
|
||||||
@@ -311,7 +304,6 @@ test.describe('toc-panel', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await toggleTocPanel(page);
|
await toggleTocPanel(page);
|
||||||
await toggleNoteSorting(page);
|
|
||||||
const docVisibleCard = page.locator(
|
const docVisibleCard = page.locator(
|
||||||
'.card-container[data-invisible="false"]'
|
'.card-container[data-invisible="false"]'
|
||||||
);
|
);
|
||||||
@@ -345,7 +337,6 @@ test.describe('toc-panel', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await toggleTocPanel(page);
|
await toggleTocPanel(page);
|
||||||
await toggleNoteSorting(page);
|
|
||||||
const docVisibleCard = page.locator(
|
const docVisibleCard = page.locator(
|
||||||
'.card-container[data-invisible="false"]'
|
'.card-container[data-invisible="false"]'
|
||||||
);
|
);
|
||||||
|
|||||||
+11
-5
@@ -8,7 +8,11 @@ import { WorkspaceService } from '@affine/core/modules/workspace';
|
|||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
|
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
|
||||||
import { matchFlavours, type NoteBlockModel } from '@blocksuite/affine/blocks';
|
import {
|
||||||
|
matchFlavours,
|
||||||
|
type NoteBlockModel,
|
||||||
|
NoteDisplayMode,
|
||||||
|
} from '@blocksuite/affine/blocks';
|
||||||
import { Bound } from '@blocksuite/affine/global/utils';
|
import { Bound } from '@blocksuite/affine/global/utils';
|
||||||
import {
|
import {
|
||||||
ExpandFullIcon,
|
ExpandFullIcon,
|
||||||
@@ -160,12 +164,14 @@ export const EdgelessNoteHeader = ({ note }: { note: NoteBlockModel }) => {
|
|||||||
|
|
||||||
if (!flags.enable_page_block_header) return null;
|
if (!flags.enable_page_block_header) return null;
|
||||||
|
|
||||||
const isFirstNote =
|
const isFirstVisibleNote =
|
||||||
note.parent?.children.find(child =>
|
note.parent?.children.find(
|
||||||
matchFlavours(child, ['affine:note'])
|
child =>
|
||||||
|
matchFlavours(child, ['affine:note']) &&
|
||||||
|
child.displayMode === NoteDisplayMode.DocAndEdgeless
|
||||||
) === note;
|
) === note;
|
||||||
|
|
||||||
if (!isFirstNote) return null;
|
if (!isFirstVisibleNote) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.header} data-testid="edgeless-page-block-header">
|
<div className={styles.header} data-testid="edgeless-page-block-header">
|
||||||
|
|||||||
@@ -229,7 +229,11 @@ test.describe('edgeless note element toolbar', () => {
|
|||||||
|
|
||||||
await displayInPage.click();
|
await displayInPage.click();
|
||||||
await viewTocButton.click();
|
await viewTocButton.click();
|
||||||
await page.waitForSelector('affine-outline-panel');
|
const toc = page.locator('affine-outline-panel');
|
||||||
expect(page.locator('affine-outline-panel')).toBeVisible();
|
await toc.waitFor({ state: 'visible' });
|
||||||
|
const highlightNoteCards = toc.locator(
|
||||||
|
'affine-outline-note-card > .selected'
|
||||||
|
);
|
||||||
|
expect(highlightNoteCards).toHaveCount(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,15 +2,25 @@ import { test } from '@affine-test/kit/playwright';
|
|||||||
import {
|
import {
|
||||||
clickEdgelessModeButton,
|
clickEdgelessModeButton,
|
||||||
clickPageModeButton,
|
clickPageModeButton,
|
||||||
|
clickView,
|
||||||
|
createEdgelessNoteBlock,
|
||||||
|
locateElementToolbar,
|
||||||
|
locateModeSwitchButton,
|
||||||
} from '@affine-test/kit/utils/editor';
|
} from '@affine-test/kit/utils/editor';
|
||||||
|
import {
|
||||||
|
pressEnter,
|
||||||
|
selectAllByKeyboard,
|
||||||
|
} from '@affine-test/kit/utils/keyboard';
|
||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
createLinkedPage,
|
createLinkedPage,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
|
type,
|
||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
waitForEmptyEditor,
|
waitForEmptyEditor,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
|
import { openRightSideBar } from '@affine-test/kit/utils/sidebar';
|
||||||
import { expect, type Locator, type Page } from '@playwright/test';
|
import { expect, type Locator, type Page } from '@playwright/test';
|
||||||
|
|
||||||
function getIndicators(container: Page | Locator) {
|
function getIndicators(container: Page | Locator) {
|
||||||
@@ -156,3 +166,66 @@ test('outline viewer should be useable in doc peek preview', async ({
|
|||||||
await expect(page.locator('affine-outline-panel')).toBeVisible();
|
await expect(page.locator('affine-outline-panel')).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('visibility sorting should be enabled in edgeless mode and disabled in page mode by default, and can be changed', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await pressEnter(page);
|
||||||
|
await type(page, '# Heading 1');
|
||||||
|
await openRightSideBar(page, 'outline');
|
||||||
|
|
||||||
|
const toc = page.locator('affine-outline-panel');
|
||||||
|
const sortingButton = toc.locator('.note-sorting-button');
|
||||||
|
await expect(sortingButton).not.toHaveClass(/active/);
|
||||||
|
expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
|
||||||
|
|
||||||
|
await clickEdgelessModeButton(page);
|
||||||
|
await expect(sortingButton).toHaveClass(/active/);
|
||||||
|
expect(toc.locator('[data-sortable="true"]')).toHaveCount(1);
|
||||||
|
|
||||||
|
await sortingButton.click();
|
||||||
|
await expect(sortingButton).not.toHaveClass(/active/);
|
||||||
|
expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('note cards of TOC should be highlight when selections contains the corresponding notes', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await locateModeSwitchButton(page, 'edgeless').click();
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await openRightSideBar(page, 'outline');
|
||||||
|
|
||||||
|
const toc = page.locator('affine-outline-panel');
|
||||||
|
const highlightNoteCards = toc.locator(
|
||||||
|
'affine-outline-note-card > .selected'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(highlightNoteCards).toHaveCount(0);
|
||||||
|
|
||||||
|
await clickView(page, [0, 0]);
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
await expect(highlightNoteCards).toHaveCount(1);
|
||||||
|
|
||||||
|
await createEdgelessNoteBlock(page, [100, 100]);
|
||||||
|
await expect(highlightNoteCards).toHaveCount(1);
|
||||||
|
|
||||||
|
await clickView(page, [200, 200]);
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
await expect(highlightNoteCards).toHaveCount(2);
|
||||||
|
|
||||||
|
await clickView(page, [100, 100]);
|
||||||
|
const toolbar = locateElementToolbar(page);
|
||||||
|
await toolbar.getByTestId('display-in-page').click();
|
||||||
|
await clickPageModeButton(page);
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await expect(highlightNoteCards).toHaveCount(1);
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
await expect(highlightNoteCards).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|||||||
@@ -19,3 +19,11 @@ export async function clickSideBarUseAvatar(page: Page) {
|
|||||||
export async function clickNewPageButton(page: Page) {
|
export async function clickNewPageButton(page: Page) {
|
||||||
return page.getByTestId('sidebar-new-page-button').click();
|
return page.getByTestId('sidebar-new-page-button').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openRightSideBar(
|
||||||
|
page: Page,
|
||||||
|
tab?: 'chat' | 'properties' | 'journal' | 'outline' | 'frame'
|
||||||
|
) {
|
||||||
|
await page.getByTestId('right-sidebar-toggle').click();
|
||||||
|
tab && (await page.getByTestId(`sidebar-tab-${tab}`).click());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user