import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import type { AttachmentBlockModel, BookmarkBlockModel, CodeBlockModel, DatabaseBlockModel, ImageBlockModel, ListBlockModel, ParagraphBlockModel, RootBlockModel, } from '@blocksuite/blocks'; import type { DeltaInsert } from '@blocksuite/inline'; import { noop, SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; import { css, html, LitElement, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { SmallLinkedDocIcon } from '../../_common/icons.js'; import { placeholderMap, previewIconMap } from '../config.js'; import { isHeadingBlock, isRootBlock } from '../utils/query.js'; type ValuesOf = T[K]; function assertType(value: unknown): asserts value is T { noop(value); } const styles = css` :host { display: block; width: 100%; font-family: var(--affine-font-family); } :host(:hover) { cursor: pointer; background: var(--affine-hover-color); } :host(.active) { color: var(--affine-text-emphasis-color); } .outline-block-preview { width: 100%; box-sizing: border-box; padding: 6px 8px; white-space: nowrap; display: flex; justify-content: start; align-items: center; gap: 8px; } .icon { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; box-sizing: border-box; padding: 4px; background: var(--affine-background-secondary-color); border-radius: 4px; color: var(--affine-icon-color); } .icon.disabled { color: var(--affine-disabled-icon-color); } .text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; font-size: var(--affine-font-sm); line-height: 22px; height: 22px; } .text.general, .subtype.text, .subtype.quote { font-weight: 400; padding-left: 28px; } .subtype.title, .subtype.h1, .subtype.h2, .subtype.h3, .subtype.h4, .subtype.h5, .subtype.h6 { font-weight: 600; } .subtype.title { padding-left: 0; } .subtype.h1 { padding-left: 0; } .subtype.h2 { padding-left: 4px; } .subtype.h3 { padding-left: 12px; } .subtype.h4 { padding-left: 16px; } .subtype.h5 { padding-left: 20px; } .subtype.h6 { padding-left: 24px; } .outline-block-preview:not(:has(span)) { display: none; } .text span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .linked-doc-preview svg { width: 1.1em; height: 1.1em; vertical-align: middle; font-size: inherit; margin-bottom: 0.1em; } .linked-doc-text { font-size: inherit; border-bottom: 0.5px solid var(--affine-divider-color); white-space: break-spaces; margin-right: 2px; } .linked-doc-preview.unavailable svg { color: var(--affine-text-disable-color); } .linked-doc-preview.unavailable .linked-doc-text { color: var(--affine-text-disable-color); text-decoration: line-through; } `; export const AFFINE_OUTLINE_BLOCK_PREVIEW = 'affine-outline-block-preview'; export class OutlineBlockPreview extends SignalWatcher( WithDisposable(LitElement) ) { static override styles = styles; private _TextBlockPreview(block: ParagraphBlockModel | ListBlockModel) { const deltas: DeltaInsert[] = block.text.yText.toDelta(); if (!block.text.length) return nothing; const iconClass = this.disabledIcon ? 'icon disabled' : 'icon'; const previewText = deltas.map(delta => { if (delta.attributes?.reference) { // If linked doc, render linked doc icon and the doc title. const refAttribute = delta.attributes.reference; const refMeta = block.doc.collection.meta.docMetas.find( doc => doc.id === refAttribute.pageId ); const unavailable = !refMeta; const title = unavailable ? 'Deleted doc' : refMeta.title; return html`${SmallLinkedDocIcon} ${title.length ? title : 'Untitled'}`; } else { // If not linked doc, render the text. return delta.insert.toString().trim().length > 0 ? html`${delta.insert.toString()}` : nothing; } }); return html`${previewText} ${this.showPreviewIcon ? html`${previewIconMap[block.type]}` : nothing}`; } override render() { return html`
${this.renderBlockByFlavour()}
`; } renderBlockByFlavour() { const { block } = this; const iconClass = this.disabledIcon ? 'icon disabled' : 'icon'; if ( !this.enableNotesSorting && !isHeadingBlock(block) && !isRootBlock(block) ) return nothing; switch (block.flavour as keyof BlockSuite.BlockModels) { case 'affine:page': assertType(block); return block.title.length > 0 ? html` ${block.title$.value} ` : nothing; case 'affine:paragraph': assertType(block); return this._TextBlockPreview(block); case 'affine:list': assertType(block); return this._TextBlockPreview(block); case 'affine:bookmark': assertType(block); return html` ${block.title || block.url || placeholderMap['bookmark']} ${this.showPreviewIcon ? html`${previewIconMap['bookmark']}` : nothing} `; case 'affine:code': assertType(block); return html` ${block.language ?? placeholderMap['code']} ${this.showPreviewIcon ? html`${previewIconMap['code']}` : nothing} `; case 'affine:database': assertType(block); return html` ${block.title.toString().length ? block.title.toString() : placeholderMap['database']} ${this.showPreviewIcon ? html`${previewIconMap['table']}` : nothing} `; case 'affine:image': assertType(block); return html` ${block.caption?.length ? block.caption : placeholderMap['image']} ${this.showPreviewIcon ? html`${previewIconMap['image']}` : nothing} `; case 'affine:attachment': assertType(block); return html` ${block.name?.length ? block.name : placeholderMap['attachment']} ${this.showPreviewIcon ? html`${previewIconMap['attachment']}` : nothing} `; default: return nothing; } } @property({ attribute: false }) accessor block!: ValuesOf; @property({ attribute: false }) accessor cardNumber!: number; @property({ attribute: false }) accessor disabledIcon = false; @property({ attribute: false }) accessor enableNotesSorting!: boolean; @property({ attribute: false }) accessor showPreviewIcon!: boolean; } declare global { interface HTMLElementTagNameMap { [AFFINE_OUTLINE_BLOCK_PREVIEW]: OutlineBlockPreview; } }