mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
refactor(editor): refactor page note empty checker (#9570)
Close [BS-2320](https://linear.app/affine-design/issue/BS-2320/内容为空的状态判断)
This commit is contained in:
@@ -111,6 +111,17 @@ export class NoteBlockModel
|
|||||||
if (!this._isSelectable()) return false;
|
if (!this._isSelectable()) return false;
|
||||||
return super.intersectsBound(bound);
|
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 {
|
declare global {
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export class ParagraphBlockModel extends BlockModel<ParagraphProps> {
|
|||||||
override flavour!: 'affine:paragraph';
|
override flavour!: 'affine:paragraph';
|
||||||
|
|
||||||
override text!: Text;
|
override text!: Text;
|
||||||
|
|
||||||
|
override isEmpty(): boolean {
|
||||||
|
return this.text$.value.length === 0 && this.children.length === 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -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({
|
export const RootBlockSchema = defineBlockSchema({
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function getDropRectByPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bounds = table.getBoundingClientRect();
|
let bounds = table.getBoundingClientRect();
|
||||||
if (model.isEmpty.value) {
|
if (model.children.length === 0) {
|
||||||
result.flag = DropFlags.EmptyDatabase;
|
result.flag = DropFlags.EmptyDatabase;
|
||||||
|
|
||||||
if (point.y < bounds.top) return result;
|
if (point.y < bounds.top) return result;
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ export class PageRootBlockComponent extends BlockComponent<
|
|||||||
|
|
||||||
clipboardController = new PageClipboard(this);
|
clipboardController = new PageClipboard(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus the first paragraph in the default note block.
|
||||||
|
* If there is no paragraph, create one.
|
||||||
|
*/
|
||||||
focusFirstParagraph = () => {
|
focusFirstParagraph = () => {
|
||||||
const defaultNote = this._getDefaultNoteBlock();
|
const defaultNote = this._getDefaultNoteBlock();
|
||||||
const firstText = defaultNote?.children.find(block =>
|
const firstText = defaultNote?.children.find(block =>
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
return Object.keys(this._elementCtorMap);
|
return Object.keys(this._elementCtorMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override isEmpty(): boolean {
|
||||||
|
return this._elementModels.size === 0 && this.children.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.created.once(() => this._init());
|
this.created.once(() => this._init());
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ export class BlockModel<
|
|||||||
|
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
isEmpty = computed(() => {
|
isEmpty() {
|
||||||
return this._children.value.length === 0;
|
return this.children.length === 0;
|
||||||
});
|
}
|
||||||
|
|
||||||
keys!: string[];
|
keys!: string[];
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isEmpty() {
|
get isEmpty() {
|
||||||
return Object.values(this._blocks.peek()).length === 0;
|
return this.root?.isEmpty() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get loaded() {
|
get loaded() {
|
||||||
|
|||||||
@@ -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';
|
import { setupEditor } from '../utils/setup.js';
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -16,3 +18,136 @@ test('basic assert', () => {
|
|||||||
|
|
||||||
expect(getSurface(window.doc, window.editor)).toBeDefined();
|
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) => {
|
private readonly _onTitleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.isComposing || this.doc.readonly) return;
|
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.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
@@ -73,10 +72,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
|||||||
const rightText = this._rootModel.title.split(inlineRange.index);
|
const rightText = this._rootModel.title.split(inlineRange.index);
|
||||||
this._pageRoot.prependParagraphWithText(rightText);
|
this._pageRoot.prependParagraphWithText(rightText);
|
||||||
}
|
}
|
||||||
} else if (event.key === 'ArrowDown' && hasContent) {
|
} else if (event.key === 'ArrowDown') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this._pageRoot.focusFirstParagraph();
|
this._pageRoot?.focusFirstParagraph();
|
||||||
} else if (event.key === 'Tab') {
|
} else if (event.key === 'Tab') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -94,9 +93,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get _pageRoot() {
|
private get _pageRoot() {
|
||||||
const pageRoot = this._viewport.querySelector('affine-page-root');
|
return this._viewport.querySelector('affine-page-root');
|
||||||
assertExists(pageRoot);
|
|
||||||
return pageRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _rootModel() {
|
private get _rootModel() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||||
import type { BlockModel, Blocks } from '@blocksuite/store';
|
import type { BlockModel, Blocks } from '@blocksuite/store';
|
||||||
import { baseTheme } from '@toeverything/theme';
|
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 { property, query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.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._displayModePopper = createButtonPopper(
|
||||||
this._displayModeButtonGroup,
|
this._displayModeButtonGroup,
|
||||||
this._displayModePanel,
|
this._displayModePanel,
|
||||||
@@ -303,8 +303,6 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
if (this.note.isEmpty.peek()) return nothing;
|
|
||||||
|
|
||||||
const { children, displayMode } = this.note;
|
const { children, displayMode } = this.note;
|
||||||
const currentMode = this._getCurrentModeLabel(displayMode);
|
const currentMode = this._getCurrentModeLabel(displayMode);
|
||||||
const cardHeaderClasses = classMap({
|
const cardHeaderClasses = classMap({
|
||||||
|
|||||||
Reference in New Issue
Block a user