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