mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 00:07:01 +08:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
13
blocksuite/affine/blocks/bookmark/src/adapters/extension.ts
Normal file
13
blocksuite/affine/blocks/bookmark/src/adapters/extension.ts
Normal file
@@ -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,
|
||||
];
|
||||
10
blocksuite/affine/blocks/bookmark/src/adapters/html.ts
Normal file
10
blocksuite/affine/blocks/bookmark/src/adapters/html.ts
Normal file
@@ -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
|
||||
);
|
||||
4
blocksuite/affine/blocks/bookmark/src/adapters/index.ts
Normal file
4
blocksuite/affine/blocks/bookmark/src/adapters/index.ts
Normal file
@@ -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);
|
||||
121
blocksuite/affine/blocks/bookmark/src/bookmark-block.ts
Normal file
121
blocksuite/affine/blocks/bookmark/src/bookmark-block.ts
Normal file
@@ -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.props.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.props.description && !this.model.props.title) {
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.subscribe(({ 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/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.props.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.props.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;
|
||||
}
|
||||
}
|
||||
22
blocksuite/affine/blocks/bookmark/src/bookmark-spec.ts
Normal file
22
blocksuite/affine/blocks/bookmark/src/bookmark-spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/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 { createBuiltinToolbarConfigExtension } 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,
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
BookmarkSlashMenuConfigExtension,
|
||||
].flat();
|
||||
2
blocksuite/affine/blocks/bookmark/src/commands/index.ts
Normal file
2
blocksuite/affine/blocks/bookmark/src/commands/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { insertBookmarkCommand } from './insert-bookmark.js';
|
||||
export { insertLinkByQuickSearchCommand } from './insert-link-by-quick-search.js';
|
||||
@@ -0,0 +1,25 @@
|
||||
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/std';
|
||||
|
||||
export const insertBookmarkCommand: Command<
|
||||
{ url: string },
|
||||
{ blockId: string; flavour: 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];
|
||||
}
|
||||
const blockId = insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
if (!blockId) return;
|
||||
next({ blockId, flavour });
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
type InsertedLinkType,
|
||||
insertEmbedIframeWithUrlCommand,
|
||||
insertEmbedLinkedDocCommand,
|
||||
type LinkableFlavour,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { QuickSearchProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { Command } from '@blocksuite/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) {
|
||||
// try to insert embed iframe block first
|
||||
const [success, { flavour }] = std.command
|
||||
.chain()
|
||||
.try(chain => [
|
||||
chain.pipe(insertEmbedIframeWithUrlCommand, {
|
||||
url: result.externalUrl,
|
||||
}),
|
||||
chain.pipe(insertBookmarkCommand, { url: result.externalUrl }),
|
||||
])
|
||||
.run();
|
||||
if (!success || !flavour) return null;
|
||||
return {
|
||||
flavour: flavour as LinkableFlavour,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
next({ insertedLinkType });
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
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 { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { OpenInNewIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection, ShadowlessElement } from '@blocksuite/std';
|
||||
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.subscribe(() => {
|
||||
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.props;
|
||||
|
||||
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-wrapper"
|
||||
@click=${this.bookmark.open}
|
||||
>
|
||||
<div class="affine-bookmark-content-url">
|
||||
<span>${getHostName(url)}</span>
|
||||
<div class="affine-bookmark-content-url-icon">
|
||||
${OpenInNewIcon({ width: '12', height: '12' })}
|
||||
</div>
|
||||
</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';
|
||||
57
blocksuite/affine/blocks/bookmark/src/configs/slash-menu.ts
Normal file
57
blocksuite/affine/blocks/bookmark/src/configs/slash-menu.ts
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
621
blocksuite/affine/blocks/bookmark/src/configs/toolbar.ts
Normal file
621
blocksuite/affine/blocks/bookmark/src/configs/toolbar.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
import {
|
||||
canEmbedAsIframe,
|
||||
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
|
||||
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { reassociateConnectorsCommand } from '@blocksuite/affine-block-surface';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
BookmarkBlockModel,
|
||||
BookmarkStyles,
|
||||
type EmbedCardStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
ActionPlacement,
|
||||
EmbedIframeService,
|
||||
EmbedOptionProvider,
|
||||
type LinkEventType,
|
||||
type ToolbarAction,
|
||||
type ToolbarActionGroup,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getBlockProps } from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
CaptionIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier, BlockSelection } from '@blocksuite/std';
|
||||
import { type ExtensionType, Slice, Text } from '@blocksuite/store';
|
||||
import { computed, 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 = {
|
||||
category: 'bookmark',
|
||||
type: 'card view',
|
||||
};
|
||||
|
||||
const previewAction = {
|
||||
id: 'a.preview',
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const { url } = model.props;
|
||||
|
||||
return html`<affine-link-preview .url=${url}></affine-link-preview>`;
|
||||
},
|
||||
} satisfies ToolbarAction;
|
||||
|
||||
const captionAction = {
|
||||
id: 'd.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(BookmarkBlockComponent);
|
||||
block?.captionEditor?.show();
|
||||
|
||||
ctx.track('OpenedCaptionEditor', {
|
||||
...trackBaseProps,
|
||||
control: 'add caption',
|
||||
});
|
||||
},
|
||||
} satisfies ToolbarAction;
|
||||
|
||||
const createOnToggleFn =
|
||||
(
|
||||
ctx: ToolbarContext,
|
||||
name: Extract<
|
||||
LinkEventType,
|
||||
| 'OpenedViewSelector'
|
||||
| 'OpenedCardStyleSelector'
|
||||
| 'OpenedCardScaleSelector'
|
||||
>,
|
||||
control: string
|
||||
) =>
|
||||
(e: CustomEvent<boolean>) => {
|
||||
e.stopPropagation();
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track(name, {
|
||||
...trackBaseProps,
|
||||
control,
|
||||
});
|
||||
};
|
||||
|
||||
const builtinToolbarConfig = {
|
||||
actions: [
|
||||
previewAction,
|
||||
{
|
||||
id: 'b.conversions',
|
||||
actions: [
|
||||
{
|
||||
id: 'inline',
|
||||
label: 'Inline view',
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return;
|
||||
|
||||
const { title, caption, url } = model.props;
|
||||
const { 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(BookmarkBlockModel);
|
||||
if (!model) return true;
|
||||
|
||||
const url = model.props.url;
|
||||
// check if the url can be embedded as iframe block or other embed blocks
|
||||
const options = ctx.std
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
return (
|
||||
!canEmbedAsIframe(ctx.std, url) && options?.viewType !== 'embed'
|
||||
);
|
||||
},
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return;
|
||||
|
||||
const { caption, url, title, description, style } = model.props;
|
||||
const { parent } = model;
|
||||
const index = parent?.children.indexOf(model);
|
||||
if (!parent) return;
|
||||
|
||||
let blockId: string | undefined;
|
||||
|
||||
// first try to embed as iframe block
|
||||
if (canEmbedAsIframe(ctx.std, url)) {
|
||||
const embedIframeService = ctx.std.get(EmbedIframeService);
|
||||
blockId = embedIframeService.addEmbedIframeBlock(
|
||||
{ url, caption, title, description },
|
||||
parent.id,
|
||||
index
|
||||
);
|
||||
} else {
|
||||
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');
|
||||
|
||||
blockId = ctx.store.addBlock(
|
||||
flavour,
|
||||
{
|
||||
url,
|
||||
caption,
|
||||
title,
|
||||
description,
|
||||
style: newStyle,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
if (!blockId) return;
|
||||
|
||||
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(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({ ...action }));
|
||||
const viewType$ = signal(actions[1].label);
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedViewSelector',
|
||||
'switch view'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></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(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 onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardStyleSelector',
|
||||
'switch card style'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-card-style-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.style$=${model.props.style$}
|
||||
></affine-card-style-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
captionAction,
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
actions: [
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon(),
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
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.getCurrentModelByType(BookmarkBlockModel);
|
||||
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 block = ctx.getCurrentBlockByType(BookmarkBlockComponent);
|
||||
block?.refreshData();
|
||||
},
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'c.delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon(),
|
||||
variant: 'destructive',
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return;
|
||||
|
||||
ctx.store.deleteBlock(model);
|
||||
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
const builtinSurfaceToolbarConfig = {
|
||||
actions: [
|
||||
previewAction,
|
||||
{
|
||||
id: 'b.conversions',
|
||||
actions: [
|
||||
{
|
||||
id: 'card',
|
||||
label: 'Card view',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'embed',
|
||||
label: 'Embed view',
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return;
|
||||
|
||||
const { id: oldId, xywh, parent } = model;
|
||||
const { url, caption, title, description } = model.props;
|
||||
|
||||
let newId: string | undefined;
|
||||
|
||||
// first try to embed as iframe block
|
||||
if (canEmbedAsIframe(ctx.std, url)) {
|
||||
const embedIframeService = ctx.std.get(EmbedIframeService);
|
||||
const config = embedIframeService.getConfig(url);
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const options = config.options;
|
||||
const { widthInSurface, heightInSurface } = options ?? {};
|
||||
bound.w = widthInSurface ?? EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE;
|
||||
bound.h =
|
||||
heightInSurface ?? EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE;
|
||||
|
||||
newId = ctx.store.addBlock(
|
||||
'affine:embed-iframe',
|
||||
{ url, caption, title, description, xywh: bound.serialize() },
|
||||
parent
|
||||
);
|
||||
} else {
|
||||
const options = ctx.std
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style = styles[0];
|
||||
}
|
||||
|
||||
const bound = Bound.deserialize(xywh);
|
||||
bound.w = EMBED_CARD_WIDTH[style];
|
||||
bound.h = EMBED_CARD_HEIGHT[style];
|
||||
|
||||
newId = ctx.store.addBlock(
|
||||
flavour,
|
||||
{
|
||||
url,
|
||||
caption,
|
||||
title,
|
||||
description,
|
||||
style,
|
||||
xywh: bound.serialize(),
|
||||
},
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
||||
ctx.command.exec(reassociateConnectorsCommand, { oldId, newId });
|
||||
|
||||
ctx.store.deleteBlock(model);
|
||||
|
||||
// Selects new block
|
||||
ctx.gfx.selection.set({ editing: false, elements: [newId] });
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
control: 'select view',
|
||||
type: 'embed view',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
when(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return false;
|
||||
|
||||
const { url } = model.props;
|
||||
const options = ctx.std
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
return canEmbedAsIframe(ctx.std, url) || options?.viewType === 'embed';
|
||||
},
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({ ...action }));
|
||||
const viewType$ = signal('Card view');
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedViewSelector',
|
||||
'switch view'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'b.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({
|
||||
...action,
|
||||
run: ({ store }) => {
|
||||
const style = action.id as EmbedCardStyle;
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
bounds.w = EMBED_CARD_WIDTH[style];
|
||||
bounds.h = EMBED_CARD_HEIGHT[style];
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
store.updateBlock(model, { style, xywh });
|
||||
|
||||
ctx.track('SelectedCardStyle', {
|
||||
...trackBaseProps,
|
||||
control: 'select card style',
|
||||
type: style,
|
||||
});
|
||||
},
|
||||
})) satisfies ToolbarAction[];
|
||||
const style$ = model.props.style$;
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardStyleSelector',
|
||||
'switch card style'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-card-style-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.style$=${style$}
|
||||
></affine-card-style-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
...captionAction,
|
||||
id: 'c.caption',
|
||||
},
|
||||
{
|
||||
id: 'd.scale',
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const scale$ = computed(() => {
|
||||
const {
|
||||
xywh$: { value: xywh },
|
||||
} = model;
|
||||
const {
|
||||
style$: { value: style },
|
||||
} = model.props;
|
||||
const bounds = Bound.deserialize(xywh);
|
||||
const height = EMBED_CARD_HEIGHT[style];
|
||||
return Math.round(100 * (bounds.h / height));
|
||||
});
|
||||
const onSelect = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const scale = e.detail / 100;
|
||||
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
const style = model.props.style;
|
||||
bounds.w = EMBED_CARD_WIDTH[style] * scale;
|
||||
bounds.h = EMBED_CARD_HEIGHT[style] * scale;
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
ctx.store.updateBlock(model, { xywh });
|
||||
|
||||
ctx.track('SelectedCardScale', {
|
||||
...trackBaseProps,
|
||||
control: 'select card scale',
|
||||
});
|
||||
};
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardScaleSelector',
|
||||
'switch card scale'
|
||||
);
|
||||
const format = (value: number) => `${value}%`;
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
@toggle=${onToggle}
|
||||
.format=${format}
|
||||
.size$=${scale$}
|
||||
></affine-size-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(BookmarkBlockModel).length === 1,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
export const createBuiltinToolbarConfigExtension = (
|
||||
flavour: string
|
||||
): ExtensionType[] => {
|
||||
const name = flavour.split(':').pop();
|
||||
|
||||
return [
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(`affine:surface:${name}`),
|
||||
config: builtinSurfaceToolbarConfig,
|
||||
}),
|
||||
];
|
||||
};
|
||||
13
blocksuite/affine/blocks/bookmark/src/configs/tooltips.ts
Normal file
13
blocksuite/affine/blocks/bookmark/src/configs/tooltips.ts
Normal file
@@ -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,29 @@
|
||||
import { EdgelessClipboardConfig } from '@blocksuite/affine-block-surface';
|
||||
import { type BlockSnapshot } from '@blocksuite/store';
|
||||
|
||||
export class EdgelessClipboardBookmarkConfig extends EdgelessClipboardConfig {
|
||||
static override readonly key = 'affine:bookmark';
|
||||
|
||||
override createBlock(bookmark: BlockSnapshot): string | null {
|
||||
if (!this.surface) return null;
|
||||
|
||||
const { xywh, style, url, caption, description, icon, image, title } =
|
||||
bookmark.props;
|
||||
|
||||
const bookmarkId = this.crud.addBlock(
|
||||
'affine:bookmark',
|
||||
{
|
||||
xywh,
|
||||
style,
|
||||
url,
|
||||
caption,
|
||||
description,
|
||||
icon,
|
||||
image,
|
||||
title,
|
||||
},
|
||||
this.surface.model.id
|
||||
);
|
||||
return bookmarkId;
|
||||
}
|
||||
}
|
||||
12
blocksuite/affine/blocks/bookmark/src/effects.ts
Normal file
12
blocksuite/affine/blocks/bookmark/src/effects.ts
Normal file
@@ -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);
|
||||
}
|
||||
7
blocksuite/affine/blocks/bookmark/src/index.ts
Normal file
7
blocksuite/affine/blocks/bookmark/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './adapters';
|
||||
export * from './bookmark-block';
|
||||
export * from './bookmark-spec';
|
||||
export * from './commands';
|
||||
export * from './components';
|
||||
export { BookmarkSlashMenuConfigIdentifier } from './configs/slash-menu';
|
||||
export * from './edgeless-clipboard-config';
|
||||
285
blocksuite/affine/blocks/bookmark/src/styles.ts
Normal file
285
blocksuite/affine/blocks/bookmark/src/styles.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
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%;
|
||||
}
|
||||
|
||||
.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-wrapper {
|
||||
max-width: fit-content;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
47
blocksuite/affine/blocks/bookmark/src/utils.ts
Normal file
47
blocksuite/affine/blocks/bookmark/src/utils.ts
Normal file
@@ -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.props.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