chore(editor): reorg packages (#10702)

This commit is contained in:
Saul-Mirone
2025-03-08 03:57:04 +00:00
parent 334912e85b
commit 8aedef0a36
961 changed files with 837 additions and 927 deletions

View File

@@ -0,0 +1,5 @@
import { FrameBlockComponent } from './frame-block';
export function effects() {
customElements.define('affine-frame', FrameBlockComponent);
}

View File

@@ -0,0 +1,90 @@
import { DefaultTheme, type FrameBlockModel } from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { GfxBlockComponent } from '@blocksuite/block-std';
import { Bound } from '@blocksuite/global/gfx';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
override connectedCallback() {
super.connectedCallback();
this._disposables.add(
this.doc.slots.blockUpdated.on(({ type, id }) => {
if (id === this.model.id && type === 'update') {
this.requestUpdate();
}
})
);
this._disposables.add(
this.gfx.viewport.viewportUpdated.on(() => {
this.requestUpdate();
})
);
}
/**
* Due to potentially very large frame sizes, CSS scaling can cause iOS Safari to crash.
* To mitigate this issue, we combine size calculations within the rendering rect.
*/
override getCSSTransform(): string {
return '';
}
override getRenderingRect() {
const viewport = this.gfx.viewport;
const { translateX, translateY, zoom } = viewport;
const { xywh, rotate } = this.model;
const bound = Bound.deserialize(xywh);
const scaledX = bound.x * zoom + translateX;
const scaledY = bound.y * zoom + translateY;
return {
x: scaledX,
y: scaledY,
w: bound.w * zoom,
h: bound.h * zoom,
rotate,
zIndex: this.toZIndex(),
};
}
override renderGfxBlock() {
const { model, showBorder, std } = this;
const backgroundColor = std
.get(ThemeProvider)
.generateColorProperty(model.background, DefaultTheme.transparent);
const _isNavigator =
this.gfx.tool.currentToolName$.value === 'frameNavigator';
const frameIndex = this.gfx.layer.getZIndex(model);
return html`
<div
class="affine-frame-container"
style=${styleMap({
zIndex: `${frameIndex}`,
backgroundColor,
height: '100%',
width: '100%',
borderRadius: '2px',
border:
_isNavigator || !showBorder
? 'none'
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
})}
></div>
`;
}
@state()
accessor showBorder = true;
}
declare global {
interface HTMLElementTagNameMap {
'affine-frame': FrameBlockComponent;
}
}

View File

