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:
L-Sun
2025-01-23 08:52:58 +00:00
parent 1b0758f111
commit 02bcecde72
14 changed files with 434 additions and 442 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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',
},

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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');

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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(

View File

@@ -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(