feat(editor): show doc title in page block (#9975)

Close [BS-2392](https://linear.app/affine-design/issue/BS-2392/page-block-需要显示文章title)

### What Changes
- Add `<doc-title>` to edgeless page block (a.k.a the first page visible note block)
- Refactors:
  - Move `<doc-title>` to `@blocksuite/affine-component`, but you can aslo import it from `@blocksuite/preset`
  - Extract `<edgeless-note-mask>` and `<edgeless-note-background>` from `<affine-edgeless-note>` to a seperate file
  - Rewrite styles of `<affine-edgeless-note>` with `@vanilla-extract/css`

https://github.com/user-attachments/assets/a0c03239-803e-4bfa-b30e-33b919213b12
This commit is contained in:
L-Sun
2025-02-06 21:18:27 +00:00
parent 41107eafae
commit 891d9df0b1
33 changed files with 626 additions and 337 deletions

View File

@@ -28,6 +28,7 @@
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7", "@toeverything/theme": "^1.1.7",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"zod": "^3.23.8" "zod": "^3.23.8"

View File

@@ -0,0 +1,27 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
import {
ACTIVE_NOTE_EXTRA_PADDING,
edgelessNoteContainer,
} from '../note-edgeless-block.css';
export const background = style({
position: 'absolute',
borderColor: cssVar('black10'),
left: 0,
top: 0,
width: '100%',
height: '100%',
selectors: {
[`${edgelessNoteContainer}[data-editing="true"] &`]: {
left: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
top: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
width: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
height: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
transition: 'left 0.3s, top 0.3s, width 0.3s, height 0.3s',
boxShadow: cssVar('activeShadow'),
},
},
});

View File

@@ -0,0 +1,195 @@
import {
DefaultTheme,
NoteBlockModel,
NoteDisplayMode,
StrokeStyle,
} from '@blocksuite/affine-model';
import {
FeatureFlagService,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import {
getClosestBlockComponentByPoint,
handleNativeRangeAtPoint,
matchFlavours,
stopPropagation,
} from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
type BlockStdScope,
PropTypes,
requiredProperties,
ShadowlessElement,
stdContext,
TextSelection,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import {
clamp,
Point,
SignalWatcher,
WithDisposable,
} from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context';
import { computed } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import * as styles from './edgeless-note-background.css';
@requiredProperties({
note: PropTypes.instanceOf(NoteBlockModel),
})
export class EdgelessNoteBackground extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
readonly backgroundStyle$ = computed(() => {
const themeProvider = this.std.get(ThemeProvider);
const theme = themeProvider.theme$.value;
const backgroundColor = themeProvider.generateColorProperty(
this.note.background$.value,
DefaultTheme.noteBackgrounColor,
theme
);
const { borderRadius, borderSize, borderStyle, shadowType } =
this.note.edgeless$.value.style;
return {
borderRadius: borderRadius + 'px',
backgroundColor: backgroundColor,
borderWidth: `${borderSize}px`,
borderStyle: borderStyle === StrokeStyle.Dash ? 'dashed' : borderStyle,
boxShadow: !shadowType ? 'none' : `var(${shadowType})`,
};
});
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
get doc() {
return this.std.host.doc;
}
private get _isPageBlock() {
return (
this.std.get(FeatureFlagService).getFlag('enable_page_block') &&
// is the first page visible note
this.note.parent?.children.find(
child =>
matchFlavours(child, ['affine:note']) &&
child.displayMode !== NoteDisplayMode.EdgelessOnly
) === this.note
);
}
private _tryAddParagraph(x: number, y: number) {
const nearest = getClosestBlockComponentByPoint(
new Point(x, y)
) as BlockComponent | null;
if (!nearest) return;
const nearestBBox = nearest.getBoundingClientRect();
const yRel = y - nearestBBox.top;
const insertPos: 'before' | 'after' =
yRel < nearestBBox.height / 2 ? 'before' : 'after';
const nearestModel = nearest.model as BlockModel;
const nearestModelIdx = this.note.children.indexOf(nearestModel);
const children = this.note.children;
const siblingModel =
children[
clamp(
nearestModelIdx + (insertPos === 'before' ? -1 : 1),
0,
children.length
)
];
if (
(!nearestModel.text ||
!matchFlavours(nearestModel, ['affine:paragraph', 'affine:list'])) &&
(!siblingModel ||
!siblingModel.text ||
!matchFlavours(siblingModel, ['affine:paragraph', 'affine:list']))
) {
const [pId] = this.doc.addSiblingBlocks(
nearestModel,
[{ flavour: 'affine:paragraph' }],
insertPos
);
this.updateComplete
.then(() => {
this.std.selection.setGroup('note', [
this.std.selection.create(TextSelection, {
from: {
blockId: pId,
index: 0,
length: 0,
},
to: null,
}),
]);
})
.catch(console.error);
}
}
private _handleClickAtBackground(e: MouseEvent) {
e.stopPropagation();
if (!this.editing) return;
const { zoom } = this.gfx.viewport;
const rect = this.getBoundingClientRect();
const offsetY = 16 * zoom;
const offsetX = 2 * zoom;
const x = clamp(e.x, rect.left + offsetX, rect.right - offsetX);
const y = clamp(e.y, rect.top + offsetY, rect.bottom - offsetY);
handleNativeRangeAtPoint(x, y);
if (this.std.host.doc.readonly) return;
this._tryAddParagraph(x, y);
}
private _renderHeader() {
const header = this.std
.getConfig('affine:note')
?.edgelessNoteHeader({ note: this.note, std: this.std });
return header;
}
override render() {
return html`<div
class=${styles.background}
style=${styleMap(this.backgroundStyle$.value)}
@pointerdown=${stopPropagation}
@click=${this._handleClickAtBackground}
>
${this._isPageBlock ? this._renderHeader() : nothing}
</div>`;
}
@consume({ context: stdContext })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor editing: boolean = false;
@property({ attribute: false })
accessor note!: NoteBlockModel;
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-note-background': EdgelessNoteBackground;
}
}

View File

@@ -0,0 +1,84 @@
import type { NoteBlockModel } from '@blocksuite/affine-model';
import { type EditorHost, ShadowlessElement } from '@blocksuite/block-std';
import {
almostEqual,
Bound,
SignalWatcher,
WithDisposable,
} from '@blocksuite/global/utils';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { ACTIVE_NOTE_EXTRA_PADDING } from '../note-edgeless-block.css';
export class EdgelessNoteMask extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
protected override firstUpdated() {
const maskDOM = this.renderRoot!.querySelector('.affine-note-mask');
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!this.model.edgeless.collapse) {
const bound = Bound.deserialize(this.model.xywh);
const scale = this.model.edgeless.scale ?? 1;
const height = entry.contentRect.height * scale;
if (!height || almostEqual(bound.h, height)) {
return;
}
bound.h = height;
this.model.stash('xywh');
this.model.xywh = bound.serialize();
this.model.pop('xywh');
}
}
});
observer.observe(maskDOM!);
this._disposables.add(() => {
observer.disconnect();
});
}
override render() {
const extra = this.editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
return html`
<div
class="affine-note-mask"
style=${styleMap({
position: 'absolute',
top: `${-extra}px`,
left: `${-extra}px`,
bottom: `${-extra}px`,
right: `${-extra}px`,
zIndex: '1',
pointerEvents: this.editing ? 'none' : 'auto',
borderRadius: `${
this.model.edgeless.style.borderRadius * this.zoom
}px`,
})}
></div>
`;
}
@property({ attribute: false })
accessor editing!: boolean;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor model!: NoteBlockModel;
@property({ attribute: false })
accessor zoom!: number;
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-note-mask': EdgelessNoteMask;
}
}

