mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user