mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(editor): add edgeless media entry (#9949)
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
||||
import { addImages } from '@blocksuite/affine-block-image';
|
||||
import { CanvasElementType } from '@blocksuite/affine-block-surface';
|
||||
import { type MindmapStyle, TextElementModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
MAX_IMAGE_WIDTH,
|
||||
type MindmapStyle,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
|
||||
import { assertInstanceOf, Bound } from '@blocksuite/global/utils';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import * as Y from 'yjs';
|
||||
@@ -19,7 +26,7 @@ export type ConfigStyle = Partial<Record<ConfigProperty, number | string>>;
|
||||
export type ToolConfig = Record<ConfigState, ConfigStyle>;
|
||||
|
||||
export type DraggableTool = {
|
||||
name: 'text' | 'mindmap';
|
||||
name: 'text' | 'mindmap' | 'media';
|
||||
icon: TemplateResult;
|
||||
config: ToolConfig;
|
||||
standardWidth?: number;
|
||||
@@ -27,7 +34,7 @@ export type DraggableTool = {
|
||||
bound: Bound,
|
||||
edgelessService: EdgelessRootService,
|
||||
edgeless: EdgelessRootBlockComponent
|
||||
) => string;
|
||||
) => Promise<string | null>;
|
||||
};
|
||||
|
||||
const unitMap = { x: 'px', y: 'px', r: 'deg', s: '', z: '', o: '' };
|
||||
@@ -38,15 +45,21 @@ export const textConfig: ToolConfig = {
|
||||
next: { x: -22, y: 64, r: 0 },
|
||||
};
|
||||
export const mindmapConfig: ToolConfig = {
|
||||
default: { x: 4, y: -4, s: 1, z: 1, r: -7 },
|
||||
default: { x: 4, y: -4, s: 1, z: 2, r: -7 },
|
||||
active: { x: 11, y: -14, r: 9, s: 1 },
|
||||
hover: { x: 11, y: -14, r: 9, s: 1.16, z: 3 },
|
||||
next: { y: 64, r: 0 },
|
||||
};
|
||||
export const mediaConfig: ToolConfig = {
|
||||
default: { x: -20, y: -8, r: -1, s: 0.92, z: 1 },
|
||||
active: { x: -20, y: -14, r: -9, s: 1 },
|
||||
hover: { x: -20, y: -14, r: -9, s: 1.16, z: 2 },
|
||||
next: { y: 64, r: 0 },
|
||||
};
|
||||
|
||||
export const getMindmapRender =
|
||||
(mindmapStyle: MindmapStyle): DraggableTool['render'] =>
|
||||
(bound, edgelessService) => {
|
||||
async (bound, edgelessService) => {
|
||||
const [x, y, _, h] = bound.toXYWH();
|
||||
|
||||
const rootW = 145;
|
||||
@@ -98,7 +111,8 @@ export const getMindmapRender =
|
||||
|
||||
return mindmapId;
|
||||
};
|
||||
export const textRender: DraggableTool['render'] = (
|
||||
|
||||
export const textRender: DraggableTool['render'] = async (
|
||||
bound,
|
||||
service,
|
||||
edgeless
|
||||
@@ -143,6 +157,41 @@ export const textRender: DraggableTool['render'] = (
|
||||
return id;
|
||||
};
|
||||
|
||||
export const mediaRender: DraggableTool['render'] = async (
|
||||
bound,
|
||||
_,
|
||||
edgeless
|
||||
) => {
|
||||
let file: File | null = null;
|
||||
try {
|
||||
file = await openFileOrFiles();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
if (!file) return null;
|
||||
|
||||
// image
|
||||
if (file.type.startsWith('image/')) {
|
||||
const [id] = await addImages(edgeless.std, [file], {
|
||||
point: [bound.x, bound.y],
|
||||
maxWidth: MAX_IMAGE_WIDTH,
|
||||
transformPoint: false,
|
||||
});
|
||||
if (id) return id;
|
||||
return null;
|
||||
}
|
||||
|
||||
// attachment
|
||||
const [id] = await addAttachments(
|
||||
edgeless.std,
|
||||
[file],
|
||||
[bound.x, bound.y],
|
||||
false
|
||||
);
|
||||
return id;
|
||||
};
|
||||
|
||||
const toolStyle2StyleObj = (state: ConfigState, style: ConfigStyle = {}) => {
|
||||
const styleObj = {} as Record<string, string>;
|
||||
for (const [key, value] of Object.entries(style)) {
|
||||
|
||||
@@ -574,3 +574,25 @@ export const importMindMapIcon = svg`<svg width="64" height="48" viewBox="0 0 64
|
||||
<rect x="5.5" y="17.9183" width="17.3771" height="12.1639" rx="3" fill="black" fill-opacity="0.03" stroke="#929292" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2 3"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const mindmapMenuMediaIcon = svg`<svg width="56" height="49" viewBox="0 0 56 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_215_65373)">
|
||||
<path d="M4.51611 7.83333C4.51611 5.99238 6.01812 4.5 7.87095 4.5H48.129C49.9818 4.5 51.4838 5.99239 51.4838 7.83334V41.1667C51.4838 43.0076 49.9818 44.5 48.129 44.5H7.87095C6.01813 44.5 4.51611 43.0076 4.51611 41.1667V7.83333Z" fill="#F4F9FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.87096 2H48.129C51.3714 2 53.9999 4.61167 53.9999 7.83333V41.1667C53.9999 44.3883 51.3714 47 48.129 47H7.87096C4.62852 47 2 44.3883 2 41.1667V7.83333C2 4.61167 4.62852 2 7.87096 2ZM7.87096 4.5C6.01814 4.5 4.51613 5.99238 4.51613 7.83333V41.1667C4.51613 43.0076 6.01814 44.5 7.87096 44.5H48.129C49.9818 44.5 51.4838 43.0076 51.4838 41.1667V7.83333C51.4838 5.99238 49.9818 4.5 48.129 4.5H7.87096Z" fill="#3883FF"/>
|
||||
<path d="M7.87095 44.5001H48.129C49.9818 44.5001 51.4838 43.0077 51.4838 41.1667V39.7435L35.8959 17.6424C33.3356 14.0123 28.3633 13.009 24.5807 15.3591L4.51611 27.8252V41.1667C4.51611 43.0077 6.01813 44.5001 7.87095 44.5001Z" fill="#3883FF"/>
|
||||
<ellipse cx="5.03225" cy="5" rx="5.03225" ry="5" transform="matrix(-1 -8.76786e-08 -8.88135e-08 1 47.9972 7.41357)" fill="#3883FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_215_65373" x="0" y="0" width="56" height="49" 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"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_215_65373"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_215_65373" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
@@ -22,8 +22,8 @@ import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { getMindMaps, type ToolbarMindmapItem } from './assets.js';
|
||||
import { textRender } from './basket-elements.js';
|
||||
import { importMindMapIcon, textIcon } from './icons.js';
|
||||
import { mediaRender, textRender } from './basket-elements.js';
|
||||
import { importMindMapIcon, mindmapMenuMediaIcon, textIcon } from './icons.js';
|
||||
import { MindMapPlaceholder } from './mindmap-importing-placeholder.js';
|
||||
|
||||
type TextItem = {
|
||||
@@ -32,6 +32,12 @@ type TextItem = {
|
||||
render: typeof textRender;
|
||||
};
|
||||
|
||||
type MediaItem = {
|
||||
type: 'media';
|
||||
icon: TemplateResult;
|
||||
render: typeof mediaRender;
|
||||
};
|
||||
|
||||
type ImportItem = {
|
||||
type: 'import';
|
||||
icon: TemplateResult;
|
||||
@@ -39,6 +45,12 @@ type ImportItem = {
|
||||
|
||||
const textItem: TextItem = { type: 'text', icon: textIcon, render: textRender };
|
||||
|
||||
const mediaItem: MediaItem = {
|
||||
type: 'media',
|
||||
icon: mindmapMenuMediaIcon,
|
||||
render: mediaRender,
|
||||
};
|
||||
|
||||
export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
@@ -60,7 +72,8 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
height: 48px;
|
||||
background: var(--affine-border-color);
|
||||
}
|
||||
.text-item {
|
||||
.text-item,
|
||||
.media-item {
|
||||
width: 60px;
|
||||
}
|
||||
.mindmap-item {
|
||||
@@ -68,6 +81,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
}
|
||||
|
||||
.text-item,
|
||||
.media-item,
|
||||
.mindmap-item {
|
||||
border-radius: 4px;
|
||||
height: 48px;
|
||||
@@ -77,6 +91,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
justify-content: center;
|
||||
}
|
||||
.text-item > button,
|
||||
.media-item > button,
|
||||
.mindmap-item > button {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
@@ -86,11 +101,13 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
padding: 0;
|
||||
}
|
||||
.text-item:hover,
|
||||
.media-item:hover,
|
||||
.mindmap-item[data-is-active='true'],
|
||||
.mindmap-item:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
.text-item > button.next,
|
||||
.media-item > button.next,
|
||||
.mindmap-item > button.next {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
@@ -103,7 +120,7 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
});
|
||||
|
||||
draggableController!: EdgelessDraggableElementController<
|
||||
ToolbarMindmapItem | TextItem | ImportItem
|
||||
ToolbarMindmapItem | TextItem | ImportItem | MediaItem
|
||||
>;
|
||||
|
||||
override type = 'empty' as const;
|
||||
@@ -215,21 +232,26 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
},
|
||||
onDrop: (element, bound) => {
|
||||
if ('render' in element.data) {
|
||||
const id = element.data.render(
|
||||
bound,
|
||||
this.edgeless.service,
|
||||
this.edgeless
|
||||
);
|
||||
if (element.data.type === 'mindmap') {
|
||||
this.onActiveStyleChange?.(element.data.style);
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({ elements: [id], editing: false });
|
||||
} else if (element.data.type === 'text') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
}
|
||||
|
||||
if (element.data.type === 'import') {
|
||||
element.data
|
||||
.render(bound, this.edgeless.service, this.edgeless)
|
||||
.then(id => {
|
||||
if (!id) return;
|
||||
if (element.data.type === 'mindmap') {
|
||||
this.onActiveStyleChange?.(element.data.style);
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: false,
|
||||
});
|
||||
} else if (
|
||||
element.data.type === 'text' ||
|
||||
element.data.type === 'media'
|
||||
) {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else if (element.data.type === 'import') {
|
||||
this._onImportMindMap?.(bound);
|
||||
}
|
||||
},
|
||||
@@ -240,10 +262,40 @@ export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
const { cancelled, draggingElement, dragOut } =
|
||||
this.draggableController?.states || {};
|
||||
|
||||
const isDraggingMedia = draggingElement?.data?.type === 'media';
|
||||
const isDraggingText = draggingElement?.data?.type === 'text';
|
||||
const showNextText = dragOut && !cancelled;
|
||||
return html`<edgeless-slide-menu .height=${'64px'}>
|
||||
<div class="text-and-mindmap">
|
||||
<div class="media-item">
|
||||
${isDraggingMedia
|
||||
? html`<button
|
||||
class="next"
|
||||
style="transform: translateY(${showNextText ? 0 : 64}px)"
|
||||
>
|
||||
${mediaItem.icon}
|
||||
</button>`
|
||||
: nothing}
|
||||
<button
|
||||
style="opacity: ${isDraggingMedia ? 0 : 1}"
|
||||
@mousedown=${(e: MouseEvent) =>
|
||||
this.draggableController.onMouseDown(e, {
|
||||
preview: mediaItem.icon,
|
||||
data: mediaItem,
|
||||
})}
|
||||
@touchstart=${(e: TouchEvent) =>
|
||||
this.draggableController.onTouchStart(e, {
|
||||
preview: mediaItem.icon,
|
||||
data: mediaItem,
|
||||
})}
|
||||
>
|
||||
${mediaItem.icon}
|
||||
</button>
|
||||
<affine-tooltip tip-position="top" .offset=${12}>
|
||||
${getTooltipWithShortcut('Add media')}
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
<div class="thin-divider"></div>
|
||||
<div class="text-item">
|
||||
${isDraggingText
|
||||
? html`<button
|
||||
|
||||
@@ -23,12 +23,19 @@ import { getMindMaps } from './assets.js';
|
||||
import {
|
||||
type DraggableTool,
|
||||
getMindmapRender,
|
||||
mediaConfig,
|
||||
mediaRender,
|
||||
mindmapConfig,
|
||||
textConfig,
|
||||
textRender,
|
||||
toolConfig2StyleObj,
|
||||
} from './basket-elements.js';
|
||||
import { basketIconDark, basketIconLight, textIcon } from './icons.js';
|
||||
import {
|
||||
basketIconDark,
|
||||
basketIconLight,
|
||||
mindmapMenuMediaIcon,
|
||||
textIcon,
|
||||
} from './icons.js';
|
||||
import { importMindmap } from './utils/import-mindmap.js';
|
||||
|
||||
export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
|
||||
@@ -142,6 +149,13 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
|
||||
const mindmap =
|
||||
this.mindmaps.find(m => m.style === style) || this.mindmaps[0];
|
||||
return [
|
||||
{
|
||||
name: 'media',
|
||||
icon: mindmapMenuMediaIcon,
|
||||
config: mediaConfig,
|
||||
standardWidth: 100,
|
||||
render: mediaRender,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
icon: textIcon,
|
||||
@@ -244,14 +258,22 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
|
||||
this.readyToDrop = false;
|
||||
},
|
||||
onDrop: (el, bound) => {
|
||||
const id = el.data.render(bound, this.edgeless.service, this.edgeless);
|
||||
this.readyToDrop = false;
|
||||
if (el.data.name === 'mindmap') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({ elements: [id], editing: false });
|
||||
} else if (el.data.name === 'text') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
el.data
|
||||
.render(bound, this.edgeless.service, this.edgeless)
|
||||
.then(id => {
|
||||
if (!id) return;
|
||||
this.readyToDrop = false;
|
||||
if (el.data.name === 'mindmap') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: false,
|
||||
});
|
||||
} else if (el.data.name === 'text') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user