Files
AFFiNE-Mirror/blocksuite/blocks/src/image-block/image-edgeless-block.ts
2024-12-20 15:38:06 +08:00

162 lines
4.0 KiB
TypeScript

import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { Peekable } from '@blocksuite/affine-components/peek';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { GfxBlockComponent } from '@blocksuite/block-std';
import { css, html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import type { ImageBlockFallbackCard } from './components/image-block-fallback.js';
import type { ImageBlockService } from './image-service.js';
import {
copyImageBlob,
downloadImageBlob,
fetchImageBlob,
resetImageSize,
turnImageIntoCardView,
} from './utils.js';
@Peekable()
export class ImageEdgelessBlockComponent extends GfxBlockComponent<
ImageBlockModel,
ImageBlockService
> {
static override styles = css`
affine-edgeless-image .resizable-img,
affine-edgeless-image .resizable-img img {
width: 100%;
height: 100%;
}
`;
convertToCardView = () => {
turnImageIntoCardView(this).catch(console.error);
};
copy = () => {
copyImageBlob(this).catch(console.error);
};
download = () => {
downloadImageBlob(this).catch(console.error);
};
refreshData = () => {
this.retryCount = 0;
fetchImageBlob(this)
.then(() => {
const { width, height } = this.model;
if (!width || !height) {
return resetImageSize(this);
}
return;
})
.catch(console.error);
};
private _handleError(error: Error) {
this.dispatchEvent(new CustomEvent('error', { detail: error }));
}
override connectedCallback() {
super.connectedCallback();
this.refreshData();
this.contentEditable = 'false';
this.disposables.add(
this.model.propsUpdated.on(({ key }) => {
if (key === 'sourceId') {
this.refreshData();
}
})
);
}
override disconnectedCallback() {
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
}
super.disconnectedCallback();
}
override renderGfxBlock() {
const rotate = this.model.rotate ?? 0;
const containerStyleMap = styleMap({
position: 'relative',
width: '100%',
transform: `rotate(${rotate}deg)`,
transformOrigin: 'center',
});
return html`
<div class="affine-image-container" style=${containerStyleMap}>
${when(
this.loading || this.error || !this.blobUrl,
() =>
html`<affine-image-fallback-card
.error=${this.error}
.loading=${this.loading}
.mode=${'page'}
></affine-image-fallback-card>`,
() =>
html`<div class="resizable-img">
<img
class="drag-target"
src=${this.blobUrl ?? ''}
draggable="false"
@error=${this._handleError}
loading="lazy"
/>
</div>`
)}
<affine-block-selection .block=${this}></affine-block-selection>
</div>
<block-caption-editor></block-caption-editor>
${Object.values(this.widgets)}
`;
}
override updated() {
this.fallbackCard?.requestUpdate();
}
@property({ attribute: false })
accessor blob: Blob | undefined = undefined;
@property({ attribute: false })
accessor blobUrl: string | undefined = undefined;
@query('block-caption-editor')
accessor captionEditor!: BlockCaptionEditor | null;
@property({ attribute: false })
accessor downloading = false;
@property({ attribute: false })
accessor error = false;
@query('affine-image-fallback-card')
accessor fallbackCard: ImageBlockFallbackCard | null = null;
@state()
accessor lastSourceId!: string;
@property({ attribute: false })
accessor loading = false;
@query('.resizable-img')
accessor resizableImg!: HTMLDivElement;
@property({ attribute: false })
accessor retryCount = 0;
}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-image': ImageEdgelessBlockComponent;
}
}