@@ -0,0 +1,472 @@
import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { Overlay } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { EditPropsStore } from '@blocksuite/affine-shared/services';
import {
generateKeyBetweenV2,
getTopElements,
GfxBlockElementModel,
type GfxController,
GfxExtension,
GfxExtensionIdentifier,
type GfxModel,
isGfxGroupCompatibleModel,
renderableInEdgeless,
} from '@blocksuite/block-std/gfx';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
Bound,
deserializeXYWH,
type IVec,
type SerializedXYWH,
} from '@blocksuite/global/gfx';
import { DisposableGroup } from '@blocksuite/global/slot';
import { type BlockModel, Text } from '@blocksuite/store';
import * as Y from 'yjs';
const FRAME_PADDING = 40;
export type NavigatorMode = 'fill' | 'fit';
export class FrameOverlay extends Overlay {
static override overlayName: string = 'frame';
private _disposable = new DisposableGroup();
private _frame: FrameBlockModel | null = null;
private _innerElements = new Set<GfxModel>();
private readonly _prevXYWH: SerializedXYWH | null = null;
private get _frameManager() {
return this.gfx.std.get(
GfxExtensionIdentifier('frame-manager')
) as EdgelessFrameManager;
}
constructor(gfx: GfxController) {
super(gfx);
}
private _reset() {
this._disposable.dispose();
this._disposable = new DisposableGroup();
this._frame = null;
this._innerElements.clear();
}
override clear() {
if (this._frame === null && this._innerElements.size === 0) return;
this._reset();
this._renderer?.refresh();
}
highlight(
frame: FrameBlockModel,
highlightElementsInBound = false,
highlightOutline = true
) {
if (!highlightElementsInBound && !highlightOutline) return;
let needRefresh = false;
if (highlightOutline && this._prevXYWH !== frame.xywh) {
needRefresh = true;
}
let innerElements = new Set<GfxModel>();
if (highlightElementsInBound) {
innerElements = new Set(
getTopElements(
this._frameManager.getElementsInFrameBound(frame)
).concat(
this._frameManager.getChildElementsInFrame(frame).filter(child => {
return frame.intersectsBound(child.elementBound);
})
)
);
if (!areSetsEqual(this._innerElements, innerElements)) {
needRefresh = true;
}
}
if (!needRefresh) return;
this._reset();
if (highlightOutline) this._frame = frame;
if (highlightElementsInBound) this._innerElements = innerElements;
this._disposable.add(
frame.deleted.once(() => {
this.clear();
})
);
this._renderer?.refresh();
}
override render(ctx: CanvasRenderingContext2D): void {
ctx.beginPath();
ctx.strokeStyle = '#1E96EB';
ctx.lineWidth = 2 / this.gfx.viewport.zoom;
const radius = 2 / this.gfx.viewport.zoom;
if (this._frame) {
const { x, y, w, h } = this._frame.elementBound;
ctx.roundRect(x, y, w, h, radius);
ctx.stroke();
}
this._innerElements.forEach(element => {
const [x, y, w, h] = deserializeXYWH(element.xywh);
ctx.translate(x + w / 2, y + h / 2);
ctx.rotate(element.rotate);
ctx.roundRect(-w / 2, -h / 2, w, h, radius);
ctx.translate(-x - w / 2, -y - h / 2);
ctx.stroke();
});
}
}
export class EdgelessFrameManager extends GfxExtension {
static override key = 'frame-manager';
private readonly _disposable = new DisposableGroup();
/**
* Get all sorted frames by presentation orderer,
* the legacy frame that uses `index` as presentation order
* will be put at the beginning of the array.
*/
get frames() {
return Object.values(this.gfx.doc.blocks.value)
.map(({ model }) => model)
.filter(isFrameBlock)
.sort(EdgelessFrameManager.framePresentationComparator);
}
constructor(gfx: GfxController) {
super(gfx);
this._watchElementAdded();
}
static framePresentationComparator<
T extends FrameBlockModel | { index: string; presentationIndex?: string },
>(a: T, b: T) {
function stringCompare(a: string, b: string) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
if (
'presentationIndex$' in a &&
'presentationIndex$' in b &&
a.presentationIndex$.value &&
b.presentationIndex$.value
) {
return stringCompare(
a.presentationIndex$.value,
b.presentationIndex$.value
);
} else if (a.presentationIndex && b.presentationIndex) {
return stringCompare(a.presentationIndex, b.presentationIndex);
} else if (a.presentationIndex) {
return -1;
} else if (b.presentationIndex) {
return 1;
} else {
return stringCompare(a.index, b.index);
}
}
private _addChildrenToLegacyFrame(frame: FrameBlockModel) {
if (frame.childElementIds !== undefined) return;
const elements = this.getElementsInFrameBound(frame);
const childElements = elements.filter(
element => this.getParentFrame(element) === null && element !== frame
);
frame.addChildren(childElements);
}
private _addFrameBlock(bound: Bound) {
const surfaceModel = this.gfx.surface as SurfaceBlockModel;
const props = this.gfx.std
.get(EditPropsStore)
.applyLastProps('affine:frame', {
title: new Text(new Y.Text(`Frame ${this.frames.length + 1}`)),
xywh: bound.serialize(),
index: this.gfx.layer.generateIndex(true),
presentationIndex: this.generatePresentationIndex(),
});
const id = this.gfx.doc.addBlock('affine:frame', props, surfaceModel);
const frameModel = this.gfx.getElementById(id);
if (!frameModel || !isFrameBlock(frameModel)) {
throw new BlockSuiteError(
ErrorCode.GfxBlockElementError,
'Frame model is not found'
);
}
return frameModel;
}
private _watchElementAdded() {
if (!this.gfx.surface) {
return;
}
const { surface: surfaceModel, doc } = this.gfx;
this._disposable.add(
surfaceModel.elementAdded.on(({ id, local }) => {
const element = surfaceModel.getElementById(id);
if (element && local) {
const frame = this.getFrameFromPoint(element.elementBound.center);
// if the container created with a frame, skip it.
if (
isGfxGroupCompatibleModel(element) &&
frame &&
element.hasChild(frame)
) {
return;
}
// new element may intended to be added to other group
// so we need to wait for the next microtask to check if the element can be added to the frame
queueMicrotask(() => {
if (!element.group && frame) {
this.addElementsToFrame(frame, [element]);
}
});
}
})
);
this._disposable.add(
doc.slots.blockUpdated.on(payload => {
if (
payload.type === 'add' &&
payload.model instanceof GfxBlockElementModel &&
renderableInEdgeless(doc, surfaceModel, payload.model)
) {
const frame = this.getFrameFromPoint(
payload.model.elementBound.center,
isFrameBlock(payload.model) ? [payload.model] : []
);
if (!frame) return;
if (
isFrameBlock(payload.model) &&
payload.model.containsBound(frame.elementBound)
) {
return;
}
this.addElementsToFrame(frame, [payload.model]);
}
})
);
}
/**
* Reset parent of elements to the frame
*/
addElementsToFrame(frame: FrameBlockModel, elements: GfxModel[]) {
if (frame.isLocked()) return;
if (frame.childElementIds === undefined) {
this._addChildrenToLegacyFrame(frame);
}
elements = elements.filter(
el => el !== frame && !frame.childElements.includes(el)
);
if (elements.length === 0) return;
frame.addChildren(elements);
}
createFrameOnBound(bound: Bound) {
const frameModel = this._addFrameBlock(bound);
this.addElementsToFrame(
frameModel,
getTopElements(this.getElementsInFrameBound(frameModel))
);
this.gfx.doc.captureSync();
this.gfx.selection.set({
elements: [frameModel.id],
editing: false,
});
return frameModel;
}
createFrameOnElements(elements: GfxModel[]) {
// make sure all elements are in the same level
for (const element of elements) {
if (element.group !== elements[0].group) return;
}
const parentFrameBound = this.getParentFrame(elements[0])?.elementBound;
let bound = this.gfx.selection.selectedBound;
if (parentFrameBound?.contains(bound)) {
bound.x -= Math.min(0.5 * (bound.x - parentFrameBound.x), FRAME_PADDING);
bound.y -= Math.min(0.5 * (bound.y - parentFrameBound.y), FRAME_PADDING);
bound.w += Math.min(
0.5 * (parentFrameBound.x + parentFrameBound.w - bound.x - bound.w),
FRAME_PADDING
);
bound.h += Math.min(
0.5 * (parentFrameBound.y + parentFrameBound.h - bound.y - bound.h),
FRAME_PADDING
);
} else {
bound = bound.expand(FRAME_PADDING);
}
const frameModel = this._addFrameBlock(bound);
this.addElementsToFrame(frameModel, getTopElements(elements));
this.gfx.doc.captureSync();
this.gfx.selection.set({
elements: [frameModel.id],
editing: false,
});
return frameModel;
}
createFrameOnSelected() {
return this.createFrameOnElements(this.gfx.selection.selectedElements);
}
createFrameOnViewportCenter(wh: [number, number]) {
const center = this.gfx.viewport.center;
const bound = new Bound(
center.x - wh[0] / 2,
center.y - wh[1] / 2,
wh[0],
wh[1]
);
this.createFrameOnBound(bound);
}
generatePresentationIndex() {
const before =
this.frames[this.frames.length - 1]?.presentationIndex ?? null;
return generateKeyBetweenV2(before, null);
}
/**
* Get all elements in the frame, there are three cases:
* 1. The frame doesn't have `childElements`, return all elements in the frame bound but not owned by another frame.
* 2. Return all child elements of the frame if `childElements` exists.
*/
getChildElementsInFrame(frame: FrameBlockModel): GfxModel[] {
if (frame.childElementIds === undefined) {
return this.getElementsInFrameBound(frame).filter(
element => this.getParentFrame(element) !== null
);
}
const childElements = frame.childIds
.map(id => this.gfx.getElementById(id))
.filter(element => element !== null);
return childElements as GfxModel[];
}
/**
* Get all elements in the frame bound,
* whatever the element already has another parent frame or not.
*/
getElementsInFrameBound(frame: FrameBlockModel, fullyContained = true) {
const bound = Bound.deserialize(frame.xywh);
const elements: GfxModel[] = this.gfx.grid
.search(bound, { strict: fullyContained })
.filter(element => element !== frame);
return elements;
}
/**
* Get most top frame from the point.
*/
getFrameFromPoint([x, y]: IVec, ignoreFrames: FrameBlockModel[] = []) {
for (let i = this.frames.length - 1; i >= 0; i--) {
const frame = this.frames[i];
if (frame.includesPoint(x, y, {}) && !ignoreFrames.includes(frame)) {
return frame;
}
}
return null;
}
getParentFrame(element: GfxModel) {
const container = element.group;
return container && isFrameBlock(container) ? container : null;
}
/**
* This method will populate `presentationIndex` for all legacy frames,
* and keep the orderer of the legacy frames.
*/
refreshLegacyFrameOrder() {
const frames = this.frames.splice(0, this.frames.length);
let splitIndex = frames.findIndex(frame => frame.presentationIndex);
if (splitIndex === 0) return;
if (splitIndex === -1) splitIndex = frames.length;
let afterPreIndex =
frames[splitIndex]?.presentationIndex || generateKeyBetweenV2(null, null);
for (let index = splitIndex - 1; index >= 0; index--) {
const preIndex = generateKeyBetweenV2(null, afterPreIndex);
frames[index].presentationIndex = preIndex;
afterPreIndex = preIndex;
}
}
removeAllChildrenFromFrame(frame: FrameBlockModel) {
this.gfx.doc.transact(() => {
frame.childElementIds = {};
});
}
removeFromParentFrame(element: GfxModel) {
const parentFrame = this.getParentFrame(element);
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
parentFrame?.removeChild(element);
}
override unmounted(): void {
this._disposable.dispose();
}
}
function areSetsEqual<T>(setA: Set<T>, setB: Set<T>) {
if (setA.size !== setB.size) return false;
for (const a of setA) if (!setB.has(a)) return false;
return true;
}
export function isFrameBlock(element: unknown): element is FrameBlockModel {
return !!element && (element as BlockModel).flavour === 'affine:frame';
}

View File

@@ -0,0 +1,7 @@
import { BlockViewExtension } from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
export const FrameBlockSpec: ExtensionType[] = [
BlockViewExtension('affine:frame', literal`affine-frame`),
];

View File

@@ -0,0 +1,4 @@
export * from './frame-block.js';
export * from './frame-manager.js';
export * from './frame-spec.js';
export * from './tool.js';

View File

@@ -0,0 +1,21 @@
import { BaseTool } from '@blocksuite/block-std/gfx';
import type { NavigatorMode } from './frame-manager';
type PresentToolOption = {
mode?: NavigatorMode;
};
export class PresentTool extends BaseTool<PresentToolOption> {
static override toolName: string = 'frameNavigator';
}
declare module '@blocksuite/block-std/gfx' {
interface GfxToolsMap {
frameNavigator: PresentTool;
}
interface GfxToolsOption {
frameNavigator: PresentToolOption;
}
}