mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 12:06:35 +08:00
refactor(editor): implement uni-component in AFFiNE (#10747)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user