mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
refactor(editor): simplify TOC implementation with signal and context (#9873)
### What Changes 1. Used `@preact/signal` and `@lit/context` to simplify repetitive passing of properties of TOC components, 2. Fixed TOC invalid when editor changed, such as click new page button.
This commit is contained in:
@@ -1,21 +1,49 @@
|
|||||||
|
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||||
import { WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
import { CloseIcon, SortIcon } from '@blocksuite/icons/lit';
|
import { CloseIcon, SortIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { consume } from '@lit/context';
|
||||||
|
import { effect, signal } from '@preact/signals-core';
|
||||||
import { html, nothing } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
|
||||||
|
|
||||||
|
import { type TocContext, tocContext } from '../config';
|
||||||
|
import { getNotesFromDoc } from '../utils/query';
|
||||||
import * as styles from './outline-notice.css';
|
import * as styles from './outline-notice.css';
|
||||||
|
|
||||||
export const AFFINE_OUTLINE_NOTICE = 'affine-outline-notice';
|
export const AFFINE_OUTLINE_NOTICE = 'affine-outline-notice';
|
||||||
|
|
||||||
export class OutlineNotice extends WithDisposable(ShadowlessElement) {
|
export class OutlineNotice extends SignalWatcher(
|
||||||
private _handleNoticeButtonClick() {
|
WithDisposable(ShadowlessElement)
|
||||||
this.toggleNotesSorting();
|
) {
|
||||||
this.setNoticeVisibility(false);
|
private readonly _visible$ = signal(false);
|
||||||
|
|
||||||
|
override connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.disposables.add(
|
||||||
|
effect(() => {
|
||||||
|
const enableSorting = this._context.enableSorting$.value;
|
||||||
|
|
||||||
|
if (enableSorting) {
|
||||||
|
if (this._visible$.peek()) {
|
||||||
|
this._visible$.value = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowNotice =
|
||||||
|
getNotesFromDoc(this._context.editor$.value.doc, [
|
||||||
|
NoteDisplayMode.DocOnly,
|
||||||
|
]).length > 0;
|
||||||
|
|
||||||
|
if (shouldShowNotice && !this._visible$.peek()) {
|
||||||
|
this._visible$.value = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
if (!this.noticeVisible) {
|
if (!this._visible$.value) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +53,9 @@ export class OutlineNotice extends WithDisposable(ShadowlessElement) {
|
|||||||
<span class=${styles.outlineNoticeLabel}>SOME CONTENTS HIDDEN</span>
|
<span class=${styles.outlineNoticeLabel}>SOME CONTENTS HIDDEN</span>
|
||||||
<span
|
<span
|
||||||
class=${styles.outlineNoticeCloseButton}
|
class=${styles.outlineNoticeCloseButton}
|
||||||
@click=${() => this.setNoticeVisibility(false)}
|
@click=${() => {
|
||||||
|
this._visible$.value = false;
|
||||||
|
}}
|
||||||
>${CloseIcon({ width: '16px', height: '16px' })}</span
|
>${CloseIcon({ width: '16px', height: '16px' })}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +63,13 @@ export class OutlineNotice extends WithDisposable(ShadowlessElement) {
|
|||||||
<div class="${styles.notice}">
|
<div class="${styles.notice}">
|
||||||
Some contents are not visible on edgeless.
|
Some contents are not visible on edgeless.
|
||||||
</div>
|
</div>
|
||||||
<div class="${styles.button}" @click=${this._handleNoticeButtonClick}>
|
<div
|
||||||
|
class="${styles.button}"
|
||||||
|
@click=${() => {
|
||||||
|
this._context.enableSorting$.value = true;
|
||||||
|
this._visible$.value = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span class=${styles.buttonSpan}>Click here or</span>
|
<span class=${styles.buttonSpan}>Click here or</span>
|
||||||
<span class=${styles.buttonSpan}
|
<span class=${styles.buttonSpan}
|
||||||
>${SortIcon({ width: '20px', height: '20px' })}</span
|
>${SortIcon({ width: '20px', height: '20px' })}</span
|
||||||
@@ -45,14 +81,8 @@ export class OutlineNotice extends WithDisposable(ShadowlessElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false })
|
@consume({ context: tocContext })
|
||||||
accessor noticeVisible!: boolean;
|
private accessor _context!: TocContext;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor setNoticeVisibility!: (visibility: boolean) => void;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor toggleNotesSorting!: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -5,22 +5,16 @@ import {
|
|||||||
matchFlavours,
|
matchFlavours,
|
||||||
NoteDisplayMode,
|
NoteDisplayMode,
|
||||||
} from '@blocksuite/blocks';
|
} from '@blocksuite/blocks';
|
||||||
import {
|
import { Bound, SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
Bound,
|
|
||||||
DisposableGroup,
|
|
||||||
SignalWatcher,
|
|
||||||
WithDisposable,
|
|
||||||
} from '@blocksuite/global/utils';
|
|
||||||
import { consume } from '@lit/context';
|
import { consume } from '@lit/context';
|
||||||
import { effect, signal } from '@preact/signals-core';
|
import { effect, signal } from '@preact/signals-core';
|
||||||
import { html, nothing, type PropertyValues } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
import { property, 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';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
import { when } from 'lit/directives/when.js';
|
import { when } from 'lit/directives/when.js';
|
||||||
|
|
||||||
import type { AffineEditorContainer } from '../../../editors/editor-container';
|
import { type TocContext, tocContext } from '../config';
|
||||||
import { editorContext } from '../config';
|
|
||||||
import type {
|
import type {
|
||||||
ClickBlockEvent,
|
ClickBlockEvent,
|
||||||
DisplayModeChangeEvent,
|
DisplayModeChangeEvent,
|
||||||
@@ -60,6 +54,8 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
) {
|
) {
|
||||||
private readonly _activeHeadingId$ = signal<string | null>(null);
|
private readonly _activeHeadingId$ = signal<string | null>(null);
|
||||||
|
|
||||||
|
private readonly _insertIndex$ = signal<number | undefined>(undefined);
|
||||||
|
|
||||||
private readonly _dragging$ = signal(false);
|
private readonly _dragging$ = signal(false);
|
||||||
|
|
||||||
private readonly _pageVisibleNoteItems$ = signal<OutlineNoteItem[]>([]);
|
private readonly _pageVisibleNoteItems$ = signal<OutlineNoteItem[]>([]);
|
||||||
@@ -68,8 +64,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
|
|
||||||
private _clearHighlightMask = () => {};
|
private _clearHighlightMask = () => {};
|
||||||
|
|
||||||
private _docDisposables: DisposableGroup | null = null;
|
|
||||||
|
|
||||||
private _indicatorTranslateY = 0;
|
private _indicatorTranslateY = 0;
|
||||||
|
|
||||||
private _lockActiveHeadingId = false;
|
private _lockActiveHeadingId = false;
|
||||||
@@ -81,6 +75,10 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get editor() {
|
||||||
|
return this._context.editor$.value;
|
||||||
|
}
|
||||||
|
|
||||||
private get doc() {
|
private get doc() {
|
||||||
return this.editor.doc;
|
return this.editor.doc;
|
||||||
}
|
}
|
||||||
@@ -90,18 +88,14 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
get viewportPadding(): [number, number, number, number] {
|
get viewportPadding(): [number, number, number, number] {
|
||||||
return this.fitPadding
|
const fitPadding = this._context.fitPadding$.value;
|
||||||
|
return fitPadding.length === 4
|
||||||
? ([0, 0, 0, 0].map((val, idx) =>
|
? ([0, 0, 0, 0].map((val, idx) =>
|
||||||
Number.isFinite(this.fitPadding[idx]) ? this.fitPadding[idx] : val
|
Number.isFinite(fitPadding[idx]) ? fitPadding[idx] : val
|
||||||
) as [number, number, number, number])
|
) as [number, number, number, number])
|
||||||
: [0, 0, 0, 0];
|
: [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearDocDisposables() {
|
|
||||||
this._docDisposables?.dispose();
|
|
||||||
this._docDisposables = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _deSelectNoteInEdgelessMode(note: NoteBlockModel) {
|
private _deSelectNoteInEdgelessMode(note: NoteBlockModel) {
|
||||||
if (!this.edgeless) return;
|
if (!this.edgeless) return;
|
||||||
|
|
||||||
@@ -119,15 +113,16 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
*/
|
*/
|
||||||
private readonly _doubleClickHandler = (e: MouseEvent) => {
|
private readonly _doubleClickHandler = (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
const enableSorting = this._context.enableSorting$.peek();
|
||||||
// check if click at outline-card, if so, do nothing
|
// check if click at outline-card, if so, do nothing
|
||||||
if (
|
if (
|
||||||
(e.target as HTMLElement).closest('outline-note-card') ||
|
(e.target as HTMLElement).closest('outline-note-card') ||
|
||||||
!this.enableNotesSorting
|
!enableSorting
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleNotesSorting();
|
this._context.enableSorting$.value = !enableSorting;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _drag() {
|
private _drag() {
|
||||||
@@ -178,7 +173,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
outlineListContainer: this._pageVisibleList,
|
outlineListContainer: this._pageVisibleList,
|
||||||
onDragEnd: insertIdx => {
|
onDragEnd: insertIdx => {
|
||||||
this._dragging$.value = false;
|
this._dragging$.value = false;
|
||||||
this.insertIndex = undefined;
|
this._insertIndex$.value = undefined;
|
||||||
|
|
||||||
if (insertIdx === undefined) return;
|
if (insertIdx === undefined) return;
|
||||||
|
|
||||||
@@ -191,7 +186,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onDragMove: (idx, indicatorTranslateY) => {
|
onDragMove: (idx, indicatorTranslateY) => {
|
||||||
this.insertIndex = idx;
|
this._insertIndex$.value = idx;
|
||||||
this._indicatorTranslateY = indicatorTranslateY ?? 0;
|
this._indicatorTranslateY = indicatorTranslateY ?? 0;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -303,20 +298,15 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
|
|
||||||
if (!hasNotEmptyHeadings) return nothing;
|
if (!hasNotEmptyHeadings) return nothing;
|
||||||
|
|
||||||
|
const rootId = this.doc.root.id;
|
||||||
|
const active = rootId === this._activeHeadingId$.value;
|
||||||
|
|
||||||
return html`<affine-outline-block-preview
|
return html`<affine-outline-block-preview
|
||||||
class=${classMap({
|
class=${classMap({ active: active })}
|
||||||
active: this.doc.root.id === this._activeHeadingId$.value,
|
|
||||||
})}
|
|
||||||
.block=${this.doc.root}
|
.block=${this.doc.root}
|
||||||
.className=${this.doc.root?.id === this._activeHeadingId$.value
|
|
||||||
? 'active'
|
|
||||||
: ''}
|
|
||||||
.cardNumber=${1}
|
.cardNumber=${1}
|
||||||
.enableNotesSorting=${false}
|
|
||||||
.showPreviewIcon=${this.showPreviewIcon}
|
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
if (!this.doc.root) return;
|
this._scrollToBlock(rootId).catch(console.error);
|
||||||
this._scrollToBlock(this.doc.root.id).catch(console.error);
|
|
||||||
}}
|
}}
|
||||||
></affine-outline-block-preview>`;
|
></affine-outline-block-preview>`;
|
||||||
}
|
}
|
||||||
@@ -332,8 +322,6 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
.number=${idx + 1}
|
.number=${idx + 1}
|
||||||
.index=${item.index}
|
.index=${item.index}
|
||||||
.activeHeadingId=${this._activeHeadingId$.value}
|
.activeHeadingId=${this._activeHeadingId$.value}
|
||||||
.showPreviewIcon=${this.showPreviewIcon}
|
|
||||||
.enableNotesSorting=${this.enableNotesSorting}
|
|
||||||
.status=${this._selectedNotes$.value.includes(item.note.id)
|
.status=${this._selectedNotes$.value.includes(item.note.id)
|
||||||
? this._dragging$.value
|
? this._dragging$.value
|
||||||
? 'placeholder'
|
? 'placeholder'
|
||||||
@@ -353,7 +341,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
private _renderPageVisibleCardList() {
|
private _renderPageVisibleCardList() {
|
||||||
return html`<div class=${`page-visible-card-list ${styles.cardList}`}>
|
return html`<div class=${`page-visible-card-list ${styles.cardList}`}>
|
||||||
${when(
|
${when(
|
||||||
this.insertIndex !== undefined,
|
this._insertIndex$.value !== undefined,
|
||||||
() =>
|
() =>
|
||||||
html`<div
|
html`<div
|
||||||
class=${styles.insertIndicator}
|
class=${styles.insertIndicator}
|
||||||
@@ -411,85 +399,54 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setDocDisposables() {
|
|
||||||
this._clearDocDisposables();
|
|
||||||
this._docDisposables = new DisposableGroup();
|
|
||||||
this._docDisposables.add(
|
|
||||||
effect(() => {
|
|
||||||
this._updateNoticeVisibility();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this._docDisposables.add(
|
|
||||||
effect(() => {
|
|
||||||
this._updateNotes();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateNotes() {
|
|
||||||
if (this._dragging$.value) return;
|
|
||||||
|
|
||||||
const isRenderableNote = (item: OutlineNoteItem) => {
|
|
||||||
let hasHeadings = false;
|
|
||||||
|
|
||||||
for (const block of item.note.children) {
|
|
||||||
if (isHeadingBlock(block)) {
|
|
||||||
hasHeadings = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasHeadings || this.enableNotesSorting;
|
|
||||||
};
|
|
||||||
|
|
||||||
this._pageVisibleNoteItems$.value = getNotesFromDoc(this.doc, [
|
|
||||||
NoteDisplayMode.DocAndEdgeless,
|
|
||||||
NoteDisplayMode.DocOnly,
|
|
||||||
]).filter(isRenderableNote);
|
|
||||||
|
|
||||||
this._edgelessOnlyNoteItems$.value = getNotesFromDoc(this.doc, [
|
|
||||||
NoteDisplayMode.EdgelessOnly,
|
|
||||||
]).filter(isRenderableNote);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateNoticeVisibility() {
|
|
||||||
if (this.enableNotesSorting) {
|
|
||||||
if (this.noticeVisible) {
|
|
||||||
this.setNoticeVisibility(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldShowNotice =
|
|
||||||
getNotesFromDoc(this.doc, [NoteDisplayMode.DocOnly]).length > 0;
|
|
||||||
|
|
||||||
if (shouldShowNotice && !this.noticeVisible) {
|
|
||||||
this.setNoticeVisibility(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _watchSelectedNotes() {
|
private _watchSelectedNotes() {
|
||||||
|
return effect(() => {
|
||||||
|
const { std, doc, mode } = this.editor;
|
||||||
|
if (mode !== 'edgeless') return;
|
||||||
|
|
||||||
|
const currSelectedNotes = std.selection
|
||||||
|
.filter(SurfaceSelection)
|
||||||
|
.filter(({ blockId }) => {
|
||||||
|
const model = doc.getBlock(blockId)?.model;
|
||||||
|
return !!model && matchFlavours(model, ['affine:note']);
|
||||||
|
})
|
||||||
|
.map(({ blockId }) => blockId);
|
||||||
|
|
||||||
|
const preSelected = this._selectedNotes$.peek();
|
||||||
|
if (
|
||||||
|
preSelected.length !== currSelectedNotes.length ||
|
||||||
|
preSelected.some(id => !currSelectedNotes.includes(id))
|
||||||
|
) {
|
||||||
|
this._selectedNotes$.value = currSelectedNotes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _watchNotes() {
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const { std, doc, mode } = this.editor;
|
if (this._dragging$.value) return;
|
||||||
if (mode !== 'edgeless') return;
|
|
||||||
|
|
||||||
const currSelectedNotes = std.selection
|
const isRenderableNote = (item: OutlineNoteItem) => {
|
||||||
.filter(SurfaceSelection)
|
let hasHeadings = false;
|
||||||
.filter(({ blockId }) => {
|
|
||||||
const model = doc.getBlock(blockId)?.model;
|
|
||||||
return !!model && matchFlavours(model, ['affine:note']);
|
|
||||||
})
|
|
||||||
.map(({ blockId }) => blockId);
|
|
||||||
|
|
||||||
const preSelected = this._selectedNotes$.peek();
|
for (const block of item.note.children) {
|
||||||
if (
|
if (isHeadingBlock(block)) {
|
||||||
preSelected.length !== currSelectedNotes.length ||
|
hasHeadings = true;
|
||||||
preSelected.some(id => !currSelectedNotes.includes(id))
|
break;
|
||||||
) {
|
}
|
||||||
this._selectedNotes$.value = currSelectedNotes;
|
}
|
||||||
}
|
return hasHeadings || this._context.enableSorting$.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
this._pageVisibleNoteItems$.value = getNotesFromDoc(this.doc, [
|
||||||
|
NoteDisplayMode.DocAndEdgeless,
|
||||||
|
NoteDisplayMode.DocOnly,
|
||||||
|
]).filter(isRenderableNote);
|
||||||
|
|
||||||
|
this._edgelessOnlyNoteItems$.value = getNotesFromDoc(this.doc, [
|
||||||
|
NoteDisplayMode.EdgelessOnly,
|
||||||
|
]).filter(isRenderableNote);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -507,12 +464,12 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
this._watchNotes();
|
||||||
this._watchSelectedNotes();
|
this._watchSelectedNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback(): void {
|
override disconnectedCallback(): void {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._clearDocDisposables();
|
|
||||||
this._clearHighlightMask();
|
this._clearHighlightMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,46 +491,11 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
override willUpdate(_changedProperties: PropertyValues) {
|
|
||||||
if (_changedProperties.has('editor')) {
|
|
||||||
this._setDocDisposables();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_changedProperties.has('enableNotesSorting')) {
|
|
||||||
this._updateNoticeVisibility();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@query('.page-visible-card-list')
|
@query('.page-visible-card-list')
|
||||||
private accessor _pageVisibleList!: HTMLElement;
|
private accessor _pageVisibleList!: HTMLElement;
|
||||||
|
|
||||||
@consume({ context: editorContext })
|
@consume({ context: tocContext })
|
||||||
@property({ attribute: false })
|
private accessor _context!: TocContext;
|
||||||
accessor editor!: AffineEditorContainer;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor enableNotesSorting: boolean = false;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor fitPadding!: number[];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor insertIndex: number | undefined = undefined;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor noticeVisible!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor renderEdgelessOnlyNotes: boolean = true;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor setNoticeVisibility!: (visibility: boolean) => void;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor showPreviewIcon!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor toggleNotesSorting!: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -48,19 +48,23 @@ export const cardHeader = style({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
cursor: 'grab',
|
||||||
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${outlineCard}[data-sortable="true"] &`]: {
|
[`${outlineCard}[data-sortable="true"] &`]: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
[`${outlineCard}[data-invisible="false"] &:hover`]: {
|
[`${outlineCard}[data-visibility="edgeless"] &:hover`]: {
|
||||||
cursor: 'grab',
|
cursor: 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const invisibleCard = style({
|
const invisibleCard = style({
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${outlineCard}[data-invisible="true"] &`]: {
|
[`${outlineCard}[data-visibility="edgeless"] &`]: {
|
||||||
color: cssVarV2('text/disable'),
|
color: cssVarV2('text/disable'),
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ import {
|
|||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
import { ArrowDownSmallIcon, InvisibleIcon } from '@blocksuite/icons/lit';
|
import { ArrowDownSmallIcon, InvisibleIcon } from '@blocksuite/icons/lit';
|
||||||
import type { BlockModel } from '@blocksuite/store';
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
|
import { consume } from '@lit/context';
|
||||||
|
import { signal } from '@preact/signals-core';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
|
||||||
|
import { type TocContext, tocContext } from '../config';
|
||||||
import type { SelectEvent } from '../utils/custom-events';
|
import type { SelectEvent } from '../utils/custom-events';
|
||||||
import * as styles from './outline-card.css';
|
import * as styles from './outline-card.css';
|
||||||
|
|
||||||
@@ -23,6 +27,8 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
private _displayModePopper: ReturnType<typeof createButtonPopper> | null =
|
private _displayModePopper: ReturnType<typeof createButtonPopper> | null =
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
private readonly _showPopper$ = signal(false);
|
||||||
|
|
||||||
private _dispatchClickBlockEvent(block: BlockModel) {
|
private _dispatchClickBlockEvent(block: BlockModel) {
|
||||||
const event = new CustomEvent('clickblock', {
|
const event = new CustomEvent('clickblock', {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -49,7 +55,7 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
|
|
||||||
private _dispatchDragEvent(e: MouseEvent) {
|
private _dispatchDragEvent(e: MouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.button !== 0 || !this.enableNotesSorting) return;
|
if (e.button !== 0 || !this._context.enableSorting$.peek()) return;
|
||||||
|
|
||||||
const { clientX: startX, clientY: startY } = e;
|
const { clientX: startX, clientY: startY } = e;
|
||||||
const disposeDragStart = on(this.ownerDocument, 'mousemove', e => {
|
const disposeDragStart = on(this.ownerDocument, 'mousemove', e => {
|
||||||
@@ -122,7 +128,7 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
this._displayModeButtonGroup,
|
this._displayModeButtonGroup,
|
||||||
this._displayModePanel,
|
this._displayModePanel,
|
||||||
({ display }) => {
|
({ display }) => {
|
||||||
this._showPopper = display === 'show';
|
this._showPopper$.value = display === 'show';
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mainAxis: 0,
|
mainAxis: 0,
|
||||||
@@ -136,11 +142,15 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
override render() {
|
override render() {
|
||||||
const { children, displayMode } = this.note;
|
const { children, displayMode } = this.note;
|
||||||
const currentMode = this._getCurrentModeLabel(displayMode);
|
const currentMode = this._getCurrentModeLabel(displayMode);
|
||||||
|
const invisible =
|
||||||
|
this.note.displayMode$.value === NoteDisplayMode.EdgelessOnly;
|
||||||
|
|
||||||
|
const enableSorting = this._context.enableSorting$.value;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
data-invisible=${this.invisible}
|
data-visibility=${this.note.displayMode}
|
||||||
data-sortable=${this.enableNotesSorting}
|
data-sortable=${enableSorting}
|
||||||
data-status=${this.status}
|
data-status=${this.status}
|
||||||
class=${styles.outlineCard}
|
class=${styles.outlineCard}
|
||||||
>
|
>
|
||||||
@@ -152,7 +162,7 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
>
|
>
|
||||||
${html`<div class=${styles.cardHeader}>
|
${html`<div class=${styles.cardHeader}>
|
||||||
${
|
${
|
||||||
this.invisible
|
invisible
|
||||||
? html`<span class=${styles.headerIcon}
|
? html`<span class=${styles.headerIcon}
|
||||||
>${InvisibleIcon({ width: '20px', height: '20px' })}</span
|
>${InvisibleIcon({ width: '20px', height: '20px' })}</span
|
||||||
>`
|
>`
|
||||||
@@ -162,7 +172,7 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
<div class=${styles.displayModeButtonGroup}>
|
<div class=${styles.displayModeButtonGroup}>
|
||||||
<span>Show in</span>
|
<span>Show in</span>
|
||||||
<edgeless-tool-icon-button
|
<edgeless-tool-icon-button
|
||||||
.tooltip=${this._showPopper ? '' : 'Display Mode'}
|
.tooltip=${this._showPopper$.value ? '' : 'Display Mode'}
|
||||||
.tipPosition=${'left-start'}
|
.tipPosition=${'left-start'}
|
||||||
.iconContainerPadding=${0}
|
.iconContainerPadding=${0}
|
||||||
data-testid="display-mode-button"
|
data-testid="display-mode-button"
|
||||||
@@ -193,14 +203,12 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
<div class=${styles.cardContent}>
|
<div class=${styles.cardContent}>
|
||||||
${children.map(block => {
|
${children.map(block => {
|
||||||
return html`<affine-outline-block-preview
|
return html`<affine-outline-block-preview
|
||||||
|
class=${classMap({ active: this.activeHeadingId === block.id })}
|
||||||
.block=${block}
|
.block=${block}
|
||||||
.className=${this.activeHeadingId === block.id ? 'active' : ''}
|
.disabledIcon=${invisible}
|
||||||
.showPreviewIcon=${this.showPreviewIcon}
|
|
||||||
.disabledIcon=${this.invisible}
|
|
||||||
.cardNumber=${this.number}
|
.cardNumber=${this.number}
|
||||||
.enableNotesSorting=${this.enableNotesSorting}
|
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
if (this.invisible) return;
|
if (invisible) return;
|
||||||
this._dispatchClickBlockEvent(block);
|
this._dispatchClickBlockEvent(block);
|
||||||
}}
|
}}
|
||||||
></affine-outline-block-preview>`;
|
></affine-outline-block-preview>`;
|
||||||
@@ -218,15 +226,9 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
@query('note-display-mode-panel')
|
@query('note-display-mode-panel')
|
||||||
private accessor _displayModePanel!: HTMLDivElement;
|
private accessor _displayModePanel!: HTMLDivElement;
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _showPopper = false;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor activeHeadingId: string | null = null;
|
accessor activeHeadingId: string | null = null;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor enableNotesSorting!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor index!: number;
|
accessor index!: number;
|
||||||
|
|
||||||
@@ -236,11 +238,11 @@ export class OutlineNoteCard extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor number!: number;
|
accessor number!: number;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor showPreviewIcon!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor status: 'selected' | 'placeholder' | 'normal' = 'normal';
|
accessor status: 'selected' | 'placeholder' | 'normal' = 'normal';
|
||||||
|
|
||||||
|
@consume({ context: tocContext })
|
||||||
|
private accessor _context!: TocContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ import { html, nothing } from 'lit';
|
|||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
|
||||||
import type { AffineEditorContainer } from '../../../editors/editor-container.js';
|
import {
|
||||||
import { editorContext, placeholderMap, previewIconMap } from '../config.js';
|
placeholderMap,
|
||||||
|
previewIconMap,
|
||||||
|
type TocContext,
|
||||||
|
tocContext,
|
||||||
|
} from '../config.js';
|
||||||
import { isHeadingBlock, isRootBlock } from '../utils/query.js';
|
import { isHeadingBlock, isRootBlock } from '../utils/query.js';
|
||||||
import * as styles from './outline-preview.css';
|
import * as styles from './outline-preview.css';
|
||||||
|
|
||||||
@@ -35,6 +39,10 @@ export const AFFINE_OUTLINE_BLOCK_PREVIEW = 'affine-outline-block-preview';
|
|||||||
export class OutlineBlockPreview extends SignalWatcher(
|
export class OutlineBlockPreview extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
|
private get _docDisplayMetaService() {
|
||||||
|
return this._context.editor$.value.std.get(DocDisplayMetaProvider);
|
||||||
|
}
|
||||||
|
|
||||||
private _TextBlockPreview(block: ParagraphBlockModel | ListBlockModel) {
|
private _TextBlockPreview(block: ParagraphBlockModel | ListBlockModel) {
|
||||||
const deltas: DeltaInsert<AffineTextAttributes>[] =
|
const deltas: DeltaInsert<AffineTextAttributes>[] =
|
||||||
block.text.yText.toDelta();
|
block.text.yText.toDelta();
|
||||||
@@ -49,16 +57,13 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
doc => doc.id === refAttribute.pageId
|
doc => doc.id === refAttribute.pageId
|
||||||
);
|
);
|
||||||
const unavailable = !refMeta;
|
const unavailable = !refMeta;
|
||||||
const docDisplayMetaService = this.editor.std.get(
|
|
||||||
DocDisplayMetaProvider
|
|
||||||
);
|
|
||||||
|
|
||||||
const icon = unavailable
|
const icon = unavailable
|
||||||
? LinkedPageIcon({ width: '1.1em', height: '1.1em' })
|
? LinkedPageIcon({ width: '1.1em', height: '1.1em' })
|
||||||
: docDisplayMetaService.icon(refMeta.id).value;
|
: this._docDisplayMetaService.icon(refMeta.id).value;
|
||||||
const title = unavailable
|
const title = unavailable
|
||||||
? 'Deleted doc'
|
? 'Deleted doc'
|
||||||
: docDisplayMetaService.title(refMeta.id).value;
|
: this._docDisplayMetaService.title(refMeta.id).value;
|
||||||
|
|
||||||
return html`<span
|
return html`<span
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
@@ -94,7 +99,7 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
class="${styles.text} ${styles.textGeneral} ${headingClass}"
|
class="${styles.text} ${styles.textGeneral} ${headingClass}"
|
||||||
>${previewText}</span
|
>${previewText}</span
|
||||||
>
|
>
|
||||||
${this.showPreviewIcon
|
${this._context.showIcons$.value
|
||||||
? html`<span class=${iconClass}>${previewIconMap[block.type]}</span>`
|
? html`<span class=${iconClass}>${previewIconMap[block.type]}</span>`
|
||||||
: nothing}`;
|
: nothing}`;
|
||||||
}
|
}
|
||||||
@@ -110,12 +115,14 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
const iconClass = this.disabledIcon ? styles.iconDisabled : styles.icon;
|
const iconClass = this.disabledIcon ? styles.iconDisabled : styles.icon;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.enableNotesSorting &&
|
!this._context.enableSorting$.value &&
|
||||||
!isHeadingBlock(block) &&
|
!isHeadingBlock(block) &&
|
||||||
!isRootBlock(block)
|
!isRootBlock(block)
|
||||||
)
|
)
|
||||||
return nothing;
|
return nothing;
|
||||||
|
|
||||||
|
const showPreviewIcon = this._context.showIcons$.value;
|
||||||
|
|
||||||
switch (block.flavour as keyof BlockSuite.BlockModels) {
|
switch (block.flavour as keyof BlockSuite.BlockModels) {
|
||||||
case 'affine:page':
|
case 'affine:page':
|
||||||
assertType<RootBlockModel>(block);
|
assertType<RootBlockModel>(block);
|
||||||
@@ -139,7 +146,7 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
<span class="${styles.text} ${styles.textGeneral}"
|
<span class="${styles.text} ${styles.textGeneral}"
|
||||||
>${block.title || block.url || placeholderMap['bookmark']}</span
|
>${block.title || block.url || placeholderMap['bookmark']}</span
|
||||||
>
|
>
|
||||||
${this.showPreviewIcon
|
${showPreviewIcon
|
||||||
? html`<span class=${iconClass}
|
? html`<span class=${iconClass}
|
||||||
>${previewIconMap['bookmark']}</span
|
>${previewIconMap['bookmark']}</span
|
||||||
>`
|
>`
|
||||||
@@ -151,7 +158,7 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
<span class="${styles.text} ${styles.textGeneral}"
|
<span class="${styles.text} ${styles.textGeneral}"
|
||||||
>${block.language ?? placeholderMap['code']}</span
|
>${block.language ?? placeholderMap['code']}</span
|
||||||
>
|
>
|
||||||
${this.showPreviewIcon
|
${showPreviewIcon
|
||||||
? html`<span class=${iconClass}>${previewIconMap['code']}</span>`
|
? html`<span class=${iconClass}>${previewIconMap['code']}</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
@@ -163,7 +170,7 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
? block.title.toString()
|
? block.title.toString()
|
||||||
: placeholderMap['database']}</span
|
: placeholderMap['database']}</span
|
||||||
>
|
>
|
||||||
${this.showPreviewIcon
|
${showPreviewIcon
|
||||||
? html`<span class=${iconClass}>${previewIconMap['table']}</span>`
|
? html`<span class=${iconClass}>${previewIconMap['table']}</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
@@ -175,7 +182,7 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
? block.caption
|
? block.caption
|
||||||
: placeholderMap['image']}</span
|
: placeholderMap['image']}</span
|
||||||
>
|
>
|
||||||
${this.showPreviewIcon
|
${showPreviewIcon
|
||||||
? html`<span class=${iconClass}>${previewIconMap['image']}</span>`
|
? html`<span class=${iconClass}>${previewIconMap['image']}</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
@@ -187,7 +194,7 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
? block.name
|
? block.name
|
||||||
: placeholderMap['attachment']}</span
|
: placeholderMap['attachment']}</span
|
||||||
>
|
>
|
||||||
${this.showPreviewIcon
|
${showPreviewIcon
|
||||||
? html`<span class=${iconClass}
|
? html`<span class=${iconClass}
|
||||||
>${previewIconMap['attachment']}</span
|
>${previewIconMap['attachment']}</span
|
||||||
>`
|
>`
|
||||||
@@ -198,10 +205,6 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@consume({ context: editorContext })
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor editor!: AffineEditorContainer;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor block!: ValuesOf<BlockSuite.BlockModels>;
|
accessor block!: ValuesOf<BlockSuite.BlockModels>;
|
||||||
|
|
||||||
@@ -211,11 +214,8 @@ export class OutlineBlockPreview extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor disabledIcon = false;
|
accessor disabledIcon = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@consume({ context: tocContext })
|
||||||
accessor enableNotesSorting!: boolean;
|
private accessor _context!: TocContext;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor showPreviewIcon!: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ParagraphBlockModel } from '@blocksuite/blocks';
|
import type { ParagraphBlockModel, Signal } from '@blocksuite/blocks';
|
||||||
import {
|
import {
|
||||||
AttachmentIcon,
|
AttachmentIcon,
|
||||||
BlockIcon,
|
BlockIcon,
|
||||||
@@ -84,10 +84,11 @@ export const headingKeys = new Set(
|
|||||||
|
|
||||||
export const outlineSettingsKey = 'outlinePanelSettings';
|
export const outlineSettingsKey = 'outlinePanelSettings';
|
||||||
|
|
||||||
export type OutlineSettingsDataType = {
|
export type TocContext = {
|
||||||
showIcons: boolean;
|
editor$: Signal<AffineEditorContainer>;
|
||||||
enableSorting: boolean;
|
enableSorting$: Signal<boolean>;
|
||||||
|
showIcons$: Signal<boolean>;
|
||||||
|
fitPadding$: Signal<number[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editorContext =
|
export const tocContext = createContext<TocContext>('tocContext');
|
||||||
createContext<AffineEditorContainer>('editorContext');
|
|
||||||
|
|||||||
@@ -1,45 +1,54 @@
|
|||||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||||
import { createButtonPopper } from '@blocksuite/blocks';
|
import { createButtonPopper } from '@blocksuite/blocks';
|
||||||
import { WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
import { SettingsIcon, SortIcon } from '@blocksuite/icons/lit';
|
import { SettingsIcon, SortIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { consume } from '@lit/context';
|
||||||
|
import { signal } from '@preact/signals-core';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { query } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import { type TocContext, tocContext } from '../config';
|
||||||
import * as styles from './outline-panel-header.css';
|
import * as styles from './outline-panel-header.css';
|
||||||
|
|
||||||
export const AFFINE_OUTLINE_PANEL_HEADER = 'affine-outline-panel-header';
|
export const AFFINE_OUTLINE_PANEL_HEADER = 'affine-outline-panel-header';
|
||||||
|
|
||||||
export class OutlinePanelHeader extends WithDisposable(ShadowlessElement) {
|
export class OutlinePanelHeader extends SignalWatcher(
|
||||||
|
WithDisposable(ShadowlessElement)
|
||||||
|
) {
|
||||||
private _notePreviewSettingMenuPopper: ReturnType<
|
private _notePreviewSettingMenuPopper: ReturnType<
|
||||||
typeof createButtonPopper
|
typeof createButtonPopper
|
||||||
> | null = null;
|
> | null = null;
|
||||||
|
|
||||||
override firstUpdated() {
|
private readonly _settingPopperShow$ = signal(false);
|
||||||
const _disposables = this._disposables;
|
|
||||||
|
|
||||||
|
override firstUpdated() {
|
||||||
this._notePreviewSettingMenuPopper = createButtonPopper(
|
this._notePreviewSettingMenuPopper = createButtonPopper(
|
||||||
this._noteSettingButton,
|
this._noteSettingButton,
|
||||||
this._notePreviewSettingMenu,
|
this._notePreviewSettingMenu,
|
||||||
({ display }) => {
|
({ display }) => {
|
||||||
this._settingPopperShow = display === 'show';
|
this._settingPopperShow$.value = display === 'show';
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mainAxis: 14,
|
mainAxis: 14,
|
||||||
crossAxis: -30,
|
crossAxis: -30,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
_disposables.add(this._notePreviewSettingMenuPopper);
|
this.disposables.add(this._notePreviewSettingMenuPopper);
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
|
const sortingEnabled = this._context.enableSorting$.value;
|
||||||
|
const showSettingPopper = this._settingPopperShow$.value;
|
||||||
|
|
||||||
return html`<div class=${styles.container}>
|
return html`<div class=${styles.container}>
|
||||||
<div class=${styles.noteSettingContainer}>
|
<div class=${styles.noteSettingContainer}>
|
||||||
<span class=${styles.label}>Table of Contents</span>
|
<span class=${styles.label}>Table of Contents</span>
|
||||||
<edgeless-tool-icon-button
|
<edgeless-tool-icon-button
|
||||||
class="${this._settingPopperShow ? 'active' : ''}"
|
data-testid="toggle-toc-setting-button"
|
||||||
.tooltip=${this._settingPopperShow ? '' : 'Preview Settings'}
|
class="${showSettingPopper ? 'active' : ''}"
|
||||||
|
.tooltip=${showSettingPopper ? '' : 'Preview Settings'}
|
||||||
.tipPosition=${'bottom'}
|
.tipPosition=${'bottom'}
|
||||||
.active=${this._settingPopperShow}
|
.active=${showSettingPopper}
|
||||||
.activeMode=${'background'}
|
.activeMode=${'background'}
|
||||||
@click=${() => this._notePreviewSettingMenuPopper?.toggle()}
|
@click=${() => this._notePreviewSettingMenuPopper?.toggle()}
|
||||||
>
|
>
|
||||||
@@ -48,22 +57,21 @@ export class OutlinePanelHeader extends WithDisposable(ShadowlessElement) {
|
|||||||
</div>
|
</div>
|
||||||
<edgeless-tool-icon-button
|
<edgeless-tool-icon-button
|
||||||
data-testid="toggle-notes-sorting-button"
|
data-testid="toggle-notes-sorting-button"
|
||||||
class="${this.enableNotesSorting ? 'active' : ''}"
|
class="${sortingEnabled ? 'active' : ''}"
|
||||||
.tooltip=${'Visibility and sort'}
|
.tooltip=${'Visibility and sort'}
|
||||||
.tipPosition=${'left'}
|
.tipPosition=${'left'}
|
||||||
.iconContainerPadding=${0}
|
.iconContainerPadding=${0}
|
||||||
.active=${this.enableNotesSorting}
|
.active=${sortingEnabled}
|
||||||
.activeMode=${'color'}
|
.activeMode=${'color'}
|
||||||
@click=${() => this.toggleNotesSorting()}
|
@click=${() => {
|
||||||
|
this._context.enableSorting$.value = !sortingEnabled;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
${SortIcon({ width: '20px', height: '20px' })}
|
${SortIcon({ width: '20px', height: '20px' })}
|
||||||
</edgeless-tool-icon-button>
|
</edgeless-tool-icon-button>
|
||||||
</div>
|
</div>
|
||||||
<div class=${styles.notePreviewSettingContainer}>
|
<div class=${styles.notePreviewSettingContainer}>
|
||||||
<affine-outline-note-preview-setting-menu
|
<affine-outline-note-preview-setting-menu></affine-outline-note-preview-setting-menu>
|
||||||
.showPreviewIcon=${this.showPreviewIcon}
|
|
||||||
.toggleShowPreviewIcon=${this.toggleShowPreviewIcon}
|
|
||||||
></affine-outline-note-preview-setting-menu>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,20 +81,8 @@ export class OutlinePanelHeader extends WithDisposable(ShadowlessElement) {
|
|||||||
@query(`.${styles.noteSettingContainer}`)
|
@query(`.${styles.noteSettingContainer}`)
|
||||||
private accessor _noteSettingButton!: HTMLDivElement;
|
private accessor _noteSettingButton!: HTMLDivElement;
|
||||||
|
|
||||||
@state()
|
@consume({ context: tocContext })
|
||||||
private accessor _settingPopperShow = false;
|
private accessor _context!: TocContext;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor enableNotesSorting!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor showPreviewIcon!: boolean;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor toggleNotesSorting!: () => void;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor toggleShowPreviewIcon!: (on: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||||
import { WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
|
import { consume } from '@lit/context';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
|
||||||
|
|
||||||
|
import { type TocContext, tocContext } from '../config';
|
||||||
import * as styles from './outline-setting-menu.css';
|
import * as styles from './outline-setting-menu.css';
|
||||||
|
|
||||||
export const AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU =
|
export const AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU =
|
||||||
'affine-outline-note-preview-setting-menu';
|
'affine-outline-note-preview-setting-menu';
|
||||||
|
|
||||||
export class OutlineNotePreviewSettingMenu extends WithDisposable(
|
export class OutlineNotePreviewSettingMenu extends SignalWatcher(
|
||||||
ShadowlessElement
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
override render() {
|
override render() {
|
||||||
|
const showPreviewIcon = this._context.showIcons$.value;
|
||||||
|
|
||||||
return html`<div
|
return html`<div
|
||||||
class=${styles.notePreviewSettingMenuContainer}
|
class=${styles.notePreviewSettingMenuContainer}
|
||||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||||
@@ -23,19 +26,18 @@ export class OutlineNotePreviewSettingMenu extends WithDisposable(
|
|||||||
<div class=${styles.actionLabel}>Show type icon</div>
|
<div class=${styles.actionLabel}>Show type icon</div>
|
||||||
<div class=${styles.toggleButton}>
|
<div class=${styles.toggleButton}>
|
||||||
<toggle-switch
|
<toggle-switch
|
||||||
.on=${this.showPreviewIcon}
|
.on=${showPreviewIcon}
|
||||||
.onChange=${this.toggleShowPreviewIcon}
|
.onChange=${() => {
|
||||||
|
this._context.showIcons$.value = !showPreviewIcon;
|
||||||
|
}}
|
||||||
></toggle-switch>
|
></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false })
|
@consume({ context: tocContext })
|
||||||
accessor showPreviewIcon!: boolean;
|
private accessor _context!: TocContext;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor toggleShowPreviewIcon!: (on: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ import {
|
|||||||
} from '@blocksuite/block-std';
|
} from '@blocksuite/block-std';
|
||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
import { provide } from '@lit/context';
|
import { provide } from '@lit/context';
|
||||||
import { effect } from '@preact/signals-core';
|
import { effect, signal } from '@preact/signals-core';
|
||||||
import { html, type PropertyValues } from 'lit';
|
import { html, type PropertyValues } from 'lit';
|
||||||
import { property, state } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
||||||
import {
|
import { outlineSettingsKey, type TocContext, tocContext } from './config.js';
|
||||||
editorContext,
|
|
||||||
type OutlineSettingsDataType,
|
|
||||||
outlineSettingsKey,
|
|
||||||
} from './config.js';
|
|
||||||
import * as styles from './outline-panel.css';
|
import * as styles from './outline-panel.css';
|
||||||
|
|
||||||
export const AFFINE_OUTLINE_PANEL = 'affine-outline-panel';
|
export const AFFINE_OUTLINE_PANEL = 'affine-outline-panel';
|
||||||
@@ -25,120 +21,81 @@ export const AFFINE_OUTLINE_PANEL = 'affine-outline-panel';
|
|||||||
export class OutlinePanel extends SignalWatcher(
|
export class OutlinePanel extends SignalWatcher(
|
||||||
WithDisposable(ShadowlessElement)
|
WithDisposable(ShadowlessElement)
|
||||||
) {
|
) {
|
||||||
private readonly _setNoticeVisibility = (visibility: boolean) => {
|
private _setContext() {
|
||||||
this._noticeVisible = visibility;
|
this._context = {
|
||||||
};
|
editor$: signal(this.editor),
|
||||||
|
showIcons$: signal<boolean>(false),
|
||||||
|
enableSorting$: signal<boolean>(false),
|
||||||
|
fitPadding$: signal<number[]>(this.fitPadding),
|
||||||
|
};
|
||||||
|
|
||||||
private _settings: OutlineSettingsDataType = {
|
this.disposables.add(
|
||||||
showIcons: false,
|
effect(() => {
|
||||||
enableSorting: false,
|
const settingsString = localStorage.getItem(outlineSettingsKey);
|
||||||
};
|
const settings = settingsString ? JSON.parse(settingsString) : null;
|
||||||
|
|
||||||
private readonly _toggleNotesSorting = () => {
|
if (settings) {
|
||||||
this._enableNotesSorting = !this._enableNotesSorting;
|
this._context.showIcons$.value = settings.showIcons;
|
||||||
this._updateAndSaveSettings({ enableSorting: this._enableNotesSorting });
|
}
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _toggleShowPreviewIcon = (on: boolean) => {
|
const editor = this._context.editor$.value;
|
||||||
this._showPreviewIcon = on;
|
if (editor.mode === 'edgeless') {
|
||||||
this._updateAndSaveSettings({ showIcons: on });
|
this._context.enableSorting$.value = true;
|
||||||
};
|
} else if (settings) {
|
||||||
|
this._context.enableSorting$.value = settings.enableSorting;
|
||||||
get doc() {
|
}
|
||||||
return this.editor.doc;
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get host() {
|
private _watchSettingsChange() {
|
||||||
return this.editor.host;
|
this.disposables.add(
|
||||||
}
|
effect(() => {
|
||||||
|
if (this._context.editor$.value.mode === 'edgeless') return;
|
||||||
|
|
||||||
get mode() {
|
const showPreviewIcon = this._context.showIcons$.value;
|
||||||
return this.editor.mode;
|
const enableNotesSorting = this._context.enableSorting$.value;
|
||||||
}
|
localStorage.setItem(
|
||||||
|
outlineSettingsKey,
|
||||||
private _loadSettingsFromLocalStorage() {
|
JSON.stringify({
|
||||||
const settings = localStorage.getItem(outlineSettingsKey);
|
showIcons: showPreviewIcon,
|
||||||
if (settings) {
|
enableSorting: enableNotesSorting,
|
||||||
this._settings = JSON.parse(settings);
|
})
|
||||||
this._showPreviewIcon = this._settings.showIcons;
|
);
|
||||||
this._enableNotesSorting = this._settings.enableSorting;
|
})
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
private _saveSettingsToLocalStorage() {
|
|
||||||
localStorage.setItem(outlineSettingsKey, JSON.stringify(this._settings));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateAndSaveSettings(
|
|
||||||
newSettings: Partial<OutlineSettingsDataType>
|
|
||||||
) {
|
|
||||||
this._settings = { ...this._settings, ...newSettings };
|
|
||||||
this._saveSettingsToLocalStorage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.classList.add(styles.outlinePanel);
|
this.classList.add(styles.outlinePanel);
|
||||||
|
|
||||||
this.disposables.add(
|
this._setContext();
|
||||||
effect(() => {
|
this._watchSettingsChange();
|
||||||
if (this.editor.mode === 'edgeless') {
|
|
||||||
this._enableNotesSorting = true;
|
|
||||||
} else {
|
|
||||||
this._loadSettingsFromLocalStorage();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override willUpdate(_changedProperties: PropertyValues): void {
|
override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||||
if (_changedProperties.has('editor')) {
|
if (changedProperties.has('editor')) {
|
||||||
if (this.editor.mode === 'edgeless') {
|
this._context.editor$.value = this.editor;
|
||||||
this._enableNotesSorting = true;
|
}
|
||||||
} else {
|
if (changedProperties.has('fitPadding')) {
|
||||||
this._loadSettingsFromLocalStorage();
|
this._context.fitPadding$.value = this.fitPadding;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
if (!this.host) return;
|
if (!this.editor.host) return;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<affine-outline-panel-header
|
<affine-outline-panel-header></affine-outline-panel-header>
|
||||||
.showPreviewIcon=${this._showPreviewIcon}
|
<affine-outline-panel-body> </affine-outline-panel-body>
|
||||||
.enableNotesSorting=${this._enableNotesSorting}
|
<affine-outline-notice></affine-outline-notice>
|
||||||
.toggleShowPreviewIcon=${this._toggleShowPreviewIcon}
|
|
||||||
.toggleNotesSorting=${this._toggleNotesSorting}
|
|
||||||
></affine-outline-panel-header>
|
|
||||||
<affine-outline-panel-body
|
|
||||||
.fitPadding=${this.fitPadding}
|
|
||||||
.mode=${this.mode}
|
|
||||||
.showPreviewIcon=${this._showPreviewIcon}
|
|
||||||
.enableNotesSorting=${this._enableNotesSorting}
|
|
||||||
.toggleNotesSorting=${this._toggleNotesSorting}
|
|
||||||
.noticeVisible=${this._noticeVisible}
|
|
||||||
.setNoticeVisibility=${this._setNoticeVisibility}
|
|
||||||
>
|
|
||||||
</affine-outline-panel-body>
|
|
||||||
<affine-outline-notice
|
|
||||||
.noticeVisible=${this._noticeVisible}
|
|
||||||
.toggleNotesSorting=${this._toggleNotesSorting}
|
|
||||||
.setNoticeVisibility=${this._setNoticeVisibility}
|
|
||||||
></affine-outline-notice>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@state()
|
@provide({ context: tocContext })
|
||||||
private accessor _enableNotesSorting = false;
|
private accessor _context!: TocContext;
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _noticeVisible = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _showPreviewIcon = false;
|
|
||||||
|
|
||||||
@provide({ context: editorContext })
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor editor!: AffineEditorContainer;
|
accessor editor!: AffineEditorContainer;
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
|||||||
import { TocIcon } from '@blocksuite/icons/lit';
|
import { TocIcon } from '@blocksuite/icons/lit';
|
||||||
import { provide } from '@lit/context';
|
import { provide } from '@lit/context';
|
||||||
import { signal } from '@preact/signals-core';
|
import { signal } from '@preact/signals-core';
|
||||||
import { css, html, nothing } from 'lit';
|
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
import type { AffineEditorContainer } from '../../editors/editor-container.js';
|
||||||
import { editorContext } from './config.js';
|
import { type TocContext, tocContext } from './config.js';
|
||||||
import { getHeadingBlocksFromDoc } from './utils/query.js';
|
import { getHeadingBlocksFromDoc } from './utils/query.js';
|
||||||
import {
|
import {
|
||||||
observeActiveHeadingDuringScroll,
|
observeActiveHeadingDuringScroll,
|
||||||
@@ -176,6 +176,15 @@ export class OutlineViewer extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _setContext() {
|
||||||
|
this._context = {
|
||||||
|
editor$: signal(this.editor),
|
||||||
|
showIcons$: signal<boolean>(false),
|
||||||
|
enableSorting$: signal<boolean>(false),
|
||||||
|
fitPadding$: signal<number[]>([]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -194,6 +203,14 @@ export class OutlineViewer extends SignalWatcher(
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._setContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||||
|
if (changedProperties.has('editor')) {
|
||||||
|
this._context.editor$.value = this.editor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
@@ -281,13 +298,15 @@ export class OutlineViewer extends SignalWatcher(
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@provide({ context: tocContext })
|
||||||
|
private accessor _context!: TocContext;
|
||||||
|
|
||||||
@query('.outline-viewer-item.active')
|
@query('.outline-viewer-item.active')
|
||||||
private accessor _activeItem: HTMLElement | null = null;
|
private accessor _activeItem: HTMLElement | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor _showViewer: boolean = false;
|
private accessor _showViewer: boolean = false;
|
||||||
|
|
||||||
@provide({ context: editorContext })
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor editor!: AffineEditorContainer;
|
accessor editor!: AffineEditorContainer;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { on, once } from '@blocksuite/blocks';
|
import { NoteDisplayMode, on, once } from '@blocksuite/blocks';
|
||||||
import type { Store } from '@blocksuite/store';
|
import type { Store } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { OutlinePanelBody } from '../body/outline-panel-body.js';
|
import type { OutlinePanelBody } from '../body/outline-panel-body.js';
|
||||||
@@ -52,13 +52,14 @@ export function startDragging(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
idx = 0;
|
idx = 0;
|
||||||
for (const note of children) {
|
for (const card of children) {
|
||||||
if (note.invisible || !note.note) break;
|
if (!card.note || card.note.displayMode === NoteDisplayMode.EdgelessOnly)
|
||||||
|
break;
|
||||||
|
|
||||||
const topBoundary =
|
const topBoundary =
|
||||||
listContainerRect.top + note.offsetTop - outlineListContainer.scrollTop;
|
listContainerRect.top + card.offsetTop - outlineListContainer.scrollTop;
|
||||||
const midBoundary = topBoundary + note.offsetHeight / 2;
|
const midBoundary = topBoundary + card.offsetHeight / 2;
|
||||||
const bottomBoundary = topBoundary + note.offsetHeight;
|
const bottomBoundary = topBoundary + card.offsetHeight;
|
||||||
|
|
||||||
if (e.clientY >= topBoundary && e.clientY <= bottomBoundary) {
|
if (e.clientY >= topBoundary && e.clientY <= bottomBoundary) {
|
||||||
idx = e.clientY > midBoundary ? idx + 1 : idx;
|
idx = e.clientY > midBoundary ? idx + 1 : idx;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
clickPageModeButton,
|
clickPageModeButton,
|
||||||
clickView,
|
clickView,
|
||||||
createEdgelessNoteBlock,
|
createEdgelessNoteBlock,
|
||||||
|
focusDocTitle,
|
||||||
locateElementToolbar,
|
locateElementToolbar,
|
||||||
} from '@affine-test/kit/utils/editor';
|
} from '@affine-test/kit/utils/editor';
|
||||||
import {
|
import {
|
||||||
@@ -33,8 +34,10 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
async function openTocPanel(page: Page) {
|
async function openTocPanel(page: Page) {
|
||||||
await openRightSideBar(page, 'outline');
|
|
||||||
const toc = page.locator('affine-outline-panel');
|
const toc = page.locator('affine-outline-panel');
|
||||||
|
if (await toc.isVisible()) return toc;
|
||||||
|
|
||||||
|
await openRightSideBar(page, 'outline');
|
||||||
await toc.waitFor({ state: 'visible' });
|
await toc.waitFor({ state: 'visible' });
|
||||||
return toc;
|
return toc;
|
||||||
}
|
}
|
||||||
@@ -43,14 +46,15 @@ function getTocHeading(panel: Locator, level: number) {
|
|||||||
return panel.getByTestId(`outline-block-preview-h${level}`).locator('span');
|
return panel.getByTestId(`outline-block-preview-h${level}`).locator('span');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) {
|
// locate cards in outline panel
|
||||||
const fromRect = await fromCard.boundingBox();
|
// ! Please note that when any card mode changed, the locator will be mutated
|
||||||
const toRect = await toCard.boundingBox();
|
function locateCards(toc: Locator, mode?: 'both' | 'page' | 'edgeless') {
|
||||||
|
const cards = toc.locator('affine-outline-note-card');
|
||||||
|
return mode ? cards.locator(`[data-visibility="${mode}"]`) : cards;
|
||||||
|
}
|
||||||
|
|
||||||
await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10);
|
function locateSortingButton(panel: Locator) {
|
||||||
await page.mouse.down();
|
return panel.getByTestId('toggle-notes-sorting-button');
|
||||||
await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 20 });
|
|
||||||
await page.mouse.up();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -132,6 +136,18 @@ test('should update panel when modify or clear title or headings', async ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should update panel when switch doc', async ({ page }) => {
|
||||||
|
const toc = await openTocPanel(page);
|
||||||
|
await focusDocTitle(page);
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await type(page, '# Heading 1');
|
||||||
|
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await expect(getTocHeading(toc, 1)).toBeHidden();
|
||||||
|
await page.goBack();
|
||||||
|
await expect(getTocHeading(toc, 1)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test('should add padding to sub-headings', async ({ page }) => {
|
test('should add padding to sub-headings', async ({ page }) => {
|
||||||
await createHeadings(page);
|
await createHeadings(page);
|
||||||
|
|
||||||
@@ -226,8 +242,7 @@ test('visibility sorting should be enabled in edgeless mode and disabled in page
|
|||||||
await type(page, '# Heading 1');
|
await type(page, '# Heading 1');
|
||||||
|
|
||||||
const toc = await openTocPanel(page);
|
const toc = await openTocPanel(page);
|
||||||
|
const sortingButton = locateSortingButton(toc);
|
||||||
const sortingButton = toc.getByTestId('toggle-notes-sorting-button');
|
|
||||||
await expect(sortingButton).not.toHaveClass(/active/);
|
await expect(sortingButton).not.toHaveClass(/active/);
|
||||||
expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
|
expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
|
||||||
|
|
||||||
@@ -240,55 +255,99 @@ test('visibility sorting should be enabled in edgeless mode and disabled in page
|
|||||||
expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
|
expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reorder notes when drag and drop note in outline panel', async ({
|
test.describe('drag and drop note in outline panel', () => {
|
||||||
page,
|
async function changeNoteDisplayMode(
|
||||||
}) => {
|
card: Locator,
|
||||||
await clickEdgelessModeButton(page);
|
mode: 'both' | 'doc' | 'edgeless'
|
||||||
await createEdgelessNoteBlock(page, [100, 100]);
|
) {
|
||||||
await type(page, 'hello');
|
|
||||||
await createEdgelessNoteBlock(page, [200, 200]);
|
|
||||||
await type(page, 'world');
|
|
||||||
|
|
||||||
const toc = await openTocPanel(page);
|
|
||||||
|
|
||||||
const docVisibleCards = toc.locator(
|
|
||||||
'affine-outline-note-card [data-invisible="false"]'
|
|
||||||
);
|
|
||||||
const docInvisibleCards = toc.locator(
|
|
||||||
'affine-outline-note-card [data-invisible="true"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(docVisibleCards).toHaveCount(1);
|
|
||||||
await expect(docInvisibleCards).toHaveCount(2);
|
|
||||||
|
|
||||||
while ((await docInvisibleCards.count()) > 0) {
|
|
||||||
const card = docInvisibleCards.first();
|
|
||||||
await card.hover();
|
await card.hover();
|
||||||
await card.getByTestId('display-mode-button').click();
|
await card.getByTestId('display-mode-button').click();
|
||||||
await card.locator('note-display-mode-panel').locator('.item.both').click();
|
await card.locator(`note-display-mode-panel .item.${mode}`).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(docVisibleCards).toHaveCount(3);
|
async function dragNoteCard(
|
||||||
const noteCard3 = docVisibleCards.nth(2);
|
page: Page,
|
||||||
const noteCard1 = docVisibleCards.nth(0);
|
fromCard: Locator,
|
||||||
|
toCard: Locator,
|
||||||
|
position: 'before' | 'after' = 'before'
|
||||||
|
) {
|
||||||
|
const fromRect = await fromCard.boundingBox();
|
||||||
|
const toRect = await toCard.boundingBox();
|
||||||
|
|
||||||
await dragNoteCard(page, noteCard3, noteCard1);
|
await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10);
|
||||||
|
await page.mouse.down();
|
||||||
|
if (position === 'before') {
|
||||||
|
await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 20 });
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(toRect!.x + 5, toRect!.y + toRect!.height - 5, {
|
||||||
|
steps: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
await clickPageModeButton(page);
|
// create 2 both cards, 2 page cards and 2 edgeless cards
|
||||||
const paragraphs = page
|
test.beforeEach(async ({ page }) => {
|
||||||
.locator('affine-paragraph')
|
const toc = await openTocPanel(page);
|
||||||
.locator('[data-v-text="true"]');
|
const edgelessCards = locateCards(toc, 'edgeless');
|
||||||
await expect(paragraphs).toHaveCount(3);
|
|
||||||
await expect(paragraphs.nth(0)).toContainText('world');
|
|
||||||
await expect(paragraphs.nth(1)).toContainText('');
|
|
||||||
await expect(paragraphs.nth(2)).toContainText('hello');
|
|
||||||
|
|
||||||
// FIXME(@L-Sun): drag and drop is not working in page mode
|
// 2 both cards
|
||||||
await dragNoteCard(page, noteCard3, noteCard1);
|
{
|
||||||
|
await focusDocTitle(page);
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await type(page, '0');
|
||||||
|
|
||||||
await expect(paragraphs.nth(0)).toContainText('hello');
|
await clickEdgelessModeButton(page);
|
||||||
await expect(paragraphs.nth(1)).toContainText('world');
|
|
||||||
await expect(paragraphs.nth(2)).toContainText('');
|
await createEdgelessNoteBlock(page, [100, 100]);
|
||||||
|
await type(page, '1');
|
||||||
|
await changeNoteDisplayMode(edgelessCards.first(), 'both');
|
||||||
|
}
|
||||||
|
// 2 page cards
|
||||||
|
{
|
||||||
|
await createEdgelessNoteBlock(page, [150, 150]);
|
||||||
|
await type(page, '2');
|
||||||
|
await changeNoteDisplayMode(edgelessCards.first(), 'doc');
|
||||||
|
await createEdgelessNoteBlock(page, [200, 200]);
|
||||||
|
await type(page, '3');
|
||||||
|
await changeNoteDisplayMode(edgelessCards.first(), 'doc');
|
||||||
|
}
|
||||||
|
// 2 edgeless cards
|
||||||
|
{
|
||||||
|
await createEdgelessNoteBlock(page, [250, 250]);
|
||||||
|
await type(page, '4');
|
||||||
|
await createEdgelessNoteBlock(page, [300, 300]);
|
||||||
|
await type(page, '5');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reorder notes when drag and drop a note in outline panel', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const toc = await openTocPanel(page);
|
||||||
|
const cards = locateCards(toc);
|
||||||
|
|
||||||
|
await dragNoteCard(page, cards.nth(3), cards.nth(1));
|
||||||
|
|
||||||
|
await clickPageModeButton(page);
|
||||||
|
const paragraphs = page
|
||||||
|
.locator('affine-paragraph')
|
||||||
|
.locator('[data-v-text="true"]');
|
||||||
|
await expect(paragraphs).toHaveCount(4);
|
||||||
|
await expect(paragraphs.nth(0)).toContainText('0');
|
||||||
|
await expect(paragraphs.nth(1)).toContainText('3');
|
||||||
|
await expect(paragraphs.nth(2)).toContainText('1');
|
||||||
|
await expect(paragraphs.nth(3)).toContainText('2');
|
||||||
|
|
||||||
|
// Note card should be able to drag and drop in page mode
|
||||||
|
await locateSortingButton(toc).click();
|
||||||
|
await dragNoteCard(page, cards.nth(3), cards.nth(1));
|
||||||
|
|
||||||
|
await expect(paragraphs.nth(0)).toContainText('0');
|
||||||
|
await expect(paragraphs.nth(1)).toContainText('2');
|
||||||
|
await expect(paragraphs.nth(2)).toContainText('3');
|
||||||
|
await expect(paragraphs.nth(3)).toContainText('1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('advanced visibility control', () => {
|
test.describe('advanced visibility control', () => {
|
||||||
@@ -311,15 +370,11 @@ test.describe('advanced visibility control', () => {
|
|||||||
|
|
||||||
const toc = await openTocPanel(page);
|
const toc = await openTocPanel(page);
|
||||||
|
|
||||||
const docVisibleCard = toc.locator(
|
const bothCard = locateCards(toc, 'both');
|
||||||
'affine-outline-note-card [data-invisible="false"]'
|
const edgelessCard = locateCards(toc, 'edgeless');
|
||||||
);
|
|
||||||
const docInvisibleCard = toc.locator(
|
|
||||||
'affine-outline-note-card [data-invisible="true"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(docVisibleCard).toHaveCount(1);
|
await expect(bothCard).toHaveCount(1);
|
||||||
await expect(docInvisibleCard).toHaveCount(1);
|
await expect(edgelessCard).toHaveCount(1);
|
||||||
|
|
||||||
await clickView(page, [100, 100]);
|
await clickView(page, [100, 100]);
|
||||||
const noteButtons = locateElementToolbar(page).locator(
|
const noteButtons = locateElementToolbar(page).locator(
|
||||||
@@ -329,8 +384,8 @@ test.describe('advanced visibility control', () => {
|
|||||||
await noteButtons.getByRole('button', { name: 'Mode' }).click();
|
await noteButtons.getByRole('button', { name: 'Mode' }).click();
|
||||||
await noteButtons.locator('note-display-mode-panel .item.both').click();
|
await noteButtons.locator('note-display-mode-panel .item.both').click();
|
||||||
|
|
||||||
await expect(docVisibleCard).toHaveCount(2);
|
await expect(bothCard).toHaveCount(2);
|
||||||
await expect(docInvisibleCard).toHaveCount(0);
|
await expect(edgelessCard).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update notes after slicing note', async ({ page }) => {
|
test('should update notes after slicing note', async ({ page }) => {
|
||||||
@@ -342,23 +397,17 @@ test.describe('advanced visibility control', () => {
|
|||||||
|
|
||||||
const toc = await openTocPanel(page);
|
const toc = await openTocPanel(page);
|
||||||
|
|
||||||
const docVisibleCard = toc.locator(
|
const bothCard = locateCards(toc, 'both');
|
||||||
'affine-outline-note-card [data-invisible="false"]'
|
const edgelessCard = locateCards(toc, 'edgeless');
|
||||||
);
|
|
||||||
const docInvisibleCard = toc.locator(
|
|
||||||
'affine-outline-note-card [data-invisible="true"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(docVisibleCard).toHaveCount(1);
|
await expect(bothCard).toHaveCount(1);
|
||||||
await expect(docInvisibleCard).toHaveCount(1);
|
await expect(edgelessCard).toHaveCount(1);
|
||||||
|
|
||||||
await docInvisibleCard.hover();
|
await edgelessCard.hover();
|
||||||
await docInvisibleCard.getByTestId('display-mode-button').click();
|
await edgelessCard.getByTestId('display-mode-button').click();
|
||||||
await docInvisibleCard
|
await edgelessCard.locator('note-display-mode-panel .item.both').click();
|
||||||
.locator('note-display-mode-panel .item.both')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(docVisibleCard).toHaveCount(2);
|
await expect(bothCard).toHaveCount(2);
|
||||||
|
|
||||||
await clickView(page, [200, 100]);
|
await clickView(page, [200, 100]);
|
||||||
const changeNoteButtons = locateElementToolbar(page).locator(
|
const changeNoteButtons = locateElementToolbar(page).locator(
|
||||||
@@ -368,6 +417,6 @@ test.describe('advanced visibility control', () => {
|
|||||||
await expect(page.locator('.note-slicer-button')).toBeVisible();
|
await expect(page.locator('.note-slicer-button')).toBeVisible();
|
||||||
await page.locator('.note-slicer-button').click();
|
await page.locator('.note-slicer-button').click();
|
||||||
|
|
||||||
await expect(docVisibleCard).toHaveCount(3);
|
await expect(bothCard).toHaveCount(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ export function locateEditorContainer(page: Page, editorIndex = 0) {
|
|||||||
return page.locator('[data-affine-editor-container]').nth(editorIndex);
|
return page.locator('[data-affine-editor-container]').nth(editorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function locateDocTitle(page: Page, editorIndex = 0) {
|
||||||
|
return locateEditorContainer(page, editorIndex).locator('doc-title');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function focusDocTitle(page: Page, editorIndex = 0) {
|
||||||
|
await locateDocTitle(page, editorIndex).locator('.inline-editor').focus();
|
||||||
|
}
|
||||||
|
|
||||||
// ================== Page ==================
|
// ================== Page ==================
|
||||||
export function locateFormatBar(page: Page, editorIndex = 0) {
|
export function locateFormatBar(page: Page, editorIndex = 0) {
|
||||||
return locateEditorContainer(page, editorIndex).locator(
|
return locateEditorContainer(page, editorIndex).locator(
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export const dragTo = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// sometimes editor loses focus, this function is to focus the editor
|
// sometimes editor loses focus, this function is to focus the editor
|
||||||
|
// FIXME: this function is not usable since the placeholder is not unstable
|
||||||
export const focusInlineEditor = async (page: Page) => {
|
export const focusInlineEditor = async (page: Page) => {
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
|
|||||||
Reference in New Issue
Block a user