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,4 @@
# @blocksuite/affine-shared/commands
This package contains the common commands used in the affine blocks.
Keep in mind that you should not put commands that are specific to a single kind of block here.
@@ -0,0 +1,44 @@
import type { BlockComponent, Command } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
export const getBlockIndexCommand: Command<
'currentSelectionPath',
'blockIndex' | 'parentBlock',
{
path?: string;
}
> = (ctx, next) => {
const path = ctx.path ?? ctx.currentSelectionPath;
assertExists(
path,
'`path` is required, you need to pass it in args or ctx before adding this command to the pipeline.'
);
const parentModel = ctx.std.doc.getParent(path);
if (!parentModel) return;
const parent = ctx.std.view.getBlock(parentModel.id);
if (!parent) return;
const index = parent.childBlocks.findIndex(x => {
return x.blockId === path;
});
next({
blockIndex: index,
parentBlock: parent as BlockComponent,
});
};
declare global {
namespace BlockSuite {
interface CommandContext {
blockIndex?: number;
parentBlock?: BlockComponent;
}
interface Commands {
getBlockIndex: typeof getBlockIndexCommand;
}
}
}
@@ -0,0 +1,45 @@
import type { BlockComponent, Command } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
import { getNextContentBlock } from '../../utils/index.js';
function getNextBlock(std: BlockSuite.Std, path: string) {
const view = std.view;
const model = std.doc.getBlock(path)?.model;
if (!model) return null;
const nextModel = getNextContentBlock(std.host, model);
if (!nextModel) return null;
return view.getBlock(nextModel.id);
}
export const getNextBlockCommand: Command<
'currentSelectionPath',
'nextBlock',
{
path?: string;
}
> = (ctx, next) => {
const path = ctx.path ?? ctx.currentSelectionPath;
assertExists(
path,
'`path` is required, you need to pass it in args or ctx before adding this command to the pipeline.'
);
const nextBlock = getNextBlock(ctx.std, path);
if (nextBlock) {
next({ nextBlock });
}
};
declare global {
namespace BlockSuite {
interface CommandContext {
nextBlock?: BlockComponent;
}
interface Commands {
getNextBlock: typeof getNextBlockCommand;
}
}
}
@@ -0,0 +1,46 @@
import type { BlockComponent, Command } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
import { getPrevContentBlock } from '../../utils/index.js';
function getPrevBlock(std: BlockSuite.Std, path: string) {
const view = std.view;
const model = std.doc.getBlock(path)?.model;
if (!model) return null;
const prevModel = getPrevContentBlock(std.host, model);
if (!prevModel) return null;
return view.getBlock(prevModel.id);
}
export const getPrevBlockCommand: Command<
'currentSelectionPath',
'prevBlock',
{
path?: string;
}
> = (ctx, next) => {
const path = ctx.path ?? ctx.currentSelectionPath;
assertExists(
path,
'`path` is required, you need to pass it in args or ctx before adding this command to the pipeline.'
);
const prevBlock = getPrevBlock(ctx.std, path);
if (prevBlock) {
next({ prevBlock });
}
};
declare global {
namespace BlockSuite {
interface CommandContext {
prevBlock?: BlockComponent;
}
interface Commands {
getPrevBlock: typeof getPrevBlockCommand;
}
}
}
@@ -0,0 +1,160 @@
import type {
BlockSelection,
Command,
TextSelection,
} from '@blocksuite/block-std';
import { BlockComponent } from '@blocksuite/block-std';
import type { RoleType } from '@blocksuite/store';
import type { ImageSelection } from '../../selection/index.js';
export const getSelectedBlocksCommand: Command<
'currentTextSelection' | 'currentBlockSelections' | 'currentImageSelections',
'selectedBlocks',
{
textSelection?: TextSelection;
blockSelections?: BlockSelection[];
imageSelections?: ImageSelection[];
filter?: (el: BlockComponent) => boolean;
types?: Extract<BlockSuite.SelectionType, 'block' | 'text' | 'image'>[];
roles?: RoleType[];
mode?: 'all' | 'flat' | 'highest';
}
> = (ctx, next) => {
const {
types = ['block', 'text', 'image'],
roles = ['content'],
mode = 'flat',
} = ctx;
let dirtyResult: BlockComponent[] = [];
const textSelection = ctx.textSelection ?? ctx.currentTextSelection;
if (types.includes('text') && textSelection) {
try {
const range = ctx.std.range.textSelectionToRange(textSelection);
if (!range) return;
const selectedBlocks = ctx.std.range.getSelectedBlockComponentsByRange(
range,
{
match: (el: BlockComponent) => roles.includes(el.model.role),
mode,
}
);
dirtyResult.push(...selectedBlocks);
} catch {
return;
}
}
const blockSelections = ctx.blockSelections ?? ctx.currentBlockSelections;
if (types.includes('block') && blockSelections) {
const viewStore = ctx.std.view;
const doc = ctx.std.doc;
const selectedBlockComponents = blockSelections.flatMap(selection => {
const el = viewStore.getBlock(selection.blockId);
if (!el) {
return [];
}
const blocks: BlockComponent[] = [el];
let selectionPath = selection.blockId;
if (mode === 'all') {
let parent = null;
do {
parent = doc.getParent(selectionPath);
if (!parent) {
break;
}
const view = parent;
if (
view instanceof BlockComponent &&
!roles.includes(view.model.role)
) {
break;
}
selectionPath = parent.id;
} while (parent);
parent = viewStore.getBlock(selectionPath);
if (parent) {
blocks.push(parent);
}
}
if (['all', 'flat'].includes(mode)) {
viewStore.walkThrough(node => {
const view = node;
if (!(view instanceof BlockComponent)) {
return true;
}
if (roles.includes(view.model.role)) {
blocks.push(view);
}
return;
}, selectionPath);
}
return blocks;
});
dirtyResult.push(...selectedBlockComponents);
}
const imageSelections = ctx.imageSelections ?? ctx.currentImageSelections;
if (types.includes('image') && imageSelections) {
const viewStore = ctx.std.view;
const selectedBlocks = imageSelections
.map(selection => {
const el = viewStore.getBlock(selection.blockId);
return el;
})
.filter((el): el is BlockComponent => Boolean(el));
dirtyResult.push(...selectedBlocks);
}
if (ctx.filter) {
dirtyResult = dirtyResult.filter(ctx.filter);
}
// remove duplicate elements
const result: BlockComponent[] = dirtyResult
.filter((el, index) => dirtyResult.indexOf(el) === index)
// sort by document position
.sort((a, b) => {
if (a === b) {
return 0;
}
const position = a.compareDocumentPosition(b);
if (
position & Node.DOCUMENT_POSITION_FOLLOWING ||
position & Node.DOCUMENT_POSITION_CONTAINED_BY
) {
return -1;
}
if (
position & Node.DOCUMENT_POSITION_PRECEDING ||
position & Node.DOCUMENT_POSITION_CONTAINS
) {
return 1;
}
return 0;
});
if (result.length === 0) return;
next({
selectedBlocks: result,
});
};
declare global {
namespace BlockSuite {
interface CommandContext {
selectedBlocks?: BlockComponent[];
}
interface Commands {
getSelectedBlocks: typeof getSelectedBlocksCommand;
}
}
}
@@ -0,0 +1,4 @@
export { getBlockIndexCommand } from './get-block-index.js';
export { getNextBlockCommand } from './get-next-block.js';
export { getPrevBlockCommand } from './get-prev-block.js';
export { getSelectedBlocksCommand } from './get-selected-blocks.js';
+5
View File
@@ -0,0 +1,5 @@
// This is a dev-only file to make other packages use commands from this package.
export type * from './block-crud/index.js';
export type * from './model-crud/index.js';
export type * from './selection/index.js';
@@ -0,0 +1,32 @@
export {
getBlockIndexCommand,
getNextBlockCommand,
getPrevBlockCommand,
getSelectedBlocksCommand,
} from './block-crud/index.js';
export {
clearAndSelectFirstModelCommand,
copySelectedModelsCommand,
deleteSelectedModelsCommand,
draftSelectedModelsCommand,
duplicateSelectedModelsCommand,
getSelectedModelsCommand,
retainFirstModelCommand,
} from './model-crud/index.js';
export {
getBlockSelectionsCommand,
getImageSelectionsCommand,
getSelectionRectsCommand,
getTextSelectionCommand,
type SelectionRect,
} from './selection/index.js';
declare global {
namespace BlockSuite {
// if we use `with` or `inline` to add command data either then use a command we
// need to update this interface
interface CommandContext {
currentSelectionPath?: string;
}
}
}
@@ -0,0 +1,41 @@
import type { Command } from '@blocksuite/block-std';
export const clearAndSelectFirstModelCommand: Command<'selectedModels'> = (
ctx,
next
) => {
const models = ctx.selectedModels;
if (!models) {
console.error(
'`selectedModels` is required, you need to use `getSelectedModels` command before adding this command to the pipeline.'
);
return;
}
if (models.length > 0) {
const firstModel = models[0];
if (firstModel.text) {
firstModel.text.clear();
const selection = ctx.std.selection.create('text', {
from: {
blockId: firstModel.id,
index: 0,
length: 0,
},
to: null,
});
ctx.std.selection.setGroup('note', [selection]);
}
}
return next();
};
declare global {
namespace BlockSuite {
interface Commands {
clearAndSelectFirstModel: typeof clearAndSelectFirstModelCommand;
}
}
}
@@ -0,0 +1,36 @@
import type { Command } from '@blocksuite/block-std';
import { Slice } from '@blocksuite/store';
export const copySelectedModelsCommand: Command<'draftedModels' | 'onCopy'> = (
ctx,
next
) => {
const models = ctx.draftedModels;
if (!models) {
console.error(
'`draftedModels` is required, you need to use `draftSelectedModels` command before adding this command to the pipeline.'
);
return;
}
models
.then(models => {
const slice = Slice.fromModels(ctx.std.doc, models);
return ctx.std.clipboard.copy(slice);
})
.then(() => ctx.onCopy?.())
.catch(console.error);
return next();
};
declare global {
namespace BlockSuite {
interface CommandContext {
onCopy?: () => void;
}
interface Commands {
copySelectedModels: typeof copySelectedModelsCommand;
}
}
}
@@ -0,0 +1,29 @@
import type { Command } from '@blocksuite/block-std';
export const deleteSelectedModelsCommand: Command<'selectedModels'> = (
ctx,
next
) => {
const models = ctx.selectedModels;
if (!models) {
console.error(
'`selectedModels` is required, you need to use `getSelectedModels` command before adding this command to the pipeline.'
);
return;
}
models.forEach(model => {
ctx.std.doc.deleteBlock(model);
});
return next();
};
declare global {
namespace BlockSuite {
interface Commands {
deleteSelectedModels: typeof deleteSelectedModelsCommand;
}
}
}
@@ -0,0 +1,58 @@
import type { Command } from '@blocksuite/block-std';
import {
type BlockModel,
type DraftModel,
toDraftModel,
} from '@blocksuite/store';
export const draftSelectedModelsCommand: Command<
'selectedModels',
'draftedModels'
> = (ctx, next) => {
const models = ctx.selectedModels;
if (!models) {
console.error(
'`selectedModels` is required, you need to use `getSelectedModels` command before adding this command to the pipeline.'
);
return;
}
const draftedModelsPromise = new Promise<DraftModel[]>(resolve => {
const draftedModels = models.map(toDraftModel);
const modelMap = new Map(draftedModels.map(model => [model.id, model]));
const traverse = (model: DraftModel) => {
const isDatabase = model.flavour === 'affine:database';
const children = isDatabase
? model.children
: model.children.filter(child => modelMap.has(child.id));
children.forEach(child => {
modelMap.delete(child.id);
traverse(child);
});
model.children = children;
};
draftedModels.forEach(traverse);
const remainingDraftedModels = Array.from(modelMap.values());
resolve(remainingDraftedModels);
});
return next({ draftedModels: draftedModelsPromise });
};
declare global {
namespace BlockSuite {
interface CommandContext {
draftedModels?: Promise<DraftModel<BlockModel<object>>[]>;
}
interface Commands {
draftSelectedModels: typeof draftSelectedModelsCommand;
}
}
}
@@ -0,0 +1,38 @@
import type { Command } from '@blocksuite/block-std';
import { Slice } from '@blocksuite/store';
export const duplicateSelectedModelsCommand: Command<
'draftedModels' | 'selectedModels'
> = (ctx, next) => {
const { std, draftedModels, selectedModels } = ctx;
if (!draftedModels || !selectedModels) return;
const model = selectedModels[selectedModels.length - 1];
const parentModel = std.doc.getParent(model.id);
if (!parentModel) return;
const index = parentModel.children.findIndex(x => x.id === model.id);
draftedModels
.then(models => {
const slice = Slice.fromModels(std.doc, models);
return std.clipboard.duplicateSlice(
slice,
std.doc,
parentModel.id,
index + 1
);
})
.catch(console.error);
return next();
};
declare global {
namespace BlockSuite {
interface Commands {
duplicateSelectedModels: typeof duplicateSelectedModelsCommand;
}
}
}
@@ -0,0 +1,72 @@
import type { Command } from '@blocksuite/block-std';
import type { BlockModel } from '@blocksuite/store';
/**
* Retrieves the selected models based on the provided selection types and mode.
*
* @param ctx - The command context, which includes the types of selections to be retrieved and the mode of the selection.
* @param ctx.types - The selection types to be retrieved. Can be an array of 'block', 'text', or 'image'.
* @param ctx.mode - The mode of the selection. Can be 'all', 'flat', or 'highest'.
* @example
* // Assuming `commandContext` is an instance of the command context
* getSelectedModelsCommand(commandContext, (result) => {
* console.log(result.selectedModels);
* });
*
* // Example selection:
* // aaa
* // b[bb
* // ccc
* // ddd
* // ee]e
*
* // all mode: [aaa, bbb, ccc, ddd, eee]
* // flat mode: [bbb, ccc, ddd, eee]
* // highest mode: [bbb, ddd]
*
* // The match function will be evaluated before filtering using mode
* @param next - The next function to be called.
* @returns An object containing the selected models as an array of BlockModel instances.
*/
export const getSelectedModelsCommand: Command<
never,
'selectedModels',
{
types?: Extract<BlockSuite.SelectionType, 'block' | 'text' | 'image'>[];
mode?: 'all' | 'flat' | 'highest';
}
> = (ctx, next) => {
const types = ctx.types ?? ['block', 'text', 'image'];
const mode = ctx.mode ?? 'flat';
const selectedModels: BlockModel[] = [];
ctx.std.command
.chain()
.tryAll(chain => [
chain.getTextSelection(),
chain.getBlockSelections(),
chain.getImageSelections(),
])
.getSelectedBlocks({
types,
mode,
})
.inline(ctx => {
const { selectedBlocks = [] } = ctx;
selectedModels.push(...selectedBlocks.map(el => el.model));
})
.run();
next({ selectedModels });
};
declare global {
namespace BlockSuite {
interface CommandContext {
selectedModels?: BlockModel[];
}
interface Commands {
getSelectedModels: typeof getSelectedModelsCommand;
}
}
}
@@ -0,0 +1,7 @@
export { clearAndSelectFirstModelCommand } from './clear-and-select-first-model.js';
export { copySelectedModelsCommand } from './copy-selected-models.js';
export { deleteSelectedModelsCommand } from './delete-selected-models.js';
export { draftSelectedModelsCommand } from './draft-selected-models.js';
export { duplicateSelectedModelsCommand } from './duplicate-selected-model.js';
export { getSelectedModelsCommand } from './get-selected-models.js';
export { retainFirstModelCommand } from './retain-first-model.js';
@@ -0,0 +1,27 @@
import type { Command } from '@blocksuite/block-std';
export const retainFirstModelCommand: Command<'selectedModels'> = (
ctx,
next
) => {
if (!ctx.selectedModels) {
console.error(
'`selectedModels` is required, you need to use `getSelectedModels` command before adding this command to the pipeline.'
);
return;
}
if (ctx.selectedModels.length > 0) {
ctx.selectedModels.shift();
}
return next();
};
declare global {
namespace BlockSuite {
interface Commands {
retainFirstModel: typeof retainFirstModelCommand;
}
}
}
@@ -0,0 +1,23 @@
import type { BlockSelection, Command } from '@blocksuite/block-std';
export const getBlockSelectionsCommand: Command<
never,
'currentBlockSelections'
> = (ctx, next) => {
const currentBlockSelections = ctx.std.selection.filter('block');
if (currentBlockSelections.length === 0) return;
next({ currentBlockSelections });
};
declare global {
namespace BlockSuite {
interface CommandContext {
currentBlockSelections?: BlockSelection[];
}
interface Commands {
getBlockSelections: typeof getBlockSelectionsCommand;
}
}
}
@@ -0,0 +1,25 @@
import type { Command } from '@blocksuite/block-std';
import type { ImageSelection } from '../../selection/index.js';
export const getImageSelectionsCommand: Command<
never,
'currentImageSelections'
> = (ctx, next) => {
const currentImageSelections = ctx.std.selection.filter('image');
if (currentImageSelections.length === 0) return;
next({ currentImageSelections });
};
declare global {
namespace BlockSuite {
interface CommandContext {
currentImageSelections?: ImageSelection[];
}
interface Commands {
getImageSelections: typeof getImageSelectionsCommand;
}
}
}
@@ -0,0 +1,200 @@
import type {
BlockSelection,
Command,
TextSelection,
} from '@blocksuite/block-std';
import { getViewportElement } from '../../utils/index.js';
export interface SelectionRect {
width: number;
height: number;
top: number;
left: number;
/**
* The block id that the rect is in. Only available for block selections.
*/
blockId?: string;
}
export const getSelectionRectsCommand: Command<
'currentTextSelection' | 'currentBlockSelections',
'selectionRects',
{
textSelection?: TextSelection;
blockSelections?: BlockSelection[];
}
> = (ctx, next) => {
let textSelection;
let blockSelections;
// priority parameters
if (ctx.textSelection) {
textSelection = ctx.textSelection;
} else if (ctx.blockSelections) {
blockSelections = ctx.blockSelections;
} else if (ctx.currentTextSelection) {
textSelection = ctx.currentTextSelection;
} else if (ctx.currentBlockSelections) {
blockSelections = ctx.currentBlockSelections;
} else {
console.error(
'No selection provided, may forgot to call getTextSelection or getBlockSelections or provide the selection directly.'
);
return;
}
const { std } = ctx;
const container = getViewportElement(std.host);
const containerRect = container?.getBoundingClientRect();
if (textSelection) {
const range = std.range.textSelectionToRange(textSelection);
if (range) {
const nativeRects = Array.from(range.getClientRects());
const rectsWithoutFiltered = nativeRects
.map(rect => ({
width: rect.right - rect.left,
height: rect.bottom - rect.top,
top:
rect.top - (containerRect?.top ?? 0) + (container?.scrollTop ?? 0),
left:
rect.left -
(containerRect?.left ?? 0) +
(container?.scrollLeft ?? 0),
}))
.filter(rect => rect.width > 0 && rect.height > 0);
return next({
selectionRects: filterCoveringRects(rectsWithoutFiltered),
});
}
} else if (blockSelections && blockSelections.length > 0) {
const result = blockSelections
.map(blockSelection => {
const block = std.view.getBlock(blockSelection.blockId);
if (!block) return null;
const rect = block.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
top:
rect.top - (containerRect?.top ?? 0) + (container?.scrollTop ?? 0),
left:
rect.left -
(containerRect?.left ?? 0) +
(container?.scrollLeft ?? 0),
blockId: blockSelection.blockId,
};
})
.filter(rect => !!rect);
return next({ selectionRects: result });
}
return;
};
declare global {
namespace BlockSuite {
interface CommandContext {
selectionRects?: SelectionRect[];
}
interface Commands {
/**
* Get the selection rects of the current selection or given selections.
*
* @chain may be `getTextSelection`, `getBlockSelections`, or nothing.
* @param textSelection The provided text selection.
* @param blockSelections The provided block selections. If `textSelection` is provided, this will be ignored.
* @returns The selection rects.
*/
getSelectionRects: typeof getSelectionRectsCommand;
}
}
}
function covers(rect1: SelectionRect, rect2: SelectionRect): boolean {
return (
rect1.left <= rect2.left &&
rect1.top <= rect2.top &&
rect1.left + rect1.width >= rect2.left + rect2.width &&
rect1.top + rect1.height >= rect2.top + rect2.height
);
}
function intersects(rect1: SelectionRect, rect2: SelectionRect): boolean {
return (
rect1.left <= rect2.left + rect2.width &&
rect1.left + rect1.width >= rect2.left &&
rect1.top <= rect2.top + rect2.height &&
rect1.top + rect1.height >= rect2.top
);
}
function merge(rect1: SelectionRect, rect2: SelectionRect): SelectionRect {
const left = Math.min(rect1.left, rect2.left);
const top = Math.min(rect1.top, rect2.top);
const right = Math.max(rect1.left + rect1.width, rect2.left + rect2.width);
const bottom = Math.max(rect1.top + rect1.height, rect2.top + rect2.height);
return {
width: right - left,
height: bottom - top,
top,
left,
};
}
export function filterCoveringRects(rects: SelectionRect[]): SelectionRect[] {
let mergedRects: SelectionRect[] = [];
let hasChanges: boolean;
do {
hasChanges = false;
const newMergedRects: SelectionRect[] = [...mergedRects];
for (const rect of rects) {
let merged = false;
for (let i = 0; i < newMergedRects.length; i++) {
if (covers(newMergedRects[i], rect)) {
merged = true;
break;
} else if (intersects(newMergedRects[i], rect)) {
newMergedRects[i] = merge(newMergedRects[i], rect);
merged = true;
hasChanges = true;
break;
}
}
if (!merged) {
newMergedRects.push(rect);
}
}
if (!hasChanges) {
for (let i = 0; i < newMergedRects.length; i++) {
for (let j = i + 1; j < newMergedRects.length; j++) {
if (intersects(newMergedRects[i], newMergedRects[j])) {
newMergedRects[i] = merge(newMergedRects[i], newMergedRects[j]);
newMergedRects.splice(j, 1);
hasChanges = true;
break;
}
}
}
}
mergedRects = newMergedRects;
} while (hasChanges);
return mergedRects;
}
@@ -0,0 +1,23 @@
import type { Command, TextSelection } from '@blocksuite/block-std';
export const getTextSelectionCommand: Command<never, 'currentTextSelection'> = (
ctx,
next
) => {
const currentTextSelection = ctx.std.selection.find('text');
if (!currentTextSelection) return;
next({ currentTextSelection });
};
declare global {
namespace BlockSuite {
interface CommandContext {
currentTextSelection?: TextSelection;
}
interface Commands {
getTextSelection: typeof getTextSelectionCommand;
}
}
}
@@ -0,0 +1,7 @@
export { getBlockSelectionsCommand } from './get-block-selections.js';
export { getImageSelectionsCommand } from './get-image-selections.js';
export {
getSelectionRectsCommand,
type SelectionRect,
} from './get-selection-rects.js';
export { getTextSelectionCommand } from './get-text-selection.js';