refactor(editor): simplify attachment and image upload handling (#11987)

Closes: [BS-3303](https://linear.app/affine-design/issue/BS-3303/改進-pack-attachment-props-流程)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced attachment and image uploads with improved file size validation and clearer notifications.
  - Upload telemetry tracking added for attachments to monitor upload success or failure.

- **Refactor**
  - Streamlined and unified the process of adding attachments and images, making uploads more reliable and efficient.
  - Parameter names updated for clarity across attachment and image insertion features.

- **Documentation**
  - Updated API documentation to reflect parameter name changes for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-04-28 07:03:30 +00:00
parent 3fdab1bec6
commit da0bae273e
8 changed files with 250 additions and 307 deletions
@@ -24,7 +24,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
const file = await openFileOrFiles(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
await addSiblingAttachmentBlocks(std, [file], model, 'after'); await addSiblingAttachmentBlocks(std, [file], model);
if (model.text?.length === 0) { if (model.text?.length === 0) {
std.store.deleteBlock(model); std.store.deleteBlock(model);
} }
@@ -47,7 +47,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
const file = await openFileOrFiles(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
await addSiblingAttachmentBlocks(std, [file], model, 'after'); await addSiblingAttachmentBlocks(std, [file], model);
if (model.text?.length === 0) { if (model.text?.length === 0) {
std.store.deleteBlock(model); std.store.deleteBlock(model);
} }
@@ -9,6 +9,7 @@ import {
EMBED_CARD_WIDTH, EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { import {
type AttachmentUploadedEvent,
FileSizeLimitService, FileSizeLimitService,
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
@@ -18,7 +19,7 @@ import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store'; import type { BlockModel } from '@blocksuite/store';
import type { AttachmentBlockComponent } from './attachment-block.js'; import type { AttachmentBlockComponent } from './attachment-block';
const attachmentUploads = new Set<string>(); const attachmentUploads = new Set<string>();
export function setAttachmentUploading(blockId: string) { export function setAttachmentUploading(blockId: string) {
@@ -34,6 +35,7 @@ function isAttachmentUploading(blockId: string) {
/** /**
* This function will not verify the size of the file. * This function will not verify the size of the file.
*/ */
// TODO(@fundon): should remove
export async function uploadAttachmentBlob( export async function uploadAttachmentBlob(
std: BlockStdScope, std: BlockStdScope,
blockId: string, blockId: string,
@@ -41,9 +43,7 @@ export async function uploadAttachmentBlob(
filetype: string, filetype: string,
isEdgeless?: boolean isEdgeless?: boolean
): Promise<void> { ): Promise<void> {
if (isAttachmentUploading(blockId)) { if (isAttachmentUploading(blockId)) return;
return;
}
let sourceId: string | undefined; let sourceId: string | undefined;
@@ -98,6 +98,7 @@ export async function getAttachmentBlob(model: AttachmentBlockModel) {
return blob; return blob;
} }
// TODO(@fundon): should remove
export async function checkAttachmentBlob(block: AttachmentBlockComponent) { export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
const model = block.model; const model = block.model;
const { id } = model; const { id } = model;
@@ -192,6 +193,57 @@ export async function getFileType(file: File) {
return fileType ? fileType.mime : ''; return fileType ? fileType.mime : '';
} }
function hasExceeded(
std: BlockStdScope,
files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
) {
const exceeded = files.some(file => file.size > maxFileSize);
if (exceeded) {
const size = humanFileSize(maxFileSize, true, 0);
toast(std.host, `You can only upload files less than ${size}`);
}
return exceeded;
}
async function buildPropsWith(
std: BlockStdScope,
file: File,
embed?: boolean,
mode: 'doc' | 'whiteboard' = 'doc'
) {
let type = file.type;
let category: AttachmentUploadedEvent['category'] = 'success';
try {
const { name, size } = file;
const sourceId = await std.store.blobSync.set(file);
type = await getFileType(file);
return {
name,
size,
type,
sourceId,
embed,
} satisfies Partial<AttachmentBlockProps>;
} catch (err) {
category = 'failure';
throw err;
} finally {
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${mode} editor`,
module: 'attachment',
segment: 'attachment',
control: 'uploader',
type,
category,
});
}
}
/** /**
* Add a new attachment block before / after the specified block. * Add a new attachment block before / after the specified block.
*/ */
@@ -199,59 +251,25 @@ export async function addSiblingAttachmentBlocks(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],
targetModel: BlockModel, targetModel: BlockModel,
place: 'before' | 'after' = 'after', placement: 'before' | 'after' = 'after',
isEmbed?: boolean embed?: boolean
) { ) {
if (!files.length) { if (!files.length) return [];
return;
}
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; if (hasExceeded(std, files)) return [];
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return;
}
const doc = targetModel.doc;
const flavour = AttachmentBlockSchema.model.flavour; const flavour = AttachmentBlockSchema.model.flavour;
const droppedInfos = await Promise.all( const propsArray = await Promise.all(
files.map(async file => { files.map(file => buildPropsWith(std, file, embed))
const { name, size } = file;
const type = await getFileType(file);
const props = {
flavour,
name,
size,
type,
embed: isEmbed,
} satisfies Partial<AttachmentBlockProps> & {
flavour: typeof flavour;
};
return { props, file };
})
); );
const blockIds = doc.addSiblingBlocks( const blockIds = std.store.addSiblingBlocks(
targetModel, targetModel,
droppedInfos.map(info => info.props), propsArray.map(props => ({ ...props, flavour })),
place placement
); );
const uploadPromises = blockIds.map(async (blockId, index) => {
const { props, file } = droppedInfos[index];
await uploadAttachmentBlob(std, blockId, file, props.type);
});
await Promise.all(uploadPromises);
return blockIds; return blockIds;
} }
@@ -259,29 +277,21 @@ export async function addAttachments(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],
point?: IVec, point?: IVec,
transformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point shouldTransformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point
): Promise<string[]> { ): Promise<string[]> {
if (!files.length) return []; if (!files.length) return [];
const gfx = std.get(GfxControllerIdentifier); if (hasExceeded(std, files)) return [];
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return [];
}
const propsArray = await Promise.all(
files.map(file => buildPropsWith(std, file, undefined, 'whiteboard'))
);
const gfx = std.get(GfxControllerIdentifier);
let { x, y } = gfx.viewport.center; let { x, y } = gfx.viewport.center;
if (point) { if (point) {
let transform = transformPoint ?? true; shouldTransformPoint = shouldTransformPoint ?? true;
if (transform) { if (shouldTransformPoint) {
[x, y] = gfx.viewport.toModelCoord(...point); [x, y] = gfx.viewport.toModelCoord(...point);
} else { } else {
[x, y] = point; [x, y] = point;
@@ -294,35 +304,15 @@ export async function addAttachments(
const width = EMBED_CARD_WIDTH.cubeThick; const width = EMBED_CARD_WIDTH.cubeThick;
const height = EMBED_CARD_HEIGHT.cubeThick; const height = EMBED_CARD_HEIGHT.cubeThick;
const droppedInfos = files.map((file, index) => { const flavour = AttachmentBlockSchema.model.flavour;
const { name, size } = file;
const blocks = propsArray.map((props, index) => {
const center = Vec.addScalar(xy, index * gap); const center = Vec.addScalar(xy, index * gap);
const xywh = Bound.fromCenter(center, width, height).serialize(); const xywh = Bound.fromCenter(center, width, height).serialize();
const props = { return { flavour, blockProps: { ...props, style, xywh } };
style,
name,
size,
xywh,
} satisfies Partial<AttachmentBlockProps>;
return { file, props };
}); });
// upload file and update the attachment model const blockIds = std.store.addBlocks(blocks);
const uploadPromises = droppedInfos.map(async ({ props, file }) => {
const type = await getFileType(file);
const blockId = std.store.addBlock(
AttachmentBlockSchema.model.flavour,
{ ...props, type },
gfx.surface
);
await uploadAttachmentBlob(std, blockId, file, type, true);
return blockId;
});
const blockIds = await Promise.all(uploadPromises);
gfx.selection.set({ gfx.selection.set({
elements: blockIds, elements: blockIds,
@@ -1,48 +1,40 @@
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import { getImageFilesFromLocal } from '@blocksuite/affine-shared/utils'; import { getImageFilesFromLocal } from '@blocksuite/affine-shared/utils';
import type { Command } from '@blocksuite/std'; import type { Command } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store'; import type { BlockModel } from '@blocksuite/store';
import { addSiblingImageBlock } from '../utils.js'; import { addSiblingImageBlocks } from '../utils';
export const insertImagesCommand: Command< export const insertImagesCommand: Command<
{ {
selectedModels?: BlockModel[]; selectedModels?: BlockModel[];
removeEmptyLine?: boolean; removeEmptyLine?: boolean;
place?: 'after' | 'before'; placement?: 'after' | 'before';
}, },
{ {
insertedImageIds: Promise<string[]>; insertedImageIds: Promise<string[]>;
} }
> = (ctx, next) => { > = (ctx, next) => {
const { selectedModels, place, removeEmptyLine, std } = ctx; const { selectedModels, placement, removeEmptyLine, std } = ctx;
if (!selectedModels) return; if (!selectedModels?.length) return;
const targetModel =
placement === 'before'
? selectedModels[0]
: selectedModels[selectedModels.length - 1];
return next({ return next({
insertedImageIds: getImageFilesFromLocal().then(imageFiles => { insertedImageIds: getImageFilesFromLocal()
if (imageFiles.length === 0) return []; .then(files => addSiblingImageBlocks(std, files, targetModel, placement))
.then(result => {
if (
result.length &&
removeEmptyLine &&
targetModel.text?.length === 0
) {
std.store.deleteBlock(targetModel);
}
if (selectedModels.length === 0) return []; return result;
}),
const targetModel =
place === 'before'
? selectedModels[0]
: selectedModels[selectedModels.length - 1];
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const result = addSiblingImageBlock(
std.host,
imageFiles,
maxFileSize,
targetModel,
place
);
if (removeEmptyLine && targetModel.text?.length === 0) {
std.store.deleteBlock(targetModel);
}
return result ?? [];
}),
}); });
}; };
@@ -1,17 +1,14 @@
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator'; import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
import { ImageBlockSchema, MAX_IMAGE_WIDTH } from '@blocksuite/affine-model'; import { ImageBlockSchema, MAX_IMAGE_WIDTH } from '@blocksuite/affine-model';
import { import { TelemetryProvider } from '@blocksuite/affine-shared/services';
FileSizeLimitService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { import {
isInsideEdgelessEditor, isInsideEdgelessEditor,
matchModels, matchModels,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { addImages, addSiblingImageBlock } from './utils.js'; import { addImages, addSiblingImageBlocks } from './utils.js';
export const ImageDropOption = FileDropConfigExtension({ export const ImageDropOption = FileDropConfigExtension({
flavour: ImageBlockSchema.model.flavour, flavour: ImageBlockSchema.model.flavour,
@@ -19,15 +16,9 @@ export const ImageDropOption = FileDropConfigExtension({
const imageFiles = files.filter(file => file.type.startsWith('image/')); const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (!imageFiles.length) return false; if (!imageFiles.length) return false;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) { if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) {
addSiblingImageBlock( addSiblingImageBlocks(std, imageFiles, targetModel, placement).catch(
std.host, console.error
imageFiles,
maxFileSize,
targetModel,
placement
); );
return true; return true;
} }
+138 -168
View File
@@ -1,9 +1,10 @@
import { autoResizeElementsCommand } from '@blocksuite/affine-block-surface'; import { autoResizeElementsCommand } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import type { import {
AttachmentBlockProps, type AttachmentBlockProps,
ImageBlockModel, type ImageBlockModel,
ImageBlockProps, type ImageBlockProps,
ImageBlockSchema,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
FileSizeLimitService, FileSizeLimitService,
@@ -18,7 +19,7 @@ import {
transformModel, transformModel,
withTempBlobData, withTempBlobData,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/gfx'; import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
import { import {
BlockSelection, BlockSelection,
type BlockStdScope, type BlockStdScope,
@@ -312,93 +313,6 @@ export async function copyImageBlob(
} }
} }
export function shouldResizeImage(node: Node, target: EventTarget | null) {
return !!(
target &&
target instanceof HTMLElement &&
node.contains(target) &&
target.classList.contains('resize')
);
}
export function addSiblingImageBlock(
editorHost: EditorHost,
files: File[],
maxFileSize: number,
targetModel: BlockModel,
place: 'after' | 'before' = 'after'
) {
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (!imageFiles.length) {
return;
}
const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
editorHost,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return;
}
const imageBlockProps: Partial<ImageBlockProps> &
{
flavour: 'affine:image';
}[] = imageFiles.map(file => ({
flavour: 'affine:image',
size: file.size,
}));
const doc = editorHost.doc;
const blockIds = doc.addSiblingBlocks(targetModel, imageBlockProps, place);
blockIds.forEach(
(blockId, index) =>
void uploadBlobForImage(editorHost, blockId, imageFiles[index])
);
return blockIds;
}
export function addImageBlocks(
editorHost: EditorHost,
files: File[],
maxFileSize: number,
parent?: BlockModel | string | null,
parentIndex?: number
) {
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (!imageFiles.length) {
return;
}
const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
editorHost,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return;
}
const doc = editorHost.doc;
const blockIds = imageFiles.map(file =>
doc.addBlock('affine:image', { size: file.size }, parent, parentIndex)
);
blockIds.forEach(
(blockId, index) =>
void uploadBlobForImage(editorHost, blockId, imageFiles[index])
);
return blockIds;
}
/** /**
* Turn the image block into a attachment block. * Turn the image block into a attachment block.
*/ */
@@ -436,115 +350,171 @@ export async function turnImageIntoCardView(
transformModel(model, 'affine:attachment', attachmentProp); transformModel(model, 'affine:attachment', attachmentProp);
} }
export function shouldResizeImage(node: Node, target: EventTarget | null) {
return !!(
target &&
target instanceof HTMLElement &&
node.contains(target) &&
target.classList.contains('resize')
);
}
function hasExceeded(
std: BlockStdScope,
files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
) {
const exceeded = files.some(file => file.size > maxFileSize);
if (exceeded) {
const size = humanFileSize(maxFileSize, true, 0);
toast(std.host, `You can only upload files less than ${size}`);
}
return exceeded;
}
async function buildPropsWith(std: BlockStdScope, file: File) {
const { size } = file;
const [imageSize, sourceId] = await Promise.all([
readImageSize(file),
std.store.blobSync.set(file),
]);
if (!(imageSize.width * imageSize.height)) {
toast(std.host, 'Failed to read image size, please try another image');
throw new Error('Failed to read image size');
}
return { size, sourceId, ...imageSize } satisfies Partial<ImageBlockProps>;
}
export async function addSiblingImageBlocks(
std: BlockStdScope,
files: File[],
targetModel: BlockModel,
placement: 'after' | 'before' = 'after'
) {
files = files.filter(file => file.type.startsWith('image/'));
if (!files.length) return [];
if (hasExceeded(std, files)) return [];
const flavour = ImageBlockSchema.model.flavour;
const propsArray = await Promise.all(
files.map(file => buildPropsWith(std, file))
);
const blockIds = std.store.addSiblingBlocks(
targetModel,
propsArray.map(props => ({ ...props, flavour })),
placement
);
return blockIds;
}
export async function addImageBlocks(
std: BlockStdScope,
files: File[],
parent?: BlockModel | string | null,
parentIndex?: number
) {
files = files.filter(file => file.type.startsWith('image/'));
if (!files.length) return [];
if (hasExceeded(std, files)) return [];
const flavour = ImageBlockSchema.model.flavour;
const propsArray = await Promise.all(
files.map(file => buildPropsWith(std, file))
);
const blockIds = propsArray.map(props =>
std.store.addBlock(flavour, props, parent, parentIndex)
);
return blockIds;
}
export async function addImages( export async function addImages(
std: BlockStdScope, std: BlockStdScope,
files: File[], files: File[],
options: { options: {
point?: IVec; point?: IVec;
maxWidth?: number; maxWidth?: number;
transformPoint?: boolean; // determines whether we should use `toModelCoord` to convert the point shouldTransformPoint?: boolean; // determines whether we should use `toModelCoord` to convert the point
} }
): Promise<string[]> { ): Promise<string[]> {
const imageFiles = [...files].filter(file => file.type.startsWith('image/')); files = files.filter(file => file.type.startsWith('image/'));
if (!imageFiles.length) return []; if (!files.length) return [];
if (hasExceeded(std, files)) return [];
const flavour = ImageBlockSchema.model.flavour;
const propsArray = await Promise.all(
files.map(file => buildPropsWith(std, file))
);
const gfx = std.get(GfxControllerIdentifier); const gfx = std.get(GfxControllerIdentifier);
const isMultiple = propsArray.length > 1;
const inTopLeft = isMultiple;
const gap = 32;
const { point, maxWidth, shouldTransformPoint = true } = options;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return [];
}
const { point, maxWidth, transformPoint = true } = options;
let { x, y } = gfx.viewport.center; let { x, y } = gfx.viewport.center;
if (point) { if (point) {
if (transformPoint) { if (shouldTransformPoint) {
[x, y] = gfx.viewport.toModelCoord(...point); [x, y] = gfx.viewport.toModelCoord(...point);
} else { } else {
[x, y] = point; [x, y] = point;
} }
} }
const dropInfos: { point: Point; blockId: string }[] = []; const xy = [x, y];
const IMAGE_STACK_GAP = 32;
const isMultipleFiles = imageFiles.length > 1;
const inTopLeft = isMultipleFiles ? true : false;
// create image cards without image data const blockIds = propsArray.map((props, index) => {
imageFiles.forEach((file, index) => { const center = Vec.addScalar(xy, index * gap);
const point = new Point(
x + index * IMAGE_STACK_GAP, // If maxWidth is provided, limit the width of the image to maxWidth
y + index * IMAGE_STACK_GAP // Otherwise, use the original width
); const width = maxWidth ? Math.min(props.width, maxWidth) : props.width;
const center = Vec.toVec(point); const height = maxWidth
const bound = calcBoundByOrigin(center, inTopLeft); ? (props.height / props.width) * width
const blockId = std.store.addBlock( : props.height;
'affine:image',
const xywh = calcBoundByOrigin(
center,
inTopLeft,
width,
height
).serialize();
return std.store.addBlock(
flavour,
{ {
size: file.size, ...props,
xywh: bound.serialize(), width,
height,
xywh,
index: gfx.layer.generateIndex(), index: gfx.layer.generateIndex(),
}, },
gfx.surface gfx.surface
); );
dropInfos.push({ point, blockId });
}); });
// upload image data and update the image model
const uploadPromises = imageFiles.map(async (file, index) => {
const { point, blockId } = dropInfos[index];
const block = std.store.getBlock(blockId);
const imageSize = await readImageSize(file);
if (!imageSize.width || !imageSize.height) {
std.store.deleteBlock(block!.model);
toast(std.host, 'Failed to read image size, please try another image');
throw new Error('Failed to read image size');
}
const sourceId = await std.store.blobSync.set(file);
const center = Vec.toVec(point);
// If maxWidth is provided, limit the width of the image to maxWidth
// Otherwise, use the original width
const width = maxWidth
? Math.min(imageSize.width, maxWidth)
: imageSize.width;
const height = maxWidth
? (imageSize.height / imageSize.width) * width
: imageSize.height;
const bound = calcBoundByOrigin(center, inTopLeft, width, height);
std.store.withoutTransact(() => {
gfx.updateElement(blockId, {
sourceId,
...imageSize,
width,
height,
xywh: bound.serialize(),
} satisfies Partial<ImageBlockProps>);
});
});
await Promise.all(uploadPromises);
const blockIds = dropInfos.map(info => info.blockId);
gfx.selection.set({ gfx.selection.set({
elements: blockIds, elements: blockIds,
editing: false, editing: false,
}); });
if (isMultipleFiles) {
if (isMultiple) {
std.command.exec(autoResizeElementsCommand); std.command.exec(autoResizeElementsCommand);
} }
return blockIds; return blockIds;
} }
@@ -170,7 +170,7 @@ export const mediaRender: DraggableTool['render'] = async (bound, edgeless) => {
const [id] = await addImages(edgeless.std, [file], { const [id] = await addImages(edgeless.std, [file], {
point: [bound.x, bound.y], point: [bound.x, bound.y],
maxWidth: MAX_IMAGE_WIDTH, maxWidth: MAX_IMAGE_WIDTH,
transformPoint: false, shouldTransformPoint: false,
}); });
if (id) return id; if (id) return id;
return null; return null;
@@ -196,7 +196,7 @@ Array of IDs of the newly created blocks
### addSiblingBlocks() ### addSiblingBlocks()
> **addSiblingBlocks**(`targetModel`, `props`, `place`): `string`[] > **addSiblingBlocks**(`targetModel`, `props`, `placement`): `string`[]
Add sibling blocks to the store Add sibling blocks to the store
@@ -214,7 +214,7 @@ The target block model
Array of block properties Array of block properties
##### place ##### placement
Optional position to place the new blocks ('after' or 'before') Optional position to place the new blocks ('after' or 'before')
@@ -764,7 +764,7 @@ export class Store {
* Add sibling blocks to the store * Add sibling blocks to the store
* @param targetModel - The target block model * @param targetModel - The target block model
* @param props - Array of block properties * @param props - Array of block properties
* @param place - Optional position to place the new blocks ('after' or 'before') * @param placement - Optional position to place the new blocks ('after' or 'before')
* @returns Array of IDs of the newly created blocks * @returns Array of IDs of the newly created blocks
* *
* @category Block CRUD * @category Block CRUD
@@ -772,7 +772,7 @@ export class Store {
addSiblingBlocks( addSiblingBlocks(
targetModel: BlockModel, targetModel: BlockModel,
props: Array<Partial<BlockProps>>, props: Array<Partial<BlockProps>>,
place: 'after' | 'before' = 'after' placement: 'after' | 'before' = 'after'
): string[] { ): string[] {
if (!props.length) return []; if (!props.length) return [];
const parent = this.getParent(targetModel); const parent = this.getParent(targetModel);
@@ -780,7 +780,7 @@ export class Store {
const targetIndex = const targetIndex =
parent.children.findIndex(({ id }) => id === targetModel.id) ?? 0; parent.children.findIndex(({ id }) => id === targetModel.id) ?? 0;
const insertIndex = place === 'before' ? targetIndex : targetIndex + 1; const insertIndex = placement === 'before' ? targetIndex : targetIndex + 1;
if (props.length <= 1) { if (props.length <= 1) {
if (!props[0]?.flavour) return []; if (!props[0]?.flavour) return [];