Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@blocksuite/block-std

Package Overview
Dependencies
Maintainers
5
Versions
906
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@blocksuite/block-std - npm Package Compare versions

Comparing version 0.7.0 to 0.8.0

dist/event/control/clipboard.d.ts

34

dist/event/dispatcher.d.ts
import { DisposableGroup } from '@blocksuite/global/utils';
import type { BlockStore } from '../store/index.js';
import type { UIEventHandler } from './base.js';
import { UIEventStateContext } from './base.js';
declare const eventNames: readonly ["click", "doubleClick", "tripleClick", "pointerDown", "pointerMove", "pointerUp", "pointerOut", "dragStart", "dragMove", "dragEnd", "keyDown", "keyUp", "beforeInput", "compositionStart", "compositionUpdate", "compositionEnd", "paste", "copy", "blur", "focus", "drop", "contextMenu", "wheel", "selectionChange", "virgo-vrange-updated"];
declare const eventNames: readonly ["click", "doubleClick", "tripleClick", "pointerDown", "pointerMove", "pointerUp", "pointerOut", "dragStart", "dragMove", "dragEnd", "keyDown", "keyUp", "selectionChange", "compositionStart", "compositionUpdate", "compositionEnd", "cut", "copy", "paste", "beforeInput", "blur", "focus", "drop", "contextMenu", "wheel"];
export type EventName = (typeof eventNames)[number];
export type EventOptions = {
flavour?: string;
path?: string[];
};
export type EventHandlerRunner = {
fn: UIEventHandler;
flavour?: string;
path?: string[];
};
export type EventScope = {
runners: EventHandlerRunner[];
flavours: string[];
paths: string[][];
};
export declare class UIEventDispatcher {
root: HTMLElement;
blockStore: BlockStore;
disposables: DisposableGroup;

@@ -12,7 +27,16 @@ private _handlersMap;

private _keyboardControl;
constructor(root: HTMLElement);
private _rangeControl;
private _clipboardControl;
constructor(blockStore: BlockStore);
mount(): void;
unmount(): void;
run(name: EventName, context: UIEventStateContext): void;
add(name: EventName, handler: UIEventHandler): () => void;
get root(): HTMLElement;
run(name: EventName, context: UIEventStateContext, scope?: EventScope): void;
add(name: EventName, handler: UIEventHandler, options?: EventOptions): () => void;
bindHotkey(keymap: Record<string, UIEventHandler>, options?: EventOptions): () => void;
private get _currentSelections();
private _getEventScope;
buildEventScope(name: EventName, flavours: string[], paths: string[][]): EventScope | undefined;
private _buildEventScopeByTarget;
private _buildEventScopeBySelection;
private _bindEvents;

@@ -19,0 +43,0 @@ }

