refactor(editor): unify directories naming (#11516)

**Directory Structure Changes**

- Renamed multiple block-related directories by removing the "block-" prefix:
  - `block-attachment` → `attachment`
  - `block-bookmark` → `bookmark`
  - `block-callout` → `callout`
  - `block-code` → `code`
  - `block-data-view` → `data-view`
  - `block-database` → `database`
  - `block-divider` → `divider`
  - `block-edgeless-text` → `edgeless-text`
  - `block-embed` → `embed`
This commit is contained in:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions

View File

@@ -0,0 +1,60 @@
import { EdgelessCRUDExtension } from '@blocksuite/affine-block-surface';
import { type SurfaceRefProps } from '@blocksuite/affine-model';
import type { Command } from '@blocksuite/std';
import { GfxPrimitiveElementModel } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
export const insertSurfaceRefBlockCommand: Command<
{
reference: string;
place: 'after' | 'before';
removeEmptyLine?: boolean;
selectedModels?: BlockModel[];
},
{
insertedSurfaceRefBlockId: string;
}
> = (ctx, next) => {
const { selectedModels, reference, place, removeEmptyLine, std } = ctx;
if (!selectedModels?.length) return;
const targetModel =
place === 'before'
? selectedModels[0]
: selectedModels[selectedModels.length - 1];
const surfaceRefProps: Partial<SurfaceRefProps> & {
flavour: 'affine:surface-ref';
} = {
flavour: 'affine:surface-ref',
reference,
};
const crud = std.get(EdgelessCRUDExtension);
const element = crud.getElementById(reference);
if (!element) {
console.error(`reference not found ${reference}`);
return;
}
surfaceRefProps.refFlavour =
element instanceof GfxPrimitiveElementModel
? element.type
: element.flavour;
const result = std.store.addSiblingBlocks(
targetModel,
[surfaceRefProps],
place
);
if (result.length === 0) return;
if (removeEmptyLine && targetModel.text?.length === 0) {
std.store.deleteBlock(targetModel);
}
next({
insertedSurfaceRefBlockId: result[0],
});
};

View File

@@ -0,0 +1,2 @@
export { SurfaceRefPlaceHolder } from './placeholder';
export { SurfaceRefToolbarTitle } from './surface-ref-toolbar-title';

View File

@@ -0,0 +1,122 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { DeleteIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { type GfxModel } from '@blocksuite/std/gfx';
import { css, html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { SurfaceRefNotFoundBackground } from '../icons';
import { getReferenceModelTitle, TYPE_ICON_MAP } from '../utils';
export class SurfaceRefPlaceHolder extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
.surface-ref-placeholder {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
}
.surface-ref-placeholder.not-found {
background: ${unsafeCSSVarV2('layer/background/secondary', '#F5F5F5')};
}
.surface-ref-placeholder-heading {
position: relative;
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
font-size: 14px;
font-weight: 500;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: ${unsafeCSSVarV2('text/primary', '#141414')};
}
.surface-ref-placeholder-body {
position: relative;
font-size: 12px;
font-weight: 400;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: ${unsafeCSSVarV2('text/disable', '#7a7a7a')};
}
.surface-ref-not-found-background {
position: absolute;
right: 12px;
bottom: -5px;
}
`;
@property({ attribute: false })
accessor referenceModel: GfxModel | null = null;
@property({ attribute: false })
accessor refFlavour = '';
@property({ attribute: false })
accessor inEdgeless = false;
override render() {
const { referenceModel, refFlavour, inEdgeless } = this;
// When surface ref is in page mode and reference exists, don't render placeholder
if (referenceModel && !inEdgeless) return nothing;
const modelNotFound = !referenceModel;
const matchedType = TYPE_ICON_MAP[refFlavour] ?? TYPE_ICON_MAP['edgeless'];
const title =
(referenceModel && getReferenceModelTitle(referenceModel)) ??
matchedType.name;
return html`
<div
class=${classMap({
'surface-ref-placeholder': true,
'not-found': modelNotFound,
})}
>
${modelNotFound
? html`<div class="surface-ref-not-found-background">
${SurfaceRefNotFoundBackground}
</div>`
: nothing}
<div class="surface-ref-placeholder-heading">
${modelNotFound ? DeleteIcon() : matchedType.icon}
<span class="surface-ref-title">
${modelNotFound
? `This ${matchedType.name} not available`
: `${title}`}
</span>
</div>
<div class="surface-ref-placeholder-body">
<span class="surface-ref-text">
${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.`}
</span>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'surface-ref-placeholder': SurfaceRefPlaceHolder;
}
}

View File

@@ -0,0 +1,76 @@
import {
FrameBlockModel,
GroupElementModel,
MindmapElementModel,
ShapeElementModel,
} from '@blocksuite/affine-model';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
EdgelessIcon,
FrameIcon,
GroupIcon,
MindmapIcon,
} from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import type { GfxModel } from '@blocksuite/std/gfx';
import { css, html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
export class SurfaceRefToolbarTitle extends ShadowlessElement {
static override styles = css`
surface-ref-toolbar-title {
display: flex;
padding: 2px 4px;
margin-right: auto;
align-items: center;
gap: 4px;
border-radius: 4px;
color: ${unsafeCSSVarV2('text/primary')};
box-shadow: ${unsafeCSSVar('buttonShadow')};
background: ${unsafeCSSVar('white')};
svg {
color: ${unsafeCSSVarV2('icon/primary')};
width: 16px;
height: 16px;
}
span {
color: ${unsafeCSSVarV2('text/primary')};
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
}
`;
@property({ attribute: false })
accessor referenceModel: GfxModel | null = null;
override render() {
const { referenceModel } = this;
let title = '';
let icon: TemplateResult = EdgelessIcon();
if (referenceModel instanceof GroupElementModel) {
title = referenceModel.title.toString();
icon = GroupIcon();
} else if (referenceModel instanceof FrameBlockModel) {
title = referenceModel.props.title.toString();
icon = FrameIcon();
} else if (referenceModel instanceof MindmapElementModel) {
const rootElement = referenceModel.tree.element;
if (rootElement instanceof ShapeElementModel) {
title = rootElement.text?.toString() ?? '';
}
icon = MindmapIcon();
}
return html`${icon}<span>${title}</span>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'surface-ref-toolbar-title': SurfaceRefToolbarTitle;
}
}

View File

@@ -0,0 +1,164 @@
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
import { EdgelessCRUDExtension } from '@blocksuite/affine-block-surface';
import { MindmapStyle, SurfaceRefBlockSchema } from '@blocksuite/affine-model';
import {
type SlashMenuActionItem,
type SlashMenuConfig,
SlashMenuConfigExtension,
type SlashMenuItem,
} from '@blocksuite/affine-widget-slash-menu';
import { Bound } from '@blocksuite/global/gfx';
import { FrameIcon, GroupingIcon, MindmapIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { insertSurfaceRefBlockCommand } from '../commands';
import { EdgelessTooltip, FrameTooltip, MindMapTooltip } from './tooltips';
const surfaceRefSlashMenuConfig: SlashMenuConfig = {
items: ({ std, model }) => {
const crud = std.get(EdgelessCRUDExtension);
const frameMgr = std.get(EdgelessFrameManagerIdentifier);
const findSpace = (bound: Bound, padding = 20) => {
const gfx = std.get(GfxControllerIdentifier);
let elementInFrameBound = gfx.grid.search(bound);
while (elementInFrameBound.length > 0) {
const rightElement = elementInFrameBound.reduce((a, b) => {
return a.x + a.w > b.x + b.w ? a : b;
});
bound.x = rightElement.x + rightElement.w + padding;
elementInFrameBound = gfx.grid.search(bound);
}
return bound;
};
const insertSurfaceRefAndSelect = (reference: string) => {
const [_, result] = std.command.exec(insertSurfaceRefBlockCommand, {
reference,
place: 'after',
removeEmptyLine: true,
selectedModels: [model],
});
if (!result.insertedSurfaceRefBlockId) return;
std.selection.set([
std.selection.create(BlockSelection, {
blockId: result.insertedSurfaceRefBlockId,
}),
]);
};
let index = 0;
const insertBlankFrameItem: SlashMenuItem = {
name: 'Frame',
description: 'Insert a blank frame',
icon: FrameIcon(),
tooltip: {
figure: FrameTooltip,
caption: 'Frame',
},
group: `5_Edgeless Element@${index++}`,
action: () => {
const frameBound = findSpace(Bound.fromXYWH([0, 0, 1600, 900]));
const frame = frameMgr.createFrameOnBound(frameBound);
insertSurfaceRefAndSelect(frame.id);
},
};
const insertMindMapItem: SlashMenuItem = {
name: 'Mind Map',
description: 'Insert a mind map',
icon: MindmapIcon(),
tooltip: {
figure: MindMapTooltip,
caption: 'Edgeless',
},
group: `5_Edgeless Element@${index++}`,
action: () => {
const bound = findSpace(Bound.fromXYWH([0, 0, 200, 200]), 150);
const { x, y, h } = bound;
const rootW = 145;
const rootH = 50;
const nodeW = 80;
const nodeH = 35;
const centerVertical = y + h / 2;
const rootX = x;
const rootY = centerVertical - rootH / 2;
type MindMapNode = {
children: MindMapNode[];
text: string;
xywh: string;
};
const root: MindMapNode = {
children: [],
text: 'Mind Map',
xywh: `[${rootX},${rootY},${rootW},${rootH}]`,
};
for (let i = 0; i < 3; i++) {
const nodeX = x + rootW + 300;
const nodeY = centerVertical - nodeH / 2 + (i - 1) * 50;
root.children.push({
children: [],
text: 'Text',
xywh: `[${nodeX},${nodeY},${nodeW},${nodeH}]`,
});
}
const mindmapId = crud.addElement('mindmap', {
style: MindmapStyle.ONE,
children: root,
});
if (!mindmapId) return;
insertSurfaceRefAndSelect(mindmapId);
},
};
const frameItems = frameMgr.frames.map<SlashMenuActionItem>(frameModel => ({
name: 'Frame: ' + frameModel.props.title,
icon: FrameIcon(),
group: `5_Edgeless Element@${index++}`,
tooltip: {
figure: EdgelessTooltip,
caption: 'Edgeless',
},
action: () => {
insertSurfaceRefAndSelect(frameModel.id);
},
}));
const groupElements = crud.getElementsByType('group');
const groupItems = groupElements.map<SlashMenuActionItem>(group => ({
name: 'Group: ' + group.title.toString(),
icon: GroupingIcon(),
group: `5_Edgeless Element@${index++}`,
tooltip: {
figure: EdgelessTooltip,
caption: 'Edgeless',
},
action: () => {
insertSurfaceRefAndSelect(group.id);
},
}));
return [
insertBlankFrameItem,
insertMindMapItem,
...frameItems,
...groupItems,
];
},
};
export const SurfaceRefSlashMenuConfigExtension = SlashMenuConfigExtension(
SurfaceRefBlockSchema.model.flavour,
surfaceRefSlashMenuConfig
);

View File

@@ -0,0 +1,95 @@
import { toast } from '@blocksuite/affine-components/toast';
import {
copySelectedModelsCommand,
draftSelectedModelsCommand,
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import { SurfaceRefBlockComponent } from '../surface-ref-block';
export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
actions: [
{
id: 'a.surface-ref-title',
when: ctx =>
!!ctx.getCurrentBlockByType(SurfaceRefBlockComponent)?.referenceModel,
content: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return null;
return html`<surface-ref-toolbar-title
.referenceModel=${surfaceRefBlock.referenceModel}
></surface-ref-toolbar-title>`;
},
},
{
id: 'c.copy-surface-ref',
label: 'Copy',
icon: CopyIcon(),
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
ctx.chain
.pipe(draftSelectedModelsCommand, {
selectedModels: [surfaceRefBlock.model],
})
.pipe(copySelectedModelsCommand)
.run();
toast(surfaceRefBlock.std.host, 'Copied to clipboard');
},
},
{
id: 'd.surface-ref-caption',
icon: CaptionIcon(),
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
surfaceRefBlock.captionElement.show();
},
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,
when: ctx => {
const surfaceRefBlock = ctx.getCurrentBlock();
if (!(surfaceRefBlock instanceof SurfaceRefBlockComponent))
return false;
return !!surfaceRefBlock.referenceModel;
},
actions: [
// TODO(@L-Sun): add duplicate action after refactoring root-block/edgeless
],
},
{
id: 'g.surface-ref-deletion',
label: 'Delete',
icon: DeleteIcon(),
placement: ActionPlacement.More,
variant: 'destructive',
run: ctx => {
const surfaceRefBlock = ctx.getCurrentBlockByType(
SurfaceRefBlockComponent
);
if (!surfaceRefBlock) return;
ctx.store.deleteBlock(surfaceRefBlock.model);
},
},
],
placement: 'inner',
};

View File

@@ -0,0 +1,111 @@
import { html } from 'lit';
// prettier-ignore
export const EdgelessTooltip = html`<svg width="170" height="106" viewBox="0 0 170 106" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="170" height="106" rx="2" fill="white"/>
<mask id="mask0_16460_1252" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="106">
<rect width="170" height="106" rx="2" fill="white"/>
</mask>
<g mask="url(#mask0_16460_1252)">
<rect x="100.5" y="42.6565" width="141" height="51" stroke="#1E96EB" stroke-width="3" stroke-dasharray="5 5"/>
<circle cx="101.5" cy="43.5" r="6" fill="white" stroke="#1E96EB" stroke-width="3"/>
<rect x="105" y="8" width="59" height="26" rx="10" fill="black" fill-opacity="0.1"/>
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="117" y="25.3636">Group</tspan></text>
<mask id="mask1_16460_1252" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="98" height="106">
<path d="M0 1.5C0 0.947717 0.447715 0.5 1 0.5H96.2527C96.8927 0.5 97.368 1.09278 97.2288 1.71742L74.1743 105.217C74.0725 105.675 73.6667 106 73.1982 106H0.999999C0.447715 106 0 105.552 0 105V1.5Z" fill="#F4F4F5"/>
</mask>
<g mask="url(#mask1_16460_1252)">
<path d="M0 1.5C0 0.947717 0.447715 0.5 1 0.5H96.2527C96.8927 0.5 97.368 1.09278 97.2288 1.71742L74.1743 105.217C74.0725 105.675 73.6667 106 73.1982 106H0.999999C0.447715 106 0 105.552 0 105V1.5Z" fill="#F4F4F5"/>
<rect x="23" y="41.6565" width="142" height="52" rx="9" stroke="black" stroke-opacity="0.5" stroke-width="2"/>
<path d="M23 12.4244C23 10.0964 24.8829 8.20923 27.2056 8.20923H71.7846C74.1073 8.20923 75.9902 10.0964 75.9902 12.4244V29.2849C75.9902 31.6128 74.1073 33.5 71.7846 33.5H27.2056C24.8829 33.5 23 31.6128 23 29.2849V12.4244Z" fill="black" fill-opacity="0.8"/>
<path d="M64.0353 25.2043C63.415 25.2043 62.8798 25.0674 62.4299 24.7935C61.9828 24.5169 61.6377 24.1313 61.3946 23.6367C61.1543 23.1393 61.0341 22.5608 61.0341 21.9014C61.0341 21.2419 61.1543 20.6606 61.3946 20.1577C61.6377 19.6519 61.9759 19.2579 62.409 18.9756C62.8449 18.6906 63.3535 18.5481 63.9347 18.5481C64.27 18.5481 64.6012 18.604 64.9281 18.7158C65.2551 18.8275 65.5527 19.0092 65.8209 19.2607C66.0892 19.5094 66.303 19.8391 66.4622 20.2499C66.6215 20.6606 66.7012 21.1664 66.7012 21.7672V22.1864H61.7383V21.3313H65.6952C65.6952 20.968 65.6225 20.6439 65.4772 20.3589C65.3347 20.0738 65.1307 19.8489 64.8652 19.684C64.6026 19.5191 64.2924 19.4367 63.9347 19.4367C63.5407 19.4367 63.1998 19.5345 62.912 19.7301C62.6269 19.9229 62.4076 20.1744 62.2539 20.4846C62.1002 20.7948 62.0234 21.1273 62.0234 21.4822V22.0523C62.0234 22.5385 62.1072 22.9506 62.2748 23.2888C62.4453 23.6241 62.6814 23.8798 62.9832 24.0558C63.285 24.2291 63.6357 24.3157 64.0353 24.3157C64.2952 24.3157 64.5299 24.2794 64.7395 24.2067C64.9519 24.1313 65.1349 24.0195 65.2886 23.8714C65.4423 23.7205 65.561 23.5333 65.6449 23.3097L66.6006 23.578C66.5 23.9021 66.3309 24.1872 66.0934 24.4331C65.8558 24.6762 65.5624 24.8662 65.2131 25.0031C64.8638 25.1373 64.4712 25.2043 64.0353 25.2043Z" fill="white"/>
<path d="M51.0779 25.0702V18.6319H52.0336V19.6379H52.1174C52.2515 19.2942 52.4681 19.0273 52.7671 18.8373C53.0661 18.6445 53.4252 18.5481 53.8443 18.5481C54.2691 18.5481 54.6226 18.6445 54.9048 18.8373C55.1898 19.0273 55.412 19.2942 55.5713 19.6379H55.6383C55.8032 19.3054 56.0505 19.0413 56.3802 18.8457C56.71 18.6473 57.1054 18.5481 57.5665 18.5481C58.1421 18.5481 58.613 18.7283 58.979 19.0888C59.3451 19.4465 59.5281 20.004 59.5281 20.7612V25.0702H58.5389V20.7612C58.5389 20.2862 58.409 19.9467 58.1491 19.7427C57.8892 19.5387 57.5832 19.4367 57.2311 19.4367C56.7784 19.4367 56.4278 19.5736 56.179 19.8475C55.9303 20.1185 55.806 20.4622 55.806 20.8786V25.0702H54.8V20.6606C54.8 20.2946 54.6813 19.9998 54.4437 19.7762C54.2062 19.5499 53.9002 19.4367 53.5258 19.4367C53.2687 19.4367 53.0284 19.5052 52.8048 19.6421C52.5841 19.779 52.4052 19.969 52.2683 20.2121C52.1342 20.4525 52.0671 20.7305 52.0671 21.0463V25.0702H51.0779Z" fill="white"/>
<path d="M46.3224 25.2211C45.9144 25.2211 45.5442 25.1442 45.2116 24.9905C44.8791 24.8341 44.615 24.6091 44.4194 24.3157C44.2238 24.0195 44.126 23.6618 44.126 23.2427C44.126 22.8738 44.1987 22.5748 44.344 22.3457C44.4893 22.1137 44.6835 21.9321 44.9266 21.8008C45.1697 21.6694 45.438 21.5716 45.7314 21.5073C46.0276 21.4403 46.3252 21.3872 46.6242 21.3481C47.0154 21.2978 47.3326 21.26 47.5757 21.2349C47.8216 21.207 48.0004 21.1608 48.1122 21.0966C48.2268 21.0323 48.284 20.9205 48.284 20.7612V20.7277C48.284 20.3141 48.1709 19.9928 47.9445 19.7637C47.721 19.5345 47.3815 19.4199 46.926 19.4199C46.4537 19.4199 46.0835 19.5233 45.8152 19.7301C45.547 19.9369 45.3583 20.1577 45.2493 20.3924L44.3104 20.0571C44.4781 19.6658 44.7017 19.3613 44.9811 19.1433C45.2633 18.9225 45.5707 18.7689 45.9032 18.6822C46.2386 18.5928 46.5683 18.5481 46.8924 18.5481C47.0992 18.5481 47.3368 18.5732 47.605 18.6235C47.8761 18.671 48.1373 18.7702 48.3888 18.9211C48.6431 19.072 48.8541 19.2998 49.0218 19.6044C49.1894 19.909 49.2733 20.3169 49.2733 20.8283V25.0702H48.284V24.1983H48.2337C48.1667 24.3381 48.0549 24.4876 47.8984 24.6468C47.7419 24.8061 47.5338 24.9416 47.2739 25.0534C47.014 25.1652 46.6968 25.2211 46.3224 25.2211ZM46.4733 24.3325C46.8645 24.3325 47.1942 24.2556 47.4625 24.1019C47.7336 23.9482 47.9375 23.7498 48.0745 23.5067C48.2142 23.2636 48.284 23.0079 48.284 22.7397V21.8343C48.2421 21.8846 48.1499 21.9307 48.0074 21.9726C47.8677 22.0117 47.7056 22.0467 47.5212 22.0774C47.3395 22.1053 47.1621 22.1305 46.9889 22.1528C46.8184 22.1724 46.6801 22.1892 46.5739 22.2031C46.3168 22.2367 46.0765 22.2912 45.8529 22.3666C45.6322 22.4393 45.4533 22.5497 45.3164 22.6978C45.1823 22.8431 45.1152 23.0415 45.1152 23.293C45.1152 23.6367 45.2424 23.8965 45.4967 24.0726C45.7537 24.2458 46.0793 24.3325 46.4733 24.3325Z" fill="white"/>
<path d="M40.0364 25.0704V18.6321H40.9921V19.6045H41.0592C41.1765 19.286 41.3889 19.0275 41.6963 18.8291C42.0037 18.6307 42.3502 18.5315 42.7358 18.5315C42.8084 18.5315 42.8993 18.5329 43.0082 18.5357C43.1172 18.5385 43.1997 18.5427 43.2556 18.5483V19.5542C43.222 19.5459 43.1452 19.5333 43.025 19.5165C42.9077 19.497 42.7833 19.4872 42.652 19.4872C42.339 19.4872 42.0596 19.5528 41.8136 19.6842C41.5705 19.8127 41.3777 19.9916 41.2352 20.2207C41.0955 20.447 41.0256 20.7055 41.0256 20.9961V25.0704H40.0364Z" fill="white"/>
<path d="M33.3126 25.0704V16.4861H38.4598V17.4082H34.3521V20.3088H38.0742V21.2309H34.3521V25.0704H33.3126Z" fill="white"/>
</g>
</g>
</svg>
`;
// prettier-ignore
export const FrameTooltip = html`<svg width="170" height="89" viewBox="0 0 170 89" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5269_147682)">
<rect width="170" height="89" fill="white"/>
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Create a blank frame in Edgeless</tspan></text>
<rect x="16" y="45" width="164" height="59" rx="3" stroke="black" stroke-opacity="0.52" stroke-width="2"/>
<rect x="15" y="27" width="32" height="13" rx="3" fill="black" fill-opacity="0.95"/>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="8" font-weight="500" letter-spacing="0px"><tspan x="19" y="35.8182">Frame</tspan></text>
</g>
<defs>
<clipPath id="clip0_5269_147682">
<rect width="170" height="89" fill="white"/>
</clipPath>
</defs>
</svg>
`
// prettier-ignore
export const MindMapTooltip = html`<svg width="170" height="106" viewBox="0 0 170 106" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5150_67028)">
<rect width="170" height="106" fill="white"/>
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Create a mind map in Edgeless</tspan></text>
<g filter="url(#filter0_d_5150_67028)">
<rect x="21" y="53" width="59" height="19" rx="5" stroke="#29A3FA" stroke-width="2"/>
<text fill="black" xml:space="preserve" style="white-space: pre" font-family="Poppins" font-size="8" font-weight="500" letter-spacing="0px"><tspan x="30.5" y="65.076">Mind Map</tspan></text>
</g>
<g filter="url(#filter1_d_5150_67028)">
<rect x="119.75" y="30" width="28.25" height="13.125" rx="5" stroke="#6E52DF" stroke-width="2"/>
</g>
<g filter="url(#filter2_d_5150_67028)">
<rect x="119.75" y="55.8013" width="28.25" height="13.125" rx="5" stroke="#E660A4" stroke-width="2"/>
</g>
<g filter="url(#filter3_d_5150_67028)">
<rect x="119.75" y="81.603" width="28.25" height="13.125" rx="5" stroke="#FF8C38" stroke-width="2"/>
</g>
<path d="M81.5139 62.7686C104.205 62.7686 97.1139 88.7686 120.514 88.7686" stroke="#FF8C38" stroke-width="2"/>
<path d="M81.5139 62.7686C104.205 62.7686 97.1139 62.7686 120.514 62.7686" stroke="#E660A4" stroke-width="2"/>
<path d="M81.5139 62.7686C104.205 62.7686 97.1139 36.7686 120.514 36.7686" stroke="#6E52DF" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d_5150_67028" x="8" y="46" width="85" height="45" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<filter id="filter1_d_5150_67028" x="106.75" y="23" width="54.25" height="39.125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<filter id="filter2_d_5150_67028" x="106.75" y="48.8013" width="54.25" height="39.125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<filter id="filter3_d_5150_67028" x="106.75" y="74.603" width="54.25" height="39.125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<clipPath id="clip0_5150_67028">
<rect width="170" height="106" fill="white"/>
</clipPath>
</defs>
</svg>
`

View File

@@ -0,0 +1,20 @@
import { SurfaceRefPlaceHolder, SurfaceRefToolbarTitle } from './components';
import { SurfaceRefGenericBlockPortal } from './portal/generic-block';
import { SurfaceRefNotePortal } from './portal/note';
import { SurfaceRefBlockComponent } from './surface-ref-block';
import { EdgelessSurfaceRefBlockComponent } from './surface-ref-block-edgeless';
export function effects() {
customElements.define(
'surface-ref-generic-block-portal',
SurfaceRefGenericBlockPortal
);
customElements.define('affine-surface-ref', SurfaceRefBlockComponent);
customElements.define(
'affine-edgeless-surface-ref',
EdgelessSurfaceRefBlockComponent
);
customElements.define('surface-ref-note-portal', SurfaceRefNotePortal);
customElements.define('surface-ref-toolbar-title', SurfaceRefToolbarTitle);
customElements.define('surface-ref-placeholder', SurfaceRefPlaceHolder);
}

View File

@@ -0,0 +1,105 @@
import { html } from 'lit';
export const SurfaceRefNotFoundBackground = html`
<svg
width="204"
height="66"
viewBox="0 0 204 66"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_d_877_26)">
<rect width="53" height="66" transform="translate(49 22)" fill="white" />
<rect
x="57.0168"
y="30.8"
width="26.0545"
height="3.85"
rx="1.925"
fill="black"
fill-opacity="0.07"
/>
<rect
x="57.0168"
y="38.5"
width="36.9664"
height="2.2"
rx="1.1"
fill="black"
fill-opacity="0.07"
/>
<rect
x="57.0168"
y="44"
width="19.5409"
height="2.2"
rx="1.1"
fill="black"
fill-opacity="0.07"
/>
<rect
x="57.0168"
y="49.5"
width="36.9664"
height="2.2"
rx="1.1"
fill="black"
fill-opacity="0.07"
/>
<rect
x="57.0168"
y="55"
width="19.5409"
height="2.2"
rx="1.1"
fill="black"
fill-opacity="0.07"
/>
</g>
<path
d="M157.341 17.6783L144.153 14.356L144.708 12.2671C145.628 8.80598 143.153 5.18897 139.18 4.18815L129.589 1.77198C125.616 0.771163 121.65 2.76557 120.73 6.22669L120.175 8.31563L106.987 4.99341C103.676 4.15942 100.371 5.82148 99.6048 8.70566L98.4947 12.8835C98.1881 14.0373 99.0131 15.2429 100.337 15.5765L157.885 30.0735C159.209 30.4071 160.531 29.7424 160.837 28.5886L161.948 24.4107C162.714 21.5266 160.651 18.5123 157.341 17.6783ZM125.526 7.43477C125.831 6.28324 127.157 5.61689 128.478 5.94987L138.07 8.36603C139.391 8.69901 140.218 9.90749 139.912 11.059L139.357 13.148L124.97 9.52372L125.526 7.43477Z"
fill="#E6E6E6"
/>
<path
d="M98.6798 34.2638C98.2253 34.2631 97.8625 34.6108 97.8834 35.0271L99.9152 75.4671C100.103 79.2097 103.451 82.1461 107.536 82.1527L146.222 82.216C150.307 82.2226 153.665 79.2973 153.866 75.5552L156.037 35.1221C156.06 34.7059 155.698 34.357 155.243 34.3563L98.6798 34.2638ZM137.141 40.1651C137.143 38.8748 138.285 37.8316 139.693 37.8339C141.1 37.8362 142.238 38.8831 142.236 40.1734L142.183 70.5327C142.181 71.8229 141.039 72.8661 139.632 72.8638C138.225 72.8615 137.086 71.8146 137.089 70.5244L137.141 40.1651ZM124.404 40.1442C124.406 38.854 125.548 37.8108 126.956 37.8131C128.363 37.8154 129.501 38.8623 129.499 40.1526L129.447 70.5119C129.444 71.8021 128.303 72.8453 126.895 72.843C125.488 72.8407 124.349 71.7938 124.352 70.5035L124.404 40.1442ZM111.667 40.1234C111.669 38.8331 112.811 37.7899 114.219 37.7922C115.626 37.7945 116.764 38.8415 116.762 40.1317L116.71 70.491C116.707 71.7813 115.566 72.8245 114.158 72.8222C112.751 72.8199 111.613 71.773 111.615 70.4827L111.667 40.1234Z"
fill="#E6E6E6"
/>
<defs>
<filter
id="filter0_d_877_26"
x="46"
y="19"
width="59"
height="72"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="1.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.258824 0 0 0 0 0.254902 0 0 0 0 0.286275 0 0 0 0.1 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_877_26"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_877_26"
result="shape"
/>
</filter>
</defs>
</svg>
`;

View File

@@ -0,0 +1,4 @@
export * from './commands.js';
export * from './surface-ref-block.js';
export * from './surface-ref-block-edgeless.js';
export * from './surface-ref-spec.js';

View File

@@ -0,0 +1,82 @@
import type {
AttachmentBlockModel,
BookmarkBlockModel,
EmbedFigmaModel,
EmbedGithubModel,
EmbedHtmlModel,
EmbedLinkedDocModel,
EmbedLoomModel,
EmbedSyncedDocModel,
EmbedYoutubeModel,
ImageBlockModel,
} from '@blocksuite/affine-model';
import { Bound } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
import { css, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
export class SurfaceRefGenericBlockPortal extends WithDisposable(
ShadowlessElement
) {
static override styles = css`
surface-ref-generic-block-portal {
position: relative;
}
`;
override firstUpdated() {
this.disposables.add(
this.model.propsUpdated.subscribe(() => this.requestUpdate())
);
}
override render() {
const { model, index } = this;
const bound = Bound.deserialize(model.xywh);
const style = {
position: 'absolute',
zIndex: `${index}`,
width: `${bound.w}px`,
height: `${bound.h}px`,
transform: `translate(${bound.x}px, ${bound.y}px)`,
};
return html`
<div
style=${styleMap(style)}
data-portal-reference-block-id="${model.id}"
>
${this.renderModel(model)}
</div>
`;
}
@property({ attribute: false })
accessor index!: number;
@property({ attribute: false })
accessor model!:
| ImageBlockModel
| AttachmentBlockModel
| BookmarkBlockModel
| EmbedGithubModel
| EmbedYoutubeModel
| EmbedFigmaModel
| EmbedLinkedDocModel
| EmbedSyncedDocModel
| EmbedHtmlModel
| EmbedLoomModel;
@property({ attribute: false })
accessor renderModel!: (model: BlockModel) => TemplateResult;
}
declare global {
interface HTMLElementTagNameMap {
'surface-ref-generic-block-portal': SurfaceRefGenericBlockPortal;
}
}

View File

@@ -0,0 +1,167 @@
import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
import type { NoteBlockModel } from '@blocksuite/affine-model';
import {
DefaultTheme,
NoteDisplayMode,
NoteShadow,
} from '@blocksuite/affine-model';
import {
EDGELESS_BLOCK_CHILD_BORDER_WIDTH,
EDGELESS_BLOCK_CHILD_PADDING,
} from '@blocksuite/affine-shared/consts';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { deserializeXYWH } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import {
BlockStdScope,
type EditorHost,
ShadowlessElement,
} from '@blocksuite/std';
import { RANGE_QUERY_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import { type BlockModel, type Query } from '@blocksuite/store';
import { css, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
export class SurfaceRefNotePortal extends WithDisposable(ShadowlessElement) {
static override styles = css`
surface-ref-note-portal {
position: relative;
}
`;
ancestors = new Set<string>();
query: Query | null = null;
override connectedCallback() {
super.connectedCallback();
const ancestors = new Set<string>();
let parent: BlockModel | null = this.model;
while (parent) {
this.ancestors.add(parent.id);
parent = this.model.doc.getParent(parent.id);
}
const query: Query = {
mode: 'include',
match: Array.from(ancestors).map(id => ({
id,
viewType: 'display',
})),
};
this.query = query;
const doc = this.model.doc;
this._disposables.add(() => {
doc.doc.clearQuery(query, true);
});
}
override firstUpdated() {
this.disposables.add(
this.model.propsUpdated.subscribe(() => this.requestUpdate())
);
}
override render() {
const { model, index } = this;
const { displayMode, edgeless } = model.props;
if (!!displayMode && displayMode === NoteDisplayMode.DocOnly)
return nothing;
const backgroundColor = this.host.std
.get(ThemeProvider)
.generateColorProperty(
model.props.background,
DefaultTheme.noteBackgrounColor
);
const [modelX, modelY, modelW, modelH] = deserializeXYWH(model.xywh);
const style = {
zIndex: `${index}`,
width: modelW + 'px',
height:
edgeless.collapse && edgeless.collapsedHeight
? edgeless.collapsedHeight + 'px'
: undefined,
transform: `translate(${modelX}px, ${modelY}px)`,
padding: `${EDGELESS_BLOCK_CHILD_PADDING}px`,
border: `${EDGELESS_BLOCK_CHILD_BORDER_WIDTH}px none var(--affine-black-10)`,
backgroundColor,
boxShadow: `var(${NoteShadow.Sticker})`,
position: 'absolute',
borderRadius: '0px',
boxSizing: 'border-box',
pointerEvents: 'none',
overflow: 'hidden',
transformOrigin: '0 0',
userSelect: 'none',
};
return html`
<div
class="surface-ref-note-portal"
style=${styleMap(style)}
data-model-height="${modelH}"
data-portal-reference-block-id="${model.id}"
>
${this.renderPreview()}
</div>
`;
}
renderPreview() {
if (!this.query) {
console.error('Query is not set before rendering note preview');
return nothing;
}
const doc = this.model.doc.doc.getStore({
query: this.query,
readonly: true,
});
const previewSpec = SpecProvider._.getSpec('preview:page');
return new BlockStdScope({
store: doc,
extensions: previewSpec.value.slice(),
}).render();
}
override updated() {
setTimeout(() => {
const editableElements = Array.from<HTMLDivElement>(
this.querySelectorAll('[contenteditable]')
);
const blocks = Array.from(this.querySelectorAll(`[data-block-id]`));
editableElements.forEach(element => {
if (element.contentEditable === 'true')
element.contentEditable = 'false';
});
blocks.forEach(element => {
element.setAttribute(RANGE_QUERY_EXCLUDE_ATTR, 'true');
});
}, 500);
}
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor index!: number;
@property({ attribute: false })
accessor model!: NoteBlockModel;
@property({ attribute: false })
accessor renderer!: CanvasRenderer;
}
declare global {
interface HTMLElementTagNameMap {
'surface-ref-note-portal': SurfaceRefNotePortal;
}
}

View File

@@ -0,0 +1,80 @@
import { type SurfaceRefBlockModel } from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { BlockComponent, BlockSelection } from '@blocksuite/std';
import { GfxControllerIdentifier, type GfxModel } from '@blocksuite/std/gfx';
import { css, html } from 'lit';
import { state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
export class EdgelessSurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockModel> {
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')};
margin: 18px 0;
}
.affine-edgeless-surface-ref-container.focused {
border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')};
}
`;
override connectedCallback(): void {
super.connectedCallback();
const elementModel = this.gfx.getElementById(
this.model.props.reference
) as GfxModel;
this._referenceModel = elementModel;
this._initSelection();
}
private _initSelection() {
const selection = this.std.selection;
this._disposables.add(
selection.slots.changed.subscribe(selList => {
this._focused = selList.some(
sel => sel.blockId === this.blockId && sel.is(BlockSelection)
);
})
);
}
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
@state()
accessor _referenceModel: GfxModel | null = null;
@state()
accessor _focused = false;
override renderBlock() {
return html` <div
class=${classMap({
'affine-edgeless-surface-ref-container': true,
focused: this._focused,
})}
>
<surface-ref-placeholder
.referenceModel=${this._referenceModel}
.refFlavour=${this.model.props.refFlavour$.value}
.inEdgeless=${true}
></surface-ref-placeholder>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-surface-ref': EdgelessSurfaceRefBlockComponent;
}
}

View File

@@ -0,0 +1,508 @@
import { type FrameBlockComponent } from '@blocksuite/affine-block-frame';
import {
EdgelessCRUDIdentifier,
getSurfaceBlock,
} from '@blocksuite/affine-block-surface';
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { whenHover } from '@blocksuite/affine-components/hover';
import { Peekable } from '@blocksuite/affine-components/peek';
import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
import {
FrameBlockModel,
type SurfaceRefBlockModel,
} from '@blocksuite/affine-model';
import {
DocModeProvider,
EditPropsStore,
type OpenDocMode,
ThemeProvider,
ToolbarRegistryIdentifier,
ViewportElementExtension,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
requestConnectedFrame,
SpecProvider,
} from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
Bound,
deserializeXYWH,
type SerializedXYWH,
} from '@blocksuite/global/gfx';
import { assertType } from '@blocksuite/global/utils';
import {
BlockComponent,
BlockSelection,
BlockStdScope,
type EditorHost,
LifeCycleWatcher,
TextSelection,
} from '@blocksuite/std';
import {
GfxBlockElementModel,
GfxControllerIdentifier,
type GfxModel,
GfxPrimitiveElementModel,
} from '@blocksuite/std/gfx';
import type { BaseSelection, Store } from '@blocksuite/store';
import { effect, signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
@Peekable({
enableOn: (block: SurfaceRefBlockComponent) => !!block.referenceModel,
})
export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockModel> {
static override styles = css`
affine-surface-ref {
position: relative;
}
affine-surface-ref:not(:hover)
affine-surface-ref-toolbar:not([data-open-menu-display='show']) {
display: none;
}
.affine-surface-ref {
position: relative;
user-select: none;
margin: 10px 0;
break-inside: avoid;
border-radius: 8px;
border: 1px solid ${unsafeCSSVarV2('edgeless/frame/border/default')};
background-color: ${unsafeCSSVarV2('layer/background/primary')};
overflow: hidden;
}
.affine-surface-ref.focused {
border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')};
}
@media print {
.affine-surface-ref {
outline: none !important;
}
}
.ref-content {
position: relative;
background-color: var(--affine-background-primary-color);
background: radial-gradient(
var(--affine-edgeless-grid-color) 1px,
var(--affine-background-primary-color) 1px
);
}
.ref-viewport {
max-width: 100%;
margin: 0 auto;
position: relative;
overflow: hidden;
pointer-events: none;
user-select: none;
}
.ref-viewport-event-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
}
`;
private _previewDoc: Store | null = null;
private readonly _previewSpec = SpecProvider._.getSpec(
'preview:edgeless'
).extend([ViewportElementExtension('.ref-viewport')]);
private _referencedModel: GfxModel | null = null;
private readonly _referenceXYWH$ = signal<SerializedXYWH | null>(null);
private get _shouldRender() {
return (
this.isConnected &&
// prevent surface-ref from render itself in loop
!this.parentComponent?.closest('affine-surface-ref')
);
}
get referenceModel() {
return this._referencedModel;
}
private _focusBlock() {
this.selection.update(() => {
return [this.selection.create(BlockSelection, { blockId: this.blockId })];
});
}
private _initHotkey() {
const selection = this.host.selection;
const addParagraph = () => {
if (!this.doc.getParent(this.model)) return;
const [paragraphId] = this.doc.addSiblingBlocks(this.model, [
{
flavour: 'affine:paragraph',
},
]);
const model = this.doc.getModelById(paragraphId);
if (!model) return;
requestConnectedFrame(() => {
selection.update(selList => {
return selList
.filter<BaseSelection>(sel => !sel.is(BlockSelection))
.concat(
selection.create(TextSelection, {
from: {
blockId: model.id,
index: 0,
length: 0,
},
to: null,
})
);
});
}, this);
};
this.bindHotKey({
Enter: () => {
if (!this._focused) return;
addParagraph();
return true;
},
});
}
private _initReferencedModel() {
const findReferencedModel = (): [GfxModel | null, string] => {
if (!this.model.props.reference) return [null, this.doc.id];
const referenceId = this.model.props.reference;
const find = (doc: Store): [GfxModel | null, string] => {
const block = doc.getBlock(referenceId)?.model;
if (block instanceof GfxBlockElementModel) {
return [block, doc.id];
}
const surfaceBlock = getSurfaceBlock(doc);
if (!surfaceBlock) return [null, doc.id];
const element = surfaceBlock.getElementById(referenceId);
if (element) return [element, doc.id];
return [null, doc.id];
};
// find current doc first
let result = find(this.doc);
if (result[0]) return result;
for (const doc of this.std.workspace.docs.values()) {
result = find(doc.getStore());
if (result[0]) return result;
}
return [null, this.doc.id];
};
const init = () => {
const [referencedModel, docId] = findReferencedModel();
this._referencedModel =
referencedModel && referencedModel.xywh ? referencedModel : null;
// TODO(@L-Sun): clear query cache
const doc = this.doc.workspace.getDoc(docId);
this._previewDoc = doc?.getStore({ readonly: true }) ?? null;
};
init();
this._disposables.add(
this.model.propsUpdated.subscribe(payload => {
if (
payload.key === 'reference' &&
this.model.props.reference !== this._referencedModel?.id
) {
init();
}
})
);
if (this._referencedModel instanceof GfxPrimitiveElementModel) {
this._disposables.add(
this._referencedModel.surface.elementRemoved.subscribe(({ id }) => {
if (this.model.props.reference === id) {
init();
}
})
);
}
if (this._referencedModel instanceof GfxBlockElementModel) {
this._disposables.add(
this.doc.slots.blockUpdated.subscribe(({ type, id }) => {
if (type === 'delete' && id === this.model.props.reference) {
init();
}
})
);
}
}
private _initSelection() {
const selection = this.host.selection;
this._disposables.add(
selection.slots.changed.subscribe(selList => {
this._focused = selList.some(
sel => sel.blockId === this.blockId && sel.is(BlockSelection)
);
})
);
}
private _initViewport() {
const refreshViewport = () => {
if (!this._referenceXYWH$.value) return;
const previewEditorHost = this.previewEditor;
if (!previewEditorHost) return;
const gfx = previewEditorHost.std.get(GfxControllerIdentifier);
const viewport = gfx.viewport;
let bound = Bound.deserialize(this._referenceXYWH$.value);
const w = Math.max(this.getBoundingClientRect().width, bound.w);
const aspectRatio = bound.w / bound.h;
const h = w / aspectRatio;
bound = Bound.fromCenter(bound.center, w, h);
viewport.setViewportByBound(bound);
};
this.disposables.add(effect(refreshViewport));
const referenceId = this.model.props.reference;
const referenceXYWH$ = this._referenceXYWH$;
class SurfaceRefViewportWatcher extends LifeCycleWatcher {
static override readonly key = 'surface-ref-viewport-watcher';
private readonly _disposable = new DisposableGroup();
override mounted() {
const crud = this.std.get(EdgelessCRUDIdentifier);
const gfx = this.std.get(GfxControllerIdentifier);
const { surface, viewport } = gfx;
if (!surface) return;
const referenceElement = crud.getElementById(referenceId);
if (!referenceElement) {
throw new BlockSuiteError(
ErrorCode.MissingViewModelError,
`can not find element(id:${referenceElement})`
);
}
referenceXYWH$.value = referenceElement.xywh;
const { _disposable } = this;
_disposable.add(viewport.sizeUpdated.subscribe(refreshViewport));
if (referenceElement instanceof FrameBlockModel) {
_disposable.add(
referenceElement.xywh$.subscribe(xywh => {
referenceXYWH$.value = xywh;
})
);
const subscription = this.std.view.viewUpdated.subscribe(
({ id, type, method, view }) => {
if (
id === referenceElement.id &&
type === 'block' &&
method === 'add'
) {
assertType<FrameBlockComponent>(view);
view.showBorder = false;
subscription.unsubscribe();
}
}
);
_disposable.add(subscription);
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
_disposable.add(
surface.elementUpdated.subscribe(({ id, oldValues }) => {
if (
id === referenceId &&
oldValues.xywh !== referenceElement.xywh
) {
referenceXYWH$.value = referenceElement.xywh;
}
})
);
}
}
override unmounted() {
this._disposable.dispose();
}
}
this._previewSpec.extend([SurfaceRefViewportWatcher]);
}
private _initHover() {
const { setReference, setFloating, dispose } = whenHover(
hovered => {
const message$ = this.std.get(ToolbarRegistryIdentifier).message$;
if (hovered) {
message$.value = {
flavour: this.model.flavour,
element: this,
setFloating,
};
return;
}
// Clears previous bindings
message$.value = null;
setFloating();
},
{ enterDelay: 500 }
);
setReference(this.hoverableContainer);
this._disposables.add(dispose);
}
private _renderRefContent(referencedModel: GfxModel) {
const [, , w, h] = deserializeXYWH(referencedModel.xywh);
const _previewSpec = this._previewSpec.value;
return html`<div class="ref-content">
<div
class="ref-viewport"
style=${styleMap({
aspectRatio: `${w} / ${h}`,
})}
>
${guard(this._previewDoc, () => {
return this._previewDoc
? new BlockStdScope({
store: this._previewDoc,
extensions: _previewSpec,
}).render()
: nothing;
})}
<div class="ref-viewport-event-mask"></div>
</div>
</div>`;
}
readonly open = ({
openMode,
event,
}: {
openMode?: OpenDocMode;
event?: MouseEvent;
} = {}) => {
const pageId = this.referenceModel?.surface?.doc.id;
if (!pageId) return;
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
pageId: pageId,
params: {
mode: 'edgeless',
elementIds: [this.model.props.reference],
},
openMode,
event,
host: this.host,
});
};
override connectedCallback() {
super.connectedCallback();
this.contentEditable = 'false';
if (!this._shouldRender) return;
this._initHotkey();
this._initViewport();
this._initReferencedModel();
this._initSelection();
}
override firstUpdated() {
if (!this._shouldRender) return;
this._initHover();
}
override render() {
if (!this._shouldRender) return nothing;
const { _referencedModel, model } = this;
const isEmpty = !_referencedModel || !_referencedModel.xywh;
const content = isEmpty
? html`<surface-ref-placeholder
.referenceModel=${_referencedModel}
.refFlavour=${model.props.refFlavour$.value}
></surface-ref-placeholder>`
: this._renderRefContent(_referencedModel);
const edgelessTheme = this.std.get(ThemeProvider).edgeless$.value;
return html`
<div
class=${classMap({
'affine-surface-ref': true,
focused: this._focused,
})}
data-theme=${edgelessTheme}
@click=${this._focusBlock}
>
${content}
</div>
<block-caption-editor></block-caption-editor>
${Object.values(this.widgets)}
`;
}
viewInEdgeless() {
if (!this._referenceXYWH$.value) return;
const viewport = {
xywh: this._referenceXYWH$.value,
padding: [60, 20, 20, 20] as [number, number, number, number],
};
this.std.get(EditPropsStore).setStorage('viewport', viewport);
this.std.get(DocModeProvider).setEditorMode('edgeless');
}
@state()
private accessor _focused: boolean = false;
@query('.affine-surface-ref')
accessor hoverableContainer!: HTMLDivElement;
@query('affine-surface-ref > block-caption-editor')
accessor captionElement!: BlockCaptionEditor;
@query('editor-host')
accessor previewEditor!: EditorHost | null;
}
declare global {
interface HTMLElementTagNameMap {
'affine-surface-ref': SurfaceRefBlockComponent;
}
}

View File

@@ -0,0 +1,30 @@
import { SurfaceRefBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import {
BlockFlavourIdentifier,
BlockViewExtension,
FlavourExtension,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { SurfaceRefSlashMenuConfigExtension } from './configs/slash-menu';
import { surfaceRefToolbarModuleConfig } from './configs/toolbar';
const flavour = SurfaceRefBlockSchema.model.flavour;
export const PageSurfaceRefBlockSpec: ExtensionType[] = [
FlavourExtension(flavour),
BlockViewExtension(flavour, literal`affine-surface-ref`),
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: surfaceRefToolbarModuleConfig,
}),
SurfaceRefSlashMenuConfigExtension,
];
export const EdgelessSurfaceRefBlockSpec: ExtensionType[] = [
FlavourExtension(flavour),
BlockViewExtension(flavour, literal`affine-edgeless-surface-ref`),
SurfaceRefSlashMenuConfigExtension,
];

View File

@@ -0,0 +1,152 @@
import { isFrameBlock } from '@blocksuite/affine-block-frame';
import {
GroupElementModel,
MindmapElementModel,
ShapeElementModel,
} from '@blocksuite/affine-model';
import {
EdgelessIcon,
FrameIcon,
GroupIcon,
MindmapIcon,
} from '@blocksuite/icons/lit';
import { type GfxModel } from '@blocksuite/std/gfx';
import { html, type TemplateResult } from 'lit';
export const noContentPlaceholder = html`
<svg
width="182"
height="182"
viewBox="0 0 182 182"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="37.645"
y="37.6452"
width="106.71"
height="106.71"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M91 144.234L37.7664 91.0003L91 37.7666L144.234 91.0003L91 144.234Z"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M90.564 37.352C99.4686 32.1345 109.836 29.1436 120.902 29.1436C154.093 29.1436 181 56.0502 181 89.2413C181 113.999 166.03 135.259 144.648 144.466"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M144.465 90.707C149.683 99.6117 152.674 109.979 152.674 121.045C152.674 154.236 125.767 181.143 92.5759 181.143C67.8187 181.143 46.5579 166.173 37.3516 144.791"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M91.436 144.465C82.5314 149.683 72.1639 152.674 61.0978 152.674C27.9068 152.674 1.0001 125.767 1.0001 92.576C1.00011 67.8188 15.9701 46.558 37.3519 37.3518"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M37.3518 91.436C32.1342 82.5314 29.1433 72.1639 29.1433 61.0978C29.1433 27.9067 56.05 1.00002 89.241 1.00001C113.998 1.00001 135.259 15.97 144.465 37.3518"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M37.3518 37.3521L144.648 144.649"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path
d="M144.648 37.3521L37.3518 144.649"
stroke="#D2D2D2"
stroke-width="0.586319"
/>
<path d="M91 37.3521V144.649" stroke="#D2D2D2" stroke-width="0.586319" />
<path d="M144.648 91L37.3518 91" stroke="#D2D2D2" stroke-width="0.586319" />
<ellipse cx="144.355" cy="37.645" rx="4.39739" ry="4.3974" fill="#5B5B5B" />
<ellipse
cx="144.355"
cy="144.355"
rx="4.39739"
ry="4.3974"
fill="#5B5B5B"
/>
<ellipse
cx="144.355"
cy="90.9999"
rx="4.39739"
ry="4.3974"
fill="#5B5B5B"
/>
<ellipse cx="37.645" cy="37.645" rx="4.39739" ry="4.3974" fill="#5B5B5B" />
<ellipse cx="37.645" cy="144.355" rx="4.39739" ry="4.3974" fill="#5B5B5B" />
<ellipse cx="37.645" cy="90.9999" rx="4.39739" ry="4.3974" fill="#5B5B5B" />
<ellipse
cx="90.9999"
cy="37.6451"
rx="4.3974"
ry="4.39739"
transform="rotate(-90 90.9999 37.6451)"
fill="#5B5B5B"
/>
<ellipse
cx="90.9999"
cy="90.4136"
rx="4.3974"
ry="4.39739"
transform="rotate(-90 90.9999 90.4136)"
fill="#5B5B5B"
/>
<ellipse
cx="90.9999"
cy="144.356"
rx="4.3974"
ry="4.39739"
transform="rotate(-90 90.9999 144.356)"
fill="#5B5B5B"
/>
</svg>
`;
export const TYPE_ICON_MAP: {
[key: string]: {
name: string;
icon: TemplateResult;
};
} = {
'affine:frame': {
name: 'Frame',
icon: FrameIcon(),
},
group: {
name: 'Group',
icon: GroupIcon(),
},
mindmap: {
name: 'Mind map',
icon: MindmapIcon(),
},
edgeless: {
name: 'Edgeless content',
icon: EdgelessIcon(),
},
};
export const getReferenceModelTitle = (model: GfxModel) => {
if (model instanceof GroupElementModel) {
return model.title.toString();
}
if (isFrameBlock(model)) {
return model.props.title.toString();
}
if (model instanceof MindmapElementModel) {
const rootElement = model.tree.element;
if (rootElement instanceof ShapeElementModel) {
return rootElement.text?.toString() ?? '';
}
}
return null;
};