View File

@@ -1,15 +1,18 @@
import { EdgelessNoteBackground } from './components/edgeless-note-background';
import { EdgelessNoteMask } from './components/edgeless-note-mask';
import type { NoteConfig } from './config'; import type { NoteConfig } from './config';
import { NoteBlockComponent } from './note-block'; import { NoteBlockComponent } from './note-block';
import { import {
AFFINE_EDGELESS_NOTE,
EdgelessNoteBlockComponent, EdgelessNoteBlockComponent,
EdgelessNoteMask,
} from './note-edgeless-block'; } from './note-edgeless-block';
import type { NoteBlockService } from './note-service'; import type { NoteBlockService } from './note-service';
export function effects() { export function effects() {
customElements.define('affine-note', NoteBlockComponent); customElements.define('affine-note', NoteBlockComponent);
customElements.define(AFFINE_EDGELESS_NOTE, EdgelessNoteBlockComponent);
customElements.define('edgeless-note-mask', EdgelessNoteMask); customElements.define('edgeless-note-mask', EdgelessNoteMask);
customElements.define('affine-edgeless-note', EdgelessNoteBlockComponent); customElements.define('edgeless-note-background', EdgelessNoteBackground);
} }
declare global { declare global {

View File

@@ -1,5 +1,6 @@
export * from './adapters'; export * from './adapters';
export * from './commands'; export * from './commands';
export * from './components/edgeless-note-background';
export * from './config'; export * from './config';
export * from './note-block'; export * from './note-block';
export * from './note-edgeless-block'; export * from './note-edgeless-block';

View File

@@ -0,0 +1,85 @@
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css';
export const ACTIVE_NOTE_EXTRA_PADDING = 20;
export const edgelessNoteContainer = style({
height: '100%',
padding: `${EDGELESS_BLOCK_CHILD_PADDING}px`,
boxSizing: 'border-box',
pointerEvents: 'all',
transformOrigin: '0 0',
fontWeight: '400',
lineHeight: cssVar('lineHeight'),
});
export const collapseButton = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '28px',
height: '28px',
zIndex: 2,
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
opacity: 0.2,
transition: 'opacity 0.3s',
':hover': {
opacity: 1,
},
selectors: {
'&.flip': {
transform: 'translateX(-50%) rotate(180deg)',
},
},
});
export const noteBackground = style({
position: 'absolute',
borderColor: cssVar('black10'),
left: 0,
top: 0,
width: '100%',
height: '100%',
selectors: {
[`${edgelessNoteContainer}[data-editing="true"] &`]: {
left: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
top: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
width: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
height: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
transition: 'left 0.3s, top 0.3s, width 0.3s, height 0.3s',
boxShadow: cssVar('activeShadow'),
},
},
});
globalStyle(`${edgelessNoteContainer} > doc-title`, {
position: 'relative',
});
globalStyle(`${edgelessNoteContainer} > doc-title .doc-title-container`, {
padding: '26px 0px',
fontSize: cssVar('fontTitle'),
fontWeight: 700,
lineHeight: '44px',
});
export const pageContent = style({
width: '100%',
height: '100%',
});
export const collapsedContent = style({
position: 'absolute',
background: cssVar('white'),
opacity: 0.5,
pointerEvents: 'none',
border: `2px ${cssVar('blue')} solid`,
borderTop: 'unset',
borderRadius: '0 0 8px 8px',
});

View File

