mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
feat(editor): add embed doc block extension (#12090)
Closes: BS-3393 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new "Embed Doc" block, enabling embedding and rendering of linked and synced documents within cards, including support for banners and note previews. - Added new toolbar and quick search options for inserting embedded linked and synced documents. - **Improvements** - Updated dependencies and internal references to support the new embed doc functionality across related blocks and components. - Enhanced support for edgeless environments with new clipboard and configuration options for embedded docs. - **Refactor** - Streamlined and reorganized embed-related code, moving linked and synced doc logic into a dedicated embed doc module. - Removed obsolete adapter and utility files to simplify maintenance. - **Chores** - Updated project and TypeScript configuration files to include the new embed doc module in builds and references. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,30 +1,22 @@
|
||||
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
|
||||
import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
|
||||
import {
|
||||
type DocMode,
|
||||
ImageBlockModel,
|
||||
ListBlockModel,
|
||||
NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { EMBED_CARD_HEIGHT } from '@blocksuite/affine-shared/consts';
|
||||
import { NotificationProvider } from '@blocksuite/affine-shared/services';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import {
|
||||
type BlockModel,
|
||||
type BlockSnapshot,
|
||||
type DraftModel,
|
||||
type Query,
|
||||
Slice,
|
||||
type Store,
|
||||
Text,
|
||||
} 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';
|
||||
|
||||
// Throttle delay for block updates to reduce unnecessary re-renders
|
||||
// - Prevents rapid-fire updates when multiple blocks are updated in quick succession
|
||||
@@ -32,197 +24,6 @@ import type { EmbedSyncedDocCard } from '../embed-synced-doc-block/components/em
|
||||
// - Small enough to feel instant to users, large enough to batch updates effectively
|
||||
export const RENDER_CARD_THROTTLE_MS = 60;
|
||||
|
||||
export function renderLinkedDocInCard(
|
||||
card: EmbedLinkedDocBlockComponent | EmbedSyncedDocCard
|
||||
) {
|
||||
const linkedDoc = card.linkedDoc;
|
||||
if (!linkedDoc) {
|
||||
console.error(
|
||||
`Trying to load page ${card.model.props.pageId} in linked page block, but the page is not found.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (!linkedDoc) {
|
||||
console.error(
|
||||
`Trying to load page ${card.model.props.pageId} in linked page block, but the page is not found.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const notes = getNotesFromDoc(linkedDoc);
|
||||
if (!notes) {
|
||||
card.isBannerEmpty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const target = notes.flatMap(note =>
|
||||
note.children.filter(child => matchModels(child, [ImageBlockModel]))
|
||||
)[0];
|
||||
|
||||
if (target) {
|
||||
await renderImageAsBanner(card, target);
|
||||
return;
|
||||
}
|
||||
|
||||
card.isBannerEmpty = true;
|
||||
}
|
||||
|
||||
async function renderImageAsBanner(
|
||||
card: EmbedSyncedDocCard,
|
||||
image: BlockModel
|
||||
) {
|
||||
const sourceId = (image as ImageBlockModel).props.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;
|
||||
if (!doc) {
|
||||
console.error(
|
||||
`Trying to load page ${card.model.props.pageId} in linked page block, but the page is not found.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const notes = getNotesFromDoc(doc);
|
||||
if (!notes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardStyle = card.model.props.style;
|
||||
const isHorizontal = cardStyle === 'horizontal';
|
||||
const allowFlavours = isHorizontal ? [] : [ImageBlockModel];
|
||||
|
||||
const noteChildren = notes.flatMap(note =>
|
||||
note.children.filter(model => {
|
||||
if (matchModels(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.getParent(parent)?.id ?? null;
|
||||
}
|
||||
});
|
||||
const query: Query = {
|
||||
mode: 'strict',
|
||||
match: ids.map(id => ({ id, viewType: 'display' })),
|
||||
};
|
||||
const previewDoc = doc.doc.getStore({ query });
|
||||
const std = card.host.std;
|
||||
const previewSpec = std
|
||||
.get(ViewExtensionManagerIdentifier)
|
||||
.get('preview-page');
|
||||
const previewStd = new BlockStdScope({
|
||||
store: previewDoc,
|
||||
extensions: previewSpec,
|
||||
});
|
||||
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 (matchModels(model, [ParagraphBlockModel, ListBlockModel])) {
|
||||
return !!model.text?.toString().length;
|
||||
|
||||
Reference in New Issue
Block a user