Files
AFFiNE-Mirror/blocksuite/affine/blocks/image/src/image-block.ts
L-Sun d768ad4af0 feat(editor): block comment extension (#12980)
#### PR Dependency Tree


* **PR #12980** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
2025-07-02 17:42:16 +08:00

193 lines
5.2 KiB
TypeScript

import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { whenHover } from '@blocksuite/affine-components/hover';
import { LoadingIcon } from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import { ResourceController } from '@blocksuite/affine-components/resource';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { ImageSelection } from '@blocksuite/affine-shared/selection';
import {
BlockCommentManager,
ToolbarRegistryIdentifier,
} from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { computed } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import type { ImageBlockPageComponent } from './components/page-image-block';
import {
copyImageBlob,
downloadImageBlob,
refreshData,
turnImageIntoCardView,
} from './utils';
@Peekable({
enableOn: () => !IS_MOBILE,
})
export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel> {
resizeable$ = computed(() =>
this.std.selection.value.some(
selection =>
selection.is(ImageSelection) && selection.blockId === this.blockId
)
);
resourceController = new ResourceController(
computed(() => this.model.props.sourceId$.value),
'Image'
);
get blobUrl() {
return this.resourceController.blobUrl$.value;
}
convertToCardView = () => {
turnImageIntoCardView(this).catch(console.error);
};
copy = () => {
copyImageBlob(this).catch(console.error);
};
download = () => {
downloadImageBlob(this).catch(console.error);
};
refreshData = () => {
refreshData(this).catch(console.error);
};
get resizableImg() {
return this.pageImage?.resizeImg;
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
}
private _handleClick(event: MouseEvent) {
// the peek view need handle shift + click
if (event.defaultPrevented) return;
event.stopPropagation();
const selectionManager = this.host.selection;
const blockSelection = selectionManager.create(BlockSelection, {
blockId: this.blockId,
});
selectionManager.setGroup('note', [blockSelection]);
}
private _initHover() {
const { setReference, setFloating, dispose } = whenHover(
hovered => {
const message$ = this.std.get(ToolbarRegistryIdentifier).message$;
if (hovered) {
message$.value = {
flavour: this.model.flavour,
element: this,
setFloating,
};
return;
}
// Clears previous bindings
message$.value = null;
setFloating();
},
{ enterDelay: 500 }
);
setReference(this.hoverableContainer);
this._disposables.add(dispose);
}
override connectedCallback() {
super.connectedCallback();
this.contentEditable = 'false';
this.resourceController.setEngine(this.std.store.blobSync);
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.disposables.add(
this.model.props.sourceId$.subscribe(() => {
this.refreshData();
})
);
}
override firstUpdated() {
// lazy bindings
this.disposables.addFromEvent(this, 'click', this._handleClick);
this._initHover();
}
override renderBlock() {
const blobUrl = this.blobUrl;
const { size = 0 } = this.model.props;
const containerStyleMap = styleMap({
position: 'relative',
width: '100%',
});
const resovledState = this.resourceController.resolveStateWith({
loadingIcon: LoadingIcon({
strokeColor: cssVarV2('button/pureWhiteText'),
ringColor: cssVarV2('loading/imageLoadingLayer', '#ffffff8f'),
}),
errorIcon: BrokenImageIcon(),
icon: ImageIcon(),
title: 'Image',
description: formatSize(size),
});
return html`
<div class="affine-image-container" style=${containerStyleMap}>
${when(
blobUrl,
() =>
html`<affine-page-image
.block=${this}
.state=${resovledState}
></affine-page-image>`,
() =>
html`<affine-image-fallback-card
.state=${resovledState}
></affine-image-fallback-card>`
)}
</div>
${Object.values(this.widgets)}
`;
}
override accessor blockContainerStyles = { margin: '18px 0' };
@query('affine-page-image')
private accessor pageImage: ImageBlockPageComponent | null = null;
@query('.affine-image-container')
accessor hoverableContainer!: HTMLDivElement;
override accessor useCaptionEditor = true;
override accessor useZeroWidth = true;
}
declare global {
interface HTMLElementTagNameMap {
'affine-image': ImageBlockComponent;
}
}