@@ -1,160 +1,35 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface'; import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { DocTitle } from '@blocksuite/affine-components/doc-title';
import { MoreIndicatorIcon } from '@blocksuite/affine-components/icons'; import { MoreIndicatorIcon } from '@blocksuite/affine-components/icons';
import type { NoteBlockModel } from '@blocksuite/affine-model'; import { NoteDisplayMode } from '@blocksuite/affine-model';
import {
DefaultTheme,
NoteDisplayMode,
StrokeStyle,
} from '@blocksuite/affine-model';
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts'; import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { import {
FeatureFlagService,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import {
getClosestBlockComponentByPoint,
handleNativeRangeAtPoint,
matchFlavours, matchFlavours,
stopPropagation, stopPropagation,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import type { BlockComponent, EditorHost } from '@blocksuite/block-std'; import { toGfxBlockComponent } from '@blocksuite/block-std';
import { import { Bound } from '@blocksuite/global/utils';
ShadowlessElement, import { html, nothing } from 'lit';
TextSelection, import { query, state } from 'lit/decorators.js';
toGfxBlockComponent,
} from '@blocksuite/block-std';
import {
almostEqual,
Bound,
clamp,
Point,
WithDisposable,
} from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { NoteBlockComponent } from './note-block.js'; import { NoteBlockComponent } from './note-block';
import { ACTIVE_NOTE_EXTRA_PADDING } from './note-edgeless-block.css';
import * as styles from './note-edgeless-block.css';
export class EdgelessNoteMask extends WithDisposable(ShadowlessElement) { export const AFFINE_EDGELESS_NOTE = 'affine-edgeless-note';
protected override firstUpdated() {
const maskDOM = this.renderRoot!.querySelector('.affine-note-mask');
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!this.model.edgeless.collapse) {
const bound = Bound.deserialize(this.model.xywh);
const scale = this.model.edgeless.scale ?? 1;
const height = entry.contentRect.height * scale;
if (!height || almostEqual(bound.h, height)) {
return;
}
bound.h = height;
this.model.stash('xywh');
this.model.xywh = bound.serialize();
this.model.pop('xywh');
}
}
});
observer.observe(maskDOM!);
this._disposables.add(() => {
observer.disconnect();
});
}
override render() {
const extra = this.editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
return html`
<div
class="affine-note-mask"
style=${styleMap({
position: 'absolute',
top: `${-extra}px`,
left: `${-extra}px`,
bottom: `${-extra}px`,
right: `${-extra}px`,
zIndex: '1',
pointerEvents: this.editing ? 'none' : 'auto',
borderRadius: `${
this.model.edgeless.style.borderRadius * this.zoom
}px`,
})}
></div>
`;
}
@property({ attribute: false })
accessor editing!: boolean;
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor model!: NoteBlockModel;
@property({ attribute: false })
accessor zoom!: number;
}
const ACTIVE_NOTE_EXTRA_PADDING = 20;
export class EdgelessNoteBlockComponent extends toGfxBlockComponent( export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
NoteBlockComponent NoteBlockComponent
) { ) {
static override styles = css` private get _isPageBlock() {
.edgeless-note-collapse-button { return (
display: flex; this.std.get(FeatureFlagService).getFlag('enable_page_block') &&
align-items: center; this._isFirstVisibleNote()
justify-content: center;
width: 28px;
height: 28px;
z-index: 2;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
opacity: 0.2;
transition: opacity 0.3s;
}
.edgeless-note-collapse-button:hover {
opacity: 1;
}
.edgeless-note-collapse-button.flip {
transform: translateX(-50%) rotate(180deg);
}
.edgeless-note-container:has(.affine-embed-synced-doc-container.editing)
> .note-background {
left: ${-ACTIVE_NOTE_EXTRA_PADDING}px !important;
top: ${-ACTIVE_NOTE_EXTRA_PADDING}px !important;
width: calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px) !important;
height: calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px) !important;
}
.edgeless-note-container:has(.affine-embed-synced-doc-container.editing)
> edgeless-note-mask {
display: none;
}
`;
private readonly _backgroundColor$ = computed(() => {
const themeProvider = this.std.get(ThemeProvider);
const theme = themeProvider.theme$.value;
return themeProvider.generateColorProperty(
this.model.background$.value,
DefaultTheme.noteBackgrounColor,
theme
); );
});
private get _enablePageHeader() {
return this.std.get(FeatureFlagService).getFlag('enable_page_block_header');
} }
private get _isShowCollapsedContent() { private get _isShowCollapsedContent() {
@@ -201,38 +76,21 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
return html` return html`
<div <div
class=${styles.collapsedContent}
style=${styleMap({ style=${styleMap({
width: `${width}px`, width: `${width}px`,
height: `${this._noteFullHeight - height}px`, height: `${this._noteFullHeight - height}px`,
position: 'absolute',
left: `${-(extraPadding + extraBorder / 2)}px`, left: `${-(extraPadding + extraBorder / 2)}px`,
top: `${height + extraPadding + extraBorder / 2}px`, top: `${height + extraPadding + extraBorder / 2}px`,
background: 'var(--affine-white)',
opacity: 0.5,
pointerEvents: 'none',
borderLeft: '2px var(--affine-blue) solid',
borderBottom: '2px var(--affine-blue) solid',
borderRight: '2px var(--affine-blue) solid',
borderRadius: '0 0 8px 8px',
})} })}
></div> ></div>
`; `;
} }
private _handleClickAtBackground(e: MouseEvent) { private _handleKeyDown(e: KeyboardEvent) {
e.stopPropagation(); if (e.key === 'ArrowUp' && this._isPageBlock) {
if (!this._editing) return; this._docTitle?.inlineEditor?.focusEnd();
}
const rect = this.getBoundingClientRect();
const offsetY = 16 * this._zoom;
const offsetX = 2 * this._zoom;
const x = clamp(e.x, rect.left + offsetX, rect.right - offsetX);
const y = clamp(e.y, rect.top + offsetY, rect.bottom - offsetY);
handleNativeRangeAtPoint(x, y);
if (this.doc.readonly) return;
this._tryAddParagraph(x, y);
} }
private _hovered() { private _hovered() {
@@ -261,14 +119,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
} }
} }
private _renderHeader() {
const header = this.host.std
.getConfig('affine:note')
?.edgelessNoteHeader({ note: this.model, std: this.std });
return header;
}
private _setCollapse(event: MouseEvent) { private _setCollapse(event: MouseEvent) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
@@ -291,61 +141,6 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
this.selection.clear(); this.selection.clear();
} }
private _tryAddParagraph(x: number, y: number) {
const nearest = getClosestBlockComponentByPoint(
new Point(x, y)
) as BlockComponent | null;
if (!nearest) return;
const nearestBBox = nearest.getBoundingClientRect();
const yRel = y - nearestBBox.top;
const insertPos: 'before' | 'after' =
yRel < nearestBBox.height / 2 ? 'before' : 'after';
const nearestModel = nearest.model as BlockModel;
const nearestModelIdx = this.model.children.indexOf(nearestModel);
const children = this.model.children;
const siblingModel =
children[
clamp(
nearestModelIdx + (insertPos === 'before' ? -1 : 1),
0,
children.length
)
];
if (
(!nearestModel.text ||
!matchFlavours(nearestModel, ['affine:paragraph', 'affine:list'])) &&
(!siblingModel ||
!siblingModel.text ||
!matchFlavours(siblingModel, ['affine:paragraph', 'affine:list']))
) {
const [pId] = this.doc.addSiblingBlocks(
nearestModel,
[{ flavour: 'affine:paragraph' }],
insertPos
);
this.updateComplete
.then(() => {
this.std.selection.setGroup('note', [
this.std.selection.create(TextSelection, {
from: {
blockId: pId,
index: 0,
length: 0,
},
to: null,
}),
]);
})
.catch(console.error);
}
}
override connectedCallback(): void { override connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
@@ -361,6 +156,8 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
} }
}) })
); );
this.disposables.addFromEvent(this, 'keydown', this._handleKeyDown);
} }
get edgelessSlots() { get edgelessSlots() {
@@ -423,49 +220,19 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
return nothing; return nothing;
const { xywh, edgeless } = model; const { xywh, edgeless } = model;
const { borderRadius, borderSize, borderStyle, shadowType } = const { borderRadius } = edgeless.style;
edgeless.style; const { collapse = false, collapsedHeight, scale = 1 } = edgeless;
const { collapse, collapsedHeight, scale = 1 } = edgeless;
const bound = Bound.deserialize(xywh); const bound = Bound.deserialize(xywh);
const width = bound.w / scale;
const height = bound.h / scale; const height = bound.h / scale;
const style = { const style = {
height: '100%',
padding: `${EDGELESS_BLOCK_CHILD_PADDING}px`,
boxSizing: 'border-box',
borderRadius: borderRadius + 'px', borderRadius: borderRadius + 'px',
pointerEvents: 'all',
transformOrigin: '0 0',
transform: `scale(${scale})`, transform: `scale(${scale})`,
fontWeight: '400',
lineHeight: 'var(--affine-line-height)',
}; };
const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0; const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
const backgroundStyle = {
position: 'absolute',
left: `${-extra}px`,
top: `${-extra}px`,
width: `${width + extra * 2}px`,
height: `calc(100% + ${extra * 2}px)`,
borderRadius: borderRadius + 'px',
transition: this._editing
? 'left 0.3s, top 0.3s, width 0.3s, height 0.3s'
: 'none',
backgroundColor: this._backgroundColor$.value,
border: `${borderSize}px ${
borderStyle === StrokeStyle.Dash ? 'dashed' : borderStyle
} var(--affine-black-10)`,
boxShadow: this._editing
? 'var(--affine-active-shadow)'
: !shadowType
? 'none'
: `var(${shadowType})`,
};
const isCollapsable = const isCollapsable =
collapse != null && collapse != null &&
collapsedHeight != null && collapsedHeight != null &&
@@ -477,21 +244,27 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
return html` return html`
<div <div
class="edgeless-note-container" class=${styles.edgelessNoteContainer}
style=${styleMap(style)} style=${styleMap(style)}
data-model-height="${bound.h}" data-model-height="${bound.h}"
data-editing=${this._editing}
data-collapse=${ifDefined(collapse)}
data-testid="edgeless-note-container"
@mouseleave=${this._leaved} @mouseleave=${this._leaved}
@mousemove=${this._hovered} @mousemove=${this._hovered}
data-scale="${scale}" data-scale="${scale}"
> >
<div <edgeless-note-background
class="note-background" .editing=${this._editing}
style=${styleMap(backgroundStyle)} .note=${this.model}
@pointerdown=${stopPropagation} ></edgeless-note-background>
@click=${this._handleClickAtBackground}
> ${this._isPageBlock && !collapse
${this._enablePageHeader ? this._renderHeader() : nothing} ? html`<doc-title
</div> .doc=${this.doc}
.wrapText=${!collapse}
></doc-title>`
: nothing}
<div <div
class="edgeless-note-page-content" class="edgeless-note-page-content"
@@ -511,16 +284,16 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
.editing=${this._editing} .editing=${this._editing}
></edgeless-note-mask> ></edgeless-note-mask>
${isCollapsable && ${isCollapsable && !this._isPageBlock
(!this._isFirstVisibleNote() || !this._enablePageHeader)
? html`<div ? html`<div
class="${classMap({ class="${classMap({
'edgeless-note-collapse-button': true, [styles.collapseButton]: true,
flip: isCollapseArrowUp, flip: isCollapseArrowUp,
})}" })}"
style=${styleMap({ style=${styleMap({
bottom: this._editing ? `${-extra}px` : '0', bottom: this._editing ? `${-extra}px` : '0',
})} })}
data-testid="edgeless-note-collapse-button"
@mousedown=${stopPropagation} @mousedown=${stopPropagation}
@mouseup=${stopPropagation} @mouseup=${stopPropagation}
@click=${this._setCollapse} @click=${this._setCollapse}
@@ -547,10 +320,13 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
@query('.edgeless-note-page-content .affine-note-block-container') @query('.edgeless-note-page-content .affine-note-block-container')
private accessor _notePageContent: HTMLElement | null = null; private accessor _notePageContent: HTMLElement | null = null;
@query('doc-title')
private accessor _docTitle: DocTitle | null = null;
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'affine-edgeless-note': EdgelessNoteBlockComponent; [AFFINE_EDGELESS_NOTE]: EdgelessNoteBlockComponent;
} }
} }

