mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 06:16:59 +08:00
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:
@@ -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"
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
85
blocksuite/affine/block-note/src/note-edgeless-block.css.ts
Normal file
85
blocksuite/affine/block-note/src/note-edgeless-block.css.ts
Normal 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',
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
11
blocksuite/affine/components/src/doc-title/effects.ts
Normal file
11
blocksuite/affine/components/src/doc-title/effects.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
blocksuite/affine/components/src/doc-title/index.ts
Normal file
3
blocksuite/affine/components/src/doc-title/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { DocTitle } from './doc-title';
|
||||||
|
export { effects } from './effects';
|
||||||
|
export { getDocTitleByEditorHost } from './utils';
|
||||||
11
blocksuite/affine/components/src/doc-title/utils.ts
Normal file
11
blocksuite/affine/components/src/doc-title/utils.ts
Normal 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');
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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:*",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
blocksuite/presets/src/fragments/doc-title/index.ts
Normal file
1
blocksuite/presets/src/fragments/doc-title/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from '@blocksuite/affine-components/doc-title';
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user