mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 08:50:50 +08:00
refactor(editor): image toolbar config extension (#11329)
Closes: [BS-2378](https://linear.app/affine-design/issue/BS-2378/image-toolbar-迁移)
This commit is contained in:
@@ -1,19 +1,121 @@
|
||||
import { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { CaptionIcon, DownloadIcon } from '@blocksuite/icons/lit';
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CaptionIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { ImageBlockComponent } from '../image-block';
|
||||
import { ImageEdgelessBlockComponent } from '../image-edgeless-block';
|
||||
import { duplicate } from '../utils';
|
||||
|
||||
const trackBaseProps = {
|
||||
category: 'image',
|
||||
type: 'card view',
|
||||
};
|
||||
|
||||
const builtinToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.download',
|
||||
tooltip: 'Download',
|
||||
icon: DownloadIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
block?.download();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
block?.captionEditor?.show();
|
||||
|
||||
ctx.track('OpenedCaptionEditor', {
|
||||
...trackBaseProps,
|
||||
control: 'add caption',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
actions: [
|
||||
{
|
||||
id: 'a.copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
block?.copy();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.duplicate',
|
||||
label: 'Duplicate',
|
||||
icon: DuplicateIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (!block) return;
|
||||
|
||||
duplicate(block);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'b.conversions',
|
||||
actions: [
|
||||
{
|
||||
id: 'a.turn-into-card-view',
|
||||
label: 'Turn into card view',
|
||||
icon: BookmarkIcon(),
|
||||
when(ctx) {
|
||||
const supported =
|
||||
ctx.store.schema.flavourSchemaMap.has('affine:attachment');
|
||||
if (!supported) return false;
|
||||
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
return Boolean(block?.blob);
|
||||
},
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
block?.convertToCardView();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'c.delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon(),
|
||||
variant: 'destructive',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (!block) return;
|
||||
|
||||
ctx.store.deleteBlock(block.model);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
placement: 'inner',
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
const builtinSurfaceToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
@@ -50,6 +152,11 @@ export const createBuiltinToolbarConfigExtension = (
|
||||
const name = flavour.split(':').pop();
|
||||
|
||||
return [
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(`affine:surface:${name}`),
|
||||
config: builtinSurfaceToolbarConfig,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import { whenHover } from '@blocksuite/affine-components/hover';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import type { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import { ToolbarRegistryIdentifier } from '@blocksuite/affine-shared/services';
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
@@ -54,9 +56,33 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
}
|
||||
|
||||
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);
|
||||
this._disposables.add(dispose);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._initHover();
|
||||
this.refreshData();
|
||||
this.contentEditable = 'false';
|
||||
this._disposables.add(
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { ImageBlockSchema } from '@blocksuite/affine-model';
|
||||
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
|
||||
import {
|
||||
BlockViewExtension,
|
||||
FlavourExtension,
|
||||
WidgetViewExtension,
|
||||
} from '@blocksuite/std';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
@@ -16,12 +12,6 @@ import { ImageDropOption } from './image-service';
|
||||
|
||||
const flavour = ImageBlockSchema.model.flavour;
|
||||
|
||||
export const imageToolbarWidget = WidgetViewExtension(
|
||||
flavour,
|
||||
'imageToolbar',
|
||||
literal`affine-image-toolbar-widget`
|
||||
);
|
||||
|
||||
export const ImageBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension(flavour),
|
||||
BlockViewExtension(flavour, model => {
|
||||
@@ -33,7 +23,6 @@ export const ImageBlockSpec: ExtensionType[] = [
|
||||
|
||||
return literal`affine-image`;
|
||||
}),
|
||||
imageToolbarWidget,
|
||||
ImageDropOption,
|
||||
ImageBlockAdapterExtensions,
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
|
||||
@@ -11,13 +11,19 @@ import {
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
downloadBlob,
|
||||
getBlockProps,
|
||||
humanFileSize,
|
||||
isInsidePageEditor,
|
||||
readImageSize,
|
||||
transformModel,
|
||||
withTempBlobData,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/gfx';
|
||||
import type { BlockStdScope, EditorHost } from '@blocksuite/std';
|
||||
import {
|
||||
BlockSelection,
|
||||
type BlockStdScope,
|
||||
type EditorHost,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
@@ -552,3 +558,49 @@ export function calcBoundByOrigin(
|
||||
? new Bound(point[0], point[1], width, height)
|
||||
: Bound.fromCenter(point, width, height);
|
||||
}
|
||||
|
||||
export function duplicate(block: ImageBlockComponent) {
|
||||
const model = block.model;
|
||||
const blockProps = getBlockProps(model);
|
||||
const {
|
||||
width: _width,
|
||||
height: _height,
|
||||
xywh: _xywh,
|
||||
rotate: _rotate,
|
||||
zIndex: _zIndex,
|
||||
...duplicateProps
|
||||
} = blockProps;
|
||||
|
||||
const { doc } = model;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
console.error(`Parent not found for block(${model.flavour}) ${model.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parent?.children.indexOf(model);
|
||||
const duplicateId = doc.addBlock(
|
||||
model.flavour,
|
||||
duplicateProps,
|
||||
parent,
|
||||
index + 1
|
||||
);
|
||||
|
||||
const editorHost = block.host;
|
||||
editorHost.updateComplete
|
||||
.then(() => {
|
||||
const { selection } = editorHost;
|
||||
selection.setGroup('note', [
|
||||
selection.create(BlockSelection, {
|
||||
blockId: duplicateId,
|
||||
}),
|
||||
]);
|
||||
if (isInsidePageEditor(editorHost)) {
|
||||
const duplicateElement = editorHost.view.getBlock(duplicateId);
|
||||
if (duplicateElement) {
|
||||
duplicateElement.scrollIntoView(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user