View File

@@ -61,7 +61,8 @@
"./toggle-switch": "./src/toggle-switch/index.ts", "./toggle-switch": "./src/toggle-switch/index.ts",
"./notification": "./src/notification/index.ts", "./notification": "./src/notification/index.ts",
"./block-zero-width": "./src/block-zero-width/index.ts", "./block-zero-width": "./src/block-zero-width/index.ts",
"./block-selection": "./src/block-selection/index.ts" "./block-selection": "./src/block-selection/index.ts",
"./doc-title": "./src/doc-title/index.ts"
}, },
"files": [ "files": [
"src", "src",

View File

@@ -1,12 +1,18 @@
import type { EditorHost } from '@blocksuite/block-std'; import {
type NoteBlockModel,
NoteDisplayMode,
type RootBlockModel,
} from '@blocksuite/affine-model';
import { matchFlavours } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std'; import { ShadowlessElement } from '@blocksuite/block-std';
import type { RichText, RootBlockModel } from '@blocksuite/blocks'; import { WithDisposable } from '@blocksuite/global/utils';
import { assertExists, WithDisposable } from '@blocksuite/global/utils';
import type { Store } from '@blocksuite/store'; import type { Store } from '@blocksuite/store';
import { effect } from '@preact/signals-core'; import { effect } from '@preact/signals-core';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query, state } from 'lit/decorators.js';
import { focusTextModel, type RichText } from '../rich-text';
const DOC_BLOCK_CHILD_PADDING = 24; const DOC_BLOCK_CHILD_PADDING = 24;
export class DocTitle extends WithDisposable(ShadowlessElement) { export class DocTitle extends WithDisposable(ShadowlessElement) {
@@ -60,23 +66,55 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
} }
`; `;
private _getOrCreateFirstPageVisibleNote() {
const note = this._rootModel.children.find(
(child): child is NoteBlockModel =>
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) => { private readonly _onTitleKeyDown = (event: KeyboardEvent) => {
if (event.isComposing || this.doc.readonly) return; if (event.isComposing || this.doc.readonly) return;
if (event.key === 'Enter' && this._pageRoot) { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const inlineEditor = this._inlineEditor; const inlineRange = this.inlineEditor?.getInlineRange();
const inlineRange = inlineEditor?.getInlineRange();
if (inlineRange) { if (inlineRange) {
const rightText = this._rootModel.title.split(inlineRange.index); 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') { } else if (event.key === 'ArrowDown') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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') { } else if (event.key === 'Tab') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -89,12 +127,8 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
}); });
}; };
private get _inlineEditor() { private get _std() {
return this._richTextElement.inlineEditor; return this._viewport?.querySelector('editor-host')?.std;
}
private get _pageRoot() {
return this._viewport.querySelector('affine-page-root');
} }
private get _rootModel() { private get _rootModel() {
@@ -102,9 +136,14 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
} }
private get _viewport() { private get _viewport() {
const el = this.closest<HTMLElement>('.affine-page-viewport'); return (
assertExists(el); this.closest<HTMLElement>('.affine-page-viewport') ??
return el; this.closest<HTMLElement>('.affine-edgeless-viewport')
);
}
get inlineEditor() {
return this._richTextElement.inlineEditor;
} }
override connectedCallback() { override connectedCallback() {
@@ -161,6 +200,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
.verticalScrollContainerGetter=${() => this._viewport} .verticalScrollContainerGetter=${() => this._viewport}
.readonly=${this.doc.readonly} .readonly=${this.doc.readonly}
.enableFormat=${false} .enableFormat=${false}
.wrapText=${this.wrapText}
></rich-text> ></rich-text>
</div> </div>
`; `;
@@ -177,18 +217,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor doc!: Store; accessor doc!: Store;
}
export function getDocTitleByEditorHost( @property({ attribute: false })
editorHost: EditorHost accessor wrapText = false;
): 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;
}
} }

