chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions

View File

@@ -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>;
}