diff --git a/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts b/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts
index c82ca5f25..7932f0ba0 100644
--- a/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts
+++ b/blocksuite/affine/components/src/rich-text/inline/adapters/markdown/markdown-inline.ts
@@ -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 [];
}
},
diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts
index 865acd4cb..7cdab0f83 100644
--- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts
+++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-node.ts
@@ -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()],
},
};
},
diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts
index 66193f7c2..4a06f183b 100644
--- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts
+++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup-chip.ts
@@ -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}
`
: nothing}
-
+
${this.suffixIcon
? html`
${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;
diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts
index f0f948329..3ea015660 100644
--- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts
+++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/footnote-node/footnote-popup.ts
@@ -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``;
+ 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`
`;