feat(editor): add local link preview data for bookmark block (#12085)
Closes: [BS-3343](https://linear.app/affine-design/issue/BS-3343/处理文档-readonly-时-bookmark-citation-preview-data-的获取和显示) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved bookmark blocks to display link previews even in readonly mode by dynamically fetching preview data when needed. - **Refactor** - Enhanced event handling for bookmark cards, providing more consistent behavior when selecting or opening bookmarks. - Refined how preview data is sourced and rendered for bookmarks, ensuring more accurate and up-to-date information is shown. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -2,10 +2,16 @@ import {
|
|||||||
CaptionedBlockComponent,
|
CaptionedBlockComponent,
|
||||||
SelectedStyle,
|
SelectedStyle,
|
||||||
} from '@blocksuite/affine-components/caption';
|
} from '@blocksuite/affine-components/caption';
|
||||||
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
|
import type {
|
||||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
BookmarkBlockModel,
|
||||||
|
LinkPreviewData,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
DocModeProvider,
|
||||||
|
LinkPreviewerService,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { BlockSelection } from '@blocksuite/std';
|
import { BlockSelection } from '@blocksuite/std';
|
||||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -28,6 +34,62 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
|||||||
|
|
||||||
protected containerStyleMap!: ReturnType<typeof styleMap>;
|
protected containerStyleMap!: ReturnType<typeof styleMap>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Local link preview data
|
||||||
|
* When the doc is in readonly mode, and the link preview data are not provided (stored in the block model),
|
||||||
|
* We will use the local link preview data fetched from the link previewer service to render the block.
|
||||||
|
*/
|
||||||
|
private readonly _localLinkPreview$ = signal<LinkPreviewData>({
|
||||||
|
icon: null,
|
||||||
|
title: null,
|
||||||
|
description: null,
|
||||||
|
image: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Link preview data for actual rendering
|
||||||
|
* When the doc is not in readonly mode, and the link preview data are provided (stored in the block model),
|
||||||
|
* We will use the model props to render the block.
|
||||||
|
* Otherwise, we will use the local link preview data to render the block.
|
||||||
|
*/
|
||||||
|
linkPreview$ = computed(() => {
|
||||||
|
const modelProps = this.model.props;
|
||||||
|
const local = this._localLinkPreview$.value;
|
||||||
|
return {
|
||||||
|
icon: modelProps.icon$.value ?? local.icon ?? null,
|
||||||
|
title: modelProps.title$.value ?? local.title ?? null,
|
||||||
|
description: modelProps.description$.value ?? local.description ?? null,
|
||||||
|
image: modelProps.image$.value ?? local.image ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly _updateLocalLinkPreview = () => {
|
||||||
|
// cancel any inflight request
|
||||||
|
this._fetchAbortController?.abort();
|
||||||
|
this._fetchAbortController = new AbortController();
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = false;
|
||||||
|
|
||||||
|
this.std.store
|
||||||
|
.get(LinkPreviewerService)
|
||||||
|
.query(this.model.props.url, this._fetchAbortController.signal)
|
||||||
|
.then(data => {
|
||||||
|
this._localLinkPreview$.value = {
|
||||||
|
icon: data.icon ?? null,
|
||||||
|
title: data.title ?? null,
|
||||||
|
description: data.description ?? null,
|
||||||
|
image: data.image ?? null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.error = true;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
selectBlock = () => {
|
selectBlock = () => {
|
||||||
const selectionManager = this.std.selection;
|
const selectionManager = this.std.selection;
|
||||||
const blockSelection = selectionManager.create(BlockSelection, {
|
const blockSelection = selectionManager.create(BlockSelection, {
|
||||||
@@ -57,17 +119,30 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleClick = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (this.model.parent?.flavour !== 'affine:surface' && !this.doc.readonly) {
|
||||||
|
this.selectBlock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDoubleClick = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.open();
|
||||||
|
};
|
||||||
|
|
||||||
private readonly _renderCitationView = () => {
|
private readonly _renderCitationView = () => {
|
||||||
const { title, description, url, icon, footnoteIdentifier } =
|
const { url, footnoteIdentifier } = this.model.props;
|
||||||
this.model.props;
|
const { icon, title, description } = this.linkPreview$.value;
|
||||||
return html`
|
return html`
|
||||||
<affine-citation-card
|
<affine-citation-card
|
||||||
.icon=${icon}
|
.icon=${icon}
|
||||||
.citationTitle=${title || url}
|
.citationTitle=${title || url}
|
||||||
.citationContent=${description}
|
.citationContent=${description}
|
||||||
.citationIdentifier=${footnoteIdentifier}
|
.citationIdentifier=${footnoteIdentifier}
|
||||||
.onClickCallback=${this.selectBlock}
|
.onClickCallback=${this.handleClick}
|
||||||
.onDoubleClickCallback=${this.open}
|
.onDoubleClickCallback=${this.handleDoubleClick}
|
||||||
.active=${this.selected$.value}
|
.active=${this.selected$.value}
|
||||||
></affine-citation-card>
|
></affine-citation-card>
|
||||||
`;
|
`;
|
||||||
@@ -97,10 +172,17 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
|||||||
|
|
||||||
this.contentEditable = 'false';
|
this.contentEditable = 'false';
|
||||||
|
|
||||||
if (!this.model.props.description && !this.model.props.title) {
|
if (
|
||||||
|
(!this.model.props.description && !this.model.props.title) ||
|
||||||
|
(!this.model.props.image && this.model.props.style === 'vertical')
|
||||||
|
) {
|
||||||
|
// When the doc is readonly, and the preview data not provided
|
||||||
|
// We should fetch the preview data and update the local link preview data
|
||||||
if (this.doc.readonly) {
|
if (this.doc.readonly) {
|
||||||
|
this._updateLocalLinkPreview();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Otherwise, we should refresh the data to the model props
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,20 +18,6 @@ export class BookmarkCard extends SignalWatcher(
|
|||||||
) {
|
) {
|
||||||
static override styles = styles;
|
static override styles = styles;
|
||||||
|
|
||||||
private _handleClick(event: MouseEvent) {
|
|
||||||
event.stopPropagation();
|
|
||||||
const model = this.bookmark.model;
|
|
||||||
|
|
||||||
if (model.parent?.flavour !== 'affine:surface') {
|
|
||||||
this.bookmark.selectBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleDoubleClick(event: MouseEvent) {
|
|
||||||
event.stopPropagation();
|
|
||||||
this.bookmark.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback(): void {
|
override connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -49,8 +35,9 @@ export class BookmarkCard extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const { icon, title, url, description, image, style } =
|
const { url, style } = this.bookmark.model.props;
|
||||||
this.bookmark.model.props;
|
const { icon, title, description, image } =
|
||||||
|
this.bookmark.linkPreview$.value;
|
||||||
|
|
||||||
const cardClassMap = classMap({
|
const cardClassMap = classMap({
|
||||||
loading: this.loading,
|
loading: this.loading,
|
||||||
@@ -98,8 +85,8 @@ export class BookmarkCard extends SignalWatcher(
|
|||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="affine-bookmark-card ${cardClassMap}"
|
class="affine-bookmark-card ${cardClassMap}"
|
||||||
@click=${this._handleClick}
|
@click=${this.bookmark.handleClick}
|
||||||
@dblclick=${this._handleDoubleClick}
|
@dblclick=${this.bookmark.handleDoubleClick}
|
||||||
>
|
>
|
||||||
<div class="affine-bookmark-content">
|
<div class="affine-bookmark-content">
|
||||||
<div class="affine-bookmark-content-title">
|
<div class="affine-bookmark-content-title">
|
||||||
|
|||||||
Reference in New Issue
Block a user