mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
chore(editor): reorg packages (#10702)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { BookmarkBlockHtmlAdapterExtension } from './html.js';
|
||||
import { BookmarkBlockMarkdownAdapterExtension } from './markdown.js';
|
||||
import { BookmarkBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
||||
import { BookmarkBlockPlainTextAdapterExtension } from './plain-text.js';
|
||||
|
||||
export const BookmarkBlockAdapterExtensions: ExtensionType[] = [
|
||||
BookmarkBlockHtmlAdapterExtension,
|
||||
BookmarkBlockMarkdownAdapterExtension,
|
||||
BookmarkBlockNotionHtmlAdapterExtension,
|
||||
BookmarkBlockPlainTextAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createEmbedBlockHtmlAdapterMatcher } from '@blocksuite/affine-block-embed';
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockHtmlAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const bookmarkBlockHtmlAdapterMatcher =
|
||||
createEmbedBlockHtmlAdapterMatcher(BookmarkBlockSchema.model.flavour);
|
||||
|
||||
export const BookmarkBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
bookmarkBlockHtmlAdapterMatcher
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './html.js';
|
||||
export * from './markdown.js';
|
||||
export * from './notion-html.js';
|
||||
export * from './plain-text.js';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed';
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const bookmarkBlockMarkdownAdapterMatcher =
|
||||
createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour);
|
||||
|
||||
export const BookmarkBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
export const bookmarkBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
|
||||
{
|
||||
flavour: BookmarkBlockSchema.model.flavour,
|
||||
toMatch: o => {
|
||||
return (
|
||||
HastUtils.isElement(o.node) &&
|
||||
o.node.tagName === 'figure' &&
|
||||
!!HastUtils.querySelector(o.node, '.bookmark')
|
||||
);
|
||||
},
|
||||
fromMatch: () => false,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const bookmark = HastUtils.querySelector(o.node, '.bookmark');
|
||||
if (!bookmark) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { walkerContext } = context;
|
||||
const bookmarkURL = bookmark.properties?.href;
|
||||
const bookmarkTitle = HastUtils.getTextContent(
|
||||
HastUtils.querySelector(bookmark, '.bookmark-title')
|
||||
);
|
||||
const bookmarkDescription = HastUtils.getTextContent(
|
||||
HastUtils.querySelector(bookmark, '.bookmark-description')
|
||||
);
|
||||
const bookmarkIcon = HastUtils.querySelector(
|
||||
bookmark,
|
||||
'.bookmark-icon'
|
||||
);
|
||||
const bookmarkIconURL =
|
||||
typeof bookmarkIcon?.properties?.src === 'string'
|
||||
? bookmarkIcon.properties.src
|
||||
: '';
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: BookmarkBlockSchema.model.flavour,
|
||||
props: {
|
||||
type: 'card',
|
||||
url: bookmarkURL ?? '',
|
||||
title: bookmarkTitle,
|
||||
description: bookmarkDescription,
|
||||
icon: bookmarkIconURL,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {},
|
||||
};
|
||||
|
||||
export const BookmarkBlockNotionHtmlAdapterExtension =
|
||||
BlockNotionHtmlAdapterExtension(bookmarkBlockNotionHtmlAdapterMatcher);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createEmbedBlockPlainTextAdapterMatcher } from '@blocksuite/affine-block-embed';
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockPlainTextAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const bookmarkBlockPlainTextAdapterMatcher =
|
||||
createEmbedBlockPlainTextAdapterMatcher(BookmarkBlockSchema.model.flavour);
|
||||
|
||||
export const BookmarkBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(bookmarkBlockPlainTextAdapterMatcher);
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
CaptionedBlockComponent,
|
||||
SelectedStyle,
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { computed, type ReadonlySignal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { refreshBookmarkUrlData } from './utils.js';
|
||||
|
||||
export const BOOKMARK_MIN_WIDTH = 450;
|
||||
|
||||
export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBlockModel> {
|
||||
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
|
||||
() => ({
|
||||
'selected-style': this.selected$.value,
|
||||
})
|
||||
);
|
||||
|
||||
private _fetchAbortController?: AbortController;
|
||||
|
||||
blockDraggable = true;
|
||||
|
||||
protected containerStyleMap!: ReturnType<typeof styleMap>;
|
||||
|
||||
open = () => {
|
||||
let link = this.model.url;
|
||||
if (!link.match(/^[a-zA-Z]+:\/\//)) {
|
||||
link = 'https://' + link;
|
||||
}
|
||||
window.open(link, '_blank');
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
refreshBookmarkUrlData(this, this._fetchAbortController?.signal).catch(
|
||||
console.error
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
const mode = this.std.get(DocModeProvider).getEditorMode();
|
||||
const miniWidth = `${BOOKMARK_MIN_WIDTH}px`;
|
||||
|
||||
this.containerStyleMap = styleMap({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
...(mode === 'edgeless' ? { miniWidth } : {}),
|
||||
});
|
||||
|
||||
this._fetchAbortController = new AbortController();
|
||||
|
||||
this.contentEditable = 'false';
|
||||
|
||||
if (!this.model.description && !this.model.title) {
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.on(({ key }) => {
|
||||
if (key === 'url') {
|
||||
this.refreshData();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._fetchAbortController?.abort();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div
|
||||
draggable="${this.blockDraggable ? 'true' : 'false'}"
|
||||
class=${classMap({
|
||||
'affine-bookmark-container': true,
|
||||
...this.selectedStyle$?.value,
|
||||
})}
|
||||
style=${this.containerStyleMap}
|
||||
>
|
||||
<bookmark-card
|
||||
.bookmark=${this}
|
||||
.loading=${this.loading}
|
||||
.error=${this.error}
|
||||
></bookmark-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override accessor blockContainerStyles: StyleInfo = {
|
||||
margin: '18px 0',
|
||||
};
|
||||
|
||||
@query('bookmark-card')
|
||||
accessor bookmarkCard!: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor error = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor loading = false;
|
||||
|
||||
override accessor selectedStyle = SelectedStyle.Border;
|
||||
|
||||
override accessor useCaptionEditor = true;
|
||||
|
||||
override accessor useZeroWidth = true;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-bookmark': BookmarkBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { toGfxBlockComponent } from '@blocksuite/block-std';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { BookmarkBlockComponent } from './bookmark-block.js';
|
||||
|
||||
export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
|
||||
BookmarkBlockComponent
|
||||
) {
|
||||
override selectedStyle$ = null;
|
||||
|
||||
override blockDraggable = false;
|
||||
|
||||
override getRenderingRect() {
|
||||
const elementBound = this.model.elementBound;
|
||||
const style = this.model.style$.value;
|
||||
|
||||
return {
|
||||
x: elementBound.x,
|
||||
y: elementBound.y,
|
||||
w: EMBED_CARD_WIDTH[style],
|
||||
h: EMBED_CARD_HEIGHT[style],
|
||||
zIndex: this.toZIndex(),
|
||||
};
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const style = this.model.style$.value;
|
||||
const width = EMBED_CARD_WIDTH[style];
|
||||
const height = EMBED_CARD_HEIGHT[style];
|
||||
const bound = this.model.elementBound;
|
||||
const scaleX = bound.w / width;
|
||||
const scaleY = bound.h / height;
|
||||
|
||||
this.containerStyleMap = styleMap({
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
transform: `scale(${scaleX}, ${scaleY})`,
|
||||
transformOrigin: '0 0',
|
||||
});
|
||||
|
||||
return this.renderPageContent();
|
||||
}
|
||||
|
||||
protected override accessor blockContainerStyles: StyleInfo = {
|
||||
height: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
BlockFlavourIdentifier,
|
||||
BlockViewExtension,
|
||||
FlavourExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { BookmarkBlockAdapterExtensions } from './adapters/extension';
|
||||
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
|
||||
import { builtinToolbarConfig } from './configs/toolbar';
|
||||
|
||||
const flavour = BookmarkBlockSchema.model.flavour;
|
||||
|
||||
export const BookmarkBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension(flavour),
|
||||
BlockViewExtension(flavour, model => {
|
||||
return model.parent?.flavour === 'affine:surface'
|
||||
? literal`affine-edgeless-bookmark`
|
||||
: literal`affine-bookmark`;
|
||||
}),
|
||||
BookmarkBlockAdapterExtensions,
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
BookmarkSlashMenuConfigExtension,
|
||||
].flat();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { insertBookmarkCommand } from './insert-bookmark.js';
|
||||
export { insertLinkByQuickSearchCommand } from './insert-link-by-quick-search.js';
|
||||
@@ -0,0 +1,21 @@
|
||||
import '@blocksuite/affine-block-embed/effects';
|
||||
|
||||
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
|
||||
import type { EmbedCardStyle } from '@blocksuite/affine-model';
|
||||
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const insertBookmarkCommand: Command<{ url: string }> = (ctx, next) => {
|
||||
const { url, std } = ctx;
|
||||
const embedOptions = std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
|
||||
|
||||
let flavour = 'affine:bookmark';
|
||||
let targetStyle: EmbedCardStyle = 'vertical';
|
||||
const props: Record<string, unknown> = { url };
|
||||
if (embedOptions) {
|
||||
flavour = embedOptions.flavour;
|
||||
targetStyle = embedOptions.styles[0];
|
||||
}
|
||||
insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
type InsertedLinkType,
|
||||
insertEmbedLinkedDocCommand,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { QuickSearchProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
import { insertBookmarkCommand } from './insert-bookmark';
|
||||
|
||||
export const insertLinkByQuickSearchCommand: Command<
|
||||
{},
|
||||
{ insertedLinkType: Promise<InsertedLinkType> }
|
||||
> = (ctx, next) => {
|
||||
const { std } = ctx;
|
||||
const quickSearchService = std.getOptional(QuickSearchProvider);
|
||||
if (!quickSearchService) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const insertedLinkType: Promise<InsertedLinkType> = quickSearchService
|
||||
.openQuickSearch()
|
||||
.then(result => {
|
||||
if (!result) return null;
|
||||
|
||||
// add linked doc
|
||||
if ('docId' in result) {
|
||||
std.command.exec(insertEmbedLinkedDocCommand, {
|
||||
docId: result.docId,
|
||||
params: result.params,
|
||||
});
|
||||
return {
|
||||
flavour: 'affine:embed-linked-doc',
|
||||
};
|
||||
}
|
||||
|
||||
// add normal link;
|
||||
if ('externalUrl' in result) {
|
||||
std.command.exec(insertBookmarkCommand, { url: result.externalUrl });
|
||||
return {
|
||||
flavour: 'affine:bookmark',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
next({ insertedLinkType });
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
|
||||
import { WebIcon16 } from '@blocksuite/affine-components/icons';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSelection, ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { OpenInNewIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import type { BookmarkBlockComponent } from '../bookmark-block.js';
|
||||
import { styles } from '../styles.js';
|
||||
|
||||
export class BookmarkCard extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private _handleClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
const model = this.bookmark.model;
|
||||
|
||||
if (model.parent?.flavour !== 'affine:surface') {
|
||||
this._selectBlock();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDoubleClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
this.bookmark.open();
|
||||
}
|
||||
|
||||
private _selectBlock() {
|
||||
const selectionManager = this.bookmark.host.selection;
|
||||
const blockSelection = selectionManager.create(BlockSelection, {
|
||||
blockId: this.bookmark.blockId,
|
||||
});
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
this.bookmark.model.propsUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.bookmark.std
|
||||
.get(ThemeProvider)
|
||||
.theme$.subscribe(() => this.requestUpdate())
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { icon, title, url, description, image, style } = this.bookmark.model;
|
||||
|
||||
const cardClassMap = classMap({
|
||||
loading: this.loading,
|
||||
error: this.error,
|
||||
[style]: true,
|
||||
selected: this.bookmark.selected$.value,
|
||||
});
|
||||
|
||||
const domainName = url.match(
|
||||
/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n]+)/im
|
||||
)?.[1];
|
||||
|
||||
const titleText = this.loading
|
||||
? 'Loading...'
|
||||
: !title
|
||||
? this.error
|
||||
? (domainName ?? 'Link card')
|
||||
: ''
|
||||
: title;
|
||||
|
||||
const theme = this.bookmark.std.get(ThemeProvider).theme;
|
||||
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
|
||||
const titleIconType =
|
||||
!icon?.split('.').pop() || icon?.split('.').pop() === 'svg'
|
||||
? 'svg+xml'
|
||||
: icon?.split('.').pop();
|
||||
|
||||
const titleIcon = this.loading
|
||||
? LoadingIcon
|
||||
: icon
|
||||
? html`<object
|
||||
type="image/${titleIconType}"
|
||||
data=${icon}
|
||||
draggable="false"
|
||||
>
|
||||
${WebIcon16}
|
||||
</object>`
|
||||
: WebIcon16;
|
||||
|
||||
const descriptionText = this.loading
|
||||
? ''
|
||||
: !description
|
||||
? this.error
|
||||
? 'Failed to retrieve link information.'
|
||||
: url
|
||||
: (description ?? '');
|
||||
|
||||
const bannerImage =
|
||||
!this.loading && image
|
||||
? html`<object type="image/webp" data=${image} draggable="false">
|
||||
${EmbedCardBannerIcon}
|
||||
</object>`
|
||||
: EmbedCardBannerIcon;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="affine-bookmark-card ${cardClassMap}"
|
||||
@click=${this._handleClick}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
>
|
||||
<div class="affine-bookmark-content">
|
||||
<div class="affine-bookmark-content-title">
|
||||
<div class="affine-bookmark-content-title-icon">${titleIcon}</div>
|
||||
<div class="affine-bookmark-content-title-text">${titleText}</div>
|
||||
</div>
|
||||
<div class="affine-bookmark-content-description">
|
||||
${descriptionText}
|
||||
</div>
|
||||
<div class="affine-bookmark-content-url" @click=${this.bookmark.open}>
|
||||
<span>${getHostName(url)}</span>
|
||||
<div class="affine-bookmark-content-url-icon">
|
||||
${OpenInNewIcon({ width: '12', height: '12' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="affine-bookmark-banner">${bannerImage}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor bookmark!: BookmarkBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor error!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor loading!: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'bookmark-card': BookmarkCard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './bookmark-card';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type SlashMenuConfig,
|
||||
SlashMenuConfigIdentifier,
|
||||
} from '@blocksuite/affine-widget-slash-menu';
|
||||
import { LinkIcon } from '@blocksuite/icons/lit';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { LinkTooltip } from './tooltips';
|
||||
|
||||
const bookmarkSlashMenuConfig: SlashMenuConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Link',
|
||||
description: 'Add a bookmark for reference.',
|
||||
icon: LinkIcon(),
|
||||
tooltip: {
|
||||
figure: LinkTooltip,
|
||||
caption: 'Link',
|
||||
},
|
||||
group: '4_Content & Media@2',
|
||||
when: ({ model }) =>
|
||||
model.doc.schema.flavourSchemaMap.has('affine:bookmark'),
|
||||
action: ({ std, model }) => {
|
||||
const { host } = std;
|
||||
const parentModel = host.doc.getParent(model);
|
||||
if (!parentModel) {
|
||||
return;
|
||||
}
|
||||
const index = parentModel.children.indexOf(model) + 1;
|
||||
toggleEmbedCardCreateModal(
|
||||
host,
|
||||
'Links',
|
||||
'The added link will be displayed as a card view.',
|
||||
{ mode: 'page', parentModel, index }
|
||||
)
|
||||
.then(() => {
|
||||
if (model.text?.length === 0) {
|
||||
model.doc.deleteBlock(model);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const BookmarkSlashMenuConfigIdentifier = SlashMenuConfigIdentifier(
|
||||
BookmarkBlockSchema.model.flavour
|
||||
);
|
||||
|
||||
export const BookmarkSlashMenuConfigExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(BookmarkSlashMenuConfigIdentifier, bookmarkSlashMenuConfig);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { BookmarkBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
EmbedOptionProvider,
|
||||
type ToolbarAction,
|
||||
type ToolbarActionGroup,
|
||||
type ToolbarModuleConfig,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getBlockProps } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSelection } from '@blocksuite/block-std';
|
||||
import {
|
||||
CaptionIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { Slice, Text } from '@blocksuite/store';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { BookmarkBlockComponent } from '../bookmark-block';
|
||||
|
||||
const trackBaseProps = {
|
||||
segment: 'doc',
|
||||
page: 'doc editor',
|
||||
module: 'toolbar',
|
||||
category: 'bookmark',
|
||||
type: 'card view',
|
||||
};
|
||||
|
||||
export const builtinToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.preview',
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(
|
||||
BlockSelection,
|
||||
BookmarkBlockModel
|
||||
);
|
||||
if (!model) return null;
|
||||
|
||||
const { url } = model;
|
||||
|
||||
return html`<affine-link-preview .url=${url}></affine-link-preview>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.conversions',
|
||||
actions: [
|
||||
{
|
||||
id: 'inline',
|
||||
label: 'Inline view',
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(
|
||||
BlockSelection,
|
||||
BookmarkBlockModel
|
||||
);
|
||||
if (!model) return;
|
||||
|
||||
const { title, caption, url, parent } = model;
|
||||
const index = parent?.children.indexOf(model);
|
||||
|
||||
const yText = new Y.Text();
|
||||
const insert = title || caption || url;
|
||||
yText.insert(0, insert);
|
||||
yText.format(0, insert.length, { link: url });
|
||||
|
||||
const text = new Text(yText);
|
||||
|
||||
ctx.store.addBlock('affine:paragraph', { text }, parent, index);
|
||||
|
||||
ctx.store.deleteBlock(model);
|
||||
|
||||
// Clears
|
||||
ctx.reset();
|
||||
ctx.select('note');
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
control: 'select view',
|
||||
type: 'inline view',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'card',
|
||||
label: 'Card view',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'embed',
|
||||
label: 'Embed view',
|
||||
disabled(ctx) {
|
||||
const model = ctx.getCurrentModelByType(
|
||||
BlockSelection,
|
||||
BookmarkBlockModel
|
||||
);
|
||||
if (!model) return true;
|
||||
|
||||
const options = ctx.std
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(model.url);
|
||||
|
||||
return options?.viewType !== 'embed';
|
||||
},
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(
|
||||
BlockSelection,
|
||||
BookmarkBlockModel
|
||||
);
|
||||
if (!model) return;
|
||||
|
||||
const { caption, url, style, parent } = model;
|
||||
const index = parent?.children.indexOf(model);
|
||||
|
||||
const options = ctx.std
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
if (!options) return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
|
||||
const newStyle = styles.includes(style)
|
||||
? style
|
||||
: styles.find(s => s !== 'vertical' && s !== 'cube');
|
||||
|
||||
const blockId = ctx.store.addBlock(
|
||||
flavour,
|
||||
{
|
||||
url,
|
||||
caption,
|
||||
style: newStyle,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
ctx.store.deleteBlock(model);
|
||||
|
||||
// Selects new block
|
||||
ctx.select('note', [
|
||||
ctx.selection.create(BlockSelection, { blockId }),
|
||||
]);
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
control: 'select view',
|
||||
type: 'embed view',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(
|
||||
BlockSelection,
|
||||
BookmarkBlockModel
|
||||
);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({ ...action }));
|
||||
const toggle = (e: CustomEvent<boolean>) => {
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track('OpenedViewSelector', {
|
||||
...trackBaseProps,
|
||||
control: 'switch view',
|
||||
});
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.toggle=${toggle}
|
||||
.viewType$=${signal(actions[1].label)}
|
||||
></affine-view-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
],
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(
|
||||
BlockSelection,
|
||||
BookmarkBlockModel
|
||||
);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({
|
||||
...action,
|
||||
run: ({ store }) => {
|
||||
store.updateBlock(model, { style: action.id });
|
||||
|
||||
ctx.track('SelectedCardStyle', {
|
||||
...trackBaseProps,
|
||||
control: 'select card style',
|
||||
type: action.id,
|
||||
});
|
||||
},
|
||||
})) satisfies ToolbarAction[];
|
||||
|
||||
const toggle = (e: CustomEvent<boolean>) => {
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track('OpenedCardStyleSelector', {
|
||||
...trackBaseProps,
|
||||
control: 'switch card style',
|
||||
});
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-card-style-dropdown-menu
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.toggle=${toggle}
|
||||
.style$=${model.style$}
|
||||
></affine-card-style-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'd.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
const component = ctx.getCurrentBlockComponentBy(
|
||||
BlockSelection,
|
||||
BookmarkBlockComponent
|
||||
);
|
||||
component?.captionEditor?.show();
|
||||
|
||||
ctx.track('OpenedCaptionEditor', {
|
||||
...trackBaseProps,
|
||||
control: 'add caption',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
actions: [
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon(),
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const slice = Slice.fromModels(ctx.store, [model]);
|
||||
ctx.clipboard
|
||||
.copySlice(slice)
|
||||
.then(() => toast(ctx.host, 'Copied to clipboard'))
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
icon: DuplicateIcon(),
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const { flavour, parent } = model;
|
||||
const props = getBlockProps(model);
|
||||
const index = parent?.children.indexOf(model);
|
||||
|
||||
ctx.store.addBlock(flavour, props, parent, index);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'b.refresh',
|
||||
label: 'Reload',
|
||||
icon: ResetIcon(),
|
||||
run(ctx) {
|
||||
const component = ctx.getCurrentBlockComponentBy(
|
||||
BlockSelection,
|
||||
BookmarkBlockComponent
|
||||
);
|
||||
component?.refreshData();
|
||||
},
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'c.delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon(),
|
||||
variant: 'destructive',
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentBlockBy(BlockSelection)?.model;
|
||||
if (!model) return;
|
||||
|
||||
ctx.store.deleteBlock(model);
|
||||
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const LinkTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_1007" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_1007)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan><tspan x="8" y="63.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="75.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="111.636">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="123.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="135.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="147.636">other users. </tspan><tspan x="8" y="183.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="195.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#1E67AF" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="11" letter-spacing="0em"><tspan x="8" y="45.5">Learn about AFFiNE</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BookmarkBlockComponent } from './bookmark-block';
|
||||
import { BookmarkEdgelessBlockComponent } from './bookmark-edgeless-block';
|
||||
import { BookmarkCard } from './components/bookmark-card';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(
|
||||
'affine-edgeless-bookmark',
|
||||
BookmarkEdgelessBlockComponent
|
||||
);
|
||||
customElements.define('affine-bookmark', BookmarkBlockComponent);
|
||||
customElements.define('bookmark-card', BookmarkCard);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './adapters';
|
||||
export * from './bookmark-block';
|
||||
export * from './bookmark-spec';
|
||||
export * from './commands';
|
||||
export * from './components';
|
||||
export { BookmarkSlashMenuConfigIdentifier } from './configs/slash-menu';
|
||||
@@ -0,0 +1,283 @@
|
||||
import { unsafeCSSVar } from '@blocksuite/affine-shared/theme';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, unsafeCSS } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
bookmark-card {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affine-bookmark-card {
|
||||
container: affine-bookmark-card / inline-size;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--affine-background-tertiary-color);
|
||||
|
||||
opacity: var(--add, 1);
|
||||
background: var(--affine-background-primary-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.affine-bookmark-content {
|
||||
width: calc(100% - 204px);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
border-radius: var(--1, 0px);
|
||||
opacity: var(--add, 1);
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
align-self: stretch;
|
||||
padding: var(--1, 0px);
|
||||
border-radius: var(--1, 0px);
|
||||
opacity: var(--add, 1);
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title-icon {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title-icon img,
|
||||
.affine-bookmark-content-title-icon object,
|
||||
.affine-bookmark-content-title-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--affine-background-primary-color);
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title-text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-text-primary-color);
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-sm);
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-text-primary-color);
|
||||
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-url {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-url > span {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--affine-text-secondary-color);
|
||||
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
.affine-bookmark-content-url:hover > span {
|
||||
color: var(--affine-link-color);
|
||||
}
|
||||
.affine-bookmark-content-url:hover {
|
||||
fill: var(--affine-link-color);
|
||||
}
|
||||
|
||||
.affine-bookmark-content-url-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 20px;
|
||||
}
|
||||
.affine-bookmark-content-url-icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
color: ${unsafeCSSVar('iconSecondary')};
|
||||
}
|
||||
|
||||
.affine-bookmark-banner {
|
||||
margin: 12px 12px 0px 0px;
|
||||
width: 204px;
|
||||
max-width: 100%;
|
||||
height: 102px;
|
||||
opacity: var(--add, 1);
|
||||
}
|
||||
|
||||
.affine-bookmark-banner img,
|
||||
.affine-bookmark-banner object,
|
||||
.affine-bookmark-banner svg {
|
||||
width: 204px;
|
||||
max-width: 100%;
|
||||
height: 102px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px 4px var(--1, 0px) var(--1, 0px);
|
||||
}
|
||||
|
||||
.affine-bookmark-card.loading {
|
||||
.affine-bookmark-content-title-text {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
}
|
||||
|
||||
.affine-bookmark-card.error {
|
||||
.affine-bookmark-content-description {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
}
|
||||
|
||||
.affine-bookmark-card.selected {
|
||||
.affine-bookmark-content-url > span {
|
||||
color: var(--affine-link-color);
|
||||
}
|
||||
.affine-bookmark-content-url .affine-bookmark-content-url-icon {
|
||||
color: var(--affine-link-color);
|
||||
}
|
||||
}
|
||||
|
||||
.affine-bookmark-card.list {
|
||||
.affine-bookmark-content {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title {
|
||||
width: calc(100% - 204px);
|
||||
}
|
||||
|
||||
.affine-bookmark-content-url {
|
||||
width: 204px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.affine-bookmark-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-bookmark-card.vertical {
|
||||
flex-direction: column-reverse;
|
||||
height: 100%;
|
||||
|
||||
.affine-bookmark-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-description {
|
||||
-webkit-line-clamp: 6;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-url {
|
||||
flex-grow: 1;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.affine-bookmark-banner {
|
||||
width: 340px;
|
||||
height: 170px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.affine-bookmark-banner img,
|
||||
.affine-bookmark-banner object,
|
||||
.affine-bookmark-banner svg {
|
||||
width: 340px;
|
||||
height: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-bookmark-card.cube {
|
||||
.affine-bookmark-content {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-title-text {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.affine-bookmark-content-description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.affine-bookmark-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container affine-bookmark-card (width < 375px) {
|
||||
.affine-bookmark-content {
|
||||
width: 100%;
|
||||
}
|
||||
.affine-bookmark-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { LinkPreviewerService } from '@blocksuite/affine-shared/services';
|
||||
import { isAbortError } from '@blocksuite/affine-shared/utils';
|
||||
|
||||
import type { BookmarkBlockComponent } from './bookmark-block.js';
|
||||
|
||||
export async function refreshBookmarkUrlData(
|
||||
bookmarkElement: BookmarkBlockComponent,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
let title = null,
|
||||
description = null,
|
||||
icon = null,
|
||||
image = null;
|
||||
|
||||
try {
|
||||
bookmarkElement.loading = true;
|
||||
|
||||
const linkPreviewer = bookmarkElement.doc.get(LinkPreviewerService);
|
||||
const bookmarkUrlData = await linkPreviewer.query(
|
||||
bookmarkElement.model.url,
|
||||
signal
|
||||
);
|
||||
|
||||
title = bookmarkUrlData.title ?? null;
|
||||
description = bookmarkUrlData.description ?? null;
|
||||
icon = bookmarkUrlData.icon ?? null;
|
||||
image = bookmarkUrlData.image ?? null;
|
||||
|
||||
if (!title && !description && !icon && !image) {
|
||||
bookmarkElement.error = true;
|
||||
}
|
||||
|
||||
if (signal?.aborted) return;
|
||||
|
||||
bookmarkElement.doc.updateBlock(bookmarkElement.model, {
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
image,
|
||||
});
|
||||
} catch (error) {
|
||||
if (signal?.aborted || isAbortError(error)) return;
|
||||
throw error;
|
||||
} finally {
|
||||
bookmarkElement.loading = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user