refactor(editor): refactor page note empty checker (#9570)

Close [BS-2320](https://linear.app/affine-design/issue/BS-2320/内容为空的状态判断)
This commit is contained in:
L-Sun
2025-01-07 11:24:40 +00:00
parent 482b534a90
commit 440239809c
11 changed files with 187 additions and 18 deletions
@@ -1,6 +1,8 @@
import { beforeEach, expect, test } from 'vitest';
import { LocalShapeElementModel } from '@blocksuite/affine-model';
import { Text } from '@blocksuite/store';
import { beforeEach, describe, expect, test } from 'vitest';
import { getSurface } from '../utils/edgeless.js';
import { addNote, getSurface } from '../utils/edgeless.js';
import { setupEditor } from '../utils/setup.js';
beforeEach(async () => {
@@ -16,3 +18,136 @@ test('basic assert', () => {
expect(getSurface(window.doc, window.editor)).toBeDefined();
});
describe('doc / note empty checker', () => {
test('a paragraph is empty if it dose not contain text and child blocks', () => {
const noteId = addNote(doc);
const paragraphId = doc.addBlock('affine:paragraph', {}, noteId);
const paragraph = doc.getBlock(paragraphId)?.model;
expect(paragraph?.isEmpty()).toBe(true);
});
test('a paragraph is not empty if it contains text', () => {
const noteId = addNote(doc);
const paragraphId = doc.addBlock(
'affine:paragraph',
{
text: new Text('hello'),
},
noteId
);
const paragraph = doc.getBlock(paragraphId)?.model;
expect(paragraph?.isEmpty()).toBe(false);
});
test('a paragraph is not empty if it contains children blocks', () => {
const noteId = addNote(doc);
const paragraphId = doc.addBlock('affine:paragraph', {}, noteId);
const paragraph = doc.getBlock(paragraphId)?.model;
// sub paragraph
doc.addBlock('affine:paragraph', {}, paragraphId);
expect(paragraph?.isEmpty()).toBe(false);
});
test('a note is empty if it dose not contain any blocks', () => {
const noteId = addNote(doc);
const note = doc.getBlock(noteId)!.model;
note.children.forEach(child => {
doc.deleteBlock(child);
});
expect(note.children.length).toBe(0);
expect(note.isEmpty()).toBe(true);
});
test('a note is empty if it only contains a empty paragraph', () => {
// `addNote` will create a empty paragraph
const noteId = addNote(doc);
const note = doc.getBlock(noteId)!.model;
expect(note.isEmpty()).toBe(true);
});
test('a note is not empty if it contains multi blocks', () => {
const noteId = addNote(doc);
const note = doc.getBlock(noteId)!.model;
doc.addBlock('affine:paragraph', {}, noteId);
expect(note.isEmpty()).toBe(false);
});
test('a surface is empty if it dose not contains any element or blocks', () => {
const surface = getSurface(doc, editor).model;
expect(surface.isEmpty()).toBe(true);
const shapeId = surface.addElement({
type: 'shape',
});
expect(surface.isEmpty()).toBe(false);
surface.deleteElement(shapeId);
expect(surface.isEmpty()).toBe(true);
const frameId = doc.addBlock('affine:frame', {}, surface.id);
const frame = doc.getBlock(frameId)!.model;
expect(surface.isEmpty()).toBe(false);
doc.deleteBlock(frame);
expect(surface.isEmpty()).toBe(true);
});
test('a surface is empty if it only contains local elements', () => {
const surface = getSurface(doc, editor).model;
const localShape = new LocalShapeElementModel(surface);
surface.addLocalElement(localShape);
expect(surface.isEmpty()).toBe(true);
});
test('a just initialized doc is empty', () => {
expect(doc.isEmpty).toBe(true);
expect(editor.rootModel.isEmpty()).toBe(true);
});
test('a doc is empty if it only contains a note', () => {
addNote(doc);
expect(doc.isEmpty).toBe(true);
addNote(doc);
expect(
doc.isEmpty,
'a doc is not empty if it contains multi-notes'
).toBeFalsy();
});
test('a note is empty if its children array is empty', () => {
const noteId = addNote(doc);
const note = doc.getBlock(noteId)?.model;
note?.children.forEach(child => doc.deleteBlock(child));
expect(note?.children.length === 0).toBe(true);
expect(note?.isEmpty()).toBe(true);
});
test('a doc is empty if its only contains an empty note and an empty surface', () => {
const noteId = addNote(doc);
const note = doc.getBlock(noteId)!.model;
expect(doc.isEmpty).toBe(true);
const newNoteId = addNote(doc);
const newNote = doc.getBlock(newNoteId)!.model;
expect(doc.isEmpty).toBe(false);
doc.deleteBlock(newNote);
expect(doc.isEmpty).toBe(true);
const newParagraphId = doc.addBlock('affine:paragraph', {}, note);
const newParagraph = doc.getBlock(newParagraphId)!.model;
expect(doc.isEmpty).toBe(false);
doc.deleteBlock(newParagraph);
expect(doc.isEmpty).toBe(true);
const surface = getSurface(doc, editor).model;
expect(doc.isEmpty).toBe(true);
const shapeId = surface.addElement({
type: 'shape',
});
expect(doc.isEmpty).toBe(false);
surface.deleteElement(shapeId);
expect(doc.isEmpty).toBe(true);
});
});
@@ -61,9 +61,8 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
private readonly _onTitleKeyDown = (event: KeyboardEvent) => {
if (event.isComposing || this.doc.readonly) return;
const hasContent = !this.doc.isEmpty;
if (event.key === 'Enter' && hasContent && !event.isComposing) {
if (event.key === 'Enter' && this._pageRoot) {
event.preventDefault();
event.stopPropagation();
@@ -73,10 +72,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
const rightText = this._rootModel.title.split(inlineRange.index);
this._pageRoot.prependParagraphWithText(rightText);
}
} else if (event.key === 'ArrowDown' && hasContent) {
} else if (event.key === 'ArrowDown') {
event.preventDefault();
event.stopPropagation();
this._pageRoot.focusFirstParagraph();
this._pageRoot?.focusFirstParagraph();
} else if (event.key === 'Tab') {
event.preventDefault();
event.stopPropagation();
@@ -94,9 +93,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
}
private get _pageRoot() {
const pageRoot = this._viewport.querySelector('affine-page-root');
assertExists(pageRoot);
return pageRoot;
return this._viewport.querySelector('affine-page-root');
}
private get _rootModel() {
@@ -9,7 +9,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel, Blocks } from '@blocksuite/store';
import { baseTheme } from '@toeverything/theme';
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { css, html, LitElement, unsafeCSS } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -286,7 +286,7 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
}
}
override firstUpdated() {
override updated() {
this._displayModePopper = createButtonPopper(
this._displayModeButtonGroup,
this._displayModePanel,
@@ -303,8 +303,6 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
}
override render() {
if (this.note.isEmpty.peek()) return nothing;
const { children, displayMode } = this.note;
const currentMode = this._getCurrentModeLabel(displayMode);
const cardHeaderClasses = classMap({