import { DisposableGroup } from '@blocksuite/global/utils';
import { UIEventStateContext } from './base.js';
import { UIEventState } from './base.js';
import { KeyboardControl } from './keyboard.js';
import { PointerControl } from './pointer.js';
import { PathFinder } from '../utils/index.js';
import { UIEventState, UIEventStateContext } from './base.js';
import { ClipboardControl } from './control/clipboard.js';
import { KeyboardControl } from './control/keyboard.js';
import { PointerControl } from './control/pointer.js';
import { RangeControl } from './control/range.js';
import { bindKeymap } from './keymap.js';
import { toLowerCase } from './utils.js';
const bypassEventNames = [
'beforeInput',
'compositionStart',
'compositionUpdate',
'compositionEnd',
'paste',
'copy',
'blur',

@@ -20,3 +18,2 @@ 'focus',

];
const globalEventNames = ['selectionChange', 'virgo-vrange-updated'];
const eventNames = [

@@ -35,8 +32,14 @@ 'click',

'keyUp',
'selectionChange',
'compositionStart',
'compositionUpdate',
'compositionEnd',
'cut',
'copy',
'paste',
...bypassEventNames,
...globalEventNames,
];
export class UIEventDispatcher {
constructor(root) {
this.root = root;
constructor(blockStore) {
this.blockStore = blockStore;
this.disposables = new DisposableGroup();

@@ -46,2 +49,4 @@ this._handlersMap = Object.fromEntries(eventNames.map((name) => [name, []]));

this._keyboardControl = new KeyboardControl(this);
this._rangeControl = new RangeControl(this);
this._clipboardControl = new ClipboardControl(this);
}

@@ -57,8 +62,16 @@ mount() {

}
run(name, context) {
const handlers = this._handlersMap[name];
if (!handlers)
return;
for (const handler of handlers) {
const result = handler(context);
get root() {
return this.blockStore.root;
}
run(name, context, scope) {
const { event } = context.get('defaultState');
if (!scope) {
scope = this._getEventScope(name, event);
if (!scope) {
return;
}
}
for (const runner of scope.runners) {
const { fn } = runner;
const result = fn(context);
if (result) {

@@ -69,25 +82,106 @@ return;

}
add(name, handler) {
this._handlersMap[name].unshift(handler);
add(name, handler, options) {
const runner = {
fn: handler,
flavour: options?.flavour,
path: options?.path,
};
this._handlersMap[name].unshift(runner);
return () => {
if (this._handlersMap[name].includes(handler)) {
this._handlersMap[name] = this._handlersMap[name].filter(f => f !== handler);
if (this._handlersMap[name].includes(runner)) {
this._handlersMap[name] = this._handlersMap[name].filter(x => x !== runner);
}
};
}
bindHotkey(keymap, options) {
return this.add('keyDown', bindKeymap(keymap), options);
}
get _currentSelections() {
return this.blockStore.selectionManager.value;
}
_getEventScope(name, event) {
const handlers = this._handlersMap[name];
if (!handlers)
return;
let output;
if (event.target && event.target instanceof Node) {
output = this._buildEventScopeByTarget(name, event.target);
}
if (!output) {
output = this._buildEventScopeBySelection(name);
}
return output;
}
buildEventScope(name, flavours, paths) {
const handlers = this._handlersMap[name];
if (!handlers)
return;
const globalEvents = handlers.filter(handler => handler.flavour === undefined && handler.path === undefined);
const pathEvents = handlers.filter(handler => {
const _path = handler.path;
if (_path === undefined)
return false;
return paths.some(path => PathFinder.includes(path, _path));
});
const flavourEvents = handlers.filter(handler => handler.flavour && flavours.includes(handler.flavour));
return {
runners: pathEvents.concat(flavourEvents).concat(globalEvents),
flavours,
paths,
};
}
_buildEventScopeByTarget(name, target) {
const handlers = this._handlersMap[name];
if (!handlers)
return;
const path = this.blockStore.viewStore.getNodeView(target)?.path;
if (!path)
return;
const flavours = path
.map(blockId => {
return this.blockStore.page.getBlockById(blockId)?.flavour;
})
.filter((flavour) => {
return !!flavour;
})
.reverse();
return this.buildEventScope(name, flavours, [path]);
}
_buildEventScopeBySelection(name) {
const handlers = this._handlersMap[name];
if (!handlers)
return;
const selections = this._currentSelections;
const seen = {};
const flavours = selections
.map(selection => selection.path)
.flatMap(path => {
return path.map(blockId => {
return this.blockStore.page.getBlockById(blockId)?.flavour;
});
})
.filter((flavour) => {
if (!flavour)
return false;
if (seen[flavour])
return false;
seen[flavour] = true;
return true;
})
.reverse();
const paths = selections.map(selection => selection.path);
return this.buildEventScope(name, flavours, paths);
}
_bindEvents() {
bypassEventNames.forEach(eventName => {
this.disposables.addFromEvent(this.root, toLowerCase(eventName), e => {
this.run(eventName, UIEventStateContext.from(new UIEventState(e)));
this.disposables.addFromEvent(this.root, toLowerCase(eventName), event => {
this.run(eventName, UIEventStateContext.from(new UIEventState(event)));
});
});
globalEventNames.forEach(eventName => {
this.disposables.addFromEvent(document, toLowerCase(eventName), e => {
this.run(eventName, UIEventStateContext.from(new UIEventState(e)));
});
});
this._pointerControl.listen();
this._keyboardControl.listen();
this._rangeControl.listen();
this._clipboardControl.listen();
}
}
//# sourceMappingURL=dispatcher.js.map

2

dist/event/index.d.ts
export * from './base.js';
export * from './dispatcher.js';
export * from './state.js';
export * from './state/index.js';
//# sourceMappingURL=index.d.ts.map
export * from './base.js';
export * from './dispatcher.js';
export * from './state.js';
export * from './state/index.js';
//# sourceMappingURL=index.js.map
export * from './event/index.js';
export * from './selection/index.js';
export * from './service/index.js';
export * from './spec/index.js';
export * from './store/index.js';
export * from './utils/index.js';
//# sourceMappingURL=index.d.ts.map
export * from './event/index.js';
export * from './selection/index.js';
export * from './service/index.js';
export * from './spec/index.js';
export * from './store/index.js';
export * from './utils/index.js';
//# sourceMappingURL=index.js.map

@@ -0,5 +1,13 @@

export type BaseSelectionOptions = {
path: string[];
};
export declare abstract class BaseSelection {
static readonly type: string;
readonly blockId: string;
constructor(blockId: string);
static readonly group: string;
readonly path: string[];
constructor({ path }: BaseSelectionOptions);
get blockId(): string;
is<T extends BlockSuiteSelectionType>(type: T): this is BlockSuiteSelectionInstance[T];
get type(): BlockSuiteSelectionType;
get group(): string;
abstract equals(other: BaseSelection): boolean;

@@ -6,0 +14,0 @@ abstract toJSON(): Record<string, unknown>;

@@ -0,5 +1,19 @@

import { PathFinder } from '../utils/index.js';
export class BaseSelection {
constructor(blockId) {
this.blockId = blockId;
constructor({ path }) {
this.path = path;
}
get blockId() {
return PathFinder.id(this.path);
}
is(type) {
return this.type === type;
}
get type() {
return this.constructor
.type;
}
get group() {
return this.constructor.group;
}
static fromJSON(_) {

@@ -6,0 +20,0 @@ throw new Error('You must override this method');

@@ -1,3 +0,3 @@

import type { Workspace } from '@blocksuite/store';
import { DisposableGroup, Slot } from '@blocksuite/store';
import { DisposableGroup, Slot } from '@blocksuite/global/utils';
import type { BlockStore } from '../store/index.js';
import type { BaseSelection } from './base.js';

@@ -10,17 +10,30 @@ interface SelectionConstructor {

export declare class SelectionManager {
private _workspace;
blockStore: BlockStore;
disposables: DisposableGroup;
_selectionConstructors: Record<string, SelectionConstructor>;
private _selectionConstructors;
slots: {
changed: Slot<BaseSelection[]>;
remoteChanged: Slot<Record<string, BaseSelection[]>>;
};
constructor(workspace: Workspace);
register(ctor: SelectionConstructor): void;
constructor(blockStore: BlockStore);
register(ctor: SelectionConstructor | SelectionConstructor[]): this;
private get _store();
private _setupDefaultSelections;
private _jsonToSelection;
getInstance<T extends BlockSuiteSelectionType>(type: T, ...args: ConstructorParameters<BlockSuiteSelection[T]>): BlockSuiteSelectionInstance[T];
get selections(): BaseSelection[];
setSelections(selections: BaseSelection[]): void;
get value(): BaseSelection[];
set(selections: BaseSelection[]): void;
setGroup(group: string, selections: BaseSelection[]): void;
getGroup(group: string): BaseSelection[];
update(fn: (currentSelections: BaseSelection[]) => BaseSelection[]): void;
clear(types?: string[]): void;
find<T extends BlockSuiteSelectionType>(type: T): BlockSuiteSelectionInstance[T] | undefined;
filter<T extends BlockSuiteSelectionType>(type: T): BlockSuiteSelectionInstance[T][];
get remoteSelections(): {
[k: string]: Record<string, unknown>[];
[k: string]: BaseSelection[];
};
private _itemAdded;
private _itemPopped;
mount(): void;
unmount(): void;
dispose(): void;

@@ -27,0 +40,0 @@ }

@@ -1,4 +0,6 @@

import { DisposableGroup, Slot } from '@blocksuite/store';
import { DisposableGroup, Slot } from '@blocksuite/global/utils';
import { BlockSelection, SurfaceSelection, TextSelection, } from './variants/index.js';
export class SelectionManager {
constructor(workspace) {
constructor(blockStore) {
this.blockStore = blockStore;
this.disposables = new DisposableGroup();

@@ -8,11 +10,34 @@ this._selectionConstructors = {};

changed: new Slot(),
remoteChanged: new Slot(),
};
this._workspace = workspace;
this._jsonToSelection = (json) => {
const ctor = this._selectionConstructors[json.type];
if (!ctor) {
throw new Error(`Unknown selection type: ${json.type}`);
}
return ctor.fromJSON(json);
};
this._itemAdded = (event) => {
event.stackItem.meta.set('selection-state', this.value);
};
this._itemPopped = (event) => {
const selection = event.stackItem.meta.get('selection-state');
if (selection) {
this.set(selection);
}
};
this._setupDefaultSelections();
}
register(ctor) {
this._selectionConstructors[ctor.type] = ctor;
[ctor].flat().forEach(ctor => {
this._selectionConstructors[ctor.type] = ctor;
});
return this;
}
get _store() {
return this._workspace.awarenessStore;
return this.blockStore.workspace.awarenessStore;
}
_setupDefaultSelections() {
this.register([TextSelection, BlockSelection, SurfaceSelection]);
}
getInstance(type, ...args) {

@@ -25,20 +50,61 @@ const ctor = this._selectionConstructors[type];

}
get selections() {
get value() {
return this._store.getLocalSelection().map(json => {
const ctor = this._selectionConstructors[json.type];
if (!ctor) {
throw new Error(`Unknown selection type: ${json.type}`);
}
return ctor.fromJSON(json);
return this._jsonToSelection(json);
});
}
setSelections(selections) {
set(selections) {
this._store.setLocalSelection(selections.map(s => s.toJSON()));
this.slots.changed.emit(selections);
}
setGroup(group, selections) {
const current = this.value.filter(s => s.group !== group);
this.set([...current, ...selections]);
}
getGroup(group) {
return this.value.filter(s => s.group === group);
}
update(fn) {
const selections = fn(this.value);
this.set(selections);
}
clear(types) {
if (types) {
const values = this.value.filter(selection => !types.includes(selection.type));
this.set(values);
}
else {
this.set([]);
}
}
find(type) {
return this.value.find((sel) => sel.is(type));
}
filter(type) {
return this.value.filter((sel) => sel.is(type));
}
get remoteSelections() {
return Object.fromEntries(Array.from(this._store.getStates().entries())
.filter(([key]) => key !== this._store.awareness.clientID)
.map(([key, value]) => [key, value.selection]));
.map(([key, value]) => [key, value.selection.map(this._jsonToSelection)]));
}
mount() {
if (this.disposables.disposed) {
this.disposables = new DisposableGroup();
}
this.blockStore.page.history.on('stack-item-added', this._itemAdded);
this.blockStore.page.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);
}));
}
unmount() {
this.blockStore.page.history.off('stack-item-added', this._itemAdded);
this.blockStore.page.history.off('stack-item-popped', this._itemPopped);
this.clear();
this.slots.changed.dispose();
this.disposables.dispose();
}
dispose() {

@@ -45,0 +111,0 @@ Object.values(this.slots).forEach(slot => slot.dispose());

import { BaseSelection } from '../base.js';
export declare class BlockSelection extends BaseSelection {
static readonly type = "block";
static type: string;
static group: string;
equals(other: BaseSelection): boolean;

@@ -5,0 +6,0 @@ toJSON(): Record<string, unknown>;

@@ -0,1 +1,2 @@

import { PathFinder } from '../../utils/index.js';
import { BaseSelection } from '../base.js';

@@ -5,3 +6,3 @@ export class BlockSelection extends BaseSelection {

if (other instanceof BlockSelection) {
return other.blockId === this.blockId;
return PathFinder.equals(this.path, other.path);
}

@@ -13,10 +14,13 @@ return false;

type: 'block',
blockId: this.blockId,
path: this.path,
};
}
static fromJSON(json) {
return new BlockSelection(json.blockId);
return new BlockSelection({
path: json.path,
});
}
}
BlockSelection.type = 'block';
BlockSelection.group = 'note';
//# sourceMappingURL=block.js.map
export * from './block.js';
export * from './surface.js';
export * from './text.js';

@@ -3,0 +4,0 @@ declare global {

export * from './block.js';
export * from './surface.js';
export * from './text.js';
//# sourceMappingURL=index.js.map
import { BaseSelection } from '../base.js';
export type TextRangePoint = {
path: string[];
index: number;
length: number;
};
export type TextSelectionProps = {
from: TextRangePoint;
to: TextRangePoint | null;
};
export declare class TextSelection extends BaseSelection {
static readonly type = "text";
from: number;
to: number;
constructor(blockId: string, from: number, to: number);
static type: string;
static group: string;
from: TextRangePoint;
to: TextRangePoint | null;
constructor({ from, to }: TextSelectionProps);
empty(): boolean;
private _equalPoint;
equals(other: BaseSelection): boolean;
toJSON(): Record<string, unknown>;
static fromJSON(json: Record<string, unknown>): TextSelection;
isCollapsed(): boolean;
isInSameBlock(): boolean;
}

@@ -12,0 +25,0 @@ declare global {

@@ -0,16 +1,27 @@

import { PathFinder } from '../../utils/path-finder.js';
import { BaseSelection } from '../base.js';
export class TextSelection extends BaseSelection {
constructor(blockId, from, to) {
super(blockId);
constructor({ from, to }) {
super({
path: from.path,
});
this.from = from;
this.to = to;
this.to = this._equalPoint(from, to) ? null : to;
}
empty() {
return this.from === this.to;
return !!this.to;
}
_equalPoint(a, b) {
if (a && b) {
return (PathFinder.equals(a.path, b.path) &&
a.index === b.index &&
a.length === b.length);
}
return a === b;
}
equals(other) {
if (other instanceof TextSelection) {
return (other.blockId === this.blockId &&
other.from === this.from &&
other.to === this.to);
return (PathFinder.equals(this.path, other.path) &&
this._equalPoint(other.from, this.from) &&
this._equalPoint(other.to, this.to));
}

@@ -22,3 +33,2 @@ return false;

type: 'text',
blockId: this.blockId,
from: this.from,

@@ -29,6 +39,16 @@ to: this.to,

static fromJSON(json) {
return new TextSelection(json.blockId, json.from, json.to);
return new TextSelection({
from: json.from,
to: json.to,
});
}
isCollapsed() {
return this.to === null && this.from.length === 0;
}
isInSameBlock() {
return this.to === null || PathFinder.equals(this.from.path, this.to.path);
}
}
TextSelection.type = 'text';
TextSelection.group = 'note';
//# sourceMappingURL=text.js.map

@@ -0,18 +1,29 @@

import { DisposableGroup } from '@blocksuite/global/utils';
import type { BaseBlockModel } from '@blocksuite/store';
import { DisposableGroup } from '@blocksuite/store';
import type { UIEventDispatcher } from '../event/index.js';
import type { EventName, UIEventHandler } from '../event/index.js';
import type { BlockStore } from '../store/index.js';
export interface BlockServiceOptions {
uiEventDispatcher: UIEventDispatcher;
flavour: string;
store: BlockStore;
}
export declare class BlockService<Model extends BaseBlockModel = BaseBlockModel> {
disposables: DisposableGroup;
uiEventDispatcher: UIEventDispatcher;
export declare class BlockService<_Model extends BaseBlockModel = BaseBlockModel> {
readonly store: BlockStore;
readonly flavour: string;
readonly disposables: DisposableGroup;
constructor(options: BlockServiceOptions);
get workspace(): import("@blocksuite/store").Workspace;
get page(): import("@blocksuite/store").Page;
get selectionManager(): import("../index.js").SelectionManager;
get uiEventDispatcher(): import("../event/dispatcher.js").UIEventDispatcher;
dispose(): void;
mounted(): void;
unmounted(): void;
handleEvent(name: EventName, fn: UIEventHandler): void;
handleEvent(name: EventName, fn: UIEventHandler, options?: {
global: boolean;
}): void;
bindHotKey(keymap: Record<string, UIEventHandler>, options?: {
global: boolean;
}): void;
}
export type BlockServiceConstructor = new (options: BlockServiceOptions) => BlockService;
//# sourceMappingURL=index.d.ts.map

@@ -1,7 +0,20 @@

import { DisposableGroup } from '@blocksuite/store';
import { DisposableGroup } from '@blocksuite/global/utils';
export class BlockService {
constructor(options) {
this.disposables = new DisposableGroup();
this.uiEventDispatcher = options.uiEventDispatcher;
this.flavour = options.flavour;
this.store = options.store;
}
get workspace() {
return this.store.workspace;
}
get page() {
return this.store.page;
}
get selectionManager() {
return this.store.selectionManager;
}
get uiEventDispatcher() {
return this.store.uiEventDispatcher;
}
// life cycle start

@@ -19,6 +32,13 @@ dispose() {

// event handlers start
handleEvent(name, fn) {
this.disposables.add(this.uiEventDispatcher.add(name, fn));
handleEvent(name, fn, options) {
this.disposables.add(this.uiEventDispatcher.add(name, fn, {
flavour: options?.global ? undefined : this.flavour,
}));
}
bindHotKey(keymap, options) {
this.disposables.add(this.uiEventDispatcher.bindHotkey(keymap, {
flavour: options?.global ? undefined : this.flavour,
}));
}
}
//# sourceMappingURL=index.js.map
import type { BlockSchemaType } from '@blocksuite/store';
import type { BlockServiceConstructor } from '../service/index.js';
export interface BlockView<ComponentType = unknown> {
export interface BlockView<ComponentType = unknown, WidgetNames extends string = string> {
component: ComponentType;
widgets?: ComponentType[];
widgets?: Record<WidgetNames, ComponentType>;
}
export interface BlockSpec<ComponentType = unknown> {
export interface BlockSpec<ComponentType = unknown, WidgetNames extends string = string> {
schema: BlockSchemaType;
service?: BlockServiceConstructor;
view: BlockView<ComponentType>;
view: BlockView<ComponentType, WidgetNames>;
}
//# sourceMappingURL=index.d.ts.map

@@ -1,20 +0,3 @@

import type { UIEventDispatcher } from '../event/index.js';
import type { BlockService } from '../service/index.js';
import type { BlockSpec } from '../spec/index.js';
export interface BlockStoreOptions {
uiEventDispatcher: UIEventDispatcher;
}
export declare class BlockStore<ComponentType = unknown> {
private _specs;
private _services;
private readonly _uiEventDispatcher;
constructor(options: BlockStoreOptions);
applySpecs(specs: Array<BlockSpec<ComponentType>>): void;
dispose(): void;
getView(flavour: string): import("../spec/index.js").BlockView<ComponentType> | null;
getService(flavour: string): BlockService<import("@blocksuite/store/base.js").BaseBlockModel<object>> | undefined;
private _diffServices;
private get _serviceOptions();
private _buildSpecMap;
}
export * from './block-store.js';
export * from './view-store.js';
//# sourceMappingURL=index.d.ts.map

@@ -1,68 +0,3 @@

export class BlockStore {
constructor(options) {
this._specs = new Map();
this._services = new Map();
this._uiEventDispatcher = options.uiEventDispatcher;
}
applySpecs(specs) {
const oldSpecs = this._specs;
const newSpecs = this._buildSpecMap(specs);
this._diffServices(oldSpecs, newSpecs);
this._specs = newSpecs;
}
dispose() {
this._services.forEach(service => {
service.dispose();
service.unmounted();
});
this._services.clear();
}
getView(flavour) {
const spec = this._specs.get(flavour);
if (!spec) {
return null;
}
return spec.view;
}
getService(flavour) {
return this._services.get(flavour);
}
_diffServices(oldSpecs, newSpecs) {
oldSpecs.forEach((oldSpec, flavour) => {
if (newSpecs.has(flavour) &&
newSpecs.get(flavour)?.service === oldSpec.service) {
return;
}
const service = this._services.get(flavour);
if (service) {
service.dispose();
service.unmounted();
}
this._services.delete(flavour);
});
newSpecs.forEach((newSpec, flavour) => {
if (this._services.has(flavour)) {
return;
}
if (!newSpec.service) {
return;
}
const service = new newSpec.service(this._serviceOptions);
this._services.set(flavour, service);
service.mounted();
});
}
get _serviceOptions() {
return {
uiEventDispatcher: this._uiEventDispatcher,
};
}
_buildSpecMap(specs) {
const specMap = new Map();
specs.forEach(spec => {
specMap.set(spec.schema.model.flavour, spec);
});
return specMap;
}
}
export * from './block-store.js';
export * from './view-store.js';
//# sourceMappingURL=index.js.map
{
"name": "@blocksuite/block-std",
"version": "0.7.0",
"version": "0.8.0",
"description": "Std for blocksuite blocks",

@@ -12,9 +12,10 @@ "main": "dist/index.js",

"peerDependencies": {
"@blocksuite/store": "0.7.0"
"@blocksuite/store": "0.8.0"
},
"dependencies": {
"@blocksuite/global": "0.7.0"
"w3c-keyname": "^2.2.8",
"@blocksuite/global": "0.8.0"
},
"devDependencies": {
"@blocksuite/store": "0.7.0"
"@blocksuite/store": "0.8.0"
},

@@ -21,0 +22,0 @@ "exports": {

import { DisposableGroup } from '@blocksuite/global/utils';
import type { BlockStore } from '../store/index.js';
import { PathFinder } from '../utils/index.js';
import type { UIEventHandler } from './base.js';
import { UIEventStateContext } from './base.js';
import { UIEventState } from './base.js';
import { KeyboardControl } from './keyboard.js';
import { PointerControl } from './pointer.js';
import { UIEventState, UIEventStateContext } from './base.js';
import { ClipboardControl } from './control/clipboard.js';
import { KeyboardControl } from './control/keyboard.js';
import { PointerControl } from './control/pointer.js';
import { RangeControl } from './control/range.js';
import { bindKeymap } from './keymap.js';
import { toLowerCase } from './utils.js';

@@ -12,8 +16,3 @@

'beforeInput',
'compositionStart',
'compositionUpdate',
'compositionEnd',
'paste',
'copy',
'blur',

@@ -26,4 +25,2 @@ 'focus',

const globalEventNames = ['selectionChange', 'virgo-vrange-updated'] as const;
const eventNames = [

@@ -46,8 +43,31 @@ 'click',

'selectionChange',
'compositionStart',
'compositionUpdate',
'compositionEnd',
'cut',
'copy',
'paste',
...bypassEventNames,
...globalEventNames,
] as const;
export type EventName = (typeof eventNames)[number];
export type EventOptions = {
flavour?: string;
path?: string[];
};
export type EventHandlerRunner = {
fn: UIEventHandler;
flavour?: string;
path?: string[];
};
export type EventScope = {
runners: EventHandlerRunner[];
flavours: string[];
paths: string[][];
};
export class UIEventDispatcher {

@@ -57,11 +77,15 @@ disposables = new DisposableGroup();

private _handlersMap = Object.fromEntries(
eventNames.map((name): [EventName, Array<UIEventHandler>] => [name, []])
) as Record<EventName, Array<UIEventHandler>>;
eventNames.map((name): [EventName, Array<EventHandlerRunner>] => [name, []])
) as Record<EventName, Array<EventHandlerRunner>>;
private _pointerControl: PointerControl;
private _keyboardControl: KeyboardControl;
private _rangeControl: RangeControl;
private _clipboardControl: ClipboardControl;
constructor(public root: HTMLElement) {
constructor(public blockStore: BlockStore) {
this._pointerControl = new PointerControl(this);
this._keyboardControl = new KeyboardControl(this);
this._rangeControl = new RangeControl(this);
this._clipboardControl = new ClipboardControl(this);
}

@@ -80,8 +104,18 @@

run(name: EventName, context: UIEventStateContext) {
const handlers = this._handlersMap[name];
if (!handlers) return;
get root() {
return this.blockStore.root;
}
for (const handler of handlers) {
const result = handler(context);
run(name: EventName, context: UIEventStateContext, scope?: EventScope) {
const { event } = context.get('defaultState');
if (!scope) {
scope = this._getEventScope(name, event);
if (!scope) {
return;
}
}
for (const runner of scope.runners) {
const { fn } = runner;
const result = fn(context);
if (result) {

@@ -93,8 +127,13 @@ return;

add(name: EventName, handler: UIEventHandler) {
this._handlersMap[name].unshift(handler);
add(name: EventName, handler: UIEventHandler, options?: EventOptions) {
const runner: EventHandlerRunner = {
fn: handler,
flavour: options?.flavour,
path: options?.path,
};
this._handlersMap[name].unshift(runner);
return () => {
if (this._handlersMap[name].includes(handler)) {
if (this._handlersMap[name].includes(runner)) {
this._handlersMap[name] = this._handlersMap[name].filter(
f => f !== handler
x => x !== runner
);

@@ -105,16 +144,121 @@ }

bindHotkey(keymap: Record<string, UIEventHandler>, options?: EventOptions) {
return this.add('keyDown', bindKeymap(keymap), options);
}
private get _currentSelections() {
return this.blockStore.selectionManager.value;
}
private _getEventScope(name: EventName, event: Event) {
const handlers = this._handlersMap[name];
if (!handlers) return;
let output: EventScope | undefined;
if (event.target && event.target instanceof Node) {
output = this._buildEventScopeByTarget(name, event.target);
}
if (!output) {
output = this._buildEventScopeBySelection(name);
}
return output;
}
buildEventScope(
name: EventName,
flavours: string[],
paths: string[][]
): EventScope | undefined {
const handlers = this._handlersMap[name];
if (!handlers) return;
const globalEvents = handlers.filter(
handler => handler.flavour === undefined && handler.path === undefined
);
const pathEvents = handlers.filter(handler => {
const _path = handler.path;
if (_path === undefined) return false;
return paths.some(path => PathFinder.includes(path, _path));
});
const flavourEvents = handlers.filter(
handler => handler.flavour && flavours.includes(handler.flavour)
);
return {
runners: pathEvents.concat(flavourEvents).concat(globalEvents),
flavours,
paths,
};
}
private _buildEventScopeByTarget(name: EventName, target: Node) {
const handlers = this._handlersMap[name];
if (!handlers) return;
const path = this.blockStore.viewStore.getNodeView(target)?.path;
if (!path) return;
const flavours = path
.map(blockId => {
return this.blockStore.page.getBlockById(blockId)?.flavour;
})
.filter((flavour): flavour is string => {
return !!flavour;
})
.reverse();
return this.buildEventScope(name, flavours, [path]);
}
private _buildEventScopeBySelection(name: EventName) {
const handlers = this._handlersMap[name];
if (!handlers) return;
const selections = this._currentSelections;
const seen: Record<string, boolean> = {};
const flavours = selections
.map(selection => selection.path)
.flatMap(path => {
return path.map(blockId => {
return this.blockStore.page.getBlockById(blockId)?.flavour;
});
})
.filter((flavour): flavour is string => {
if (!flavour) return false;
if (seen[flavour]) return false;
seen[flavour] = true;
return true;
})
.reverse();
const paths = selections.map(selection => selection.path);
return this.buildEventScope(name, flavours, paths);
}
private _bindEvents() {
bypassEventNames.forEach(eventName => {
this.disposables.addFromEvent(this.root, toLowerCase(eventName), e => {
this.run(eventName, UIEventStateContext.from(new UIEventState(e)));
});
this.disposables.addFromEvent(
this.root,
toLowerCase(eventName),
event => {
this.run(
eventName,
UIEventStateContext.from(new UIEventState(event))
);
}
);
});
globalEventNames.forEach(eventName => {
this.disposables.addFromEvent(document, toLowerCase(eventName), e => {
this.run(eventName, UIEventStateContext.from(new UIEventState(e)));
});
});
this._pointerControl.listen();
this._keyboardControl.listen();
this._rangeControl.listen();
this._clipboardControl.listen();
}
}
export * from './base.js';
export * from './dispatcher.js';
export * from './state.js';
export * from './state/index.js';
export * from './event/index.js';
export * from './selection/index.js';
export * from './service/index.js';
export * from './spec/index.js';
export * from './store/index.js';
export * from './utils/index.js';

@@ -0,8 +1,41 @@

import { PathFinder } from '../utils/index.js';
type SelectionConstructor<T = unknown> = {
new (...args: unknown[]): T;
type: string;
group: string;
};
export type BaseSelectionOptions = {
path: string[];
};
export abstract class BaseSelection {
static readonly type: string;
readonly blockId: string;
constructor(blockId: string) {
this.blockId = blockId;
static readonly group: string;
readonly path: string[];
constructor({ path }: BaseSelectionOptions) {
this.path = path;
}
get blockId(): string {
return PathFinder.id(this.path);
}
is<T extends BlockSuiteSelectionType>(
type: T
): this is BlockSuiteSelectionInstance[T] {
return this.type === type;
}
get type(): BlockSuiteSelectionType {
return (this.constructor as SelectionConstructor)
.type as BlockSuiteSelectionType;
}
get group(): string {
return (this.constructor as SelectionConstructor).group;
}
abstract equals(other: BaseSelection): boolean;

@@ -9,0 +42,0 @@

@@ -1,5 +0,11 @@

import type { Workspace } from '@blocksuite/store';
import { DisposableGroup, Slot } from '@blocksuite/store';
import { DisposableGroup, Slot } from '@blocksuite/global/utils';
import type { StackItem } from '@blocksuite/store';
import type { BlockStore } from '../store/index.js';
import type { BaseSelection } from './base.js';
import {
BlockSelection,
SurfaceSelection,
TextSelection,
} from './variants/index.js';

@@ -15,23 +21,37 @@ interface SelectionConstructor {

export class SelectionManager {
private _workspace: Workspace;
disposables = new DisposableGroup();
_selectionConstructors: Record<string, SelectionConstructor> = {};
private _selectionConstructors: Record<string, SelectionConstructor> = {};
slots = {
changed: new Slot<BaseSelection[]>(),
remoteChanged: new Slot<Record<string, BaseSelection[]>>(),
};
constructor(workspace: Workspace) {
this._workspace = workspace;
constructor(public blockStore: BlockStore) {
this._setupDefaultSelections();
}
register(ctor: SelectionConstructor) {
this._selectionConstructors[ctor.type] = ctor;
register(ctor: SelectionConstructor | SelectionConstructor[]) {
[ctor].flat().forEach(ctor => {
this._selectionConstructors[ctor.type] = ctor;
});
return this;
}
private get _store() {
return this._workspace.awarenessStore;
return this.blockStore.workspace.awarenessStore;
}
private _setupDefaultSelections() {
this.register([TextSelection, BlockSelection, SurfaceSelection]);
}
private _jsonToSelection = (json: Record<string, unknown>) => {
const ctor = this._selectionConstructors[json.type as string];
if (!ctor) {
throw new Error(`Unknown selection type: ${json.type}`);
}
return ctor.fromJSON(json);
};
getInstance<T extends BlockSuiteSelectionType>(

@@ -48,13 +68,9 @@ type: T,

get selections() {
get value() {
return this._store.getLocalSelection().map(json => {
const ctor = this._selectionConstructors[json.type as string];
if (!ctor) {
throw new Error(`Unknown selection type: ${json.type}`);
}
return ctor.fromJSON(json);
return this._jsonToSelection(json);
});
}
setSelections(selections: BaseSelection[]) {
set(selections: BaseSelection[]) {
this._store.setLocalSelection(selections.map(s => s.toJSON()));

@@ -64,2 +80,39 @@ this.slots.changed.emit(selections);

setGroup(group: string, selections: BaseSelection[]) {
const current = this.value.filter(s => s.group !== group);
this.set([...current, ...selections]);
}
getGroup(group: string) {
return this.value.filter(s => s.group === group);
}
update(fn: (currentSelections: BaseSelection[]) => BaseSelection[]) {
const selections = fn(this.value);
this.set(selections);
}
clear(types?: string[]) {
if (types) {
const values = this.value.filter(
selection => !types.includes(selection.type)
);
this.set(values);
} else {
this.set([]);
}
}
find<T extends BlockSuiteSelectionType>(type: T) {
return this.value.find((sel): sel is BlockSuiteSelectionInstance[T] =>
sel.is(type)
);
}
filter<T extends BlockSuiteSelectionType>(type: T) {
return this.value.filter((sel): sel is BlockSuiteSelectionInstance[T] =>
sel.is(type)
);
}
get remoteSelections() {

@@ -69,6 +122,42 @@ return Object.fromEntries(

.filter(([key]) => key !== this._store.awareness.clientID)
.map(([key, value]) => [key, value.selection] as const)
.map(
([key, value]) =>
[key, value.selection.map(this._jsonToSelection)] as const
)
);
}
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[]);
}
};
mount() {
if (this.disposables.disposed) {
this.disposables = new DisposableGroup();
}
this.blockStore.page.history.on('stack-item-added', this._itemAdded);
this.blockStore.page.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);
})
);
}
unmount() {
this.blockStore.page.history.off('stack-item-added', this._itemAdded);
this.blockStore.page.history.off('stack-item-popped', this._itemPopped);
this.clear();
this.slots.changed.dispose();
this.disposables.dispose();
}
dispose() {

@@ -75,0 +164,0 @@ Object.values(this.slots).forEach(slot => slot.dispose());

@@ -0,9 +1,11 @@

import { PathFinder } from '../../utils/index.js';
import { BaseSelection } from '../base.js';
export class BlockSelection extends BaseSelection {
static override readonly type = 'block';
static override type = 'block';
static override group = 'note';
override equals(other: BaseSelection): boolean {
if (other instanceof BlockSelection) {
return other.blockId === this.blockId;
return PathFinder.equals(this.path, other.path);
}

@@ -16,3 +18,3 @@ return false;

type: 'block',
blockId: this.blockId,
path: this.path,
};

@@ -22,3 +24,5 @@ }

static override fromJSON(json: Record<string, unknown>): BlockSelection {
return new BlockSelection(json.blockId as string);
return new BlockSelection({
path: json.path as string[],
});
}

@@ -25,0 +29,0 @@ }

export * from './block.js';
export * from './surface.js';
export * from './text.js';

@@ -3,0 +4,0 @@

@@ -0,26 +1,57 @@

import { PathFinder } from '../../utils/path-finder.js';
import { BaseSelection } from '../base.js';
export type TextRangePoint = {
path: string[];
index: number;
length: number;
};
export type TextSelectionProps = {
from: TextRangePoint;
to: TextRangePoint | null;
};
export class TextSelection extends BaseSelection {
static override readonly type = 'text';
static override type = 'text';
static override group = 'note';
from: number;
from: TextRangePoint;
to: number;
to: TextRangePoint | null;
constructor(blockId: string, from: number, to: number) {
super(blockId);
constructor({ from, to }: TextSelectionProps) {
super({
path: from.path,
});
this.from = from;
this.to = to;
this.to = this._equalPoint(from, to) ? null : to;
}
empty(): boolean {
return this.from === this.to;
return !!this.to;
}
private _equalPoint(
a: TextRangePoint | null,
b: TextRangePoint | null
): boolean {
if (a && b) {
return (
PathFinder.equals(a.path, b.path) &&
a.index === b.index &&
a.length === b.length
);
}
return a === b;
}
override equals(other: BaseSelection): boolean {
if (other instanceof TextSelection) {
return (
other.blockId === this.blockId &&
other.from === this.from &&
other.to === this.to
PathFinder.equals(this.path, other.path) &&
this._equalPoint(other.from, this.from) &&
this._equalPoint(other.to, this.to)
);

@@ -30,7 +61,5 @@ }

}
override toJSON(): Record<string, unknown> {
return {
type: 'text',
blockId: this.blockId,
from: this.from,

@@ -42,8 +71,15 @@ to: this.to,

static override fromJSON(json: Record<string, unknown>): TextSelection {
return new TextSelection(
json.blockId as string,
json.from as number,
json.to as number
);
return new TextSelection({
from: json.from as TextRangePoint,
to: json.to as TextRangePoint | null,
});
}
isCollapsed(): boolean {
return this.to === null && this.from.length === 0;
}
isInSameBlock(): boolean {
return this.to === null || PathFinder.equals(this.from.path, this.to.path);
}
}

@@ -50,0 +86,0 @@

@@ -0,23 +1,38 @@

import { DisposableGroup } from '@blocksuite/global/utils';
import type { BaseBlockModel } from '@blocksuite/store';
import { DisposableGroup } from '@blocksuite/store';
import type { UIEventDispatcher } from '../event/index.js';
import type { EventName, UIEventHandler } from '../event/index.js';
import type { BlockStore } from '../store/index.js';
export interface BlockServiceOptions {
// TODO: add these
// selectionManager;
// transformer;
uiEventDispatcher: UIEventDispatcher;
flavour: string;
store: BlockStore;
}
export class BlockService<Model extends BaseBlockModel = BaseBlockModel> {
disposables = new DisposableGroup();
uiEventDispatcher: UIEventDispatcher;
export class BlockService<_Model extends BaseBlockModel = BaseBlockModel> {
readonly store: BlockStore;
readonly flavour: string;
readonly disposables = new DisposableGroup();
constructor(options: BlockServiceOptions) {
this.uiEventDispatcher = options.uiEventDispatcher;
this.flavour = options.flavour;
this.store = options.store;
}
get workspace() {
return this.store.workspace;
}
get page() {
return this.store.page;
}
get selectionManager() {
return this.store.selectionManager;
}
get uiEventDispatcher() {
return this.store.uiEventDispatcher;
}
// life cycle start

@@ -38,5 +53,24 @@ dispose() {

// event handlers start
handleEvent(name: EventName, fn: UIEventHandler) {
this.disposables.add(this.uiEventDispatcher.add(name, fn));
handleEvent(
name: EventName,
fn: UIEventHandler,
options?: { global: boolean }
) {
this.disposables.add(
this.uiEventDispatcher.add(name, fn, {
flavour: options?.global ? undefined : this.flavour,
})
);
}
bindHotKey(
keymap: Record<string, UIEventHandler>,
options?: { global: boolean }
) {
this.disposables.add(
this.uiEventDispatcher.bindHotkey(keymap, {
flavour: options?.global ? undefined : this.flavour,
})
);
}
// event handlers end

@@ -43,0 +77,0 @@ }

@@ -5,11 +5,17 @@ import type { BlockSchemaType } from '@blocksuite/store';

export interface BlockView<ComponentType = unknown> {
export interface BlockView<
ComponentType = unknown,
WidgetNames extends string = string
> {
component: ComponentType;
widgets?: ComponentType[];
widgets?: Record<WidgetNames, ComponentType>;
}
export interface BlockSpec<ComponentType = unknown> {
export interface BlockSpec<
ComponentType = unknown,
WidgetNames extends string = string
> {
schema: BlockSchemaType;
service?: BlockServiceConstructor;
view: BlockView<ComponentType>;
view: BlockView<ComponentType, WidgetNames>;
}

@@ -1,92 +0,2 @@

import type { UIEventDispatcher } from '../event/index.js';
import type { BlockService, BlockServiceOptions } from '../service/index.js';
import type { BlockSpec } from '../spec/index.js';
export interface BlockStoreOptions {
uiEventDispatcher: UIEventDispatcher;
}
export class BlockStore<ComponentType = unknown> {
private _specs: Map<string, BlockSpec<ComponentType>> = new Map();
private _services: Map<string, BlockService> = new Map();
private readonly _uiEventDispatcher: UIEventDispatcher;
constructor(options: BlockStoreOptions) {
this._uiEventDispatcher = options.uiEventDispatcher;
}
applySpecs(specs: Array<BlockSpec<ComponentType>>) {
const oldSpecs = this._specs;
const newSpecs = this._buildSpecMap(specs);
this._diffServices(oldSpecs, newSpecs);
this._specs = newSpecs;
}
dispose() {
this._services.forEach(service => {
service.dispose();
service.unmounted();
});
this._services.clear();
}
getView(flavour: string) {
const spec = this._specs.get(flavour);
if (!spec) {
return null;
}
return spec.view;
}
getService(flavour: string) {
return this._services.get(flavour);
}
private _diffServices(
oldSpecs: Map<string, BlockSpec<ComponentType>>,
newSpecs: Map<string, BlockSpec<ComponentType>>
) {
oldSpecs.forEach((oldSpec, flavour) => {
if (
newSpecs.has(flavour) &&
newSpecs.get(flavour)?.service === oldSpec.service
) {
return;
}
const service = this._services.get(flavour);
if (service) {
service.dispose();
service.unmounted();
}
this._services.delete(flavour);
});
newSpecs.forEach((newSpec, flavour) => {
if (this._services.has(flavour)) {
return;
}
if (!newSpec.service) {
return;
}
const service = new newSpec.service(this._serviceOptions);
this._services.set(flavour, service);
service.mounted();
});
}
private get _serviceOptions(): BlockServiceOptions {
return {
uiEventDispatcher: this._uiEventDispatcher,
};
}
private _buildSpecMap(specs: Array<BlockSpec<ComponentType>>) {
const specMap = new Map<string, BlockSpec<ComponentType>>();
specs.forEach(spec => {
specMap.set(spec.schema.model.flavour, spec);
});
return specMap;
}
}
export * from './block-store.js';
export * from './view-store.js';

@@ -9,3 +9,10 @@ {

"include": ["./src"],
"references": []
"references": [
{
"path": "../global"
},
{
"path": "../store"
}
]
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc