refactor(editor): extract root block (#10356)

Closes: [BS-2207](https://linear.app/affine-design/issue/BS-2207/move-root-block-to-affineblock-root)
This commit is contained in:
Saul-Mirone
2025-02-21 12:38:26 +00:00
parent 4a66ec7400
commit 55651503df
336 changed files with 626 additions and 423 deletions
@@ -0,0 +1,23 @@
import type { BlockComponent, EditorHost } from '@blocksuite/block-std';
import type { BlockModel } from '@blocksuite/store';
// Run the callback until a model's element updated.
// Please notice that the callback will be called **once the element itself is ready**.
// The children may be not updated.
// If you want to wait for the text elements,
// please use `onModelTextUpdated`.
export async function onModelElementUpdated(
editorHost: EditorHost,
model: BlockModel,
callback: (block: BlockComponent) => void
) {
const page = model.doc;
if (!page.root) return;
const rootComponent = editorHost.view.getBlock(page.root.id);
if (!rootComponent) return;
await rootComponent.updateComplete;
const element = editorHost.view.getBlock(model.id);
if (element) callback(element);
}
@@ -0,0 +1,2 @@
export * from './callback';
export * from './types';
@@ -0,0 +1,18 @@
export function formatDate(date: Date) {
// yyyy-mm-dd
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const strTime = `${year}-${month}-${day}`;
return strTime;
}
export function formatTime(date: Date) {
// mm-dd hh:mm
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const strTime = `${month}-${day} ${hours}:${minutes}`;
return strTime;
}
@@ -0,0 +1,139 @@
import { clamp } from '@blocksuite/affine-shared/utils';
type CollisionBox = {
/**
* The point that the objRect is positioned to.
*/
positioningPoint: { x: number; y: number };
/**
* The boundary rect of the obj that is being positioned.
*/
objRect?: { height: number; width: number };
/**
* The boundary rect of the container that the obj is in.
*/
boundaryRect?: DOMRect;
offsetX?: number;
offsetY?: number;
edgeGap?: number;
};
export function calcSafeCoordinate({
positioningPoint,
objRect = { width: 0, height: 0 },
boundaryRect = document.body.getBoundingClientRect(),
offsetX = 0,
offsetY = 0,
edgeGap = 20,
}: CollisionBox) {
const safeX = clamp(
positioningPoint.x + offsetX,
edgeGap,
boundaryRect.width - objRect.width - edgeGap
);
const y = positioningPoint.y + offsetY;
// Not use clamp for y coordinate to avoid the quick bar always showing after scrolling
// const safeY = clamp(
// positioningPoint.y + offsetY,
// edgeGap,
// boundaryRect.height - objRect.height - edgeGap
// );
return {
x: safeX,
y,
};
}
/**
* Used to compare the space available
* at the top and bottom of an element within a container.
*
* Please give preference to {@link getPopperPosition}
*/
export function compareTopAndBottomSpace(
obj: { getBoundingClientRect: () => DOMRect },
container = document.body,
gap = 20
) {
const objRect = obj.getBoundingClientRect();
const spaceRect = container.getBoundingClientRect();
const topSpace = objRect.top - spaceRect.top;
const bottomSpace = spaceRect.bottom - objRect.bottom;
const topOrBottom: 'top' | 'bottom' =
topSpace > bottomSpace ? 'top' : 'bottom';
return {
placement: topOrBottom,
// the height is the available space.
height: (topOrBottom === 'top' ? topSpace : bottomSpace) - gap,
};
}
/**
* Get the position of the popper element with flip.
* return null if the reference rect is all zero.
*/
export function getPopperPosition(
popper: {
getBoundingClientRect: () => DOMRect;
},
reference: {
getBoundingClientRect: () => DOMRect;
},
{ gap = 12, offsetY = 5 }: { gap?: number; offsetY?: number } = {}
) {
if (!popper) {
// foolproof, someone may use element with non-null assertion
console.warn(
'The popper element is not exist. Popper position maybe incorrect'
);
}
const { placement, height } = compareTopAndBottomSpace(
reference,
document.body,
gap + offsetY
);
const referenceRect = reference.getBoundingClientRect();
if (
referenceRect.x === 0 &&
referenceRect.y === 0 &&
referenceRect.width === 0 &&
referenceRect.height === 0
)
return null;
const positioningPoint = {
x: referenceRect.x,
y: referenceRect.y + (placement === 'bottom' ? referenceRect.height : 0),
};
// TODO maybe use the editor container as the boundary rect to avoid the format bar being covered by other elements
const boundaryRect = document.body.getBoundingClientRect();
// Note: the popperRect.height maybe incorrect
// because we are calculated its correct height
const popperRect = popper?.getBoundingClientRect();
const safeCoordinate = calcSafeCoordinate({
positioningPoint,
objRect: popperRect,
boundaryRect,
offsetY: placement === 'bottom' ? offsetY : -offsetY,
});
return {
placement,
/**
* The height is the available space height.
*
* Note: it's a max height, not the real height,
* because sometimes the popper's height is smaller than the available space.
*/
height,
x: `${safeCoordinate.x}px`,
y:
placement === 'bottom'
? `${safeCoordinate.y}px`
: // We need to use `calc(-100%)` since the height of popper maybe incorrect
`calc(${safeCoordinate.y}px - 100%)`,
};
}
@@ -0,0 +1,7 @@
import type { RootBlockComponent } from '../types.js';
export function getClosestRootBlockComponent(
el: HTMLElement
): RootBlockComponent | null {
return el.closest('affine-edgeless-root, affine-page-root');
}
@@ -0,0 +1,45 @@
import { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark';
import {
EmbedFigmaBlockComponent,
EmbedGithubBlockComponent,
EmbedHtmlBlockComponent,
EmbedLinkedDocBlockComponent,
EmbedLoomBlockComponent,
EmbedSyncedDocBlockComponent,
EmbedYoutubeBlockComponent,
} from '@blocksuite/affine-block-embed';
import type { BlockComponent } from '@blocksuite/block-std';
export type ExternalEmbedBlockComponent =
| BookmarkBlockComponent
| EmbedFigmaBlockComponent
| EmbedGithubBlockComponent
| EmbedLoomBlockComponent
| EmbedYoutubeBlockComponent;
export type InternalEmbedBlockComponent =
| EmbedLinkedDocBlockComponent
| EmbedSyncedDocBlockComponent;
export type LinkableEmbedBlockComponent =
| ExternalEmbedBlockComponent
| InternalEmbedBlockComponent;
export type BuiltInEmbedBlockComponent =
| LinkableEmbedBlockComponent
| EmbedHtmlBlockComponent;
export function isEmbedCardBlockComponent(
block: BlockComponent
): block is BuiltInEmbedBlockComponent {
return (
block instanceof BookmarkBlockComponent ||
block instanceof EmbedFigmaBlockComponent ||
block instanceof EmbedGithubBlockComponent ||
block instanceof EmbedHtmlBlockComponent ||
block instanceof EmbedLoomBlockComponent ||
block instanceof EmbedYoutubeBlockComponent ||
block instanceof EmbedLinkedDocBlockComponent ||
block instanceof EmbedSyncedDocBlockComponent
);
}