mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat(editor): add link preview to footnote popup (#9869)
[BS-2399](https://linear.app/affine-design/issue/BS-2399/ai-link-的预览支持:获取-fav-icon-标题)
This commit is contained in:
@@ -173,7 +173,7 @@ export const markdownFootnoteReferenceToDeltaMatcher =
|
||||
};
|
||||
return [{ insert: ' ', attributes: { footnote } }];
|
||||
} catch (error) {
|
||||
console.error('Error parsing footnote reference', error);
|
||||
console.warn('Error parsing footnote reference', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ZERO_WIDTH_NON_JOINER,
|
||||
ZERO_WIDTH_SPACE,
|
||||
} from '@blocksuite/inline';
|
||||
import { shift } from '@floating-ui/dom';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
@@ -129,6 +130,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
referenceElement: this,
|
||||
placement: 'top',
|
||||
autoUpdate: true,
|
||||
middleware: [shift()],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -26,9 +26,11 @@ export class FootNotePopupChip extends LitElement {
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
svg,
|
||||
object {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ export class FootNotePopupChip extends LitElement {
|
||||
${this.prefixIcon}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="popup-chip-label">${this.label}</div>
|
||||
<div class="popup-chip-label" title=${this.tooltip}>${this.label}</div>
|
||||
${this.suffixIcon
|
||||
? html`<div class="suffix-icon" @click=${this.onSuffixClick}>
|
||||
${this.suffixIcon}
|
||||
@@ -78,6 +80,9 @@ export class FootNotePopupChip extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor suffixIcon: TemplateResult | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tooltip: string = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onClick: (() => void) | undefined = undefined;
|
||||
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import type { FootNote } from '@blocksuite/affine-model';
|
||||
import { DocDisplayMetaProvider } from '@blocksuite/affine-shared/services';
|
||||
import { ColorScheme, type FootNote } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocDisplayMetaProvider,
|
||||
LinkPreviewerService,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { DualLinkIcon, LinkIcon } from '@blocksuite/icons/lit';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { css, html, LitElement, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { getAttachmentFileIcon } from '../../../../../icons';
|
||||
import {
|
||||
DarkLoadingIcon,
|
||||
getAttachmentFileIcon,
|
||||
LightLoadingIcon,
|
||||
WebIcon16,
|
||||
} from '../../../../../icons';
|
||||
import { RefNodeSlotsProvider } from '../../../../extension/ref-node-slots';
|
||||
|
||||
export class FootNotePopup extends WithDisposable(LitElement) {
|
||||
export class FootNotePopup extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
.footnote-popup-container {
|
||||
border-radius: 4px;
|
||||
@@ -21,7 +31,13 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _prefixIcon = () => {
|
||||
private readonly _isLoading$ = signal(false);
|
||||
|
||||
private readonly _linkPreview$ = signal<
|
||||
{ favicon: string | undefined; title?: string } | undefined
|
||||
>({ favicon: undefined, title: undefined });
|
||||
|
||||
private readonly _prefixIcon$ = computed(() => {
|
||||
const referenceType = this.footnote.reference.type;
|
||||
if (referenceType === 'doc') {
|
||||
const docId = this.footnote.reference.docId;
|
||||
@@ -35,9 +51,31 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
return getAttachmentFileIcon(fileType);
|
||||
} else if (referenceType === 'url') {
|
||||
if (this._isLoading$.value) {
|
||||
return this._LoadingIcon();
|
||||
}
|
||||
|
||||
const favicon = this._linkPreview$.value?.favicon;
|
||||
if (!favicon) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const titleIconType =
|
||||
favicon.split('.').pop() === 'svg'
|
||||
? 'svg+xml'
|
||||
: favicon.split('.').pop();
|
||||
const titleIcon = html`<object
|
||||
type="image/${titleIconType}"
|
||||
data=${favicon}
|
||||
draggable="false"
|
||||
>
|
||||
${WebIcon16}
|
||||
</object>`;
|
||||
return titleIcon;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
});
|
||||
|
||||
private readonly _suffixIcon = (): TemplateResult | undefined => {
|
||||
const referenceType = this.footnote.reference.type;
|
||||
@@ -49,7 +87,7 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private readonly _popupLabel = () => {
|
||||
private readonly _popupLabel$ = computed(() => {
|
||||
const referenceType = this.footnote.reference.type;
|
||||
let label = '';
|
||||
const { docId, fileName, url } = this.footnote.reference;
|
||||
@@ -70,11 +108,23 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
||||
if (!url) {
|
||||
return label;
|
||||
}
|
||||
// TODO(@chen): get url title from url, need to implement after LinkPreviewer refactored as an extension
|
||||
label = url;
|
||||
label = this._linkPreview$.value?.title ?? url;
|
||||
break;
|
||||
}
|
||||
return label;
|
||||
});
|
||||
|
||||
private readonly _tooltip$ = computed(() => {
|
||||
const referenceType = this.footnote.reference.type;
|
||||
if (referenceType === 'url') {
|
||||
return this.footnote.reference.url ?? '';
|
||||
}
|
||||
return this._popupLabel$.value;
|
||||
});
|
||||
|
||||
private readonly _LoadingIcon = () => {
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
return theme === ColorScheme.Light ? LightLoadingIcon : DarkLoadingIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,14 +153,35 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.footnote.reference.type === 'url' && this.footnote.reference.url) {
|
||||
this._isLoading$.value = true;
|
||||
this.std.store
|
||||
.get(LinkPreviewerService)
|
||||
.query(this.footnote.reference.url)
|
||||
.then(data => {
|
||||
this._linkPreview$.value = {
|
||||
favicon: data.icon ?? undefined,
|
||||
title: data.title ?? undefined,
|
||||
};
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
this._isLoading$.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="footnote-popup-container">
|
||||
<footnote-popup-chip
|
||||
.prefixIcon=${this._prefixIcon()}
|
||||
.label=${this._popupLabel()}
|
||||
.prefixIcon=${this._prefixIcon$.value}
|
||||
.label=${this._popupLabel$.value}
|
||||
.suffixIcon=${this._suffixIcon()}
|
||||
.onClick=${this._onChipClick}
|
||||
.tooltip=${this._tooltip$.value}
|
||||
></footnote-popup-chip>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user