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