chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,66 @@
import type { BlockHtmlAdapterMatcher } from '@blocksuite/affine-shared/adapters';
export function createEmbedBlockHtmlAdapterMatcher(
flavour: string,
{
toMatch = () => false,
fromMatch = o => o.node.flavour === flavour,
toBlockSnapshot = {},
fromBlockSnapshot = {
enter: (o, context) => {
const { walkerContext } = context;
// Parse as link
if (
typeof o.node.props.title !== 'string' ||
typeof o.node.props.url !== 'string'
) {
return;
}
walkerContext
.openNode(
{
type: 'element',
tagName: 'div',
properties: {
className: ['affine-paragraph-block-container'],
},
children: [],
},
'children'
)
.openNode(
{
type: 'element',
tagName: 'a',
properties: {
href: o.node.props.url,
},
children: [
{
type: 'text',
value: o.node.props.title,
},
],
},
'children'
)
.closeNode()
.closeNode();
},
},
}: {
toMatch?: BlockHtmlAdapterMatcher['toMatch'];
fromMatch?: BlockHtmlAdapterMatcher['fromMatch'];
toBlockSnapshot?: BlockHtmlAdapterMatcher['toBlockSnapshot'];
fromBlockSnapshot?: BlockHtmlAdapterMatcher['fromBlockSnapshot'];
} = Object.create(null)
): BlockHtmlAdapterMatcher {
return {
flavour,
toMatch,
fromMatch,
toBlockSnapshot,
fromBlockSnapshot,
};
}
@@ -0,0 +1,58 @@
import type { BlockMarkdownAdapterMatcher } from '@blocksuite/affine-shared/adapters';
export function createEmbedBlockMarkdownAdapterMatcher(
flavour: string,
{
toMatch = () => false,
fromMatch = o => o.node.flavour === flavour,
toBlockSnapshot = {},
fromBlockSnapshot = {
enter: (o, context) => {
const { walkerContext } = context;
// Parse as link
if (
typeof o.node.props.title !== 'string' ||
typeof o.node.props.url !== 'string'
) {
return;
}
walkerContext
.openNode(
{
type: 'paragraph',
children: [],
},
'children'
)
.openNode(
{
type: 'link',
url: o.node.props.url,
children: [
{
type: 'text',
value: o.node.props.title,
},
],
},
'children'
)
.closeNode()
.closeNode();
},
},
}: {
toMatch?: BlockMarkdownAdapterMatcher['toMatch'];
fromMatch?: BlockMarkdownAdapterMatcher['fromMatch'];
toBlockSnapshot?: BlockMarkdownAdapterMatcher['toBlockSnapshot'];
fromBlockSnapshot?: BlockMarkdownAdapterMatcher['fromBlockSnapshot'];
} = {}
): BlockMarkdownAdapterMatcher {
return {
flavour,
toMatch,
fromMatch,
toBlockSnapshot,
fromBlockSnapshot,
};
}
@@ -0,0 +1,40 @@
import type { BlockPlainTextAdapterMatcher } from '@blocksuite/affine-shared/adapters';
export function createEmbedBlockPlainTextAdapterMatcher(
flavour: string,
{
toMatch = () => false,
fromMatch = o => o.node.flavour === flavour,
toBlockSnapshot = {},
fromBlockSnapshot = {
enter: (o, context) => {
const { textBuffer } = context;
// Parse as link
if (
typeof o.node.props.title !== 'string' ||
typeof o.node.props.url !== 'string'
) {
return;
}
const buffer = `[${o.node.props.title}](${o.node.props.url})`;
if (buffer.length > 0) {
textBuffer.content += buffer;
textBuffer.content += '\n';
}
},
},
}: {
toMatch?: BlockPlainTextAdapterMatcher['toMatch'];
fromMatch?: BlockPlainTextAdapterMatcher['fromMatch'];
toBlockSnapshot?: BlockPlainTextAdapterMatcher['toBlockSnapshot'];
fromBlockSnapshot?: BlockPlainTextAdapterMatcher['fromBlockSnapshot'];
} = {}
): BlockPlainTextAdapterMatcher {
return {
flavour,
toMatch,
fromMatch,
toBlockSnapshot,
fromBlockSnapshot,
};
}
@@ -0,0 +1,13 @@
import type { ReferenceParams } from '@blocksuite/affine-model';
import { TextUtils } from '@blocksuite/affine-shared/adapters';
export function generateDocUrl(
docBaseUrl: string,
pageId: string,
params: ReferenceParams
) {
const search = TextUtils.toURLSearchParams(params);
const query = search?.size ? `?${search.toString()}` : '';
const url = docBaseUrl ? `${docBaseUrl}/${pageId}${query}` : '';
return url;
}
@@ -0,0 +1,172 @@
import {
CaptionedBlockComponent,
SelectedStyle,
} from '@blocksuite/affine-components/caption';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_MIN_WIDTH,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
DragHandleConfigExtension,
} from '@blocksuite/affine-shared/services';
import {
captureEventTarget,
convertDragPreviewDocToEdgeless,
convertDragPreviewEdgelessToDoc,
} from '@blocksuite/affine-shared/utils';
import { type BlockService, isGfxBlockComponent } from '@blocksuite/block-std';
import type { GfxCompatibleProps } from '@blocksuite/block-std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
export const EmbedDragHandleOption = DragHandleConfigExtension({
flavour: /affine:embed-*/,
edgeless: true,
onDragEnd: props => {
const { state, draggingElements } = props;
if (
draggingElements.length !== 1 ||
draggingElements[0].model.flavour.match(/affine:embed-*/) === null
)
return false;
const blockComponent = draggingElements[0] as EmbedBlockComponent;
const isInSurface = isGfxBlockComponent(blockComponent);
const target = captureEventTarget(state.raw.target);
const isTargetEdgelessContainer =
target?.classList.contains('edgeless-container');
if (isInSurface) {
const style = blockComponent._cardStyle;
const targetStyle =
style === 'vertical' || style === 'cube' ? 'horizontal' : style;
return convertDragPreviewEdgelessToDoc({
blockComponent,
style: targetStyle,
...props,
});
} else if (isTargetEdgelessContainer) {
const style = blockComponent._cardStyle;
return convertDragPreviewDocToEdgeless({
blockComponent,
cssSelector: '.embed-block-container',
width: EMBED_CARD_WIDTH[style],
height: EMBED_CARD_HEIGHT[style],
...props,
});
}
return false;
},
});
export class EmbedBlockComponent<
Model extends BlockModel<GfxCompatibleProps> = BlockModel<GfxCompatibleProps>,
Service extends BlockService = BlockService,
WidgetName extends string = string,
> extends CaptionedBlockComponent<Model, Service, WidgetName> {
private _fetchAbortController = new AbortController();
_cardStyle: EmbedCardStyle = 'horizontal';
/**
* The actual rendered scale of the embed card.
* By default, it is set to 1.
*/
protected _scale = 1;
blockDraggable = true;
/**
* The style of the embed card.
* You can use this to change the height and width of the card.
* By default, the height and width are set to `_cardHeight` and `_cardWidth` respectively.
*/
protected embedContainerStyle: StyleInfo = {};
renderEmbed = (content: () => TemplateResult) => {
if (
this._cardStyle === 'horizontal' ||
this._cardStyle === 'horizontalThin' ||
this._cardStyle === 'list'
) {
this.style.display = 'block';
const mode = this.std.get(DocModeProvider).getEditorMode();
if (mode === 'edgeless') {
this.style.minWidth = `${EMBED_CARD_MIN_WIDTH}px`;
}
}
const selected = !!this.selected?.is('block');
return html`
<div
draggable="${this.blockDraggable ? 'true' : 'false'}"
class=${classMap({
'embed-block-container': true,
'selected-style': selected,
})}
style=${styleMap({
height: `${this._cardHeight}px`,
width: '100%',
...this.embedContainerStyle,
})}
>
${content()}
</div>
`;
};
/**
* The height of the current embed card. Changes based on the card style.
*/
get _cardHeight() {
return EMBED_CARD_HEIGHT[this._cardStyle];
}
/**
* The width of the current embed card. Changes based on the card style.
*/
get _cardWidth() {
return EMBED_CARD_WIDTH[this._cardStyle];
}
get fetchAbortController() {
return this._fetchAbortController;
}
override connectedCallback() {
super.connectedCallback();
if (this._fetchAbortController.signal.aborted)
this._fetchAbortController = new AbortController();
this.contentEditable = 'false';
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this._fetchAbortController.abort();
}
protected override accessor blockContainerStyles: StyleInfo | undefined = {
margin: '18px 0',
};
@query('.embed-block-container')
protected accessor embedBlock!: HTMLDivElement;
override accessor selectedStyle = SelectedStyle.Border;
override accessor useCaptionEditor = true;
override accessor useZeroWidth = true;
}
@@ -0,0 +1,80 @@
import { css } from 'lit';
export const embedNoteContentStyles = css`
.affine-embed-doc-content-note-blocks affine-divider,
.affine-embed-doc-content-note-blocks affine-divider > * {
margin-top: 0px !important;
margin-bottom: 0px !important;
padding-top: 8px;
padding-bottom: 8px;
}
.affine-embed-doc-content-note-blocks affine-paragraph,
.affine-embed-doc-content-note-blocks affine-list {
margin-top: 4px !important;
margin-bottom: 4px !important;
padding: 0 2px;
}
.affine-embed-doc-content-note-blocks affine-paragraph *,
.affine-embed-doc-content-note-blocks affine-list * {
margin-top: 0px !important;
margin-bottom: 0px !important;
padding-top: 0;
padding-bottom: 0;
line-height: 20px;
font-size: var(--affine-font-xs);
font-weight: 400;
}
.affine-embed-doc-content-note-blocks affine-list .affine-list-block__prefix {
height: 20px;
}
.affine-embed-doc-content-note-blocks affine-paragraph .quote {
padding-left: 15px;
padding-top: 8px;
padding-bottom: 8px;
}
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h1),
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h2),
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h3),
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h4),
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h5),
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h6) {
margin-top: 6px !important;
margin-bottom: 4px !important;
padding: 0 2px;
}
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h1) *,
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h2) *,
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h3) *,
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h4) *,
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h5) *,
.affine-embed-doc-content-note-blocks affine-paragraph:has(.h6) * {
margin-top: 0px !important;
margin-bottom: 0px !important;
padding-top: 0;
padding-bottom: 0;
line-height: 20px;
font-size: var(--affine-font-xs);
font-weight: 600;
}
.affine-embed-linked-doc-block.horizontal {
affine-paragraph,
affine-list {
margin-top: 0 !important;
margin-bottom: 0 !important;
max-height: 40px;
overflow: hidden;
display: flex;
}
affine-paragraph .quote {
padding-top: 4px;
padding-bottom: 4px;
height: 28px;
}
affine-paragraph .quote::after {
height: 20px;
margin-top: 4px !important;
margin-bottom: 4px !important;
}
}
`;
@@ -0,0 +1,81 @@
import { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import type { BlockStdScope } from '@blocksuite/block-std';
import { Bound, Vec } from '@blocksuite/global/utils';
interface EmbedCardProperties {
flavour: string;
targetStyle: EmbedCardStyle;
props: Record<string, unknown>;
}
export function insertEmbedCard(
std: BlockStdScope,
properties: EmbedCardProperties
) {
const { host } = std;
const { flavour, targetStyle, props } = properties;
const selectionManager = host.selection;
let blockId: string | undefined;
const textSelection = selectionManager.find('text');
const blockSelection = selectionManager.find('block');
const surfaceSelection = selectionManager.find('surface');
if (textSelection) {
blockId = textSelection.blockId;
} else if (blockSelection) {
blockId = blockSelection.blockId;
} else if (surfaceSelection && surfaceSelection.editing) {
blockId = surfaceSelection.blockId;
}
if (blockId) {
const block = host.view.getBlock(blockId);
if (!block) return;
const parent = host.doc.getParent(block.model);
if (!parent) return;
const index = parent.children.indexOf(block.model);
host.doc.addBlock(flavour as never, props, parent, index + 1);
} else {
const rootId = std.doc.root?.id;
if (!rootId) return;
const edgelessRoot = std.view.getBlock(rootId);
if (!edgelessRoot) return;
// @ts-expect-error TODO: fix after edgeless refactor
edgelessRoot.service.viewport.smoothZoom(1);
// @ts-expect-error TODO: fix after edgeless refactor
const surfaceBlock = edgelessRoot.surface;
if (!(surfaceBlock instanceof SurfaceBlockComponent)) return;
const center = Vec.toVec(surfaceBlock.renderer.viewport.center);
// @ts-expect-error TODO: fix after edgeless refactor
const cardId = edgelessRoot.service.addBlock(
flavour,
{
...props,
xywh: Bound.fromCenter(
center,
EMBED_CARD_WIDTH[targetStyle],
EMBED_CARD_HEIGHT[targetStyle]
).serialize(),
style: targetStyle,
},
surfaceBlock.model
);
// @ts-expect-error TODO: fix after edgeless refactor
edgelessRoot.service.selection.set({
elements: [cardId],
editing: false,
});
// @ts-expect-error TODO: fix after edgeless refactor
edgelessRoot.tools.setEdgelessTool({
type: 'default',
});
}
}
@@ -0,0 +1,99 @@
import type { LinkPreviewData } from '@blocksuite/affine-model';
import { DEFAULT_LINK_PREVIEW_ENDPOINT } from '@blocksuite/affine-shared/consts';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
export type LinkPreviewResponseData = {
url: string;
title?: string;
siteName?: string;
description?: string;
images?: string[];
mediaType?: string;
contentType?: string;
charset?: string;
videos?: string[];
favicons?: string[];
};
export class LinkPreviewer {
private _endpoint = DEFAULT_LINK_PREVIEW_ENDPOINT;
query = async (
url: string,
signal?: AbortSignal
): Promise<Partial<LinkPreviewData>> => {
if (
(url.startsWith('https://x.com/') ||
url.startsWith('https://www.x.com/') ||
url.startsWith('https://www.twitter.com/') ||
url.startsWith('https://twitter.com/')) &&
url.includes('/status/')
) {
// use api.fxtwitter.com
url =
'https://api.fxtwitter.com/status/' + /\/status\/(.*)/.exec(url)?.[1];
try {
const { tweet } = await fetch(url, { signal }).then(res => res.json());
return {
title: tweet.author.name,
icon: tweet.author.avatar_url,
description: tweet.text,
image: tweet.media?.photos?.[0].url || tweet.author.banner_url,
};
} catch (e) {
console.error(`Failed to fetch tweet: ${url}`);
console.error(e);
return {};
}
} else {
const response = await fetch(this._endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
}),
signal,
})
.then(r => {
if (!r || !r.ok) {
throw new BlockSuiteError(
ErrorCode.DefaultRuntimeError,
`Failed to fetch link preview: ${url}`
);
}
return r;
})
.catch(err => {
if (isAbortError(err)) return null;
console.error(`Failed to fetch link preview: ${url}`);
console.error(err);
return null;
});
if (!response) return {};
const data: LinkPreviewResponseData = await response.json();
return {
title: data.title ? this._getStringFromHTML(data.title) : null,
description: data.description
? this._getStringFromHTML(data.description)
: null,
icon: data.favicons?.[0],
image: data.images?.[0],
};
}
};
setEndpoint = (endpoint: string) => {
this._endpoint = endpoint;
};
private _getStringFromHTML(html: string) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent;
}
}
@@ -0,0 +1,297 @@
import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import {
type DocMode,
type ImageBlockModel,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { EMBED_CARD_HEIGHT } from '@blocksuite/affine-shared/consts';
import { matchFlavours, SpecProvider } from '@blocksuite/affine-shared/utils';
import { BlockStdScope } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
import {
type BlockModel,
BlockViewType,
type Doc,
type Query,
} from '@blocksuite/store';
import { render, type TemplateResult } from 'lit';
import type { EmbedLinkedDocBlockComponent } from '../embed-linked-doc-block/index.js';
import type { EmbedSyncedDocCard } from '../embed-synced-doc-block/components/embed-synced-doc-card.js';
export function renderLinkedDocInCard(
card: EmbedLinkedDocBlockComponent | EmbedSyncedDocCard
) {
const linkedDoc = card.linkedDoc;
assertExists(
linkedDoc,
`Trying to load page ${card.model.pageId} in linked page block, but the page is not found.`
);
// eslint-disable-next-line sonarjs/no-collapsible-if
if ('bannerContainer' in card) {
if (card.editorMode === 'page') {
renderPageAsBanner(card).catch(e => {
console.error(e);
card.isError = true;
});
}
}
renderNoteContent(card).catch(e => {
console.error(e);
card.isError = true;
});
}
async function renderPageAsBanner(card: EmbedSyncedDocCard) {
const linkedDoc = card.linkedDoc;
assertExists(
linkedDoc,
`Trying to load page ${card.model.pageId} in linked page block, but the page is not found.`
);
const notes = getNotesFromDoc(linkedDoc);
if (!notes) {
card.isBannerEmpty = true;
return;
}
const target = notes.flatMap(note =>
note.children.filter(child => matchFlavours(child, ['affine:image']))
)[0];
if (target) {
await renderImageAsBanner(card, target);
return;
}
card.isBannerEmpty = true;
}
async function renderImageAsBanner(
card: EmbedSyncedDocCard,
image: BlockModel
) {
const sourceId = (image as ImageBlockModel).sourceId;
if (!sourceId) return;
const storage = card.linkedDoc?.blobSync;
if (!storage) return;
const blob = await storage.get(sourceId);
if (!blob) return;
const url = URL.createObjectURL(blob);
const $img = document.createElement('img');
$img.src = url;
await addCover(card, $img);
card.isBannerEmpty = false;
}
async function addCover(
card: EmbedSyncedDocCard,
cover: HTMLElement | TemplateResult<1>
) {
const coverContainer = await card.bannerContainer;
if (!coverContainer) return;
while (coverContainer.firstChild) {
coverContainer.firstChild.remove();
}
if (cover instanceof HTMLElement) {
coverContainer.append(cover);
} else {
render(cover, coverContainer);
}
}
async function renderNoteContent(
card: EmbedLinkedDocBlockComponent | EmbedSyncedDocCard
) {
card.isNoteContentEmpty = true;
const doc = card.linkedDoc;
assertExists(
doc,
`Trying to load page ${card.model.pageId} in linked page block, but the page is not found.`
);
const notes = getNotesFromDoc(doc);
if (!notes) {
return;
}
const cardStyle = card.model.style;
const isHorizontal = cardStyle === 'horizontal';
const allowFlavours: (keyof BlockSuite.BlockModels)[] = isHorizontal
? []
: ['affine:image'];
const noteChildren = notes.flatMap(note =>
note.children.filter(model => {
if (matchFlavours(model, allowFlavours)) {
return true;
}
return filterTextModel(model);
})
);
if (!noteChildren.length) {
return;
}
card.isNoteContentEmpty = false;
const noteContainer = await card.noteContainer;
if (!noteContainer) {
return;
}
while (noteContainer.firstChild) {
noteContainer.firstChild.remove();
}
const noteBlocksContainer = document.createElement('div');
noteBlocksContainer.classList.add('affine-embed-doc-content-note-blocks');
noteBlocksContainer.contentEditable = 'false';
noteContainer.append(noteBlocksContainer);
if (isHorizontal) {
// When the card is horizontal, we only render the first block
noteChildren.splice(1);
} else {
// Before rendering, we can not know the height of each block
// But we can limit the number of blocks to render simply by the height of the card
const cardHeight = EMBED_CARD_HEIGHT[cardStyle];
const minSingleBlockHeight = 20;
const maxBlockCount = Math.floor(cardHeight / minSingleBlockHeight);
if (noteChildren.length > maxBlockCount) {
noteChildren.splice(maxBlockCount);
}
}
const childIds = noteChildren.map(child => child.id);
const ids: string[] = [];
childIds.forEach(block => {
let parent: string | null = block;
while (parent && !ids.includes(parent)) {
ids.push(parent);
parent = doc.blockCollection.crud.getParent(parent);
}
});
const query: Query = {
mode: 'strict',
match: ids.map(id => ({ id, viewType: BlockViewType.Display })),
};
const previewDoc = doc.blockCollection.getDoc({ query });
const previewSpec = SpecProvider.getInstance().getSpec('page:preview');
const previewStd = new BlockStdScope({
doc: previewDoc,
extensions: previewSpec.value,
});
const previewTemplate = previewStd.render();
const fragment = document.createDocumentFragment();
render(previewTemplate, fragment);
noteBlocksContainer.append(fragment);
const contentEditableElements = noteBlocksContainer.querySelectorAll(
'[contenteditable="true"]'
);
contentEditableElements.forEach(element => {
(element as HTMLElement).contentEditable = 'false';
});
}
function filterTextModel(model: BlockModel) {
if (matchFlavours(model, ['affine:paragraph', 'affine:list'])) {
return !!model.text?.toString().length;
}
return false;
}
export function getNotesFromDoc(doc: Doc) {
const notes = doc.root?.children.filter(
child =>
matchFlavours(child, ['affine:note']) &&
child.displayMode !== NoteDisplayMode.EdgelessOnly
);
if (!notes || !notes.length) {
return null;
}
return notes;
}
export function isEmptyDoc(doc: Doc | null, mode: DocMode) {
if (!doc) {
return true;
}
if (mode === 'page') {
const notes = getNotesFromDoc(doc);
if (!notes || !notes.length) {
return true;
}
return notes.every(note => isEmptyNote(note));
} else {
const surface = getSurfaceBlock(doc);
if (surface?.elementModels.length || doc.blockSize > 2) {
return false;
}
return true;
}
}
export function isEmptyNote(note: BlockModel) {
return note.children.every(block => {
return (
block.flavour === 'affine:paragraph' &&
(!block.text || block.text.length === 0)
);
});
}
function getSurfaceBlock(doc: Doc) {
const blocks = doc.getBlocksByFlavour('affine:surface');
return blocks.length !== 0 ? (blocks[0].model as SurfaceBlockModel) : null;
}
/**
* Gets the document content with a max length.
*/
export function getDocContentWithMaxLength(doc: Doc, maxlength = 500) {
const notes = getNotesFromDoc(doc);
if (!notes) return;
const noteChildren = notes.flatMap(note =>
note.children.filter(model => filterTextModel(model))
);
if (!noteChildren.length) return;
let count = 0;
let reached = false;
const texts = [];
for (const model of noteChildren) {
let t = model.text?.toString();
if (t?.length) {
const c: number = count + Math.max(0, texts.length - 1);
if (t.length + c > maxlength) {
t = t.substring(0, maxlength - c);
reached = true;
}
texts.push(t);
count += t.length;
if (reached) {
break;
}
}
}
return texts.join('\n');
}
@@ -0,0 +1,92 @@
import {
blockComponentSymbol,
type BlockService,
type GfxBlockComponent,
GfxElementSymbol,
toGfxBlockComponent,
} from '@blocksuite/block-std';
import type {
GfxBlockElementModel,
GfxCompatibleProps,
} from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/utils';
import type { StyleInfo } from 'lit/directives/style-map.js';
import type { EmbedBlockComponent } from './embed-block-element.js';
export function toEdgelessEmbedBlock<
Model extends GfxBlockElementModel<GfxCompatibleProps>,
Service extends BlockService,
WidgetName extends string,
B extends typeof EmbedBlockComponent<Model, Service, WidgetName>,
>(block: B) {
return class extends toGfxBlockComponent(block) {
_isDragging = false;
_isResizing = false;
_isSelected = false;
_showOverlay = false;
override [blockComponentSymbol] = true;
override blockDraggable = false;
protected override embedContainerStyle: StyleInfo = {};
override [GfxElementSymbol] = true;
get bound(): Bound {
return Bound.deserialize(this.model.xywh);
}
get rootService() {
return this.std.getService('affine:page');
}
_handleClick(_: MouseEvent): void {
return;
}
override connectedCallback(): void {
super.connectedCallback();
const rootService = this.rootService;
this._disposables.add(
// @ts-expect-error TODO: fix after edgeless slots are migrated to extension
rootService.slots.elementResizeStart.on(() => {
this._isResizing = true;
this._showOverlay = true;
})
);
this._disposables.add(
// @ts-expect-error TODO: fix after edgeless slots are migrated to extension
rootService.slots.elementResizeEnd.on(() => {
this._isResizing = false;
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
})
);
}
override renderGfxBlock() {
const bound = Bound.deserialize(this.model.xywh);
this.embedContainerStyle.width = `${bound.w}px`;
this.embedContainerStyle.height = `${bound.h}px`;
this.blockContainerStyles = {
width: `${bound.w}px`,
};
this._scale = bound.w / this._cardWidth;
return this.renderPageContent();
}
protected override accessor blockContainerStyles: StyleInfo | undefined =
undefined;
} as B & {
new (...args: any[]): GfxBlockComponent;
};
}
@@ -0,0 +1,47 @@
import {
DarkLoadingIcon,
EmbedCardDarkBannerIcon,
EmbedCardDarkCubeIcon,
EmbedCardDarkHorizontalIcon,
EmbedCardDarkListIcon,
EmbedCardDarkVerticalIcon,
EmbedCardLightBannerIcon,
EmbedCardLightCubeIcon,
EmbedCardLightHorizontalIcon,
EmbedCardLightListIcon,
EmbedCardLightVerticalIcon,
LightLoadingIcon,
} from '@blocksuite/affine-components/icons';
import { ColorScheme } from '@blocksuite/affine-model';
import type { TemplateResult } from 'lit';
type EmbedCardIcons = {
LoadingIcon: TemplateResult<1>;
EmbedCardBannerIcon: TemplateResult<1>;
EmbedCardHorizontalIcon: TemplateResult<1>;
EmbedCardListIcon: TemplateResult<1>;
EmbedCardVerticalIcon: TemplateResult<1>;
EmbedCardCubeIcon: TemplateResult<1>;
};
export function getEmbedCardIcons(theme: ColorScheme): EmbedCardIcons {
if (theme === ColorScheme.Light) {
return {
LoadingIcon: LightLoadingIcon,
EmbedCardBannerIcon: EmbedCardLightBannerIcon,
EmbedCardHorizontalIcon: EmbedCardLightHorizontalIcon,
EmbedCardListIcon: EmbedCardLightListIcon,
EmbedCardVerticalIcon: EmbedCardLightVerticalIcon,
EmbedCardCubeIcon: EmbedCardLightCubeIcon,
};
} else {
return {
LoadingIcon: DarkLoadingIcon,
EmbedCardBannerIcon: EmbedCardDarkBannerIcon,
EmbedCardHorizontalIcon: EmbedCardDarkHorizontalIcon,
EmbedCardListIcon: EmbedCardDarkListIcon,
EmbedCardVerticalIcon: EmbedCardDarkVerticalIcon,
EmbedCardCubeIcon: EmbedCardDarkCubeIcon,
};
}
}