refactor(editor): implement uni-component in AFFiNE (#10747)

This commit is contained in:
zzj3720
2025-03-10 14:23:24 +00:00
parent 027d3a51dc
commit 4a45cc9ba4
23 changed files with 177 additions and 63 deletions
@@ -5,7 +5,7 @@ import type {
import { ShadowlessElement } from '@blocksuite/block-std';
import { IS_MOBILE } from '@blocksuite/global/env';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { css, unsafeCSS } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -63,7 +63,7 @@ export class DataViewRenderer extends SignalWatcher(
}
`;
private readonly _view = createRef<{
private readonly _view = signal<{
expose: DataViewInstance;
}>();
@@ -3,6 +3,7 @@ import {
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import {
@@ -19,10 +20,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js';
import { dataViewCommonStyle } from '../common/css-variable.js';
import {
renderUniLit,
type UniComponent,
} from '../utils/uni-component/uni-component.js';
import { renderUniLit } from '../utils/uni-component/uni-component.js';
import type { SingleView } from '../view-manager/single-view.js';
import { DetailSelection } from './selection.js';
@@ -11,11 +11,10 @@ import {
MoveLeftIcon,
MoveRightIcon,
} from '@blocksuite/icons/lit';
import { computed } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { inputConfig, typeConfig } from '../common/property-menu.js';
@@ -109,7 +108,7 @@ export class RecordField extends SignalWatcher(
}
`;
private readonly _cell = createRef<DataViewCellLifeCycle>();
private readonly _cell = signal<DataViewCellLifeCycle>();
_click = (e: MouseEvent) => {
e.stopPropagation();
@@ -1,5 +1,6 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { TypeInstance } from '../logical/type.js';
import type { UniComponent } from '../utils/index.js';
export type VariableRef = {
type: 'ref';
@@ -1,6 +1,7 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { TypeInstance } from '../logical/type.js';
import type { DVJSON } from '../property/types.js';
import type { UniComponent } from '../utils/index.js';
export interface GroupRenderProps<
Data extends NonNullable<unknown> = NonNullable<unknown>,
@@ -13,6 +13,10 @@ export abstract class BaseCellRenderer<
extends SignalWatcher(WithDisposable(ShadowlessElement))
implements DataViewCellLifeCycle, CellRenderProps<Data, Value>
{
get expose() {
return this;
}
@property({ attribute: false })
accessor cell!: Cell<Value, Data>;
@@ -1,4 +1,5 @@
import type { UniComponent } from '../utils/uni-component/index.js';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { Cell } from '../view-manager/cell.js';
export interface CellRenderProps<
@@ -1,7 +1,6 @@
import {
createUniComponentFromWebComponent,
type UniComponent,
} from '../utils/uni-component/index.js';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { createUniComponentFromWebComponent } from '../utils/uni-component/index.js';
import type { BaseCellRenderer } from './base-cell.js';
import type { CellRenderer, DataViewCellComponent } from './manager.js';
@@ -1,17 +1,16 @@
import type { UniComponent } from './uni-component.js';
import type { UniComponent } from '@blocksuite/affine-shared/types';
export const uniMap = <T, R, P extends NonNullable<unknown>>(
component: UniComponent<T, P>,
map: (r: R) => T
): UniComponent<R, P> => {
return (ele, props) => {
const result = component(ele, map(props));
return (ele, props, expose) => {
const result = component(ele, map(props), expose);
return {
unmount: result.unmount,
update: props => {
result.update(map(props));
},
expose: result.expose,
};
};
};
@@ -1,28 +1,20 @@
import type {
UniComponent,
UniComponentReturn,
} from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher } from '@blocksuite/global/lit';
import type { Signal } from '@preact/signals-core';
import type { LitElement, PropertyValues, TemplateResult } from 'lit';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import type { Ref } from 'lit/directives/ref.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
export type UniComponentReturn<
Props = NonNullable<unknown>,
Expose extends NonNullable<unknown> = NonNullable<unknown>,
> = {
update: (props: Props) => void;
unmount: () => void;
expose: Expose;
};
export type UniComponent<
Props = NonNullable<unknown>,
Expose extends NonNullable<unknown> = NonNullable<unknown>,
> = (ele: HTMLElement, props: Props) => UniComponentReturn<Props, Expose>;
export const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
uni: UniComponent<Props, Expose> | undefined,
props?: Props,
options?: {
ref?: Ref<Expose>;
ref?: Signal<Expose | undefined>;
style?: Readonly<StyleInfo>;
class?: string;
}
@@ -45,18 +37,21 @@ export class UniLit<
}
`;
uniReturn?: UniComponentReturn<Props, Expose>;
uniReturn?: UniComponentReturn<Props>;
private _expose?: Expose;
get expose(): Expose | undefined {
return this.uniReturn?.expose;
return this._expose;
}
private mount() {
this.uniReturn = this.uni?.(this, this.props);
if (this.ref) {
// @ts-expect-error FIXME: ts error
this.ref.value = this.uniReturn?.expose;
}
this.uniReturn = this.uni?.(this, this.props, value => {
if (this.ref) {
this.ref.value = value;
this._expose = value;
}
});
}
private unmount() {
@@ -91,7 +86,7 @@ export class UniLit<
accessor props!: Props;
@property({ attribute: false })
accessor ref: Ref<Expose> | undefined = undefined;
accessor ref: Signal<Expose | undefined> | undefined = undefined;
@property({ attribute: false })
accessor uni: UniComponent<Props, Expose> | undefined = undefined;
@@ -103,10 +98,12 @@ export const createUniComponentFromWebComponent = <
>(
component: typeof LitElement
): UniComponent<T, Expose> => {
return (ele, props) => {
return (ele, props, expose) => {
const ins = new component();
Object.assign(ins, props);
ele.append(ins);
// @ts-expect-error ins.expose may not exist in all component instances
expose(ins.expose);
return {
update: props => {
Object.assign(ins, props);
@@ -115,7 +112,6 @@ export const createUniComponentFromWebComponent = <
unmount: () => {
ins.remove();
},
expose: ins as never as Expose,
};
};
};
@@ -140,12 +136,13 @@ export class UniAnyRender<
export const defineUniComponent = <T, Expose extends NonNullable<unknown>>(
renderTemplate: (props: T, expose: Expose) => TemplateResult
): UniComponent<T, Expose> => {
return (ele, props) => {
return (ele, props, expose) => {
const ins = new UniAnyRender<T, Expose>();
ins.props = props;
ins.expose = {} as Expose;
ins.renderTemplate = renderTemplate;
ele.append(ins);
expose(ins.expose);
return {
update: props => {
ins.props = props;
@@ -154,7 +151,6 @@ export const defineUniComponent = <T, Expose extends NonNullable<unknown>>(
unmount: () => {
ins.remove();
},
expose: ins.expose,
};
};
};
@@ -1,9 +1,9 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { TypeInstance } from '../logical/type.js';
import type { CellRenderer } from '../property/index.js';
import type { PropertyDataUpdater } from '../types.js';
import type { UniComponent } from '../utils/uni-component/index.js';
import type { Cell } from './cell.js';
import type { SingleView } from './single-view.js';
@@ -1,3 +1,4 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
@@ -8,7 +9,6 @@ import type { TypeInstance } from '../logical/type.js';
import type { PropertyMetaConfig } from '../property/property-config.js';
import type { TraitKey } from '../traits/key.js';
import type { DatabaseFlags } from '../types.js';
import type { UniComponent } from '../utils/uni-component/index.js';
import type { DataViewDataType, ViewMeta } from '../view/data-view.js';
import { type Cell, CellBase } from './cell.js';
import type { Property } from './property.js';
@@ -1,4 +1,5 @@
import type { UniComponent } from '../utils/uni-component/index.js';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { SingleView } from '../view-manager/single-view.js';
import type { ViewManager } from '../view-manager/view-manager.js';
import type { DataViewInstance, DataViewProps } from './types.js';
@@ -1,4 +1,5 @@
import type { UniComponent } from '../utils/uni-component/index.js';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { DataViewInstance } from '../view/types.js';
export type DataViewWidgetProps = {
@@ -3,10 +3,9 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, effect } from '@preact/signals-core';
import { computed, effect, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import type {
@@ -51,7 +50,7 @@ export class MobileKanbanCell extends SignalWatcher(
) {
static override styles = styles;
private readonly _cell = createRef<DataViewCellLifeCycle>();
private readonly _cell = signal<DataViewCellLifeCycle>();
isEditing$ = computed(() => {
const selection = this.kanban?.props.selection$.value;
@@ -2,9 +2,9 @@
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import type {
@@ -59,7 +59,7 @@ export class KanbanCell extends SignalWatcher(
) {
static override styles = styles;
private readonly _cell = createRef<DataViewCellLifeCycle>();
private readonly _cell = signal<DataViewCellLifeCycle>();
selectCurrentCell = (editing: boolean) => {
const selectionView = this.closest(
@@ -1,9 +1,8 @@
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, effect } from '@preact/signals-core';
import { computed, effect, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef } from 'lit/directives/ref.js';
import {
type CellRenderProps,
@@ -36,7 +35,7 @@ export class MobileTableCell extends SignalWatcher(
}
`;
private readonly _cell = createRef<DataViewCellLifeCycle>();
private readonly _cell = signal<DataViewCellLifeCycle>();
@property({ attribute: false })
accessor column!: TableColumn;
@@ -1,9 +1,8 @@
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef } from 'lit/directives/ref.js';
import { renderUniLit } from '../../../core/index.js';
import type {
@@ -40,7 +39,7 @@ export class DatabaseCellContainer extends SignalWatcher(
}
`;
private readonly _cell = createRef<DataViewCellLifeCycle>();
private readonly _cell = signal<DataViewCellLifeCycle>();
@property({ attribute: false })
accessor column!: TableColumn;
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
} from '@blocksuite/affine-model';
import type { BlockComponent } from '@blocksuite/block-std';
import type { BlockModel } from '@blocksuite/store';
export * from './uni-component';
export interface EditingState {
element: BlockComponent;
model: BlockModel;
@@ -0,0 +1,15 @@
export type ExposeWrapper<
Expose extends NonNullable<unknown> = NonNullable<unknown>,
> = (value?: Expose) => void;
export type UniComponentReturn<Props = NonNullable<unknown>> = {
update: (props: Props) => void;
unmount: () => void;
};
export type UniComponent<
Props = NonNullable<unknown>,
Expose extends NonNullable<unknown> = NonNullable<unknown>,
> = (
ele: HTMLElement,
props: Props,
expose: ExposeWrapper<Expose>
) => UniComponentReturn<Props>;
@@ -3,3 +3,4 @@ export {
useLitPortal,
useLitPortalFactory,
} from './lit-portal';
export { uniReactRoot } from './uni-component';
@@ -0,0 +1,96 @@
import type {
ExposeWrapper,
UniComponent,
UniComponentReturn,
} from '@blocksuite/affine-shared/types';
import { nanoid } from 'nanoid';
import {
type ComponentType,
memo,
type ReactNode,
useEffect,
useState,
} from 'react';
import { createPortal } from 'react-dom';
const UniReactNode = memo(
function UniReactNode(props: {
ele: HTMLElement;
component: ComponentType<any>;
dataRef: DataRef<any>;
expose: ExposeWrapper<any>;
}) {
const Component = props.component;
const [data, setData] = useState(props.dataRef.value);
useEffect(() => {
return props.dataRef.subscribe(() => {
setData(props.dataRef.value);
});
}, [props.dataRef]);
return createPortal(<Component ref={props.expose} {...data} />, props.ele);
},
() => true
);
type DataRef<T> = {
value: T;
subscribe: (update: () => void) => void;
update: (props: T) => void;
};
const createDataRef = <T,>(data: T): DataRef<T> => {
let listener = () => {};
const ref = {
value: data,
subscribe: (update: () => void) => {
listener = update;
return () => {
listener = () => {};
};
},
update: (props: T) => {
ref.value = props;
listener();
},
};
return ref;
};
export const createUniReactRoot = () => {
const nodes: Set<ReactNode> = new Set();
let updateNodes = () => {};
return {
Root: () => {
const [, forceUpdate] = useState({});
useEffect(() => {
updateNodes = () => forceUpdate({});
}, []);
return nodes;
},
createUniComponent: <T, E extends NonNullable<unknown>>(
component: ComponentType<T>
): UniComponent<T, E> => {
return (ele: HTMLElement, props: T, expose) => {
const dataRef = createDataRef(props);
const node = (
<UniReactNode
key={nanoid()}
expose={expose}
ele={ele}
component={component}
dataRef={dataRef}
/>
);
nodes.add(node);
updateNodes();
return {
update: (props: T) => {
dataRef.update(props);
},
unmount: () => {
nodes.delete(node);
updateNodes();
},
} satisfies UniComponentReturn<T>;
};
},
};
};
export const uniReactRoot = createUniReactRoot();
@@ -1,4 +1,8 @@
import { ConfirmModalProvider, PromptModalProvider } from '@affine/component';
import {
ConfirmModalProvider,
PromptModalProvider,
uniReactRoot,
} from '@affine/component';
import { ProviderComposer } from '@affine/component/provider-composer';
import { ThemeProvider } from '@affine/core/components/theme-provider';
import type { createStore } from 'jotai';
@@ -25,6 +29,7 @@ export function AffineContext(props: AffineContextProps) {
)}
>
{props.children}
<uniReactRoot.Root />,
</ProviderComposer>
);
}