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,8 @@
import type { BlockModel } from '@blocksuite/store';
export function getBlockProps(model: BlockModel): Record<string, unknown> {
const keys = model.keys as (keyof typeof model)[];
const values = keys.map(key => model[key]);
const blockProps = Object.fromEntries(keys.map((key, i) => [key, values[i]]));
return blockProps;
}
@@ -0,0 +1,29 @@
import type { BlockModel, Doc, DraftModel } from '@blocksuite/store';
import { minimatch } from 'minimatch';
export function matchFlavours<Key extends (keyof BlockSuite.BlockModels)[]>(
model: DraftModel | null,
expected: Key
): model is BlockSuite.BlockModels[Key[number]] {
return (
!!model &&
expected.some(key =>
minimatch(model.flavour as keyof BlockSuite.BlockModels, key)
)
);
}
export function isInsideBlockByFlavour(
doc: Doc,
block: BlockModel | string,
flavour: string
): boolean {
const parent = doc.getParent(block);
if (parent === null) {
return false;
}
if (flavour === parent.flavour) {
return true;
}
return isInsideBlockByFlavour(doc, parent, flavour);
}
@@ -0,0 +1,27 @@
import type { DocCollection } from '@blocksuite/store';
export function createDefaultDoc(
collection: DocCollection,
options: { id?: string; title?: string } = {}
) {
const doc = collection.createDoc({ id: options.id });
doc.load();
const title = options.title ?? '';
const rootId = doc.addBlock('affine:page', {
title: new doc.Text(title),
});
collection.setDocMeta(doc.id, {
title,
});
// @ts-expect-error FIXME: will be fixed when surface model migrated to affine-model
doc.addBlock('affine:surface', {}, rootId);
const noteId = doc.addBlock('affine:note', {}, rootId);
doc.addBlock('affine:paragraph', {}, noteId);
// To make sure the content of new doc would not be clear
// By undo operation for the first time
doc.resetHistory();
return doc;
}
@@ -0,0 +1,139 @@
import type { EditorHost } from '@blocksuite/block-std';
import type { BlockModel } from '@blocksuite/store';
import { DocModeProvider } from '../../services/doc-mode-service.js';
import { matchFlavours } from './checker.js';
/**
*
* @example
* ```md
* doc
* - note
* - paragraph <- 5
* - note <- 4 (will be skipped)
* - paragraph <- 3
* - child <- 2
* - child <- 1
* - paragraph <- when invoked here, the traverse order will be above
* ```
*
* NOTE: this method will just return blocks with `content` role
*/
export function getPrevContentBlock(
editorHost: EditorHost,
model: BlockModel
): BlockModel | null {
const getPrev = (model: BlockModel) => {
const parent = model.doc.getParent(model);
if (!parent) return null;
const index = parent.children.indexOf(model);
if (index > 0) {
let tmpIndex = index - 1;
let prev = parent.children[tmpIndex];
if (parent.role === 'root' && model.role === 'hub') {
while (prev && prev.flavour !== 'affine:note') {
prev = parent.children[tmpIndex];
tmpIndex--;
}
}
if (!prev) return null;
while (prev.children.length > 0) {
prev = prev.children[prev.children.length - 1];
}
return prev;
}
// in edgeless mode, limit search for the previous block within the same note
if (
editorHost.std.get(DocModeProvider).getEditorMode() === 'edgeless' &&
parent.role === 'hub'
) {
return null;
}
return parent;
};
const map: Record<string, true> = {};
const iterate: (model: BlockModel) => BlockModel | null = (
model: BlockModel
) => {
if (model.id in map) {
console.error(
"Can't get previous block! There's a loop in the block tree!"
);
return null;
}
map[model.id] = true;
const prev = getPrev(model);
if (prev) {
if (prev.role === 'content' && !matchFlavours(prev, ['affine:frame'])) {
return prev;
}
return iterate(prev);
}
return null;
};
return iterate(model);
}
/**
*
* @example
* ```md
* page
* - note
* - paragraph <- when invoked here, the traverse order will be following
* - child <- 1
* - sibling <- 2
* - note <- 3 (will be skipped)
* - paragraph <- 4
* ```
*
* NOTE: this method will skip the `affine:note` block
*/
export function getNextContentBlock(
editorHost: EditorHost,
model: BlockModel,
map: Record<string, true> = {}
): BlockModel | null {
if (model.id in map) {
console.error("Can't get next block! There's a loop in the block tree!");
return null;
}
map[model.id] = true;
const doc = model.doc;
if (model.children.length) {
return model.children[0];
}
let currentBlock: typeof model | null = model;
while (currentBlock) {
const nextSibling = doc.getNext(currentBlock);
if (nextSibling) {
// Assert nextSibling is not possible to be `affine:page`
if (nextSibling.role === 'hub') {
// in edgeless mode, limit search for the next block within the same note
if (
editorHost.std.get(DocModeProvider).getEditorMode() === 'edgeless'
) {
return null;
}
return getNextContentBlock(editorHost, nextSibling);
}
return nextSibling;
}
currentBlock = doc.getParent(currentBlock);
}
return null;
}
@@ -0,0 +1,59 @@
import { type NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
import type { BlockComponent, EditorHost } from '@blocksuite/block-std';
import type { BlockModel, Doc } from '@blocksuite/store';
import { matchFlavours } from './checker.js';
export function findAncestorModel(
model: BlockModel,
match: (m: BlockModel) => boolean
) {
let curModel: BlockModel | null = model;
while (curModel) {
if (match(curModel)) {
return curModel;
}
curModel = curModel.parent;
}
return null;
}
/**
* Get block component by its model and wait for the doc element to finish updating.
*
*/
export async function asyncGetBlockComponent(
editorHost: EditorHost,
id: string
): Promise<BlockComponent | null> {
const rootBlockId = editorHost.doc.root?.id;
if (!rootBlockId) return null;
const rootComponent = editorHost.view.getBlock(rootBlockId);
if (!rootComponent) return null;
await rootComponent.updateComplete;
return editorHost.view.getBlock(id);
}
export function findNoteBlockModel(model: BlockModel) {
return findAncestorModel(model, m =>
matchFlavours(m, ['affine:note'])
) as NoteBlockModel | null;
}
export function getLastNoteBlock(doc: Doc) {
let note: NoteBlockModel | null = null;
if (!doc.root) return null;
const { children } = doc.root;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (
matchFlavours(child, ['affine:note']) &&
child.displayMode !== NoteDisplayMode.EdgelessOnly
) {
note = child as NoteBlockModel;
break;
}
}
return note;
}
@@ -0,0 +1,6 @@
export * from './block-props.js';
export * from './checker.js';
export * from './doc.js';
export * from './get-content-block.js';
export * from './getter.js';
export * from './list.js';
@@ -0,0 +1,103 @@
import type { ListBlockModel } from '@blocksuite/affine-model';
import type { BlockStdScope } from '@blocksuite/block-std';
import type { BlockModel, Doc } from '@blocksuite/store';
import { matchFlavours } from './checker.js';
/**
* Pass in a list model, and this function will look forward to find continuous sibling numbered lists,
* typically used for updating list numbers. The result not contains the list passed in.
*/
export function getNextContinuousNumberedLists(
doc: Doc,
modelOrId: BlockModel | string
): ListBlockModel[] {
const model =
typeof modelOrId === 'string' ? doc.getBlock(modelOrId)?.model : modelOrId;
if (!model) return [];
const parent = doc.getParent(model);
if (!parent) return [];
const modelIndex = parent.children.indexOf(model);
if (modelIndex === -1) return [];
const firstNotNumberedListIndex = parent.children.findIndex(
(model, i) =>
i > modelIndex &&
(!matchFlavours(model, ['affine:list']) || model.type !== 'numbered')
);
const newContinuousLists = parent.children.slice(
modelIndex + 1,
firstNotNumberedListIndex === -1 ? undefined : firstNotNumberedListIndex
);
if (
!newContinuousLists.every(
model =>
matchFlavours(model, ['affine:list']) && model.type === 'numbered'
)
)
return [];
return newContinuousLists as ListBlockModel[];
}
export function toNumberedList(
std: BlockStdScope,
model: BlockModel,
order: number
) {
const { doc } = std;
if (!model.text) return;
const parent = doc.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
const prevSibling = doc.getPrev(model);
let realOrder = order;
// if there is a numbered list before, the order continues from the previous list
if (
prevSibling &&
matchFlavours(prevSibling, ['affine:list']) &&
prevSibling.type === 'numbered'
) {
doc.transact(() => {
if (!prevSibling.order) prevSibling.order = 1;
realOrder = prevSibling.order + 1;
});
}
// add a new list block and delete the current block
const newListId = doc.addBlock(
'affine:list',
{
type: 'numbered',
text: model.text.clone(),
order: realOrder,
},
parent,
index
);
const newList = doc.getBlock(newListId)?.model;
if (!newList) {
return;
}
doc.deleteBlock(model, {
deleteChildren: false,
bringChildrenTo: newList,
});
// if there is a numbered list following, correct their order to keep them continuous
const nextContinuousNumberedLists = getNextContinuousNumberedLists(
doc,
newList
);
let base = realOrder + 1;
nextContinuousNumberedLists.forEach(list => {
doc.transact(() => {
list.order = base;
});
base += 1;
});
return newList.id;
}