- ${isCollapsable &&
- (!this._isFirstVisibleNote() || !this._enablePageHeader)
+ ${isCollapsable && !this._isPageBlock
? html`
+ matchFlavours(child, ['affine:note']) &&
+ child.displayMode !== NoteDisplayMode.EdgelessOnly
+ );
+ if (note) return note;
+
+ const noteId = this.doc.addBlock('affine:note', {}, this._rootModel, 0);
+ return this.doc.getBlock(noteId)?.model as NoteBlockModel;
+ }
+
private readonly _onTitleKeyDown = (event: KeyboardEvent) => {
if (event.isComposing || this.doc.readonly) return;
- if (event.key === 'Enter' && this._pageRoot) {
+ if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
- const inlineEditor = this._inlineEditor;
- const inlineRange = inlineEditor?.getInlineRange();
+ const inlineRange = this.inlineEditor?.getInlineRange();
if (inlineRange) {
const rightText = this._rootModel.title.split(inlineRange.index);
- this._pageRoot.prependParagraphWithText(rightText);
+ const newFirstParagraphId = this.doc.addBlock(
+ 'affine:paragraph',
+ { text: rightText },
+ this._getOrCreateFirstPageVisibleNote(),
+ 0
+ );
+ if (this._std) focusTextModel(this._std, newFirstParagraphId);
}
} else if (event.key === 'ArrowDown') {
event.preventDefault();
event.stopPropagation();
- this._pageRoot?.focusFirstParagraph();
+
+ const note = this._getOrCreateFirstPageVisibleNote();
+ const firstText = note?.children.find(block =>
+ matchFlavours(block, ['affine:paragraph', 'affine:list', 'affine:code'])
+ );
+ if (firstText) {
+ if (this._std) focusTextModel(this._std, firstText.id);
+ } else {
+ const newFirstParagraphId = this.doc.addBlock(
+ 'affine:paragraph',
+ {},
+ note,
+ 0
+ );
+ if (this._std) focusTextModel(this._std, newFirstParagraphId);
+ }
} else if (event.key === 'Tab') {
event.preventDefault();
event.stopPropagation();
@@ -89,12 +127,8 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
});
};
- private get _inlineEditor() {
- return this._richTextElement.inlineEditor;
- }
-
- private get _pageRoot() {
- return this._viewport.querySelector('affine-page-root');
+ private get _std() {
+ return this._viewport?.querySelector('editor-host')?.std;
}
private get _rootModel() {
@@ -102,9 +136,14 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
}
private get _viewport() {
- const el = this.closest('.affine-page-viewport');
- assertExists(el);
- return el;
+ return (
+ this.closest('.affine-page-viewport') ??
+ this.closest('.affine-edgeless-viewport')
+ );
+ }
+
+ get inlineEditor() {
+ return this._richTextElement.inlineEditor;
}
override connectedCallback() {
@@ -161,6 +200,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
.verticalScrollContainerGetter=${() => this._viewport}
.readonly=${this.doc.readonly}
.enableFormat=${false}
+ .wrapText=${this.wrapText}
>
`;
@@ -177,18 +217,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor doc!: Store;
-}
-export function getDocTitleByEditorHost(
- editorHost: EditorHost
-): DocTitle | null {
- const docViewport = editorHost.closest('.affine-page-viewport');
- if (!docViewport) return null;
- return docViewport.querySelector('doc-title');
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'doc-title': DocTitle;
- }
+ @property({ attribute: false })
+ accessor wrapText = false;
}
diff --git a/blocksuite/affine/components/src/doc-title/effects.ts b/blocksuite/affine/components/src/doc-title/effects.ts
new file mode 100644
index 0000000000..2a42c219ad
--- /dev/null
+++ b/blocksuite/affine/components/src/doc-title/effects.ts
@@ -0,0 +1,11 @@
+import { DocTitle } from './doc-title';
+
+export function effects() {
+ customElements.define('doc-title', DocTitle);
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'doc-title': DocTitle;
+ }
+}
diff --git a/blocksuite/affine/components/src/doc-title/index.ts b/blocksuite/affine/components/src/doc-title/index.ts
new file mode 100644
index 0000000000..f5565c645f
--- /dev/null
+++ b/blocksuite/affine/components/src/doc-title/index.ts
@@ -0,0 +1,3 @@
+export { DocTitle } from './doc-title';
+export { effects } from './effects';
+export { getDocTitleByEditorHost } from './utils';
diff --git a/blocksuite/affine/components/src/doc-title/utils.ts b/blocksuite/affine/components/src/doc-title/utils.ts
new file mode 100644
index 0000000000..d70c225941
--- /dev/null
+++ b/blocksuite/affine/components/src/doc-title/utils.ts
@@ -0,0 +1,11 @@
+import type { EditorHost } from '@blocksuite/block-std';
+
+import type { DocTitle } from './doc-title';
+
+export function getDocTitleByEditorHost(
+ editorHost: EditorHost
+): DocTitle | null {
+ const docViewport = editorHost.closest('.affine-page-viewport');
+ if (!docViewport) return null;
+ return docViewport.querySelector('doc-title');
+}
diff --git a/blocksuite/affine/shared/src/services/feature-flag-service.ts b/blocksuite/affine/shared/src/services/feature-flag-service.ts
index d38f82f8b8..bde494fa28 100644
--- a/blocksuite/affine/shared/src/services/feature-flag-service.ts
+++ b/blocksuite/affine/shared/src/services/feature-flag-service.ts
@@ -18,7 +18,7 @@ export interface BlockSuiteFlags {
enable_shape_shadow_blur: boolean;
enable_mobile_keyboard_toolbar: boolean;
enable_mobile_linked_doc_menu: boolean;
- enable_page_block_header: boolean;
+ enable_page_block: boolean;
}
export class FeatureFlagService extends StoreExtension {
@@ -41,7 +41,7 @@ export class FeatureFlagService extends StoreExtension {
enable_shape_shadow_blur: false,
enable_mobile_keyboard_toolbar: false,
enable_mobile_linked_doc_menu: false,
- enable_page_block_header: false,
+ enable_page_block: false,
});
setFlag(key: keyof BlockSuiteFlags, value: boolean) {
diff --git a/blocksuite/affine/widget-drag-handle/src/utils.ts b/blocksuite/affine/widget-drag-handle/src/utils.ts
index e1e5b2043d..82b7257b50 100644
--- a/blocksuite/affine/widget-drag-handle/src/utils.ts
+++ b/blocksuite/affine/widget-drag-handle/src/utils.ts
@@ -1,3 +1,7 @@
+import {
+ AFFINE_EDGELESS_NOTE,
+ type EdgelessNoteBlockComponent,
+} from '@blocksuite/affine-block-note';
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
@@ -155,7 +159,7 @@ export const getClosestNoteBlock = (
editorHost.std.get(DocModeProvider).getEditorMode() === 'page';
return isInsidePageEditor
? findClosestBlockComponent(rootComponent, point, 'affine-note')
- : getHoveringNote(point)?.closest('affine-edgeless-note');
+ : getHoveringNote(point);
};
export const getClosestBlockByPoint = (
@@ -261,11 +265,11 @@ export function getDuplicateBlocks(blocks: BlockModel[]) {
*/
function getHoveringNote(point: Point) {
return (
- document.elementsFromPoint(point.x, point.y).find(isEdgelessChildNote) ||
- null
+ document
+ .elementsFromPoint(point.x, point.y)
+ .find(
+ (e): e is EdgelessNoteBlockComponent =>
+ e.tagName.toLowerCase() === AFFINE_EDGELESS_NOTE
+ ) || null
);
}
-
-function isEdgelessChildNote({ classList }: Element) {
- return classList.contains('note-background');
-}
diff --git a/blocksuite/blocks/package.json b/blocksuite/blocks/package.json
index 5df6b4519f..c327d19887 100644
--- a/blocksuite/blocks/package.json
+++ b/blocksuite/blocks/package.json
@@ -84,6 +84,7 @@
"devDependencies": {
"@types/katex": "^0.16.7",
"@types/lodash.isequal": "^4.5.8",
+ "@vanilla-extract/vite-plugin": "^5.0.0",
"vitest": "3.0.5"
}
}
diff --git a/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts b/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts
index 6708abface..6963a1b89f 100644
--- a/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts
+++ b/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts
@@ -145,8 +145,8 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
.getFlag('enable_advanced_block_visibility');
}
- private get _pageBlockHeaderEnabled() {
- return this.doc.get(FeatureFlagService).getFlag('enable_page_block_header');
+ private get _pageBlockEnabled() {
+ return this.doc.get(FeatureFlagService).getFlag('enable_page_block');
}
private get doc() {
@@ -155,7 +155,7 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
private get _enableAutoHeight() {
return !(
- this._pageBlockHeaderEnabled &&
+ this._pageBlockEnabled &&
this.notes.length === 1 &&
this.notes[0].parent?.children.find(child =>
matchFlavours(child, ['affine:note'])
@@ -373,7 +373,7 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
onlyOne &&
!isFirstNote &&
- this._pageBlockHeaderEnabled &&
+ this._pageBlockEnabled &&
!this._advancedVisibilityEnabled
? html`
{
{ x: box.x + 50, y: box.y + box.height + 100 }
);
let noteRect = await getNoteRect(page, noteId);
- await expect(page.locator('.edgeless-note-collapse-button')).toBeVisible();
+ await expect(page.getByTestId('edgeless-note-collapse-button')).toBeVisible();
assertRectEqual(noteRect, {
x: initRect.x,
y: initRect.y,
@@ -102,11 +102,11 @@ test('resize note then collapse note', async ({ page }) => {
h: initRect.h + 100,
});
- await page.locator('.edgeless-note-collapse-button')!.click();
+ await page.getByTestId('edgeless-note-collapse-button')!.click();
let domRect = await page.locator('affine-edgeless-note').boundingBox();
expect(domRect!.height).toBeCloseTo(NOTE_MIN_HEIGHT);
- await page.locator('.edgeless-note-collapse-button')!.click();
+ await page.getByTestId('edgeless-note-collapse-button')!.click();
domRect = await page.locator('affine-edgeless-note').boundingBox();
expect(domRect!.height).toBeCloseTo(initRect.h + 100);
@@ -120,7 +120,7 @@ test('resize note then collapse note', async ({ page }) => {
);
noteRect = await getNoteRect(page, noteId);
await expect(
- page.locator('.edgeless-note-collapse-button')
+ page.getByTestId('edgeless-note-collapse-button')
).not.toBeVisible();
assertRectEqual(noteRect, {
x: initRect.x,
diff --git a/blocksuite/tests-legacy/edgeless/note/scale.spec.ts b/blocksuite/tests-legacy/edgeless/note/scale.spec.ts
index 97c65157f9..76ec6fc3d9 100644
--- a/blocksuite/tests-legacy/edgeless/note/scale.spec.ts
+++ b/blocksuite/tests-legacy/edgeless/note/scale.spec.ts
@@ -48,7 +48,7 @@ async function checkNoteScale(
const edgelessNote = page.locator(
`affine-edgeless-note[data-block-id="${noteId}"]`
);
- const noteContainer = edgelessNote.locator('.edgeless-note-container');
+ const noteContainer = edgelessNote.getByTestId('edgeless-note-container');
const style = await noteContainer.getAttribute('style');
if (!style) {
diff --git a/blocksuite/tests-legacy/linked-page.spec.ts b/blocksuite/tests-legacy/linked-page.spec.ts
index e83b2917b6..c0a832085b 100644
--- a/blocksuite/tests-legacy/linked-page.spec.ts
+++ b/blocksuite/tests-legacy/linked-page.spec.ts
@@ -665,6 +665,8 @@ test('linked doc can be dragged from note to surface top level block', async ({
}) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
+ await focusTitle(page);
+ await type(page, 'title0');
await focusRichText(page);
await createAndConvertToEmbedLinkedDoc(page);
diff --git a/blocksuite/tests-legacy/utils/asserts.ts b/blocksuite/tests-legacy/utils/asserts.ts
index 856a8da7ce..4431ff7148 100644
--- a/blocksuite/tests-legacy/utils/asserts.ts
+++ b/blocksuite/tests-legacy/utils/asserts.ts
@@ -1,5 +1,6 @@
import './declare-test-window.js';
+import type { EdgelessNoteBackground } from '@blocksuite/affine-block-note';
import type {
BlockComponent,
EditorHost,
@@ -965,12 +966,13 @@ export async function assertEdgelessNoteBackground(
const backgroundColor = await editor
.locator(`affine-edgeless-note[data-block-id="${noteId}"]`)
.evaluate(ele => {
- const noteWrapper =
- ele?.querySelector('.note-background');
+ const noteWrapper = ele?.querySelector(
+ 'edgeless-note-background'
+ );
if (!noteWrapper) {
throw new Error(`Could not find note: ${noteId}`);
}
- return noteWrapper.style.backgroundColor;
+ return noteWrapper.backgroundStyle$.value.backgroundColor;
});
expect(toHex(backgroundColor)).toEqual(color);
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx
index 2168439c88..4096971f83 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx
@@ -171,7 +171,7 @@ export const EdgelessNoteHeader = ({ note }: { note: NoteBlockModel }) => {
const flags = useService(FeatureFlagService).flags;
const insidePeekView = useInsidePeekView();
- if (!flags.enable_page_block_header) return null;
+ if (!flags.enable_page_block) return null;
const isFirstVisibleNote =
note.parent?.children.find(
diff --git a/packages/frontend/core/src/modules/feature-flag/constant.ts b/packages/frontend/core/src/modules/feature-flag/constant.ts
index d8d7793ceb..914be87d43 100644
--- a/packages/frontend/core/src/modules/feature-flag/constant.ts
+++ b/packages/frontend/core/src/modules/feature-flag/constant.ts
@@ -240,9 +240,9 @@ export const AFFINE_FLAGS = {
defaultState: isCanaryBuild,
},
// TODO(@L-Sun): remove this flag when ready
- enable_page_block_header: {
+ enable_page_block: {
category: 'blocksuite',
- bsFlag: 'enable_page_block_header',
+ bsFlag: 'enable_page_block',
displayName:
'com.affine.settings.workspace.experimental-features.enable-page-block-header.name',
description:
diff --git a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
index fa086e8797..54e70d1e1d 100644
--- a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
+++ b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
@@ -11,12 +11,15 @@ import {
} from '@affine-test/kit/utils/editor';
import {
pasteByKeyboard,
+ pressBackspace,
+ pressEnter,
selectAllByKeyboard,
undoByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
+ type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Page } from '@playwright/test';
@@ -36,7 +39,8 @@ test.beforeEach(async ({ page }) => {
await container.click();
});
-test.describe('edgeless page header toolbar', () => {
+// the first note block is called page block
+test.describe('edgeless page block', () => {
const locateHeaderToolbar = (page: Page) =>
page.getByTestId('edgeless-page-block-header');
@@ -77,7 +81,7 @@ test.describe('edgeless page header toolbar', () => {
expect(newNoteBox2).toEqual(noteBox);
});
- test('page title should be displayed when page block is collapsed and hidden when page block is not collapsed', async ({
+ test('page title in toolbar should be displayed when page block is collapsed and hidden when page block is not collapsed', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
@@ -143,6 +147,45 @@ test.describe('edgeless page header toolbar', () => {
await expect(toolbar).toBeVisible();
await expect(infoButton).toBeHidden();
});
+
+ test('page title should show in note when page block is not collapsed', async ({
+ page,
+ }) => {
+ const note = page.locator('affine-edgeless-note');
+ const docTitle = note.locator('doc-title');
+ await expect(docTitle).toBeVisible();
+ await expect(docTitle).toHaveText(title);
+
+ await note.dblclick();
+ await docTitle.click();
+
+ // clear the title
+ await selectAllByKeyboard(page);
+ await pressBackspace(page);
+ await expect(docTitle).toHaveText('');
+
+ // type new title
+ await type(page, 'New Title');
+ await expect(docTitle).toHaveText('New Title');
+
+ // cursor could move between doc title and note content
+ await page.keyboard.press('ArrowDown');
+ await type(page, 'xx');
+
+ const paragraphs = note.locator('affine-paragraph v-line');
+ const numParagraphs = await paragraphs.count();
+ await expect(paragraphs.first()).toHaveText('xxHello');
+
+ await page.keyboard.press('ArrowUp');
+ await type(page, 'yy');
+ await expect(docTitle).toHaveText('yyNew Title');
+
+ await pressEnter(page);
+ await expect(docTitle).toHaveText('yy');
+ await expect(paragraphs).toHaveCount(numParagraphs + 1);
+ await expect(paragraphs.nth(0)).toHaveText('New Title');
+ await expect(paragraphs.nth(1)).toHaveText('xxHello');
+ });
});
test.describe('edgeless note element toolbar', () => {
diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts
index ea14803769..0a46715589 100644
--- a/tools/utils/src/workspace.gen.ts
+++ b/tools/utils/src/workspace.gen.ts
@@ -450,6 +450,7 @@ export const PackageList = [
workspaceDependencies: [
'blocksuite/affine/block-note',
'blocksuite/affine/block-surface',
+ 'blocksuite/affine/components',
'blocksuite/affine/model',
'blocksuite/affine/shared',
'blocksuite/framework/block-std',
diff --git a/yarn.lock b/yarn.lock
index 6ce0761898..54d4b06abe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3557,7 +3557,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@blocksuite/affine-block-note@workspace:*, @blocksuite/affine-block-note@workspace:^, @blocksuite/affine-block-note@workspace:blocksuite/affine/block-note":
+"@blocksuite/affine-block-note@workspace:*, @blocksuite/affine-block-note@workspace:blocksuite/affine/block-note":
version: 0.0.0-use.local
resolution: "@blocksuite/affine-block-note@workspace:blocksuite/affine/block-note"
dependencies:
@@ -3576,6 +3576,7 @@ __metadata:
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.7"
"@types/mdast": "npm:^4.0.4"
+ "@vanilla-extract/css": "npm:^1.17.0"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
zod: "npm:^3.23.8"
@@ -3941,6 +3942,7 @@ __metadata:
"@types/katex": "npm:^0.16.7"
"@types/lodash.isequal": "npm:^4.5.8"
"@vanilla-extract/css": "npm:^1.17.0"
+ "@vanilla-extract/vite-plugin": "npm:^5.0.0"
date-fns: "npm:^4.0.0"
dompurify: "npm:^3.1.6"
fflate: "npm:^0.8.2"
@@ -4090,8 +4092,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@blocksuite/presets@workspace:blocksuite/presets"
dependencies:
- "@blocksuite/affine-block-note": "workspace:^"
+ "@blocksuite/affine-block-note": "workspace:*"
"@blocksuite/affine-block-surface": "workspace:*"
+ "@blocksuite/affine-components": "workspace:*"
"@blocksuite/affine-model": "workspace:*"
"@blocksuite/affine-shared": "workspace:*"
"@blocksuite/block-std": "workspace:*"