mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-22 08:47:10 +08:00
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
202 lines
5.2 KiB
TypeScript
202 lines
5.2 KiB
TypeScript
import type { EditorHost } from '@blocksuite/block-std';
|
|
import { NoteDisplayMode } from '@blocksuite/blocks';
|
|
import { clamp, DisposableGroup } from '@blocksuite/global/utils';
|
|
|
|
import type { AffineEditorContainer } from '../../../editors/editor-container.js';
|
|
import { getDocTitleByEditorHost } from '../../doc-title/index.js';
|
|
import { getHeadingBlocksFromDoc } from './query.js';
|
|
|
|
export function scrollToBlock(editor: AffineEditorContainer, blockId: string) {
|
|
const { host, mode } = editor;
|
|
if (mode === 'edgeless' || !host) return;
|
|
|
|
if (editor.doc.root?.id === blockId) {
|
|
const docTitle = getDocTitleByEditorHost(host);
|
|
if (!docTitle) return;
|
|
|
|
docTitle.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start',
|
|
});
|
|
} else {
|
|
const block = host.view.getBlock(blockId);
|
|
if (!block) return;
|
|
block.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center',
|
|
});
|
|
}
|
|
}
|
|
|
|
export function isBlockBeforeViewportCenter(
|
|
blockId: string,
|
|
editorHost: EditorHost
|
|
) {
|
|
const block = editorHost.view.getBlock(blockId);
|
|
if (!block) return false;
|
|
|
|
const editorRect = (
|
|
editorHost.parentElement ?? editorHost
|
|
).getBoundingClientRect();
|
|
const blockRect = block.getBoundingClientRect();
|
|
|
|
const editorCenter =
|
|
clamp(editorRect.top, 0, document.documentElement.clientHeight) +
|
|
Math.min(editorRect.height, document.documentElement.clientHeight) / 2;
|
|
|
|
const blockCenter = blockRect.top + blockRect.height / 2;
|
|
|
|
return blockCenter < editorCenter + blockRect.height;
|
|
}
|
|
|
|
export const observeActiveHeadingDuringScroll = (
|
|
getEditor: () => AffineEditorContainer, // workaround for editor changed
|
|
update: (activeHeading: string | null) => void
|
|
) => {
|
|
const handler = () => {
|
|
const { host } = getEditor();
|
|
if (!host) return;
|
|
|
|
const headings = getHeadingBlocksFromDoc(
|
|
host.doc,
|
|
[NoteDisplayMode.DocAndEdgeless, NoteDisplayMode.DocOnly],
|
|
true
|
|
);
|
|
|
|
let activeHeadingId = host.doc.root?.id ?? null;
|
|
headings.forEach(heading => {
|
|
if (isBlockBeforeViewportCenter(heading.id, host)) {
|
|
activeHeadingId = heading.id;
|
|
}
|
|
});
|
|
update(activeHeadingId);
|
|
};
|
|
handler();
|
|
|
|
const disposables = new DisposableGroup();
|
|
disposables.addFromEvent(window, 'scroll', handler, true);
|
|
|
|
return disposables;
|
|
};
|
|
|
|
let highlightMask: HTMLDivElement | null = null;
|
|
let highlightTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function highlightBlock(editor: AffineEditorContainer, blockId: string) {
|
|
const emptyClear = () => {};
|
|
|
|
const { host } = editor;
|
|
if (!host) return emptyClear;
|
|
|
|
if (editor.doc.root?.id === blockId) return emptyClear;
|
|
|
|
const rootComponent = host.querySelector('affine-page-root');
|
|
if (!rootComponent) return emptyClear;
|
|
|
|
if (!rootComponent.viewport) {
|
|
console.error('viewport should exist');
|
|
return emptyClear;
|
|
}
|
|
|
|
const {
|
|
top: offsetY,
|
|
left: offsetX,
|
|
scrollTop,
|
|
scrollLeft,
|
|
} = rootComponent.viewport;
|
|
|
|
const block = host.view.getBlock(blockId);
|
|
if (!block) return emptyClear;
|
|
|
|
const blockRect = block.getBoundingClientRect();
|
|
const { top, left, width, height } = blockRect;
|
|
|
|
if (!highlightMask) {
|
|
highlightMask = document.createElement('div');
|
|
rootComponent.append(highlightMask);
|
|
}
|
|
|
|
Object.assign(highlightMask.style, {
|
|
position: 'absolute',
|
|
top: `${top - offsetY + scrollTop}px`,
|
|
left: `${left - offsetX + scrollLeft}px`,
|
|
width: `${width}px`,
|
|
height: `${height}px`,
|
|
background: 'var(--affine-hover-color)',
|
|
borderRadius: '4px',
|
|
display: 'block',
|
|
});
|
|
|
|
// Clear the previous timeout if it exists
|
|
if (highlightTimeoutId !== null) {
|
|
clearTimeout(highlightTimeoutId);
|
|
}
|
|
|
|
highlightTimeoutId = setTimeout(() => {
|
|
if (highlightMask) {
|
|
highlightMask.style.display = 'none';
|
|
}
|
|
}, 1000);
|
|
|
|
return () => {
|
|
if (highlightMask !== null) {
|
|
highlightMask.remove();
|
|
highlightMask = null;
|
|
}
|
|
if (highlightTimeoutId !== null) {
|
|
clearTimeout(highlightTimeoutId);
|
|
highlightTimeoutId = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
// this function is useful when the scroll need smooth animation
|
|
let highlightIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
export async function scrollToBlockWithHighlight(
|
|
editor: AffineEditorContainer,
|
|
blockId: string,
|
|
timeout = 3000
|
|
) {
|
|
scrollToBlock(editor, blockId);
|
|
|
|
let timeCount = 0;
|
|
|
|
return new Promise<ReturnType<typeof highlightBlock>>(resolve => {
|
|
if (highlightIntervalId !== null) {
|
|
clearInterval(highlightIntervalId);
|
|
}
|
|
|
|
// wait block be scrolled into view
|
|
let lastTop = -1;
|
|
highlightIntervalId = setInterval(() => {
|
|
if (highlightIntervalId === null) {
|
|
console.error('unreachable code');
|
|
return;
|
|
}
|
|
|
|
const { host } = editor;
|
|
const block = host?.view.getBlock(blockId);
|
|
|
|
if (!host || !block || timeCount > timeout) {
|
|
clearInterval(highlightIntervalId);
|
|
resolve(() => {});
|
|
return;
|
|
}
|
|
|
|
const blockRect = block.getBoundingClientRect();
|
|
const { top } = blockRect;
|
|
|
|
if (top !== lastTop) {
|
|
timeCount += 100;
|
|
lastTop = top;
|
|
return;
|
|
}
|
|
|
|
clearInterval(highlightIntervalId);
|
|
|
|
// highlight block
|
|
resolve(highlightBlock(editor, blockId));
|
|
}, 100);
|
|
});
|
|
}
|