mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08: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 } }];
|
return [{ insert: ' ', attributes: { footnote } }];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing footnote reference', error);
|
console.warn('Error parsing footnote reference', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ZERO_WIDTH_NON_JOINER,
|
ZERO_WIDTH_NON_JOINER,
|
||||||
ZERO_WIDTH_SPACE,
|
ZERO_WIDTH_SPACE,
|
||||||
} from '@blocksuite/inline';
|
} from '@blocksuite/inline';
|
||||||
|
import { shift } from '@floating-ui/dom';
|
||||||
import { baseTheme } from '@toeverything/theme';
|
import { baseTheme } from '@toeverything/theme';
|
||||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
@@ -129,6 +130,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
|||||||
referenceElement: this,
|
referenceElement: this,
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
autoUpdate: true,
|
autoUpdate: true,
|
||||||
|
middleware: [shift()],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ export class FootNotePopupChip extends LitElement {
|
|||||||
color: ${unsafeCSSVarV2('icon/primary')};
|
color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
svg {
|
svg,
|
||||||
|
object {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
fill: ${unsafeCSSVarV2('icon/primary')};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ export class FootNotePopupChip extends LitElement {
|
|||||||
${this.prefixIcon}
|
${this.prefixIcon}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div class="popup-chip-label">${this.label}</div>
|
<div class="popup-chip-label" title=${this.tooltip}>${this.label}</div>
|
||||||
${this.suffixIcon
|
${this.suffixIcon
|
||||||
? html`<div class="suffix-icon" @click=${this.onSuffixClick}>
|
? html`<div class="suffix-icon" @click=${this.onSuffixClick}>
|
||||||
${this.suffixIcon}
|
${this.suffixIcon}
|
||||||
@@ -78,6 +80,9 @@ export class FootNotePopupChip extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor suffixIcon: TemplateResult | undefined = undefined;
|
accessor suffixIcon: TemplateResult | undefined = undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor tooltip: string = '';
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onClick: (() => void) | undefined = undefined;
|
accessor onClick: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import type { FootNote } from '@blocksuite/affine-model';
|
import { ColorScheme, type FootNote } from '@blocksuite/affine-model';
|
||||||
import { DocDisplayMetaProvider } from '@blocksuite/affine-shared/services';
|
import {
|
||||||
|
DocDisplayMetaProvider,
|
||||||
|
LinkPreviewerService,
|
||||||
|
ThemeProvider,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
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 { DualLinkIcon, LinkIcon } from '@blocksuite/icons/lit';
|
||||||
|
import { computed, signal } from '@preact/signals-core';
|
||||||
import { css, html, LitElement, type TemplateResult } from 'lit';
|
import { css, html, LitElement, type TemplateResult } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import { getAttachmentFileIcon } from '../../../../../icons';
|
import {
|
||||||
|
DarkLoadingIcon,
|
||||||
|
getAttachmentFileIcon,
|
||||||
|
LightLoadingIcon,
|
||||||
|
WebIcon16,
|
||||||
|
} from '../../../../../icons';
|
||||||
import { RefNodeSlotsProvider } from '../../../../extension/ref-node-slots';
|
import { RefNodeSlotsProvider } from '../../../../extension/ref-node-slots';
|
||||||
|
|
||||||
export class FootNotePopup extends WithDisposable(LitElement) {
|
export class FootNotePopup extends SignalWatcher(WithDisposable(LitElement)) {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
.footnote-popup-container {
|
.footnote-popup-container {
|
||||||
border-radius: 4px;
|
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;
|
const referenceType = this.footnote.reference.type;
|
||||||
if (referenceType === 'doc') {
|
if (referenceType === 'doc') {
|
||||||
const docId = this.footnote.reference.docId;
|
const docId = this.footnote.reference.docId;
|
||||||
@@ -35,9 +51,31 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return getAttachmentFileIcon(fileType);
|
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;
|
return undefined;
|
||||||
};
|
});
|
||||||
|
|
||||||
private readonly _suffixIcon = (): TemplateResult | undefined => {
|
private readonly _suffixIcon = (): TemplateResult | undefined => {
|
||||||
const referenceType = this.footnote.reference.type;
|
const referenceType = this.footnote.reference.type;
|
||||||
@@ -49,7 +87,7 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _popupLabel = () => {
|
private readonly _popupLabel$ = computed(() => {
|
||||||
const referenceType = this.footnote.reference.type;
|
const referenceType = this.footnote.reference.type;
|
||||||
let label = '';
|
let label = '';
|
||||||
const { docId, fileName, url } = this.footnote.reference;
|
const { docId, fileName, url } = this.footnote.reference;
|
||||||
@@ -70,11 +108,23 @@ export class FootNotePopup extends WithDisposable(LitElement) {
|
|||||||
if (!url) {
|
if (!url) {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
// TODO(@chen): get url title from url, need to implement after LinkPreviewer refactored as an extension
|
label = this._linkPreview$.value?.title ?? url;
|
||||||
label = url;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return label;
|
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();
|
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() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="footnote-popup-container">
|
<div class="footnote-popup-container">
|
||||||
<footnote-popup-chip
|
<footnote-popup-chip
|
||||||
.prefixIcon=${this._prefixIcon()}
|
.prefixIcon=${this._prefixIcon$.value}
|
||||||
.label=${this._popupLabel()}
|
.label=${this._popupLabel$.value}
|
||||||
.suffixIcon=${this._suffixIcon()}
|
.suffixIcon=${this._suffixIcon()}
|
||||||
.onClick=${this._onChipClick}
|
.onClick=${this._onChipClick}
|
||||||
|
.tooltip=${this._tooltip$.value}
|
||||||
></footnote-popup-chip>
|
></footnote-popup-chip>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user