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
@@ -0,0 +1,49 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
type SelectionConstructor<T = unknown> = {
type: string;
group: string;
new (...args: unknown[]): T;
};
export type BaseSelectionOptions = {
blockId: string;
};
export abstract class BaseSelection {
static readonly group: string;
static readonly type: string;
readonly blockId: string;
get group(): string {
return (this.constructor as SelectionConstructor).group;
}
get type(): BlockSuite.SelectionType {
return (this.constructor as SelectionConstructor)
.type as BlockSuite.SelectionType;
}
constructor({ blockId }: BaseSelectionOptions) {
this.blockId = blockId;
}
static fromJSON(_: Record<string, unknown>): BaseSelection {
throw new BlockSuiteError(
ErrorCode.SelectionError,
'You must override this method'
);
}
abstract equals(other: BaseSelection): boolean;
is<T extends BlockSuite.SelectionType>(
type: T
): this is BlockSuite.SelectionInstance[T] {
return this.type === type;
}
abstract toJSON(): Record<string, unknown>;
}
@@ -0,0 +1,27 @@
import type {
BlockSelection,
CursorSelection,
SurfaceSelection,
TextSelection,
} from './variants/index.js';
export * from './base.js';
export * from './manager.js';
export * from './variants/index.js';
declare global {
namespace BlockSuite {
interface Selection {
block: typeof BlockSelection;
cursor: typeof CursorSelection;
surface: typeof SurfaceSelection;
text: typeof TextSelection;
}
type SelectionType = keyof Selection;
type SelectionInstance = {
[P in SelectionType]: InstanceType<Selection[P]>;
};
}
}
@@ -0,0 +1,248 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { DisposableGroup, Slot } from '@blocksuite/global/utils';
import { nanoid, type StackItem } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import { LifeCycleWatcher } from '../extension/index.js';
import { SelectionIdentifier } from '../identifier.js';
import type { BlockStdScope } from '../scope/index.js';
import type { BaseSelection } from './base.js';
export interface SelectionConstructor {
type: string;
new (...args: any[]): BaseSelection;
fromJSON(json: Record<string, unknown>): BaseSelection;
}
export class SelectionManager extends LifeCycleWatcher {
static override readonly key = 'selectionManager';
private _id: string;
private _itemAdded = (event: { stackItem: StackItem }) => {
event.stackItem.meta.set('selection-state', this.value);
};
private _itemPopped = (event: { stackItem: StackItem }) => {
const selection = event.stackItem.meta.get('selection-state');
if (selection) {
this.set(selection as BaseSelection[]);
}
};
private _jsonToSelection = (json: Record<string, unknown>) => {
const ctor = this._selectionConstructors[json.type as string];
if (!ctor) {
throw new BlockSuiteError(
ErrorCode.SelectionError,
`Unknown selection type: ${json.type}`
);
}
return ctor.fromJSON(json);
};
private _remoteSelections = signal<Map<number, BaseSelection[]>>(new Map());
private _selectionConstructors: Record<string, SelectionConstructor> = {};
private _selections = signal<BaseSelection[]>([]);
disposables = new DisposableGroup();
slots = {
changed: new Slot<BaseSelection[]>(),
remoteChanged: new Slot<Map<number, BaseSelection[]>>(),
};
private get _store() {
return this.std.collection.awarenessStore;
}
get id() {
return this._id;
}
get remoteSelections() {
return this._remoteSelections.value;
}
get value() {
return this._selections.value;
}
constructor(std: BlockStdScope) {
super(std);
this._id = `${this.std.doc.blockCollection.id}:${nanoid()}`;
this._setupDefaultSelections();
this._store.awareness.on(
'change',
(change: { updated: number[]; added: number[]; removed: number[] }) => {
const all = change.updated.concat(change.added).concat(change.removed);
const localClientID = this._store.awareness.clientID;
const exceptLocal = all.filter(id => id !== localClientID);
const hasLocal = all.includes(localClientID);
if (hasLocal) {
const localSelectionJson = this._store.getLocalSelection(this.id);
const localSelection = localSelectionJson.map(json => {
return this._jsonToSelection(json);
});
this._selections.value = localSelection;
}
// Only consider remote selections from other clients
if (exceptLocal.length > 0) {
const map = new Map<number, BaseSelection[]>();
this._store.getStates().forEach((state, id) => {
if (id === this._store.awareness.clientID) return;
// selection id starts with the same block collection id from others clients would be considered as remote selections
const selection = Object.entries(state.selectionV2)
.filter(([key]) =>
key.startsWith(this.std.doc.blockCollection.id)
)
.flatMap(([_, selection]) => selection);
const selections = selection
.map(json => {
try {
return this._jsonToSelection(json);
} catch (error) {
console.error(
'Parse remote selection failed:',
id,
json,
error
);
return null;
}
})
.filter((sel): sel is BaseSelection => !!sel);
map.set(id, selections);
});
this._remoteSelections.value = map;
}
}
);
}
private _setupDefaultSelections() {
this.std.provider.getAll(SelectionIdentifier).forEach(ctor => {
this.register(ctor);
});
}
clear(types?: string[]) {
if (types) {
const values = this.value.filter(
selection => !types.includes(selection.type)
);
this.set(values);
} else {
this.set([]);
}
}
create<T extends BlockSuite.SelectionType>(
type: T,
...args: ConstructorParameters<BlockSuite.Selection[T]>
): BlockSuite.SelectionInstance[T] {
const ctor = this._selectionConstructors[type];
if (!ctor) {
throw new BlockSuiteError(
ErrorCode.SelectionError,
`Unknown selection type: ${type}`
);
}
return new ctor(...args) as BlockSuite.SelectionInstance[T];
}
dispose() {
Object.values(this.slots).forEach(slot => slot.dispose());
this.disposables.dispose();
}
filter<T extends BlockSuite.SelectionType>(type: T) {
return this.filter$(type).value;
}
filter$<T extends BlockSuite.SelectionType>(type: T) {
return computed(() =>
this.value.filter((sel): sel is BlockSuite.SelectionInstance[T] =>
sel.is(type)
)
);
}
find<T extends BlockSuite.SelectionType>(type: T) {
return this.find$(type).value;
}
find$<T extends BlockSuite.SelectionType>(type: T) {
return computed(() =>
this.value.find((sel): sel is BlockSuite.SelectionInstance[T] =>
sel.is(type)
)
);
}
fromJSON(json: Record<string, unknown>[]) {
const selections = json.map(json => {
return this._jsonToSelection(json);
});
return this.set(selections);
}
getGroup(group: string) {
return this.value.filter(s => s.group === group);
}
override mounted() {
if (this.disposables.disposed) {
this.disposables = new DisposableGroup();
}
this.std.doc.history.on('stack-item-added', this._itemAdded);
this.std.doc.history.on('stack-item-popped', this._itemPopped);
this.disposables.add(
this._store.slots.update.on(({ id }) => {
if (id === this._store.awareness.clientID) {
return;
}
this.slots.remoteChanged.emit(this.remoteSelections);
})
);
}
register(ctor: SelectionConstructor | SelectionConstructor[]) {
[ctor].flat().forEach(ctor => {
this._selectionConstructors[ctor.type] = ctor;
});
return this;
}
set(selections: BaseSelection[]) {
this._store.setLocalSelection(
this.id,
selections.map(s => s.toJSON())
);
this.slots.changed.emit(selections);
}
setGroup(group: string, selections: BaseSelection[]) {
const current = this.value.filter(s => s.group !== group);
this.set([...current, ...selections]);
}
override unmounted() {
this.std.doc.history.off('stack-item-added', this._itemAdded);
this.std.doc.history.off('stack-item-popped', this._itemPopped);
this.slots.changed.dispose();
this.disposables.dispose();
this.clear();
}
update(fn: (currentSelections: BaseSelection[]) => BaseSelection[]) {
const selections = fn(this.value);
this.set(selections);
}
}
@@ -0,0 +1,35 @@
import z from 'zod';
import { SelectionExtension } from '../../extension/selection.js';
import { BaseSelection } from '../base.js';
const BlockSelectionSchema = z.object({
blockId: z.string(),
});
export class BlockSelection extends BaseSelection {
static override group = 'note';
static override type = 'block';
static override fromJSON(json: Record<string, unknown>): BlockSelection {
const result = BlockSelectionSchema.parse(json);
return new BlockSelection(result);
}
override equals(other: BaseSelection): boolean {
if (other instanceof BlockSelection) {
return this.blockId === other.blockId;
}
return false;
}
override toJSON(): Record<string, unknown> {
return {
type: 'block',
blockId: this.blockId,
};
}
}
export const BlockSelectionExtension = SelectionExtension(BlockSelection);
@@ -0,0 +1,47 @@
import z from 'zod';
import { SelectionExtension } from '../../extension/selection.js';
import { BaseSelection } from '../base.js';
const CursorSelectionSchema = z.object({
x: z.number(),
y: z.number(),
});
export class CursorSelection extends BaseSelection {
static override group = 'gfx';
static override type = 'cursor';
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
super({ blockId: '[gfx-cursor]' });
this.x = x;
this.y = y;
}
static override fromJSON(json: Record<string, unknown>): CursorSelection {
const { x, y } = CursorSelectionSchema.parse(json);
return new CursorSelection(x, y);
}
override equals(other: BaseSelection): boolean {
if (other instanceof CursorSelection) {
return this.x === other.x && this.y === other.y;
}
return false;
}
override toJSON(): Record<string, unknown> {
return {
type: 'cursor',
x: this.x,
y: this.y,
};
}
}
export const CursorSelectionExtension = SelectionExtension(CursorSelection);
@@ -0,0 +1,4 @@
export * from './block.js';
export * from './cursor.js';
export * from './surface.js';
export * from './text.js';
@@ -0,0 +1,72 @@
import z from 'zod';
import { SelectionExtension } from '../../extension/selection.js';
import { BaseSelection } from '../base.js';
const SurfaceSelectionSchema = z.object({
blockId: z.string(),
elements: z.array(z.string()),
editing: z.boolean(),
inoperable: z.boolean().optional(),
});
export class SurfaceSelection extends BaseSelection {
static override group = 'gfx';
static override type = 'surface';
readonly editing: boolean;
readonly elements: string[];
readonly inoperable: boolean;
constructor(
blockId: string,
elements: string[],
editing: boolean,
inoperable = false
) {
super({ blockId });
this.elements = elements;
this.editing = editing;
this.inoperable = inoperable;
}
static override fromJSON(json: Record<string, unknown>): SurfaceSelection {
const { blockId, elements, editing, inoperable } =
SurfaceSelectionSchema.parse(json);
return new SurfaceSelection(blockId, elements, editing, inoperable);
}
override equals(other: BaseSelection): boolean {
if (other instanceof SurfaceSelection) {
return (
this.blockId === other.blockId &&
this.editing === other.editing &&
this.inoperable === other.inoperable &&
this.elements.length === other.elements.length &&
this.elements.every((id, idx) => id === other.elements[idx])
);
}
return false;
}
isEmpty() {
return this.elements.length === 0 && !this.editing;
}
override toJSON(): Record<string, unknown> {
return {
type: 'surface',
blockId: this.blockId,
elements: this.elements,
editing: this.editing,
inoperable: this.inoperable,
};
}
}
export const SurfaceSelectionExtension = SelectionExtension(SurfaceSelection);
@@ -0,0 +1,117 @@
import z from 'zod';
import { SelectionExtension } from '../../extension/selection.js';
import { BaseSelection } from '../base.js';
export type TextRangePoint = {
blockId: string;
index: number;
length: number;
};
export type TextSelectionProps = {
from: TextRangePoint;
to: TextRangePoint | null;
reverse?: boolean;
};
const TextSelectionSchema = z.object({
from: z.object({
blockId: z.string(),
index: z.number(),
length: z.number(),
}),
to: z
.object({
blockId: z.string(),
index: z.number(),
length: z.number(),
})
.nullable(),
// The `optional()` is for backward compatibility,
// since `reverse` may not exist in remote selection.
reverse: z.boolean().optional(),
});
export class TextSelection extends BaseSelection {
static override group = 'note';
static override type = 'text';
from: TextRangePoint;
reverse: boolean;
to: TextRangePoint | null;
get end(): TextRangePoint {
return this.reverse ? this.from : (this.to ?? this.from);
}
get start(): TextRangePoint {
return this.reverse ? (this.to ?? this.from) : this.from;
}
constructor({ from, to, reverse }: TextSelectionProps) {
super({
blockId: from.blockId,
});
this.from = from;
this.to = this._equalPoint(from, to) ? null : to;
this.reverse = !!reverse;
}
static override fromJSON(json: Record<string, unknown>): TextSelection {
const result = TextSelectionSchema.parse(json);
return new TextSelection(result);
}
private _equalPoint(
a: TextRangePoint | null,
b: TextRangePoint | null
): boolean {
if (a && b) {
return (
a.blockId === b.blockId && a.index === b.index && a.length === b.length
);
}
return a === b;
}
empty(): boolean {
return !!this.to;
}
override equals(other: BaseSelection): boolean {
if (other instanceof TextSelection) {
return (
this.blockId === other.blockId &&
this._equalPoint(other.from, this.from) &&
this._equalPoint(other.to, this.to)
);
}
return false;
}
isCollapsed(): boolean {
return this.to === null && this.from.length === 0;
}
isInSameBlock(): boolean {
return this.to === null || this.from.blockId === this.to.blockId;
}
override toJSON(): Record<string, unknown> {
return {
type: 'text',
from: this.from,
to: this.to,
reverse: this.reverse,
};
}
}
export const TextSelectionExtension = SelectionExtension(TextSelection);