mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +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);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { AffineTemplateLoading } from './edgeless/components/toolbar/template/te
|
||||
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
|
||||
import { EdgelessTemplateButton } from './edgeless/components/toolbar/template/template-tool-button.js';
|
||||
import {
|
||||
AffineImageToolbarWidget,
|
||||
AffineModalWidget,
|
||||
EdgelessRootBlockComponent,
|
||||
EdgelessRootPreviewBlockComponent,
|
||||
@@ -48,8 +47,6 @@ import {
|
||||
} from './widgets/edgeless-zoom-toolbar/index.js';
|
||||
import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js';
|
||||
import { EdgelessZoomToolbar } from './widgets/edgeless-zoom-toolbar/zoom-toolbar.js';
|
||||
import { AffineImageToolbar } from './widgets/image-toolbar/components/image-toolbar.js';
|
||||
import { AFFINE_IMAGE_TOOLBAR_WIDGET } from './widgets/image-toolbar/index.js';
|
||||
import {
|
||||
AFFINE_INNER_MODAL_WIDGET,
|
||||
AffineInnerModalWidget,
|
||||
@@ -109,7 +106,6 @@ function registerWidgets() {
|
||||
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
|
||||
AffinePageDraggingAreaWidget
|
||||
);
|
||||
customElements.define(AFFINE_IMAGE_TOOLBAR_WIDGET, AffineImageToolbarWidget);
|
||||
customElements.define(
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
AffineViewportOverlayWidget
|
||||
@@ -146,7 +142,6 @@ function registerMiscComponents() {
|
||||
customElements.define('affine-template-loading', AffineTemplateLoading);
|
||||
|
||||
// Toolbar and UI components
|
||||
customElements.define('affine-image-toolbar', AffineImageToolbar);
|
||||
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
|
||||
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
|
||||
customElements.define('overlay-scrollbar', OverlayScrollbar);
|
||||
@@ -200,10 +195,8 @@ declare global {
|
||||
'affine-page-root': PageRootBlockComponent;
|
||||
'zoom-bar-toggle-button': ZoomBarToggleButton;
|
||||
'edgeless-zoom-toolbar': EdgelessZoomToolbar;
|
||||
'affine-image-toolbar': AffineImageToolbar;
|
||||
|
||||
[AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET]: AffineEdgelessZoomToolbarWidget;
|
||||
[AFFINE_IMAGE_TOOLBAR_WIDGET]: AffineImageToolbarWidget;
|
||||
[AFFINE_INNER_MODAL_WIDGET]: AffineInnerModalWidget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import type {
|
||||
EditorIconButton,
|
||||
MenuItemGroup,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import { renderGroups } from '@blocksuite/affine-components/toolbar';
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { html, LitElement } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { ImageToolbarContext } from '../context.js';
|
||||
import { styles } from '../styles.js';
|
||||
|
||||
export class AffineImageToolbar extends LitElement {
|
||||
static override styles = styles;
|
||||
|
||||
private _currentOpenMenu: AbortController | null = null;
|
||||
|
||||
private _popMenuAbortController: AbortController | null = null;
|
||||
|
||||
closeCurrentMenu = () => {
|
||||
if (this._currentOpenMenu && !this._currentOpenMenu.signal.aborted) {
|
||||
this._currentOpenMenu.abort();
|
||||
this._currentOpenMenu = null;
|
||||
}
|
||||
};
|
||||
|
||||
private _clearPopMenu() {
|
||||
if (this._popMenuAbortController) {
|
||||
this._popMenuAbortController.abort();
|
||||
this._popMenuAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMoreMenu() {
|
||||
// If the menu we're trying to open is already open, return
|
||||
if (
|
||||
this._currentOpenMenu &&
|
||||
!this._currentOpenMenu.signal.aborted &&
|
||||
this._currentOpenMenu === this._popMenuAbortController
|
||||
) {
|
||||
this.closeCurrentMenu();
|
||||
this._moreMenuOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeCurrentMenu();
|
||||
this._popMenuAbortController = new AbortController();
|
||||
this._popMenuAbortController.signal.addEventListener('abort', () => {
|
||||
this._moreMenuOpen = false;
|
||||
this.onActiveStatusChange(false);
|
||||
});
|
||||
this.onActiveStatusChange(true);
|
||||
|
||||
this._currentOpenMenu = this._popMenuAbortController;
|
||||
|
||||
if (!this._moreButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
createLitPortal({
|
||||
template: html`
|
||||
<editor-menu-content
|
||||
data-show
|
||||
class="image-more-popup-menu"
|
||||
style=${styleMap({
|
||||
'--content-padding': '8px',
|
||||
'--packed-height': '4px',
|
||||
})}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${renderGroups(this.moreGroups, this.context)}
|
||||
</div>
|
||||
</editor-menu-content>
|
||||
`,
|
||||
container: this.context.host,
|
||||
// stacking-context(editor-host)
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
computePosition: {
|
||||
referenceElement: this._moreButton,
|
||||
placement: 'bottom-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
autoUpdate: { animationFrame: true },
|
||||
},
|
||||
abortController: this._popMenuAbortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
this._moreMenuOpen = true;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.closeCurrentMenu();
|
||||
this._clearPopMenu();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<editor-toolbar class="affine-image-toolbar-container" data-without-bg>
|
||||
${renderGroups(this.primaryGroups, this.context)}
|
||||
<editor-icon-button
|
||||
class="image-toolbar-button more"
|
||||
aria-label="More"
|
||||
.tooltip=${'More'}
|
||||
.tooltipOffset=${4}
|
||||
.showTooltip=${!this._moreMenuOpen}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => this._toggleMoreMenu()}
|
||||
>
|
||||
${MoreVerticalIcon()}
|
||||
</editor-icon-button>
|
||||
</editor-toolbar>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('editor-icon-button.more')
|
||||
private accessor _moreButton!: EditorIconButton;
|
||||
|
||||
@state()
|
||||
private accessor _moreMenuOpen = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor context!: ImageToolbarContext;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor moreGroups!: MenuItemGroup<ImageToolbarContext>[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onActiveStatusChange: (active: boolean) => void = noop;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor primaryGroups!: MenuItemGroup<ImageToolbarContext>[];
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
CaptionIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
||||
import { BookmarkIcon, DuplicateIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
import type { ImageToolbarContext } from './context.js';
|
||||
import { duplicate } from './utils.js';
|
||||
|
||||
export const PRIMARY_GROUPS: MenuItemGroup<ImageToolbarContext>[] = [
|
||||
{
|
||||
type: 'primary',
|
||||
items: [
|
||||
{
|
||||
type: 'download',
|
||||
label: 'Download',
|
||||
icon: DownloadIcon,
|
||||
generate: ({ blockComponent }) => {
|
||||
return {
|
||||
action: () => {
|
||||
blockComponent.download();
|
||||
},
|
||||
render: item => html`
|
||||
<editor-icon-button
|
||||
class="image-toolbar-button download"
|
||||
aria-label=${ifDefined(item.label)}
|
||||
.tooltip=${item.label}
|
||||
.tooltipOffset=${4}
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
>
|
||||
${item.icon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'caption',
|
||||
label: 'Caption',
|
||||
icon: CaptionIcon,
|
||||
when: ({ doc }) => !doc.readonly,
|
||||
generate: ({ blockComponent }) => {
|
||||
return {
|
||||
action: () => {
|
||||
blockComponent.captionEditor?.show();
|
||||
},
|
||||
render: item => html`
|
||||
<editor-icon-button
|
||||
class="image-toolbar-button caption"
|
||||
aria-label=${ifDefined(item.label)}
|
||||
.tooltip=${item.label}
|
||||
.tooltipOffset=${4}
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
>
|
||||
${item.icon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Clipboard Group
|
||||
export const clipboardGroup: MenuItemGroup<ImageToolbarContext> = {
|
||||
type: 'clipboard',
|
||||
items: [
|
||||
{
|
||||
type: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon,
|
||||
action: ({ blockComponent, close }) => {
|
||||
blockComponent.copy();
|
||||
close();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
icon: DuplicateIcon(),
|
||||
when: ({ doc }) => !doc.readonly,
|
||||
action: ({ blockComponent, abortController }) => {
|
||||
duplicate(blockComponent, abortController);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Conversions Group
|
||||
export const conversionsGroup: MenuItemGroup<ImageToolbarContext> = {
|
||||
type: 'conversions',
|
||||
items: [
|
||||
{
|
||||
label: 'Turn into card view',
|
||||
type: 'turn-into-card-view',
|
||||
icon: BookmarkIcon(),
|
||||
when: ({ doc, blockComponent }) => {
|
||||
const supportAttachment =
|
||||
doc.schema.flavourSchemaMap.has('affine:attachment');
|
||||
const readonly = doc.readonly;
|
||||
return supportAttachment && !readonly && !!blockComponent.blob;
|
||||
},
|
||||
action: ({ blockComponent, close }) => {
|
||||
blockComponent.convertToCardView();
|
||||
close();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Delete Group
|
||||
export const deleteGroup: MenuItemGroup<ImageToolbarContext> = {
|
||||
type: 'delete',
|
||||
items: [
|
||||
{
|
||||
type: 'delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon,
|
||||
when: ({ doc }) => !doc.readonly,
|
||||
action: ({ doc, blockComponent, close }) => {
|
||||
doc.deleteBlock(blockComponent.model);
|
||||
close();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const MORE_GROUPS: MenuItemGroup<ImageToolbarContext>[] = [
|
||||
clipboardGroup,
|
||||
conversionsGroup,
|
||||
deleteGroup,
|
||||
];
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { MenuContext } from '@blocksuite/affine-components/toolbar';
|
||||
|
||||
export class ImageToolbarContext extends MenuContext {
|
||||
override close = () => {
|
||||
this.abortController.abort();
|
||||
};
|
||||
|
||||
get doc() {
|
||||
return this.blockComponent.doc;
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.blockComponent.host;
|
||||
}
|
||||
|
||||
get selectedBlockModels() {
|
||||
return [this.blockComponent.model];
|
||||
}
|
||||
|
||||
get std() {
|
||||
return this.blockComponent.std;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public blockComponent: ImageBlockComponent,
|
||||
public abortController: AbortController
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMultiple() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSingle() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import type {
|
||||
AdvancedMenuItem,
|
||||
MenuItemGroup,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import {
|
||||
cloneGroups,
|
||||
getMoreMenuConfig,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import type { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import { PAGE_HEADER_HEIGHT } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
BlockSelection,
|
||||
TextSelection,
|
||||
WidgetComponent,
|
||||
} from '@blocksuite/std';
|
||||
import { limitShift, shift } from '@floating-ui/dom';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { MORE_GROUPS, PRIMARY_GROUPS } from './config.js';
|
||||
import { ImageToolbarContext } from './context.js';
|
||||
|
||||
export const AFFINE_IMAGE_TOOLBAR_WIDGET = 'affine-image-toolbar-widget';
|
||||
|
||||
export class AffineImageToolbarWidget extends WidgetComponent<
|
||||
ImageBlockModel,
|
||||
ImageBlockComponent
|
||||
> {
|
||||
private _hoverController: HoverController | null = null;
|
||||
|
||||
private _isActivated = false;
|
||||
|
||||
private readonly _setHoverController = () => {
|
||||
this._hoverController = null;
|
||||
this._hoverController = new HoverController(
|
||||
this,
|
||||
({ abortController }) => {
|
||||
const imageBlock = this.block;
|
||||
if (!imageBlock) {
|
||||
return null;
|
||||
}
|
||||
const selection = this.host.selection;
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (
|
||||
!!textSelection &&
|
||||
(!!textSelection.to || !!textSelection.from.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blockSelections = selection.filter(BlockSelection);
|
||||
if (
|
||||
blockSelections.length > 1 ||
|
||||
(blockSelections.length === 1 &&
|
||||
blockSelections[0].blockId !== imageBlock.blockId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageContainer =
|
||||
imageBlock.resizableImg ?? imageBlock.fallbackCard;
|
||||
if (!imageContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context = new ImageToolbarContext(imageBlock, abortController);
|
||||
|
||||
return {
|
||||
template: html`<affine-image-toolbar
|
||||
.context=${context}
|
||||
.primaryGroups=${this.primaryGroups}
|
||||
.moreGroups=${this.moreGroups}
|
||||
.onActiveStatusChange=${(active: boolean) => {
|
||||
this._isActivated = active;
|
||||
if (!active && !this._hoverController?.isHovering) {
|
||||
this._hoverController?.abort();
|
||||
}
|
||||
}}
|
||||
></affine-image-toolbar>`,
|
||||
container: this.block,
|
||||
// stacking-context(editor-host)
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
computePosition: {
|
||||
referenceElement: imageContainer,
|
||||
placement: 'right-start',
|
||||
middleware: [
|
||||
shift({
|
||||
crossAxis: true,
|
||||
padding: {
|
||||
top: PAGE_HEADER_HEIGHT + 12,
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
},
|
||||
limiter: limitShift(),
|
||||
}),
|
||||
],
|
||||
autoUpdate: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
{ allowMultiple: true }
|
||||
);
|
||||
|
||||
const imageBlock = this.block;
|
||||
if (!imageBlock) {
|
||||
return;
|
||||
}
|
||||
this._hoverController.setReference(imageBlock);
|
||||
this._hoverController.onAbort = () => {
|
||||
// If the more menu is opened, don't close it.
|
||||
if (this._isActivated) return;
|
||||
this._hoverController?.abort();
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
addMoreItems = (
|
||||
items: AdvancedMenuItem<ImageToolbarContext>[],
|
||||
index?: number,
|
||||
type?: string
|
||||
) => {
|
||||
let group;
|
||||
if (type) {
|
||||
group = this.moreGroups.find(g => g.type === type);
|
||||
}
|
||||
if (!group) {
|
||||
group = this.moreGroups[0];
|
||||
}
|
||||
|
||||
if (index === undefined) {
|
||||
group.items.push(...items);
|
||||
return this;
|
||||
}
|
||||
|
||||
group.items.splice(index, 0, ...items);
|
||||
return this;
|
||||
};
|
||||
|
||||
addPrimaryItems = (
|
||||
items: AdvancedMenuItem<ImageToolbarContext>[],
|
||||
index?: number
|
||||
) => {
|
||||
if (index === undefined) {
|
||||
this.primaryGroups[0].items.push(...items);
|
||||
return this;
|
||||
}
|
||||
|
||||
this.primaryGroups[0].items.splice(index, 0, ...items);
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
* Caches the more menu items.
|
||||
* Currently only supports configuring more menu.
|
||||
*/
|
||||
moreGroups: MenuItemGroup<ImageToolbarContext>[] = cloneGroups(MORE_GROUPS);
|
||||
|
||||
primaryGroups: MenuItemGroup<ImageToolbarContext>[] =
|
||||
cloneGroups(PRIMARY_GROUPS);
|
||||
|
||||
override firstUpdated() {
|
||||
if (this.doc.getParent(this.model.id)?.flavour === 'affine:surface') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
|
||||
this._setHoverController();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
}
|
||||
|
||||
.affine-image-toolbar-container {
|
||||
height: 24px;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.image-toolbar-button {
|
||||
color: var(--affine-icon-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import {
|
||||
getBlockProps,
|
||||
isInsidePageEditor,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
|
||||
export function duplicate(
|
||||
block: ImageBlockComponent,
|
||||
abortController?: AbortController
|
||||
) {
|
||||
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
|
||||
);
|
||||
abortController?.abort();
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js';
|
||||
export { AffineImageToolbarWidget } from './image-toolbar/index.js';
|
||||
export { AffineInnerModalWidget } from './inner-modal/inner-modal.js';
|
||||
export * from './keyboard-toolbar/index.js';
|
||||
export {
|
||||
|
||||
@@ -192,11 +192,8 @@ abstract class ToolbarContextBase {
|
||||
};
|
||||
|
||||
const getFromMessage = () => {
|
||||
const msgEle = this.message$.peek()?.element;
|
||||
if (msgEle instanceof BlockComponent) {
|
||||
return msgEle;
|
||||
}
|
||||
return null;
|
||||
const block = this.message$.peek()?.element;
|
||||
return block instanceof BlockComponent ? block : null;
|
||||
};
|
||||
|
||||
return getFromSelection() ?? getFromMessage();
|
||||
@@ -234,11 +231,8 @@ abstract class ToolbarContextBase {
|
||||
};
|
||||
|
||||
const getFromMessage = () => {
|
||||
const msgEle = this.message$.peek()?.element;
|
||||
if (msgEle instanceof BlockComponent) {
|
||||
return msgEle.model;
|
||||
}
|
||||
return null;
|
||||
const block = this.message$.peek()?.element;
|
||||
return block instanceof BlockComponent ? block.model : null;
|
||||
};
|
||||
|
||||
return getFromSelection() ?? getFromMessage();
|
||||
|
||||
@@ -88,6 +88,7 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
box-sizing: border-box;
|
||||
gap: 4px;
|
||||
|
||||
.inner-button,
|
||||
editor-icon-button,
|
||||
editor-menu-button {
|
||||
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
|
||||
|
||||
Reference in New Issue
Block a user