mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
297
blocksuite/affine/block-embed/src/common/render-linked-doc.ts
Normal file
297
blocksuite/affine/block-embed/src/common/render-linked-doc.ts
Normal file
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user