View File

@@ -0,0 +1,11 @@
import { DocTitle } from './doc-title';
export function effects() {
customElements.define('doc-title', DocTitle);
}
declare global {
interface HTMLElementTagNameMap {
'doc-title': DocTitle;
}
}

View File

@@ -0,0 +1,3 @@
export { DocTitle } from './doc-title';
export { effects } from './effects';
export { getDocTitleByEditorHost } from './utils';

View File

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

View File

@@ -18,7 +18,7 @@ export interface BlockSuiteFlags {
enable_shape_shadow_blur: boolean; enable_shape_shadow_blur: boolean;
enable_mobile_keyboard_toolbar: boolean; enable_mobile_keyboard_toolbar: boolean;
enable_mobile_linked_doc_menu: boolean; enable_mobile_linked_doc_menu: boolean;
enable_page_block_header: boolean; enable_page_block: boolean;
} }
export class FeatureFlagService extends StoreExtension { export class FeatureFlagService extends StoreExtension {
@@ -41,7 +41,7 @@ export class FeatureFlagService extends StoreExtension {
enable_shape_shadow_blur: false, enable_shape_shadow_blur: false,
enable_mobile_keyboard_toolbar: false, enable_mobile_keyboard_toolbar: false,
enable_mobile_linked_doc_menu: false, enable_mobile_linked_doc_menu: false,
enable_page_block_header: false, enable_page_block: false,
}); });
setFlag(key: keyof BlockSuiteFlags, value: boolean) { setFlag(key: keyof BlockSuiteFlags, value: boolean) {

View File

@@ -1,3 +1,7 @@
import {
AFFINE_EDGELESS_NOTE,
type EdgelessNoteBlockComponent,
} from '@blocksuite/affine-block-note';
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph'; import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import type { ParagraphBlockModel } from '@blocksuite/affine-model'; import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services'; import { DocModeProvider } from '@blocksuite/affine-shared/services';
@@ -155,7 +159,7 @@ export const getClosestNoteBlock = (
editorHost.std.get(DocModeProvider).getEditorMode() === 'page'; editorHost.std.get(DocModeProvider).getEditorMode() === 'page';
return isInsidePageEditor return isInsidePageEditor
? findClosestBlockComponent(rootComponent, point, 'affine-note') ? findClosestBlockComponent(rootComponent, point, 'affine-note')
: getHoveringNote(point)?.closest('affine-edgeless-note'); : getHoveringNote(point);
}; };
export const getClosestBlockByPoint = ( export const getClosestBlockByPoint = (
@@ -261,11 +265,11 @@ export function getDuplicateBlocks(blocks: BlockModel[]) {
*/ */
function getHoveringNote(point: Point) { function getHoveringNote(point: Point) {
return ( return (
document.elementsFromPoint(point.x, point.y).find(isEdgelessChildNote) || document
null .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');
}

View File

@@ -84,6 +84,7 @@
"devDependencies": { "devDependencies": {
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@vanilla-extract/vite-plugin": "^5.0.0",
"vitest": "3.0.5" "vitest": "3.0.5"
} }
} }

View File

@@ -145,8 +145,8 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
.getFlag('enable_advanced_block_visibility'); .getFlag('enable_advanced_block_visibility');
} }
private get _pageBlockHeaderEnabled() { private get _pageBlockEnabled() {
return this.doc.get(FeatureFlagService).getFlag('enable_page_block_header'); return this.doc.get(FeatureFlagService).getFlag('enable_page_block');
} }
private get doc() { private get doc() {
@@ -155,7 +155,7 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
private get _enableAutoHeight() { private get _enableAutoHeight() {
return !( return !(
this._pageBlockHeaderEnabled && this._pageBlockEnabled &&
this.notes.length === 1 && this.notes.length === 1 &&
this.notes[0].parent?.children.find(child => this.notes[0].parent?.children.find(child =>
matchFlavours(child, ['affine:note']) matchFlavours(child, ['affine:note'])
@@ -373,7 +373,7 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
onlyOne && onlyOne &&
!isFirstNote && !isFirstNote &&
this._pageBlockHeaderEnabled && this._pageBlockEnabled &&
!this._advancedVisibilityEnabled !this._advancedVisibilityEnabled
? html`<editor-icon-button ? html`<editor-icon-button
aria-label="Display In Page" aria-label="Display In Page"

View File

@@ -1,9 +1,11 @@
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
export default defineConfig({ export default defineConfig({
esbuild: { esbuild: {
target: 'es2018', target: 'es2018',
}, },
plugins: [vanillaExtractPlugin()],
test: { test: {
globalSetup: '../../scripts/vitest-global.js', globalSetup: '../../scripts/vitest-global.js',
include: ['src/__tests__/**/*.unit.spec.ts'], include: ['src/__tests__/**/*.unit.spec.ts'],

View File

@@ -12,8 +12,9 @@
"author": "toeverything", "author": "toeverything",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@blocksuite/affine-block-note": "workspace:^", "@blocksuite/affine-block-note": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*", "@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*", "@blocksuite/block-std": "workspace:*",

View File

@@ -8,6 +8,7 @@ import {
} from './editors/index.js'; } from './editors/index.js';
import { CommentInput } from './fragments/comment/comment-input.js'; import { CommentInput } from './fragments/comment/comment-input.js';
import { BacklinkButton } from './fragments/doc-meta-tags/backlink-popover.js'; import { BacklinkButton } from './fragments/doc-meta-tags/backlink-popover.js';
import { effects as docTitleEffects } from './fragments/doc-title/index.js';
import { import {
AFFINE_FRAME_PANEL_BODY, AFFINE_FRAME_PANEL_BODY,
FramePanelBody, FramePanelBody,
@@ -38,7 +39,6 @@ import {
AFFINE_OUTLINE_PANEL, AFFINE_OUTLINE_PANEL,
AFFINE_OUTLINE_VIEWER, AFFINE_OUTLINE_VIEWER,
CommentPanel, CommentPanel,
DocTitle,
FramePanel, FramePanel,
MobileOutlineMenu, MobileOutlineMenu,
OutlinePanel, OutlinePanel,
@@ -70,9 +70,10 @@ import {
} from './fragments/outline/header/outline-setting-menu.js'; } from './fragments/outline/header/outline-setting-menu.js';
export function effects() { export function effects() {
docTitleEffects();
customElements.define('page-editor', PageEditor); customElements.define('page-editor', PageEditor);
customElements.define('comment-input', CommentInput); customElements.define('comment-input', CommentInput);
customElements.define('doc-title', DocTitle);
customElements.define( customElements.define(
AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU, AFFINE_OUTLINE_NOTE_PREVIEW_SETTING_MENU,
OutlineNotePreviewSettingMenu OutlineNotePreviewSettingMenu

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-components/doc-title';

View File

@@ -1,4 +1,4 @@
export * from './comment/index.js'; export * from './comment/index.js';
export * from './doc-title/doc-title.js'; export * from './doc-title/index.js';
export * from './frame-panel/index.js'; export * from './frame-panel/index.js';
export * from './outline/index.js'; export * from './outline/index.js';

View File

@@ -3,7 +3,7 @@ import { NoteDisplayMode } from '@blocksuite/blocks';
import { clamp, DisposableGroup } from '@blocksuite/global/utils'; import { clamp, DisposableGroup } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '../../../editors/editor-container.js'; import type { AffineEditorContainer } from '../../../editors/editor-container.js';
import { getDocTitleByEditorHost } from '../../doc-title/doc-title.js'; import { getDocTitleByEditorHost } from '../../doc-title/index.js';
import { getHeadingBlocksFromDoc } from './query.js'; import { getHeadingBlocksFromDoc } from './query.js';
export function scrollToBlock(editor: AffineEditorContainer, blockId: string) { export function scrollToBlock(editor: AffineEditorContainer, blockId: string) {

View File

@@ -9,6 +9,7 @@
"references": [ "references": [
{ "path": "../affine/block-note" }, { "path": "../affine/block-note" },
{ "path": "../affine/block-surface" }, { "path": "../affine/block-surface" },
{ "path": "../affine/components" },
{ "path": "../affine/model" }, { "path": "../affine/model" },
{ "path": "../affine/shared" }, { "path": "../affine/shared" },
{ "path": "../framework/block-std" }, { "path": "../framework/block-std" },

View File

@@ -94,7 +94,7 @@ test('resize note then collapse note', async ({ page }) => {
{ x: box.x + 50, y: box.y + box.height + 100 } { x: box.x + 50, y: box.y + box.height + 100 }
); );
let noteRect = await getNoteRect(page, noteId); 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, { assertRectEqual(noteRect, {
x: initRect.x, x: initRect.x,
y: initRect.y, y: initRect.y,
@@ -102,11 +102,11 @@ test('resize note then collapse note', async ({ page }) => {
h: initRect.h + 100, 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(); let domRect = await page.locator('affine-edgeless-note').boundingBox();
expect(domRect!.height).toBeCloseTo(NOTE_MIN_HEIGHT); 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(); domRect = await page.locator('affine-edgeless-note').boundingBox();
expect(domRect!.height).toBeCloseTo(initRect.h + 100); expect(domRect!.height).toBeCloseTo(initRect.h + 100);
@@ -120,7 +120,7 @@ test('resize note then collapse note', async ({ page }) => {
); );
noteRect = await getNoteRect(page, noteId); noteRect = await getNoteRect(page, noteId);
await expect( await expect(
page.locator('.edgeless-note-collapse-button') page.getByTestId('edgeless-note-collapse-button')
).not.toBeVisible(); ).not.toBeVisible();
assertRectEqual(noteRect, { assertRectEqual(noteRect, {
x: initRect.x, x: initRect.x,

View File

@@ -48,7 +48,7 @@ async function checkNoteScale(
const edgelessNote = page.locator( const edgelessNote = page.locator(
`affine-edgeless-note[data-block-id="${noteId}"]` `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'); const style = await noteContainer.getAttribute('style');
if (!style) { if (!style) {

View File

@@ -665,6 +665,8 @@ test('linked doc can be dragged from note to surface top level block', async ({
}) => { }) => {
await enterPlaygroundRoom(page); await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page); await initEmptyEdgelessState(page);
await focusTitle(page);
await type(page, 'title0');
await focusRichText(page); await focusRichText(page);
await createAndConvertToEmbedLinkedDoc(page); await createAndConvertToEmbedLinkedDoc(page);

View File

@@ -1,5 +1,6 @@
import './declare-test-window.js'; import './declare-test-window.js';
import type { EdgelessNoteBackground } from '@blocksuite/affine-block-note';
import type { import type {
BlockComponent, BlockComponent,
EditorHost, EditorHost,
@@ -965,12 +966,13 @@ export async function assertEdgelessNoteBackground(
const backgroundColor = await editor const backgroundColor = await editor
.locator(`affine-edgeless-note[data-block-id="${noteId}"]`) .locator(`affine-edgeless-note[data-block-id="${noteId}"]`)
.evaluate(ele => { .evaluate(ele => {
const noteWrapper = const noteWrapper = ele?.querySelector<EdgelessNoteBackground>(
ele?.querySelector<HTMLDivElement>('.note-background'); 'edgeless-note-background'
);
if (!noteWrapper) { if (!noteWrapper) {
throw new Error(`Could not find note: ${noteId}`); throw new Error(`Could not find note: ${noteId}`);
} }
return noteWrapper.style.backgroundColor; return noteWrapper.backgroundStyle$.value.backgroundColor;
}); });
expect(toHex(backgroundColor)).toEqual(color); expect(toHex(backgroundColor)).toEqual(color);

View File

@@ -171,7 +171,7 @@ export const EdgelessNoteHeader = ({ note }: { note: NoteBlockModel }) => {
const flags = useService(FeatureFlagService).flags; const flags = useService(FeatureFlagService).flags;
const insidePeekView = useInsidePeekView(); const insidePeekView = useInsidePeekView();
if (!flags.enable_page_block_header) return null; if (!flags.enable_page_block) return null;
const isFirstVisibleNote = const isFirstVisibleNote =
note.parent?.children.find( note.parent?.children.find(

View File

@@ -240,9 +240,9 @@ export const AFFINE_FLAGS = {
defaultState: isCanaryBuild, defaultState: isCanaryBuild,
}, },
// TODO(@L-Sun): remove this flag when ready // TODO(@L-Sun): remove this flag when ready
enable_page_block_header: { enable_page_block: {
category: 'blocksuite', category: 'blocksuite',
bsFlag: 'enable_page_block_header', bsFlag: 'enable_page_block',
displayName: displayName:
'com.affine.settings.workspace.experimental-features.enable-page-block-header.name', 'com.affine.settings.workspace.experimental-features.enable-page-block-header.name',
description: description:

View File

@@ -11,12 +11,15 @@ import {
} from '@affine-test/kit/utils/editor'; } from '@affine-test/kit/utils/editor';
import { import {
pasteByKeyboard, pasteByKeyboard,
pressBackspace,
pressEnter,
selectAllByKeyboard, selectAllByKeyboard,
undoByKeyboard, undoByKeyboard,
} from '@affine-test/kit/utils/keyboard'; } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page'; import { openHomePage } from '@affine-test/kit/utils/load-page';
import { import {
clickNewPageButton, clickNewPageButton,
type,
waitForEditorLoad, waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { expect, type Page } from '@playwright/test'; import { expect, type Page } from '@playwright/test';
@@ -36,7 +39,8 @@ test.beforeEach(async ({ page }) => {
await container.click(); 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) => const locateHeaderToolbar = (page: Page) =>
page.getByTestId('edgeless-page-block-header'); page.getByTestId('edgeless-page-block-header');
@@ -77,7 +81,7 @@ test.describe('edgeless page header toolbar', () => {
expect(newNoteBox2).toEqual(noteBox); 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, page,
}) => { }) => {
const toolbar = locateHeaderToolbar(page); const toolbar = locateHeaderToolbar(page);
@@ -143,6 +147,45 @@ test.describe('edgeless page header toolbar', () => {
await expect(toolbar).toBeVisible(); await expect(toolbar).toBeVisible();
await expect(infoButton).toBeHidden(); 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', () => { test.describe('edgeless note element toolbar', () => {

View File

@@ -450,6 +450,7 @@ export const PackageList = [
workspaceDependencies: [ workspaceDependencies: [
'blocksuite/affine/block-note', 'blocksuite/affine/block-note',
'blocksuite/affine/block-surface', 'blocksuite/affine/block-surface',
'blocksuite/affine/components',
'blocksuite/affine/model', 'blocksuite/affine/model',
'blocksuite/affine/shared', 'blocksuite/affine/shared',
'blocksuite/framework/block-std', 'blocksuite/framework/block-std',

View File

@@ -3557,7 +3557,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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 version: 0.0.0-use.local
resolution: "@blocksuite/affine-block-note@workspace:blocksuite/affine/block-note" resolution: "@blocksuite/affine-block-note@workspace:blocksuite/affine/block-note"
dependencies: dependencies:
@@ -3576,6 +3576,7 @@ __metadata:
"@preact/signals-core": "npm:^1.8.0" "@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.7" "@toeverything/theme": "npm:^1.1.7"
"@types/mdast": "npm:^4.0.4" "@types/mdast": "npm:^4.0.4"
"@vanilla-extract/css": "npm:^1.17.0"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1" minimatch: "npm:^10.0.1"
zod: "npm:^3.23.8" zod: "npm:^3.23.8"
@@ -3941,6 +3942,7 @@ __metadata:
"@types/katex": "npm:^0.16.7" "@types/katex": "npm:^0.16.7"
"@types/lodash.isequal": "npm:^4.5.8" "@types/lodash.isequal": "npm:^4.5.8"
"@vanilla-extract/css": "npm:^1.17.0" "@vanilla-extract/css": "npm:^1.17.0"
"@vanilla-extract/vite-plugin": "npm:^5.0.0"
date-fns: "npm:^4.0.0" date-fns: "npm:^4.0.0"
dompurify: "npm:^3.1.6" dompurify: "npm:^3.1.6"
fflate: "npm:^0.8.2" fflate: "npm:^0.8.2"
@@ -4090,8 +4092,9 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@blocksuite/presets@workspace:blocksuite/presets" resolution: "@blocksuite/presets@workspace:blocksuite/presets"
dependencies: dependencies:
"@blocksuite/affine-block-note": "workspace:^" "@blocksuite/affine-block-note": "workspace:*"
"@blocksuite/affine-block-surface": "workspace:*" "@blocksuite/affine-block-surface": "workspace:*"
"@blocksuite/affine-components": "workspace:*"
"@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-model": "workspace:*"
"@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-shared": "workspace:*"
"@blocksuite/block-std": "workspace:*" "@blocksuite/block-std": "workspace:*"