mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { type BlockModel, BlockViewType, Doc } from '@blocksuite/store';
|
||||
import { consume, provide } from '@lit/context';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { nothing, type TemplateResult } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { EventName, UIEventHandler } from '../../event/index.js';
|
||||
import type { BlockService } from '../../extension/index.js';
|
||||
import type { BlockStdScope } from '../../scope/index.js';
|
||||
import { PropTypes, requiredProperties } from '../decorators/index.js';
|
||||
import {
|
||||
blockComponentSymbol,
|
||||
modelContext,
|
||||
serviceContext,
|
||||
} from './consts.js';
|
||||
import { docContext, stdContext } from './lit-host.js';
|
||||
import { ShadowlessElement } from './shadowless-element.js';
|
||||
import type { WidgetComponent } from './widget-component.js';
|
||||
|
||||
@requiredProperties({
|
||||
doc: PropTypes.instanceOf(Doc),
|
||||
std: PropTypes.object,
|
||||
widgets: PropTypes.recordOf(PropTypes.object),
|
||||
})
|
||||
export class BlockComponent<
|
||||
Model extends BlockModel = BlockModel,
|
||||
Service extends BlockService = BlockService,
|
||||
WidgetName extends string = string,
|
||||
> extends SignalWatcher(WithDisposable(ShadowlessElement)) {
|
||||
@consume({ context: stdContext })
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
private _selected = computed(() => {
|
||||
const selection = this.std.selection.value.find(selection => {
|
||||
return selection.blockId === this.model?.id;
|
||||
});
|
||||
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return selection;
|
||||
});
|
||||
|
||||
[blockComponentSymbol] = true;
|
||||
|
||||
handleEvent = (
|
||||
name: EventName,
|
||||
handler: UIEventHandler,
|
||||
options?: { global?: boolean; flavour?: boolean }
|
||||
) => {
|
||||
this._disposables.add(
|
||||
this.host.event.add(name, handler, {
|
||||
flavour: options?.global
|
||||
? undefined
|
||||
: options?.flavour
|
||||
? this.model?.flavour
|
||||
: undefined,
|
||||
blockId: options?.global || options?.flavour ? undefined : this.blockId,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
get blockId() {
|
||||
return this.dataset.blockId as string;
|
||||
}
|
||||
|
||||
get childBlocks() {
|
||||
const childModels = this.model.children;
|
||||
return childModels
|
||||
.map(child => {
|
||||
return this.std.view.getBlock(child.id);
|
||||
})
|
||||
.filter((x): x is BlockComponent => !!x);
|
||||
}
|
||||
|
||||
get flavour(): string {
|
||||
return this.model.flavour;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.std.host;
|
||||
}
|
||||
|
||||
get isVersionMismatch() {
|
||||
const schema = this.doc.schema.flavourSchemaMap.get(this.model.flavour);
|
||||
if (!schema) {
|
||||
console.warn(
|
||||
`Schema not found for block ${this.model.id}, flavour ${this.model.flavour}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const expectedVersion = schema.version;
|
||||
const actualVersion = this.model.version;
|
||||
if (expectedVersion !== actualVersion) {
|
||||
console.warn(
|
||||
`Version mismatch for block ${this.model.id}, expected ${expectedVersion}, actual ${actualVersion}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get model() {
|
||||
if (this._model) {
|
||||
return this._model;
|
||||
}
|
||||
const model = this.doc.getBlockById<Model>(this.blockId);
|
||||
if (!model) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.MissingViewModelError,
|
||||
`Cannot find block model for id ${this.blockId}`
|
||||
);
|
||||
}
|
||||
this._model = model;
|
||||
return model;
|
||||
}
|
||||
|
||||
get parentComponent(): BlockComponent | null {
|
||||
const parent = this.model.parent;
|
||||
if (!parent) return null;
|
||||
return this.std.view.getBlock(parent.id);
|
||||
}
|
||||
|
||||
get renderChildren() {
|
||||
return this.host.renderChildren.bind(this);
|
||||
}
|
||||
|
||||
get rootComponent(): BlockComponent | null {
|
||||
const rootId = this.doc.root?.id;
|
||||
if (!rootId) {
|
||||
return null;
|
||||
}
|
||||
const rootComponent = this.host.view.getBlock(rootId);
|
||||
return rootComponent ?? null;
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this._selected.value;
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.host.selection;
|
||||
}
|
||||
|
||||
get service(): Service {
|
||||
if (this._service) {
|
||||
return this._service;
|
||||
}
|
||||
const service = this.std.getService(this.model.flavour) as Service;
|
||||
this._service = service;
|
||||
return service;
|
||||
}
|
||||
|
||||
get topContenteditableElement(): BlockComponent | null {
|
||||
return this.rootComponent;
|
||||
}
|
||||
|
||||
get widgetComponents(): Partial<Record<WidgetName, WidgetComponent>> {
|
||||
return Object.keys(this.widgets).reduce(
|
||||
(mapping, key) => ({
|
||||
...mapping,
|
||||
[key]: this.host.view.getWidget(key, this.blockId),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
private _renderMismatchBlock(content: unknown) {
|
||||
return when(
|
||||
this.isVersionMismatch,
|
||||
() => {
|
||||
const actualVersion = this.model.version;
|
||||
const schema = this.doc.schema.flavourSchemaMap.get(this.model.flavour);
|
||||
const expectedVersion = schema?.version ?? -1;
|
||||
return this.renderVersionMismatch(expectedVersion, actualVersion);
|
||||
},
|
||||
() => content
|
||||
);
|
||||
}
|
||||
|
||||
private _renderViewType(content: unknown) {
|
||||
return choose(this.viewType, [
|
||||
[BlockViewType.Display, () => content],
|
||||
[BlockViewType.Hidden, () => nothing],
|
||||
[BlockViewType.Bypass, () => this.renderChildren(this.model)],
|
||||
]);
|
||||
}
|
||||
|
||||
addRenderer(renderer: (content: unknown) => unknown) {
|
||||
this._renderers.push(renderer);
|
||||
}
|
||||
|
||||
bindHotKey(
|
||||
keymap: Record<string, UIEventHandler>,
|
||||
options?: { global?: boolean; flavour?: boolean }
|
||||
) {
|
||||
const dispose = this.host.event.bindHotkey(keymap, {
|
||||
flavour: options?.global
|
||||
? undefined
|
||||
: options?.flavour
|
||||
? this.model.flavour
|
||||
: undefined,
|
||||
blockId: options?.global || options?.flavour ? undefined : this.blockId,
|
||||
});
|
||||
this._disposables.add(dispose);
|
||||
return dispose;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.std.view.setBlock(this);
|
||||
|
||||
const disposable = this.std.doc.slots.blockUpdated.on(({ type, id }) => {
|
||||
if (id === this.model.id && type === 'delete') {
|
||||
this.std.view.deleteBlock(this);
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
this._disposables.add(disposable);
|
||||
|
||||
this._disposables.add(
|
||||
this.model.propsUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.service?.specSlots.viewConnected.emit({
|
||||
service: this.service,
|
||||
component: this,
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this.service?.specSlots.viewDisconnected.emit({
|
||||
service: this.service,
|
||||
component: this,
|
||||
});
|
||||
}
|
||||
|
||||
protected override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await Promise.all(this.childBlocks.map(el => el.updateComplete));
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this._renderers.reduce(
|
||||
(acc, cur) => cur.call(this, acc),
|
||||
nothing as unknown
|
||||
);
|
||||
}
|
||||
|
||||
renderBlock(): unknown {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a warning message when the block version is mismatched.
|
||||
* @param expectedVersion If the schema is not found, the expected version is -1.
|
||||
* Which means the block is not supported in the current editor.
|
||||
* @param actualVersion The version of the block's crdt data.
|
||||
*/
|
||||
renderVersionMismatch(
|
||||
expectedVersion: number,
|
||||
actualVersion: number
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<dl class="version-mismatch-warning" contenteditable="false">
|
||||
<dt>
|
||||
<h4>Block Version Mismatched</h4>
|
||||
</dt>
|
||||
<dd>
|
||||
<p>
|
||||
We can not render this <var>${this.model.flavour}</var> block
|
||||
because the version is mismatched.
|
||||
</p>
|
||||
<p>Editor version: <var>${expectedVersion}</var></p>
|
||||
<p>Data version: <var>${actualVersion}</var></p>
|
||||
</dd>
|
||||
</dl>
|
||||
`;
|
||||
}
|
||||
|
||||
@provide({ context: modelContext as never })
|
||||
@state()
|
||||
private accessor _model: Model | null = null;
|
||||
|
||||
@state()
|
||||
protected accessor _renderers: Array<(content: unknown) => unknown> = [
|
||||
this.renderBlock,
|
||||
this._renderMismatchBlock,
|
||||
this._renderViewType,
|
||||
];
|
||||
|
||||
@provide({ context: serviceContext as never })
|
||||
@state()
|
||||
private accessor _service: Service | null = null;
|
||||
|
||||
@consume({ context: docContext })
|
||||
accessor doc!: Doc;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor viewType: BlockViewType = BlockViewType.Display;
|
||||
|
||||
@property({
|
||||
attribute: false,
|
||||
hasChanged(value, oldValue) {
|
||||
if (!value || !oldValue) {
|
||||
return value !== oldValue;
|
||||
}
|
||||
// Is empty object
|
||||
if (!Object.keys(value).length && !Object.keys(oldValue).length) {
|
||||
return false;
|
||||
}
|
||||
return value !== oldValue;
|
||||
},
|
||||
})
|
||||
accessor widgets!: Record<WidgetName, TemplateResult>;
|
||||
}
|
||||
Reference in New Issue
Block a user