Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions blocksuite/affine/model/src/blocks/note/note-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ export class NoteBlockModel
if (!this._isSelectable()) return false;
return super.intersectsBound(bound);
}

override isEmpty(): boolean {
if (this.children.length === 0) return true;
if (this.children.length === 1) {
const firstChild = this.children[0];
if (firstChild.flavour === 'affine:paragraph') {
return firstChild.isEmpty();
}
}
return false;
}
}

declare global {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export class ParagraphBlockModel extends BlockModel<ParagraphProps> {
override flavour!: 'affine:paragraph';

override text!: Text;

override isEmpty(): boolean {
return this.text$.value.length === 0 && this.children.length === 0;
}
}

declare global {
Expand Down
16 changes: 16 additions & 0 deletions blocksuite/affine/model/src/blocks/root/root-block-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ export class RootBlockModel extends BlockModel<RootBlockProps> {
});
});
}

/**
* A page is empty if it only contains one empty note and the canvas is empty
*/
override isEmpty() {
let numNotes = 0;
let empty = true;
for (const child of this.children) {
empty = empty && child.isEmpty();

if (child.flavour === 'affine:note') numNotes++;
if (numNotes > 1) return false;
}

return empty;
}
}

export const RootBlockSchema = defineBlockSchema({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function getDropRectByPoint(
}

let bounds = table.getBoundingClientRect();
if (model.isEmpty.value) {
if (model.children.length === 0) {
result.flag = DropFlags.EmptyDatabase;

if (point.y < bounds.top) return result;
Expand Down
4 changes: 4 additions & 0 deletions blocksuite/blocks/src/root-block/page/page-root-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export class PageRootBlockComponent extends BlockComponent<

clipboardController = new PageClipboard(this);

/**
* Focus the first paragraph in the default note block.
* If there is no paragraph, create one.
*/
focusFirstParagraph = () => {
const defaultNote = this._getDefaultNoteBlock();
const firstText = defaultNote?.children.find(block =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
return Object.keys(this._elementCtorMap);
}

override isEmpty(): boolean {
return this._elementModels.size === 0 && this.children.length === 0;
}

constructor() {
super();
this.created.once(() => this._init());
Expand Down
6 changes: 3 additions & 3 deletions blocksuite/framework/store/src/model/block/block-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ export class BlockModel<

id!: string;

isEmpty = computed(() => {
return this._children.value.length === 0;
});
isEmpty() {
return this.children.length === 0;
}

keys!: string[];

Expand Down
2 changes: 1 addition & 1 deletion blocksuite/framework/store/src/model/blocks/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export class Blocks {
}

get isEmpty() {
return Object.values(this._blocks.peek()).length === 0;
return this.root?.isEmpty() ?? true;
}

get loaded() {
Expand Down
139 changes: 137 additions & 2 deletions blocksuite/presets/src/__tests__/edgeless/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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);
});
});
11 changes: 4 additions & 7 deletions blocksuite/presets/src/fragments/doc-title/doc-title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -286,7 +286,7 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
}
}

override firstUpdated() {
override updated() {
this._displayModePopper = createButtonPopper(
this._displayModeButtonGroup,
this._displayModePanel,
Expand All @@ -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({
Expand Down
Loading