refactor(editor): extract image block (#9309)

This commit is contained in:
Saul-Mirone
2024-12-25 13:26:29 +00:00
parent ebd97752bf
commit 5274441e14
44 changed files with 350 additions and 204 deletions

View File

@@ -1,4 +1,5 @@
import { addAttachments } from '@blocksuite/affine-block-attachment';
import { addImages } from '@blocksuite/affine-block-image';
import {
CanvasElementType,
SurfaceGroupLikeModel,
@@ -76,7 +77,6 @@ import {
getSortedCloneElements,
serializeElement,
} from '../utils/clone-utils.js';
import { addImages } from '../utils/common.js';
import { deleteElements } from '../utils/crud.js';
import {
isAttachmentBlock,

View File

@@ -1,4 +1,5 @@
import { addAttachments } from '@blocksuite/affine-block-attachment';
import { addImages, LoadedImageIcon } from '@blocksuite/affine-block-image';
import { AttachmentIcon, LinkIcon } from '@blocksuite/affine-components/icons';
import { MAX_IMAGE_WIDTH } from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
@@ -13,9 +14,7 @@ import {
type NoteChildrenFlavour,
openFileOrFiles,
} from '../../../../../_common/utils/index.js';
import { ImageIcon } from '../../../../../image-block/styles.js';
import type { NoteToolOption } from '../../../gfx-tool/note-tool.js';
import { addImages } from '../../../utils/common.js';
import { getTooltipWithShortcut } from '../../utils.js';
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
import { NOTE_MENU_ITEMS } from './note-menu-config.js';
@@ -118,7 +117,7 @@ export class EdgelessNoteMenu extends EdgelessToolbarToolMixin(LitElement) {
@click=${this._addImages}
.disabled=${this._imageLoading}
>
${ImageIcon}
${LoadedImageIcon}
</edgeless-tool-icon-button>
<edgeless-tool-icon-button

View File

@@ -1,14 +1,9 @@
import { CommonUtils } from '@blocksuite/affine-block-surface';
import type { CursorType, StandardCursor } from '@blocksuite/block-std/gfx';
import type { IVec } from '@blocksuite/global/utils';
import { assertExists, Bound, Vec } from '@blocksuite/global/utils';
import { assertExists, Vec } from '@blocksuite/global/utils';
import { css, html } from 'lit';
import {
SURFACE_IMAGE_CARD_HEIGHT,
SURFACE_IMAGE_CARD_WIDTH,
} from '../../../image-block/components/image-block-fallback.js';
// "<svg width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'><g><path fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/><path d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/></g></svg>";
export function generateCursorUrl(
angle = 0,
@@ -79,27 +74,6 @@ export function getTooltipWithShortcut(
</div>`;
}
export function readImageSize(file: File) {
return new Promise<{ width: number; height: number }>(resolve => {
const size = { width: 0, height: 0 };
const img = new Image();
img.onload = () => {
size.width = img.width;
size.height = img.height;
URL.revokeObjectURL(img.src);
resolve(size);
};
img.onerror = () => {
URL.revokeObjectURL(img.src);
resolve(size);
};
img.src = URL.createObjectURL(file);
});
}
const RESIZE_CURSORS: CursorType[] = [
'ew-resize',
'nwse-resize',
@@ -245,14 +219,3 @@ export function launchIntoFullscreen(element: Element) {
element.msRequestFullscreen();
}
}
export function calcBoundByOrigin(
point: IVec,
inTopLeft = false,
width = SURFACE_IMAGE_CARD_WIDTH,
height = SURFACE_IMAGE_CARD_HEIGHT
) {
return inTopLeft
? new Bound(point[0], point[1], width, height)
: Bound.fromCenter(point, width, height);
}

View File

@@ -1,136 +1,25 @@
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
import { toast } from '@blocksuite/affine-components/toast';
import {
DEFAULT_NOTE_HEIGHT,
DEFAULT_NOTE_WIDTH,
type ImageBlockProps,
NOTE_MIN_HEIGHT,
type NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
import {
handleNativeRangeAtPoint,
humanFileSize,
} from '@blocksuite/affine-shared/utils';
import { handleNativeRangeAtPoint } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import {
type IPoint,
type IVec,
Point,
type Point,
serializeXYWH,
Vec,
} from '@blocksuite/global/utils';
import { calcBoundByOrigin, readImageSize } from '../components/utils.js';
import { DEFAULT_NOTE_OFFSET_X, DEFAULT_NOTE_OFFSET_Y } from './consts.js';
import { addBlock } from './crud.js';
export async function addImages(
std: BlockStdScope,
files: File[],
options: {
point?: IVec;
maxWidth?: number;
}
): Promise<string[]> {
const imageFiles = [...files].filter(file => file.type.startsWith('image/'));
if (!imageFiles.length) return [];
const imageService = std.getService('affine:image');
const gfx = std.get(GfxControllerIdentifier);
if (!imageService) {
console.error('Image service not found');
return [];
}
const maxFileSize = imageService.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 } = options;
let { x, y } = gfx.viewport.center;
if (point) [x, y] = gfx.viewport.toModelCoord(...point);
const dropInfos: { point: Point; blockId: string }[] = [];
const IMAGE_STACK_GAP = 32;
const isMultipleFiles = imageFiles.length > 1;
const inTopLeft = isMultipleFiles ? true : false;
// create image cards without image data
imageFiles.forEach((file, index) => {
const point = new Point(
x + index * IMAGE_STACK_GAP,
y + index * IMAGE_STACK_GAP
);
const center = Vec.toVec(point);
const bound = calcBoundByOrigin(center, inTopLeft);
const blockId = std.doc.addBlock(
'affine:image',
{
size: file.size,
xywh: bound.serialize(),
index: gfx.layer.generateIndex(),
},
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 sourceId = await std.doc.blobSync.set(file);
const imageSize = await readImageSize(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.doc.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({
elements: blockIds,
editing: false,
});
if (isMultipleFiles) {
std.command.exec('autoResizeElements');
}
return blockIds;
}
export function addNoteAtPoint(
std: BlockStdScope,
/**

View File

@@ -1,11 +1,13 @@
import {
downloadImageBlob,
type ImageBlockComponent,
} from '@blocksuite/affine-block-image';
import { CaptionIcon, DownloadIcon } from '@blocksuite/affine-components/icons';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { WithDisposable } from '@blocksuite/global/utils';
import { html, LitElement, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
import { downloadImageBlob } from '../../../image-block/utils.js';
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
export class EdgelessChangeImageButton extends WithDisposable(LitElement) {

View File

@@ -6,6 +6,7 @@ import type {
EmbedLoomBlockComponent,
EmbedYoutubeBlockComponent,
} from '@blocksuite/affine-block-embed';
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import { isPeekable, peek } from '@blocksuite/affine-components/peek';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
@@ -32,7 +33,6 @@ import {
notifyDocCreated,
promptDocTitle,
} from '../../../../_common/utils/render-linked-doc.js';
import type { ImageBlockComponent } from '../../../../image-block/image-block.js';
import { duplicate } from '../../../edgeless/utils/clipboard-utils.js';
import { getSortedCloneElements } from '../../../edgeless/utils/clone-utils.js';
import { moveConnectors } from '../../../edgeless/utils/connector.js';

View File

@@ -1,7 +1,6 @@
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import { MenuContext } from '@blocksuite/affine-components/toolbar';
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
export class ImageToolbarContext extends MenuContext {
override close = () => {
this.abortController.abort();

View File

@@ -1,3 +1,4 @@
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import { HoverController } from '@blocksuite/affine-components/hover';
import type {
AdvancedMenuItem,
@@ -13,7 +14,6 @@ import { WidgetComponent } from '@blocksuite/block-std';
import { limitShift, shift } from '@floating-ui/dom';
import { html } from 'lit';
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
import { MORE_GROUPS, PRIMARY_GROUPS } from './config.js';
import { ImageToolbarContext } from './context.js';

View File

@@ -1,11 +1,10 @@
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import {
getBlockProps,
isInsidePageEditor,
} from '@blocksuite/affine-shared/utils';
import { assertExists } from '@blocksuite/global/utils';
import type { ImageBlockComponent } from '../../../image-block/image-block.js';
export function duplicate(
block: ImageBlockComponent,
abortController?: AbortController