@blocksuite/store
Advanced tools
Comparing version 0.3.1 to 0.4.0-20230110000526-9d95ace
@@ -0,1 +1,2 @@ | ||
/// <reference types="blocksuite__global-types" /> | ||
import type { Page } from './workspace/index.js'; | ||
@@ -8,12 +9,5 @@ import type { TextType } from './text-adapter.js'; | ||
} | ||
export interface IBaseBlockProps { | ||
flavour: string; | ||
type: string; | ||
id: string; | ||
children: IBaseBlockProps[]; | ||
text?: TextType; | ||
} | ||
export declare class BaseBlockModel implements IBaseBlockProps { | ||
export declare class BaseBlockModel<Props = unknown> implements BlockSuiteInternal.IBaseBlockProps { | ||
static version: number; | ||
flavour: string; | ||
flavour: keyof BlockSuiteInternal.BlockModels & string; | ||
tag: StaticValue; | ||
@@ -29,4 +23,4 @@ id: string; | ||
sourceId?: string; | ||
constructor(page: Page, props: Partial<IBaseBlockProps>); | ||
firstChild(): BaseBlockModel | null; | ||
constructor(page: Page, props: Pick<BlockSuiteInternal.IBaseBlockProps, 'id'>); | ||
firstChild(): BaseBlockModel<unknown> | null; | ||
lastChild(): BaseBlockModel | null; | ||
@@ -33,0 +27,0 @@ block2html(childText: string, _previousSiblingId: string, _nextSiblingId: string, begin?: number, end?: number): string; |
@@ -17,5 +17,7 @@ export * from './space.js'; | ||
? window | ||
: typeof global !== 'undefined' | ||
? global | ||
: {}; | ||
: // @ts-ignore | ||
typeof global !== 'undefined' | ||
? // @ts-ignore | ||
global | ||
: {}; | ||
const importIdentifier = '__ $BLOCKSUITE_STORE$ __'; | ||
@@ -22,0 +24,0 @@ // @ts-ignore |
@@ -54,10 +54,6 @@ import type { Space } from './space.js'; | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
serializeDoc(): SerializedStore; | ||
/** | ||
* @internal Only for testing, 'page0' should be replaced by props 'spaceId' | ||
*/ | ||
toJSXElement(id?: string): import("./utils/jsx.js").JSXElement | null; | ||
exportJSX(id?: string): import("./utils/jsx.js").JSXElement | null; | ||
} | ||
//# sourceMappingURL=store.d.ts.map |
@@ -56,12 +56,6 @@ import { Awareness } from 'y-protocols/awareness.js'; | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
serializeDoc() { | ||
return serializeYDoc(this.doc); | ||
} | ||
/** | ||
* @internal Only for testing, 'page0' should be replaced by props 'spaceId' | ||
*/ | ||
toJSXElement(id = '0') { | ||
const json = this.serializeDoc(); | ||
exportJSX(id = '0') { | ||
const json = serializeYDoc(this.doc); | ||
if (!('space:page0' in json)) { | ||
@@ -68,0 +62,0 @@ throw new Error("Failed to convert to JSX: 'space:page0' not found"); |
@@ -10,4 +10,4 @@ import { Doc } from 'yjs'; | ||
} | ||
export declare const yDocToJSXNode: (serializedDoc: Record<string, unknown>, nodeId: string) => JSXElement; | ||
export declare const serializeYDoc: (doc: Doc) => Record<string, unknown>; | ||
export declare function yDocToJSXNode(serializedDoc: Record<string, unknown>, nodeId: string): JSXElement; | ||
export declare function serializeYDoc(doc: Doc): Record<string, unknown>; | ||
//# sourceMappingURL=jsx.d.ts.map |
@@ -5,3 +5,3 @@ import { AbstractType, Map, Text, Array } from 'yjs'; | ||
const testSymbol = Symbol.for('react.test.json'); | ||
const isValidRecord = (data) => { | ||
function isValidRecord(data) { | ||
if (typeof data !== 'object' || data === null) { | ||
@@ -12,5 +12,5 @@ return false; | ||
return true; | ||
}; | ||
} | ||
const IGNORE_PROPS = ['sys:id', 'sys:flavour', 'sys:children']; | ||
export const yDocToJSXNode = (serializedDoc, nodeId) => { | ||
export function yDocToJSXNode(serializedDoc, nodeId) { | ||
if (!isValidRecord(serializedDoc)) { | ||
@@ -37,4 +37,4 @@ throw new Error('Failed to parse doc record! Invalid data.'); | ||
}; | ||
}; | ||
export const serializeYDoc = (doc) => { | ||
} | ||
export function serializeYDoc(doc) { | ||
const json = {}; | ||
@@ -50,4 +50,4 @@ doc.share.forEach((value, key) => { | ||
return json; | ||
}; | ||
const serializeYMap = (map) => { | ||
} | ||
function serializeYMap(map) { | ||
const json = {}; | ||
@@ -72,8 +72,8 @@ map.forEach((value, key) => { | ||
return json; | ||
}; | ||
const serializeYText = (text) => { | ||
} | ||
function serializeYText(text) { | ||
const delta = text.toDelta(); | ||
return delta; | ||
}; | ||
const parseDelta = (text) => { | ||
} | ||
function parseDelta(text) { | ||
if (!text.length) { | ||
@@ -104,3 +104,3 @@ return undefined; | ||
}; | ||
}; | ||
} | ||
//# sourceMappingURL=jsx.js.map |
@@ -0,1 +1,2 @@ | ||
/// <reference types="blocksuite__global-types" /> | ||
import type { BaseBlockModel } from '../base.js'; | ||
@@ -7,3 +8,3 @@ import type { BlockProps, PrefixedBlockProps, YBlock, YBlocks } from '../workspace/page.js'; | ||
export declare function assertFlavours(model: BaseBlockModel, allowed: string[]): void; | ||
export declare function matchFlavours(model: BaseBlockModel, expected: string[]): boolean; | ||
export declare function matchFlavours<Key extends keyof BlockSuiteInternal.BlockModels & string = keyof BlockSuiteInternal.BlockModels & string>(model: BaseBlockModel, expected: Key[]): boolean; | ||
export declare function assertValidChildren(yBlocks: YBlocks, props: Partial<BlockProps>): void; | ||
@@ -10,0 +11,0 @@ export declare function initSysProps(yBlock: YBlock, props: Partial<BlockProps>): void; |
@@ -67,3 +67,3 @@ import * as Y from 'yjs'; | ||
} | ||
if (props.flavour === 'affine:group' && !yBlock.has('prop:xywh')) { | ||
if (props.flavour === 'affine:frame' && !yBlock.has('prop:xywh')) { | ||
yBlock.set('prop:xywh', props.xywh ?? '[0,0,720,480]'); | ||
@@ -70,0 +70,0 @@ } |
@@ -0,1 +1,2 @@ | ||
/// <reference types="blocksuite__global-types" /> | ||
import * as Y from 'yjs'; | ||
@@ -38,4 +39,4 @@ import type { Quill } from 'quill'; | ||
historyUpdated: Signal<void>; | ||
rootAdded: Signal<BaseBlockModel>; | ||
rootDeleted: Signal<string>; | ||
rootAdded: Signal<BaseBlockModel<unknown> | BaseBlockModel<unknown>[]>; | ||
rootDeleted: Signal<string | string[]>; | ||
textUpdated: Signal<Y.YTextEvent>; | ||
@@ -49,3 +50,4 @@ updated: Signal<void>; | ||
private get _yBlocks(); | ||
get root(): BaseBlockModel | null; | ||
get root(): BaseBlockModel<unknown> | null; | ||
get rootLayer(): BaseBlockModel<unknown> | null; | ||
get isEmpty(): boolean; | ||
@@ -60,12 +62,17 @@ get canUndo(): boolean; | ||
resetHistory(): void; | ||
getBlockById(id: string): BaseBlockModel | null; | ||
getBlockByFlavour(blockFlavour: string): BaseBlockModel[]; | ||
getBlockById(id: string): BaseBlockModel<unknown> | null; | ||
getBlockByFlavour(blockFlavour: string): BaseBlockModel<unknown>[]; | ||
getParentById(rootId: string, target: BaseBlockModel): BaseBlockModel | null; | ||
getParent(block: BaseBlockModel): BaseBlockModel | null; | ||
getPreviousSibling(block: BaseBlockModel): BaseBlockModel | null; | ||
getPreviousSiblings(block: BaseBlockModel): BaseBlockModel[]; | ||
getNextSibling(block: BaseBlockModel): BaseBlockModel | null; | ||
getNextSiblings(block: BaseBlockModel): BaseBlockModel[]; | ||
addBlock<T extends BlockProps>(blockProps: Partial<T>, parent?: BaseBlockModel | string, parentIndex?: number): string; | ||
getParent(block: BaseBlockModel): BaseBlockModel<unknown> | null; | ||
getPreviousSibling(block: BaseBlockModel): BaseBlockModel<unknown> | null; | ||
getPreviousSiblings(block: BaseBlockModel): BaseBlockModel<unknown>[]; | ||
getNextSibling(block: BaseBlockModel): BaseBlockModel<unknown> | null; | ||
getNextSiblings(block: BaseBlockModel): BaseBlockModel<unknown>[]; | ||
addBlockByFlavour<ALLProps extends Record<string, any> = BlockSuiteModelProps.ALL, Flavour extends keyof ALLProps & string = keyof ALLProps & string>(flavour: Flavour, blockProps?: Partial<ALLProps[Flavour] & Omit<BlockSuiteInternal.IBaseBlockProps, 'flavour' | 'id'>>, parent?: BaseBlockModel | string | null, parentIndex?: number): string; | ||
/** | ||
* @deprecated use `addBlockByFlavour` | ||
*/ | ||
addBlock<T extends BlockProps>(blockProps: Partial<T>, parent?: BaseBlockModel | string | null, parentIndex?: number): string; | ||
updateBlockById(id: string, props: Partial<BlockProps>): void; | ||
moveBlock(model: BaseBlockModel, targetModel: BaseBlockModel, top?: boolean): void; | ||
updateBlock<T extends Partial<BlockProps>>(model: BaseBlockModel, props: T): void; | ||
@@ -72,0 +79,0 @@ deleteBlockById(id: string): void; |
@@ -8,2 +8,3 @@ import * as Y from 'yjs'; | ||
import { assertValidChildren, initSysProps, syncBlockProps, trySyncTextProp, toBlockProps, matchFlavours, } from '../utils/utils.js'; | ||
import { tryMigrate } from './migrations.js'; | ||
const isWeb = typeof window !== 'undefined'; | ||
@@ -21,3 +22,5 @@ function createChildMap(yChildIds) { | ||
// TODO use schema | ||
this._ignoredKeys = new Set(Object.keys(new BaseBlockModel(this, {}))); | ||
this._ignoredKeys = new Set( | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
Object.keys(new BaseBlockModel(this, { id: null }))); | ||
this.signals = { | ||
@@ -70,4 +73,7 @@ historyUpdated: new Signal(), | ||
get root() { | ||
return this._root; | ||
return Array.isArray(this._root) ? this._root[0] : this._root; | ||
} | ||
get rootLayer() { | ||
return Array.isArray(this._root) ? this._root[1] : null; | ||
} | ||
get isEmpty() { | ||
@@ -120,5 +126,5 @@ return this._yBlocks.size === 0; | ||
getParent(block) { | ||
if (!this._root) | ||
if (!this.root) | ||
return null; | ||
return this.getParentById(this._root.id, block); | ||
return this.getParentById(this.root.id, block); | ||
} | ||
@@ -169,2 +175,11 @@ getPreviousSibling(block) { | ||
} | ||
addBlockByFlavour(flavour, blockProps = {}, parent, parentIndex) { | ||
return this.addBlock({ | ||
flavour, | ||
...blockProps, | ||
}, parent, parentIndex); | ||
} | ||
/** | ||
* @deprecated use `addBlockByFlavour` | ||
*/ | ||
addBlock(blockProps, parent, parentIndex) { | ||
@@ -174,7 +189,7 @@ if (!blockProps.flavour) { | ||
} | ||
if (blockProps.flavour === 'affine:shape') { | ||
if (parent != null || parentIndex != null) { | ||
throw new Error('Shape block should only be appear under page'); | ||
} | ||
} | ||
// if (blockProps.flavour === 'affine:shape') { | ||
// if (parent != null || parentIndex != null) { | ||
// throw new Error('Shape block should only be appear under page'); | ||
// } | ||
// } | ||
const clonedProps = { ...blockProps }; | ||
@@ -192,3 +207,3 @@ const id = this._idGenerator(); | ||
} | ||
const parentId = parent?.id ?? this._root?.id; | ||
const parentId = parent === null ? null : parent?.id ?? this.root?.id; | ||
if (parentId) { | ||
@@ -208,2 +223,28 @@ const yParent = this._yBlocks.get(parentId); | ||
} | ||
moveBlock(model, targetModel, top = true) { | ||
const currentParentModel = this.getParent(model); | ||
const nextParentModel = this.getParent(targetModel); | ||
if (currentParentModel === null || nextParentModel === null) { | ||
throw new Error('cannot find parent model'); | ||
} | ||
this.transact(() => { | ||
const yParentA = this._yBlocks.get(currentParentModel.id); | ||
const yChildrenA = yParentA.get('sys:children'); | ||
const idx = yChildrenA.toArray().findIndex(id => id === model.id); | ||
yChildrenA.delete(idx); | ||
const yParentB = this._yBlocks.get(nextParentModel.id); | ||
const yChildrenB = yParentB.get('sys:children'); | ||
const nextIdx = yChildrenB | ||
.toArray() | ||
.findIndex(id => id === targetModel.id); | ||
if (top) { | ||
yChildrenB.insert(nextIdx, [model.id]); | ||
} | ||
else { | ||
yChildrenB.insert(nextIdx + 1, [model.id]); | ||
} | ||
}); | ||
currentParentModel.propsUpdated.emit(); | ||
nextParentModel.propsUpdated.emit(); | ||
} | ||
updateBlock(model, props) { | ||
@@ -283,2 +324,3 @@ const yBlock = this._yBlocks.get(model.id); | ||
} | ||
tryMigrate(this.doc); | ||
this._handleVersion(); | ||
@@ -335,2 +377,5 @@ this._initYBlocks(); | ||
} | ||
else if (!props.id) { | ||
throw new Error('Block id is not defined'); | ||
} | ||
const blockModel = new BlockModelCtor(this, props); | ||
@@ -342,5 +387,9 @@ return blockModel; | ||
const isRoot = this._blockMap.size === 0; | ||
let isSurface = false; | ||
const prefixedProps = yBlock.toJSON(); | ||
const props = toBlockProps(prefixedProps); | ||
const model = this._createBlockModel({ ...props, id }); | ||
if (model.flavour === 'affine:surface') { | ||
isSurface = true; | ||
} | ||
this._blockMap.set(props.id, model); | ||
@@ -380,2 +429,6 @@ if ( | ||
} | ||
else if (isSurface) { | ||
this._root = [this.root, model]; | ||
this.signals.rootAdded.emit(this._root); | ||
} | ||
else { | ||
@@ -382,0 +435,0 @@ const parent = this.getParent(model); |
@@ -78,7 +78,13 @@ import * as Y from 'yjs'; | ||
removePage(pageId: string): void; | ||
serializeDoc(): import("../store.js").SerializedStore; | ||
search(query: QueryContent): Map<string, string>; | ||
toJSXElement(id?: string): import("../utils/jsx.js").JSXElement | null; | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
exportYDoc(): void; | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
exportJSX(id?: string): import("../utils/jsx.js").JSXElement | null; | ||
} | ||
export {}; | ||
//# sourceMappingURL=workspace.d.ts.map |
@@ -237,13 +237,26 @@ import * as Y from 'yjs'; | ||
} | ||
serializeDoc() { | ||
return this._store.serializeDoc(); | ||
} | ||
search(query) { | ||
return this._indexer.search(query); | ||
} | ||
toJSXElement(id = '0') { | ||
return this._store.toJSXElement(id); | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
exportYDoc() { | ||
const binary = Y.encodeStateAsUpdate(this.doc); | ||
const file = new Blob([binary], { type: 'application/octet-stream' }); | ||
const fileUrl = URL.createObjectURL(file); | ||
const link = document.createElement('a'); | ||
link.href = fileUrl; | ||
link.download = 'workspace.ydoc'; | ||
link.click(); | ||
URL.revokeObjectURL(fileUrl); | ||
} | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
exportJSX(id = '0') { | ||
return this._store.exportJSX(id); | ||
} | ||
} | ||
Workspace.Y = Y; | ||
//# sourceMappingURL=workspace.js.map |
{ | ||
"name": "@blocksuite/store", | ||
"version": "0.3.1", | ||
"version": "0.4.0-20230110000526-9d95ace", | ||
"description": "BlockSuite data store built for general purpose state management.", | ||
@@ -24,3 +24,3 @@ "main": "dist/index.js", | ||
"lit": "^2.5.0", | ||
"yjs": "^13.5.43" | ||
"yjs": "^13.5.44" | ||
}, | ||
@@ -27,0 +27,0 @@ "peerDependencies": { |
@@ -7,2 +7,2 @@ # `@blocksuite/store` | ||
WIP | ||
TBD |
@@ -17,3 +17,3 @@ /* eslint-disable @typescript-eslint/no-restricted-imports */ | ||
import { ListBlockModel } from '../../../blocks/src/list-block/list-model.js'; | ||
import { GroupBlockModel } from '../../../blocks/src/group-block/group-model.js'; | ||
import { FrameBlockModel } from '../../../blocks/src/frame-block/frame-model.js'; | ||
import { DividerBlockModel } from '../../../blocks/src/divider-block/divider-model.js'; | ||
@@ -33,3 +33,3 @@ import type { PageMeta } from '../workspace/index.js'; | ||
'affine:list': ListBlockModel, | ||
'affine:group': GroupBlockModel, | ||
'affine:frame': FrameBlockModel, | ||
'affine:divider': DividerBlockModel, | ||
@@ -72,3 +72,3 @@ } as const; | ||
describe.concurrent('basic', () => { | ||
it('can init store', async () => { | ||
it('can init workspace', async () => { | ||
const options = createTestOptions(); | ||
@@ -346,4 +346,4 @@ const workspace = new Workspace(options); | ||
// Inline snapshot is not supported under describe.parallel config | ||
describe('store.toJSXElement works', async () => { | ||
it('store matches snapshot', async () => { | ||
describe('workspace.exportJSX works', async () => { | ||
it('workspace matches snapshot', async () => { | ||
const options = createTestOptions(); | ||
@@ -355,3 +355,3 @@ const workspace = new Workspace(options).register(BlockSchema); | ||
expect(workspace.toJSXElement()).toMatchInlineSnapshot(` | ||
expect(workspace.exportJSX()).toMatchInlineSnapshot(` | ||
<affine:page | ||
@@ -363,3 +363,3 @@ prop:title="hello" | ||
it('empty store matches snapshot', async () => { | ||
it('empty workspace matches snapshot', async () => { | ||
const options = createTestOptions(); | ||
@@ -369,6 +369,6 @@ const workspace = new Workspace(options).register(BlockSchema); | ||
expect(workspace.toJSXElement()).toMatchInlineSnapshot('null'); | ||
expect(workspace.exportJSX()).toMatchInlineSnapshot('null'); | ||
}); | ||
it('store with multiple blocks children matches snapshot', async () => { | ||
it('workspace with multiple blocks children matches snapshot', async () => { | ||
const options = createTestOptions(); | ||
@@ -382,3 +382,3 @@ const workspace = new Workspace(options).register(BlockSchema); | ||
expect(workspace.toJSXElement()).toMatchInlineSnapshot(/* xml */ ` | ||
expect(workspace.exportJSX()).toMatchInlineSnapshot(/* xml */ ` | ||
<affine:page> | ||
@@ -396,4 +396,4 @@ <affine:paragraph | ||
describe.concurrent('store.search works', async () => { | ||
it('store search matching', async () => { | ||
describe.concurrent('workspace.search works', async () => { | ||
it('workspace search matching', async () => { | ||
const options = createTestOptions(); | ||
@@ -400,0 +400,0 @@ const workspace = new Workspace(options).register(BlockSchema); |
@@ -11,15 +11,7 @@ import type { Page } from './workspace/index.js'; | ||
export interface IBaseBlockProps { | ||
flavour: string; | ||
type: string; | ||
id: string; | ||
children: IBaseBlockProps[]; | ||
// TODO use schema | ||
text?: TextType; | ||
} | ||
export class BaseBlockModel implements IBaseBlockProps { | ||
export class BaseBlockModel<Props = unknown> | ||
implements BlockSuiteInternal.IBaseBlockProps | ||
{ | ||
static version: number; | ||
flavour!: string; | ||
flavour!: keyof BlockSuiteInternal.BlockModels & string; | ||
tag!: StaticValue; | ||
@@ -39,5 +31,8 @@ id: string; | ||
constructor(page: Page, props: Partial<IBaseBlockProps>) { | ||
constructor( | ||
page: Page, | ||
props: Pick<BlockSuiteInternal.IBaseBlockProps, 'id'> | ||
) { | ||
this.page = page; | ||
this.id = props.id as string; | ||
this.id = props.id; | ||
this.children = []; | ||
@@ -44,0 +39,0 @@ } |
@@ -24,4 +24,6 @@ export * from './space.js'; | ||
? window | ||
: typeof global !== 'undefined' | ||
? global | ||
: // @ts-ignore | ||
typeof global !== 'undefined' | ||
? // @ts-ignore | ||
global | ||
: {}; | ||
@@ -28,0 +30,0 @@ const importIdentifier = '__ $BLOCKSUITE_STORE$ __'; |
@@ -104,13 +104,6 @@ import type { Space } from './space.js'; | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
serializeDoc() { | ||
return serializeYDoc(this.doc) as unknown as SerializedStore; | ||
} | ||
/** | ||
* @internal Only for testing, 'page0' should be replaced by props 'spaceId' | ||
*/ | ||
toJSXElement(id = '0') { | ||
const json = this.serializeDoc(); | ||
exportJSX(id = '0') { | ||
const json = serializeYDoc(this.doc) as unknown as SerializedStore; | ||
if (!('space:page0' in json)) { | ||
@@ -117,0 +110,0 @@ throw new Error("Failed to convert to JSX: 'space:page0' not found"); |
@@ -24,3 +24,3 @@ import { AbstractType, Doc, Map, Text, Array } from 'yjs'; | ||
const isValidRecord = (data: unknown): data is DocRecord => { | ||
function isValidRecord(data: unknown): data is DocRecord { | ||
if (typeof data !== 'object' || data === null) { | ||
@@ -31,10 +31,10 @@ return false; | ||
return true; | ||
}; | ||
} | ||
const IGNORE_PROPS = ['sys:id', 'sys:flavour', 'sys:children']; | ||
export const yDocToJSXNode = ( | ||
export function yDocToJSXNode( | ||
serializedDoc: Record<string, unknown>, | ||
nodeId: string | ||
): JSXElement => { | ||
): JSXElement { | ||
if (!isValidRecord(serializedDoc)) { | ||
@@ -67,5 +67,5 @@ throw new Error('Failed to parse doc record! Invalid data.'); | ||
}; | ||
}; | ||
} | ||
export const serializeYDoc = (doc: Doc) => { | ||
export function serializeYDoc(doc: Doc) { | ||
const json: Record<string, unknown> = {}; | ||
@@ -80,5 +80,5 @@ doc.share.forEach((value, key) => { | ||
return json; | ||
}; | ||
} | ||
const serializeYMap = (map: Map<unknown>): unknown => { | ||
function serializeYMap(map: Map<unknown>) { | ||
const json: Record<string, unknown> = {}; | ||
@@ -99,3 +99,3 @@ map.forEach((value, key) => { | ||
return json; | ||
}; | ||
} | ||
@@ -107,8 +107,8 @@ type DeltaText = { | ||
const serializeYText = (text: Text): DeltaText => { | ||
function serializeYText(text: Text): DeltaText { | ||
const delta = text.toDelta(); | ||
return delta; | ||
}; | ||
} | ||
const parseDelta = (text: DeltaText) => { | ||
function parseDelta(text: DeltaText) { | ||
if (!text.length) { | ||
@@ -139,2 +139,2 @@ return undefined; | ||
}; | ||
}; | ||
} |
@@ -34,4 +34,10 @@ import * as Y from 'yjs'; | ||
export function matchFlavours(model: BaseBlockModel, expected: string[]) { | ||
return expected.includes(model.flavour); | ||
export function matchFlavours< | ||
Key extends keyof BlockSuiteInternal.BlockModels & | ||
string = keyof BlockSuiteInternal.BlockModels & string | ||
>( | ||
model: BaseBlockModel, | ||
expected: Key[] | ||
): boolean /* model is BlockModels[Key] */ { | ||
return expected.includes(model.flavour as Key); | ||
} | ||
@@ -99,3 +105,3 @@ | ||
} | ||
if (props.flavour === 'affine:group' && !yBlock.has('prop:xywh')) { | ||
if (props.flavour === 'affine:frame' && !yBlock.has('prop:xywh')) { | ||
yBlock.set('prop:xywh', props.xywh ?? '[0,0,720,480]'); | ||
@@ -102,0 +108,0 @@ } |
@@ -25,2 +25,3 @@ import * as Y from 'yjs'; | ||
import type { BlockSuiteDoc } from '../yjs/index.js'; | ||
import { tryMigrate } from './migrations.js'; | ||
@@ -58,3 +59,3 @@ export type YBlock = Y.Map<unknown>; | ||
private _history!: Y.UndoManager; | ||
private _root: BaseBlockModel | null = null; | ||
private _root: BaseBlockModel | BaseBlockModel[] | null = null; | ||
private _blockMap = new Map<string, BaseBlockModel>(); | ||
@@ -66,3 +67,4 @@ private _splitSet = new Set<Text | PrelimText>(); | ||
private _ignoredKeys = new Set<string>( | ||
Object.keys(new BaseBlockModel(this, {})) | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
Object.keys(new BaseBlockModel(this, { id: null! })) | ||
); | ||
@@ -72,4 +74,4 @@ | ||
historyUpdated: new Signal(), | ||
rootAdded: new Signal<BaseBlockModel>(), | ||
rootDeleted: new Signal<string>(), | ||
rootAdded: new Signal<BaseBlockModel | BaseBlockModel[]>(), | ||
rootDeleted: new Signal<string | string[]>(), | ||
textUpdated: new Signal<Y.YTextEvent>(), | ||
@@ -105,5 +107,9 @@ updated: new Signal(), | ||
get root() { | ||
return this._root; | ||
return Array.isArray(this._root) ? this._root[0] : this._root; | ||
} | ||
get rootLayer() { | ||
return Array.isArray(this._root) ? this._root[1] : null; | ||
} | ||
get isEmpty() { | ||
@@ -168,5 +174,5 @@ return this._yBlocks.size === 0; | ||
getParent(block: BaseBlockModel) { | ||
if (!this._root) return null; | ||
if (!this.root) return null; | ||
return this.getParentById(this._root.id, block); | ||
return this.getParentById(this.root.id, block); | ||
} | ||
@@ -230,5 +236,31 @@ | ||
public addBlockByFlavour< | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
ALLProps extends Record<string, any> = BlockSuiteModelProps.ALL, | ||
Flavour extends keyof ALLProps & string = keyof ALLProps & string | ||
>( | ||
flavour: Flavour, | ||
blockProps: Partial< | ||
ALLProps[Flavour] & | ||
Omit<BlockSuiteInternal.IBaseBlockProps, 'flavour' | 'id'> | ||
> = {}, | ||
parent?: BaseBlockModel | string | null, | ||
parentIndex?: number | ||
) { | ||
return this.addBlock( | ||
{ | ||
flavour, | ||
...blockProps, | ||
}, | ||
parent, | ||
parentIndex | ||
); | ||
} | ||
/** | ||
* @deprecated use `addBlockByFlavour` | ||
*/ | ||
addBlock<T extends BlockProps>( | ||
blockProps: Partial<T>, | ||
parent?: BaseBlockModel | string, | ||
parent?: BaseBlockModel | string | null, | ||
parentIndex?: number | ||
@@ -240,7 +272,7 @@ ): string { | ||
if (blockProps.flavour === 'affine:shape') { | ||
if (parent != null || parentIndex != null) { | ||
throw new Error('Shape block should only be appear under page'); | ||
} | ||
} | ||
// if (blockProps.flavour === 'affine:shape') { | ||
// if (parent != null || parentIndex != null) { | ||
// throw new Error('Shape block should only be appear under page'); | ||
// } | ||
// } | ||
@@ -263,3 +295,3 @@ const clonedProps: Partial<BlockProps> = { ...blockProps }; | ||
const parentId = parent?.id ?? this._root?.id; | ||
const parentId = parent === null ? null : parent?.id ?? this.root?.id; | ||
@@ -283,2 +315,28 @@ if (parentId) { | ||
moveBlock(model: BaseBlockModel, targetModel: BaseBlockModel, top = true) { | ||
const currentParentModel = this.getParent(model); | ||
const nextParentModel = this.getParent(targetModel); | ||
if (currentParentModel === null || nextParentModel === null) { | ||
throw new Error('cannot find parent model'); | ||
} | ||
this.transact(() => { | ||
const yParentA = this._yBlocks.get(currentParentModel.id) as YBlock; | ||
const yChildrenA = yParentA.get('sys:children') as Y.Array<string>; | ||
const idx = yChildrenA.toArray().findIndex(id => id === model.id); | ||
yChildrenA.delete(idx); | ||
const yParentB = this._yBlocks.get(nextParentModel.id) as YBlock; | ||
const yChildrenB = yParentB.get('sys:children') as Y.Array<string>; | ||
const nextIdx = yChildrenB | ||
.toArray() | ||
.findIndex(id => id === targetModel.id); | ||
if (top) { | ||
yChildrenB.insert(nextIdx, [model.id]); | ||
} else { | ||
yChildrenB.insert(nextIdx + 1, [model.id]); | ||
} | ||
}); | ||
currentParentModel.propsUpdated.emit(); | ||
nextParentModel.propsUpdated.emit(); | ||
} | ||
updateBlock<T extends Partial<BlockProps>>(model: BaseBlockModel, props: T) { | ||
@@ -376,2 +434,4 @@ const yBlock = this._yBlocks.get(model.id) as YBlock; | ||
tryMigrate(this.doc); | ||
this._handleVersion(); | ||
@@ -461,5 +521,10 @@ this._initYBlocks(); | ||
throw new Error(`Block flavour ${props.flavour} is not registered`); | ||
} else if (!props.id) { | ||
throw new Error('Block id is not defined'); | ||
} | ||
const blockModel = new BlockModelCtor(this, props); | ||
const blockModel = new BlockModelCtor( | ||
this, | ||
props as PropsWithId<Omit<BlockProps, 'children'>> | ||
); | ||
return blockModel; | ||
@@ -471,2 +536,3 @@ } | ||
const isRoot = this._blockMap.size === 0; | ||
let isSurface = false; | ||
@@ -476,2 +542,5 @@ const prefixedProps = yBlock.toJSON() as PrefixedBlockProps; | ||
const model = this._createBlockModel({ ...props, id }); | ||
if (model.flavour === 'affine:surface') { | ||
isSurface = true; | ||
} | ||
this._blockMap.set(props.id, model); | ||
@@ -518,2 +587,5 @@ | ||
this.signals.rootAdded.emit(model); | ||
} else if (isSurface) { | ||
this._root = [this.root as BaseBlockModel, model]; | ||
this.signals.rootAdded.emit(this._root); | ||
} else { | ||
@@ -520,0 +592,0 @@ const parent = this.getParent(model); |
@@ -334,6 +334,2 @@ import * as Y from 'yjs'; | ||
serializeDoc() { | ||
return this._store.serializeDoc(); | ||
} | ||
search(query: QueryContent) { | ||
@@ -343,5 +339,24 @@ return this._indexer.search(query); | ||
toJSXElement(id = '0') { | ||
return this._store.toJSXElement(id); | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
exportYDoc() { | ||
const binary = Y.encodeStateAsUpdate(this.doc); | ||
const file = new Blob([binary], { type: 'application/octet-stream' }); | ||
const fileUrl = URL.createObjectURL(file); | ||
const link = document.createElement('a'); | ||
link.href = fileUrl; | ||
link.download = 'workspace.ydoc'; | ||
link.click(); | ||
URL.revokeObjectURL(fileUrl); | ||
} | ||
/** | ||
* @internal Only for testing | ||
*/ | ||
exportJSX(id = '0') { | ||
return this._store.exportJSX(id); | ||
} | ||
} |
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
447999
165
7607
3