From 36b1ca4327532e39c99a1ed953d22070fcc71661 Mon Sep 17 00:00:00 2001 From: doouding Date: Wed, 2 Apr 2025 06:59:25 +0000 Subject: [PATCH] feat: render placeholder in edgeless mode (#11387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete [BS-2997](https://linear.app/affine-design/issue/BS-2997/在白板上渲染-inserted-frame-group-时提供占位) --- .../blocks/block-surface-ref/src/icons.ts | 105 ++++++++++ .../src/surface-ref-block-edgeless.ts | 180 +++++++++++++++++- blocksuite/framework/std/src/gfx/index.ts | 2 +- .../framework/std/src/gfx/model/model.ts | 10 +- .../__tests__/edgeless/surface-ref.spec.ts | 19 -- 5 files changed, 290 insertions(+), 26 deletions(-) create mode 100644 blocksuite/affine/blocks/block-surface-ref/src/icons.ts diff --git a/blocksuite/affine/blocks/block-surface-ref/src/icons.ts b/blocksuite/affine/blocks/block-surface-ref/src/icons.ts new file mode 100644 index 0000000000..63ea88b29a --- /dev/null +++ b/blocksuite/affine/blocks/block-surface-ref/src/icons.ts @@ -0,0 +1,105 @@ +import { html } from 'lit'; + +export const SurfaceRefNotFoundBackground = html` + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block-edgeless.ts b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block-edgeless.ts index f7e72f2ef3..177ee33d8c 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block-edgeless.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block-edgeless.ts @@ -1,10 +1,182 @@ -import type { SurfaceRefBlockModel } from '@blocksuite/affine-model'; +import { isFrameBlock } from '@blocksuite/affine-block-frame'; +import { + GroupElementModel, + type SurfaceRefBlockModel, +} from '@blocksuite/affine-model'; +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { + DeleteIcon, + EdgelessIcon, + FrameIcon, + GroupIcon, + MindmapIcon, +} from '@blocksuite/icons/lit'; import { BlockComponent } from '@blocksuite/std'; -import { nothing } from 'lit'; +import { + GfxControllerIdentifier, + type GfxModel, + isPrimitiveModel, +} from '@blocksuite/std/gfx'; +import { css, html, nothing } from 'lit'; +import { state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { SurfaceRefNotFoundBackground } from './icons'; + +const TYPE_ICON_MAP: { + [key: string]: { + name: string; + icon: typeof DeleteIcon; + }; +} = { + 'affine:frame': { + name: 'Frame', + icon: FrameIcon, + }, + group: { + name: 'Group', + icon: GroupIcon, + }, + mindmap: { + name: 'Mind map', + icon: MindmapIcon, + }, + edgeless: { + name: 'Edgeless content', + icon: EdgelessIcon, + }, +}; export class EdgelessSurfaceRefBlockComponent extends BlockComponent { - override render() { - return nothing; + static override styles = css` + affine-edgeless-surface-ref { + position: relative; + overflow: hidden; + } + + .affine-edgeless-surface-ref-container { + border-radius: 8px; + border: 1px solid + ${unsafeCSSVarV2('layer/insideBorder/border', '#e6e6e6')}; + padding: 12px; + } + + .affine-edgeless-surface-ref-container.not-found { + background: ${unsafeCSSVarV2('layer/background/secondary', '#F5F5F5')}; + } + + .affine-edgeless-surface-ref-container .not-found-background { + position: absolute; + right: 12px; + bottom: -5px; + } + + .edgeless-surface-ref-content { + display: flex; + flex-direction: column; + gap: 12px; + } + + .edgeless-surface-ref-content > .surface-ref-heading { + display: flex; + align-items: center; + gap: 8px; + align-self: stretch; + + font-size: 14px; + font-weight: 500; + line-height: 22px; + + text-overflow: ellipsis; + overflow: hidden; + + color: ${unsafeCSSVarV2('text/primary', '#141414')}; + } + + .edgeless-surface-ref-content > .surface-ref-heading svg { + color: ${unsafeCSSVarV2('text/primary', '#141414')}; + } + + .edgeless-surface-ref-content > .surface-ref-body { + font-size: 12px; + font-weight: 400; + line-height: 20px; + color: ${unsafeCSSVarV2('text/disable', '#7a7a7a')}; + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + + const elementModel = this.gfx.getElementById( + this.model.props.reference + ) as GfxModel; + + this._referenceModel = elementModel; + } + + get gfx() { + return this.std.get(GfxControllerIdentifier); + } + + @state() + accessor _referenceModel: GfxModel | null = null; + + private _renderRefContent(referenceModel: GfxModel | null) { + const modelNotFound = !referenceModel; + const flavourOrType = modelNotFound + ? (this.model.props.refFlavour ?? 'edgeless') + : isPrimitiveModel(referenceModel) + ? referenceModel.type + : referenceModel.flavour; + const matchedType = + TYPE_ICON_MAP[flavourOrType] ?? TYPE_ICON_MAP['edgeless']; + + const title = modelNotFound + ? matchedType.name + : isFrameBlock(referenceModel) + ? referenceModel.props.title.toString() + : referenceModel instanceof GroupElementModel + ? referenceModel.title.toString() + : matchedType.name; + + return html` +
+
+ ${modelNotFound + ? DeleteIcon({ width: '16px', height: '16px' }) + : matchedType.icon({ width: '16px', height: '16px' })} + + ${modelNotFound + ? `This ${matchedType.name} not available` + : `${title}`} + +
+
+ + ${modelNotFound + ? `The ${matchedType.name.toLowerCase()} is deleted or not in this doc.` + : `The ${matchedType.name.toLowerCase()} is inserted but cannot display in edgeless mode. Switch to page mode to view the block.`} + +
+
+ ${modelNotFound + ? html`
+ ${SurfaceRefNotFoundBackground} +
` + : nothing} + `; + } + + override renderBlock() { + return html`
+ ${this._renderRefContent(this._referenceModel)} +
`; } } diff --git a/blocksuite/framework/std/src/gfx/index.ts b/blocksuite/framework/std/src/gfx/index.ts index 73b7c9c682..a7db7d78cc 100644 --- a/blocksuite/framework/std/src/gfx/index.ts +++ b/blocksuite/framework/std/src/gfx/index.ts @@ -52,7 +52,7 @@ export { GfxCompatibleBlockModel as GfxCompatible, type GfxCompatibleProps, } from './model/gfx-block-model.js'; -export { type GfxModel } from './model/model.js'; +export { type GfxModel, isPrimitiveModel } from './model/model.js'; export { convert, convertProps, diff --git a/blocksuite/framework/std/src/gfx/model/model.ts b/blocksuite/framework/std/src/gfx/model/model.ts index 0e6c38d577..669946b4c3 100644 --- a/blocksuite/framework/std/src/gfx/model/model.ts +++ b/blocksuite/framework/std/src/gfx/model/model.ts @@ -1,7 +1,7 @@ import type { GfxGroupCompatibleInterface } from './base.js'; import type { GfxBlockElementModel } from './gfx-block-model.js'; -import type { - GfxGroupLikeElementModel, +import { + type GfxGroupLikeElementModel, GfxPrimitiveElementModel, } from './surface/element-model.js'; @@ -10,3 +10,9 @@ export type GfxModel = GfxBlockElementModel | GfxPrimitiveElementModel; export type GfxGroupModel = | (GfxGroupCompatibleInterface & GfxBlockElementModel) | GfxGroupLikeElementModel; + +export const isPrimitiveModel = ( + model: GfxModel +): model is GfxPrimitiveElementModel => { + return model instanceof GfxPrimitiveElementModel; +}; diff --git a/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts index 7f30c3e799..a75f72e13d 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts @@ -71,25 +71,6 @@ describe('basic', () => { ).instanceOf(Element); }); - test('surface-ref should be rendered as empty surface-ref-block-edgeless component page mode', async () => { - const surfaceRefId = doc.addBlock( - 'affine:surface-ref', - { - reference: frameId, - }, - noteAId - ); - - await wait(); - - const refBlock = document.querySelector( - `affine-edgeless-surface-ref[data-block-id="${surfaceRefId}"]` - )! as HTMLElement; - - expect(refBlock).instanceOf(Element); - expect(refBlock.innerText).toBe(''); - }); - test('content in frame should be rendered in the correct order', async () => { const surfaceRefId = doc.addBlock( 'affine:surface-ref',