mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(editor): edgeless toolbar more actions (#10882)
This commit is contained in:
@@ -30,8 +30,8 @@ import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
|
|||||||
import type { ExtensionType } from '@blocksuite/store';
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import { EdgelessRootBlockComponent } from '../..';
|
|
||||||
import { mountFrameTitleEditor } from '../../utils/text';
|
import { mountFrameTitleEditor } from '../../utils/text';
|
||||||
|
import { getEdgelessWith } from './utils';
|
||||||
|
|
||||||
const builtinSurfaceToolbarConfig = {
|
const builtinSurfaceToolbarConfig = {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -87,15 +87,8 @@ const builtinSurfaceToolbarConfig = {
|
|||||||
const model = ctx.getCurrentModelByType(FrameBlockModel);
|
const model = ctx.getCurrentModelByType(FrameBlockModel);
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mountFrameTitleEditor(model, edgeless);
|
mountFrameTitleEditor(model, edgeless);
|
||||||
},
|
},
|
||||||
@@ -108,15 +101,8 @@ const builtinSurfaceToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModelsByType(FrameBlockModel);
|
const models = ctx.getSurfaceModelsByType(FrameBlockModel);
|
||||||
if (!models.length) return;
|
if (!models.length) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.store.captureSync();
|
ctx.store.captureSync();
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { matchModels } from '@blocksuite/affine-shared/utils';
|
|||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
|
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
|
||||||
|
|
||||||
import { EdgelessRootBlockComponent } from '../..';
|
|
||||||
import { mountGroupTitleEditor } from '../../utils/text';
|
import { mountGroupTitleEditor } from '../../utils/text';
|
||||||
|
import { getEdgelessWith } from './utils';
|
||||||
|
|
||||||
export const builtinGroupToolbarConfig = {
|
export const builtinGroupToolbarConfig = {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -69,15 +69,8 @@ export const builtinGroupToolbarConfig = {
|
|||||||
const model = ctx.getCurrentModelByType(GroupElementModel);
|
const model = ctx.getCurrentModelByType(GroupElementModel);
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mountGroupTitleEditor(model, edgeless);
|
mountGroupTitleEditor(model, edgeless);
|
||||||
},
|
},
|
||||||
@@ -90,15 +83,8 @@ export const builtinGroupToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModelsByType(GroupElementModel);
|
const models = ctx.getSurfaceModelsByType(GroupElementModel);
|
||||||
if (!models.length) return;
|
if (!models.length) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
edgeless.service.ungroup(model);
|
edgeless.service.ungroup(model);
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ import {
|
|||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import { EdgelessRootBlockComponent } from '../..';
|
|
||||||
import { renderAlignmentMenu } from './alignment';
|
import { renderAlignmentMenu } from './alignment';
|
||||||
|
import { moreActions } from './more';
|
||||||
|
import { getEdgelessWith } from './utils';
|
||||||
|
|
||||||
export const builtinMiscToolbarConfig = {
|
export const builtinMiscToolbarConfig = {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -86,15 +87,8 @@ export const builtinMiscToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModels();
|
const models = ctx.getSurfaceModels();
|
||||||
if (models.length < 2) return;
|
if (models.length < 2) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = edgeless.service.frame.createFrameOnSelected();
|
const frame = edgeless.service.frame.createFrameOnSelected();
|
||||||
if (!frame) return;
|
if (!frame) return;
|
||||||
@@ -135,15 +129,8 @@ export const builtinMiscToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModels();
|
const models = ctx.getSurfaceModels();
|
||||||
if (models.length < 2) return;
|
if (models.length < 2) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(@fundon): should be a command
|
// TODO(@fundon): should be a command
|
||||||
edgeless.service.createGroupFromSelected();
|
edgeless.service.createGroupFromSelected();
|
||||||
@@ -155,7 +142,6 @@ export const builtinMiscToolbarConfig = {
|
|||||||
when(ctx) {
|
when(ctx) {
|
||||||
const models = ctx.getSurfaceModels();
|
const models = ctx.getSurfaceModels();
|
||||||
if (models.length < 2) return false;
|
if (models.length < 2) return false;
|
||||||
if (models.some(model => model.isLocked())) return false;
|
|
||||||
if (models.some(model => model.group instanceof MindmapElementModel))
|
if (models.some(model => model.group instanceof MindmapElementModel))
|
||||||
return false;
|
return false;
|
||||||
if (
|
if (
|
||||||
@@ -227,15 +213,8 @@ export const builtinMiscToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModels();
|
const models = ctx.getSurfaceModels();
|
||||||
if (!models.length) return;
|
if (!models.length) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get most top selected elements(*) from tree, like in a tree below
|
// get most top selected elements(*) from tree, like in a tree below
|
||||||
// G0
|
// G0
|
||||||
@@ -318,6 +297,12 @@ export const builtinMiscToolbarConfig = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// More actions
|
||||||
|
...moreActions.map(action => ({
|
||||||
|
...action,
|
||||||
|
placement: ActionPlacement.More,
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
when(ctx) {
|
when(ctx) {
|
||||||
const models = ctx.getSurfaceModels();
|
const models = ctx.getSurfaceModels();
|
||||||
@@ -336,15 +321,8 @@ export const builtinLockedToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModels();
|
const models = ctx.getSurfaceModels();
|
||||||
if (!models.length) return;
|
if (!models.length) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = new Set(
|
const elements = new Set(
|
||||||
models.map(model =>
|
models.map(model =>
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import { AttachmentBlockComponent } from '@blocksuite/affine-block-attachment';
|
||||||
|
import { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark';
|
||||||
|
import {
|
||||||
|
isExternalEmbedBlockComponent,
|
||||||
|
notifyDocCreated,
|
||||||
|
promptDocTitle,
|
||||||
|
} from '@blocksuite/affine-block-embed';
|
||||||
|
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||||
|
import { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||||
|
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
|
import {
|
||||||
|
AttachmentBlockModel,
|
||||||
|
BookmarkBlockModel,
|
||||||
|
EmbedLinkedDocBlockSchema,
|
||||||
|
EmbedLinkedDocModel,
|
||||||
|
EmbedSyncedDocBlockSchema,
|
||||||
|
EmbedSyncedDocModel,
|
||||||
|
FrameBlockModel,
|
||||||
|
ImageBlockModel,
|
||||||
|
isExternalEmbedModel,
|
||||||
|
NoteBlockModel,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
|
import type {
|
||||||
|
ToolbarActions,
|
||||||
|
ToolbarContext,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import { type ReorderingType } from '@blocksuite/affine-shared/utils';
|
||||||
|
import type { BlockComponent } from '@blocksuite/block-std';
|
||||||
|
import { GfxBlockElementModel, type GfxModel } from '@blocksuite/block-std/gfx';
|
||||||
|
import { Bound, getCommonBoundWithRotation } from '@blocksuite/global/gfx';
|
||||||
|
import {
|
||||||
|
ArrowDownBigBottomIcon,
|
||||||
|
ArrowDownBigIcon,
|
||||||
|
ArrowUpBigIcon,
|
||||||
|
ArrowUpBigTopIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
DuplicateIcon,
|
||||||
|
FrameIcon,
|
||||||
|
GroupIcon,
|
||||||
|
LinkedPageIcon,
|
||||||
|
ResetIcon,
|
||||||
|
} from '@blocksuite/icons/lit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createLinkedDocFromEdgelessElements,
|
||||||
|
createLinkedDocFromNote,
|
||||||
|
} from '../../../widgets/element-toolbar/more-menu/render-linked-doc';
|
||||||
|
import { duplicate } from '../../utils/clipboard-utils';
|
||||||
|
import { getSortedCloneElements } from '../../utils/clone-utils';
|
||||||
|
import { moveConnectors } from '../../utils/connector';
|
||||||
|
import { deleteElements } from '../../utils/crud';
|
||||||
|
import { getEdgelessWith } from './utils';
|
||||||
|
|
||||||
|
export const moreActions = [
|
||||||
|
// Selection Group: frame & group
|
||||||
|
{
|
||||||
|
id: 'Z.a.selection',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'a.create-frame',
|
||||||
|
label: 'Frame section',
|
||||||
|
icon: FrameIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const frame = ctx.std
|
||||||
|
.get(EdgelessFrameManagerIdentifier)
|
||||||
|
.createFrameOnSelected();
|
||||||
|
if (!frame) return;
|
||||||
|
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh));
|
||||||
|
|
||||||
|
ctx.track('CanvasElementAdded', {
|
||||||
|
control: 'context-menu',
|
||||||
|
type: 'frame',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b.create-group',
|
||||||
|
label: 'Group section',
|
||||||
|
icon: GroupIcon(),
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (models.length === 0) return false;
|
||||||
|
return !models.some(model => ctx.matchModel(model, FrameBlockModel));
|
||||||
|
},
|
||||||
|
run(ctx) {
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
edgeless.service.createGroupFromSelected();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reordering Group
|
||||||
|
{
|
||||||
|
id: 'Z.b.reordering',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'a.bring-to-front',
|
||||||
|
label: 'Bring to Front',
|
||||||
|
icon: ArrowUpBigTopIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
reorderElements(ctx, models, 'front');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b.bring-forward',
|
||||||
|
label: 'Bring Forward',
|
||||||
|
icon: ArrowUpBigIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
reorderElements(ctx, models, 'forward');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c.send-backward',
|
||||||
|
label: 'Send Backward',
|
||||||
|
icon: ArrowDownBigIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
reorderElements(ctx, models, 'backward');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c.send-to-back',
|
||||||
|
label: 'Send to Back',
|
||||||
|
icon: ArrowDownBigBottomIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
reorderElements(ctx, models, 'back');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clipboard Group
|
||||||
|
// Uses the same `ID` for both page and edgeless modes.
|
||||||
|
{
|
||||||
|
id: 'a.clipboard',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'copy',
|
||||||
|
label: 'Copy',
|
||||||
|
icon: CopyIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (!models.length) return;
|
||||||
|
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
edgeless.clipboardController.copy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duplicate',
|
||||||
|
label: 'Duplicate',
|
||||||
|
icon: DuplicateIcon(),
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (!models.length) return;
|
||||||
|
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
duplicate(edgeless, models).catch(console.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reload',
|
||||||
|
label: 'Reload',
|
||||||
|
icon: ResetIcon(),
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (models.length === 0) return false;
|
||||||
|
return models.every(isRefreshableModel);
|
||||||
|
},
|
||||||
|
run(ctx) {
|
||||||
|
const blocks = ctx
|
||||||
|
.getSurfaceModels()
|
||||||
|
.map(model => ctx.view.getBlock(model.id))
|
||||||
|
.filter(isRefreshableBlock);
|
||||||
|
|
||||||
|
if (!blocks.length) return;
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
block.refreshData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Conversions Group
|
||||||
|
{
|
||||||
|
id: 'd.conversions',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'a.turn-into-linked-doc',
|
||||||
|
label: 'Turn into linked doc',
|
||||||
|
icon: LinkedPageIcon(),
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (models.length !== 1) return false;
|
||||||
|
return ctx.matchModel(models[0], NoteBlockModel);
|
||||||
|
},
|
||||||
|
run(ctx) {
|
||||||
|
const model = ctx.getCurrentModelByType(NoteBlockModel);
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
const title = await promptDocTitle(ctx.std);
|
||||||
|
if (title === null) return;
|
||||||
|
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
const surfaceId = edgeless.surfaceBlockModel.id;
|
||||||
|
if (!surfaceId) return;
|
||||||
|
|
||||||
|
const linkedDoc = createLinkedDocFromNote(ctx.store, model, title);
|
||||||
|
|
||||||
|
// Inserts linked doc card
|
||||||
|
const cardId = ctx.std.get(EdgelessCRUDIdentifier).addBlock(
|
||||||
|
EmbedSyncedDocBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
xywh: model.xywh,
|
||||||
|
style: 'syncedDoc',
|
||||||
|
pageId: linkedDoc.id,
|
||||||
|
index: model.index,
|
||||||
|
},
|
||||||
|
surfaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.track('CanvasElementAdded', {
|
||||||
|
control: 'context-menu',
|
||||||
|
type: 'embed-synced-doc',
|
||||||
|
});
|
||||||
|
ctx.track('DocCreated', {
|
||||||
|
control: 'turn into linked doc',
|
||||||
|
type: 'embed-linked-doc',
|
||||||
|
});
|
||||||
|
ctx.track('LinkedDocCreated', {
|
||||||
|
control: 'turn into linked doc',
|
||||||
|
type: 'embed-linked-doc',
|
||||||
|
other: 'new doc',
|
||||||
|
});
|
||||||
|
|
||||||
|
moveConnectors(model.id, cardId, edgeless.service);
|
||||||
|
|
||||||
|
// Deletes selected note
|
||||||
|
ctx.store.transact(() => {
|
||||||
|
ctx.store.deleteBlock(model);
|
||||||
|
});
|
||||||
|
ctx.gfx.selection.set({
|
||||||
|
elements: [cardId],
|
||||||
|
editing: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
create().catch(console.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b.create-linked-doc',
|
||||||
|
label: 'Create linked doc',
|
||||||
|
icon: LinkedPageIcon(),
|
||||||
|
when(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (models.length === 0) return false;
|
||||||
|
if (models.length === 1) {
|
||||||
|
return ![
|
||||||
|
NoteBlockModel,
|
||||||
|
EmbedLinkedDocModel,
|
||||||
|
EmbedSyncedDocModel,
|
||||||
|
].some(k => ctx.matchModel(models[0], k));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (!models.length) return;
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
const surfaceId = edgeless.surfaceBlockModel.id;
|
||||||
|
if (!surfaceId) return;
|
||||||
|
|
||||||
|
const title = await promptDocTitle(ctx.std);
|
||||||
|
if (title === null) return;
|
||||||
|
|
||||||
|
const clonedModels = getSortedCloneElements(models);
|
||||||
|
const linkedDoc = createLinkedDocFromEdgelessElements(
|
||||||
|
ctx.host,
|
||||||
|
clonedModels,
|
||||||
|
title
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.store.transact(() => {
|
||||||
|
deleteElements(edgeless, clonedModels);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inserts linked doc card
|
||||||
|
const width = 364;
|
||||||
|
const height = 390;
|
||||||
|
const bound = getCommonBoundWithRotation(clonedModels);
|
||||||
|
const cardId = ctx.std.get(EdgelessCRUDIdentifier).addBlock(
|
||||||
|
EmbedLinkedDocBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
xywh: `[${bound.center[0] - width / 2}, ${bound.center[1] - height / 2}, ${width}, ${height}]`,
|
||||||
|
style: 'vertical',
|
||||||
|
pageId: linkedDoc.id,
|
||||||
|
},
|
||||||
|
surfaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.gfx.selection.set({
|
||||||
|
elements: [cardId],
|
||||||
|
editing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.track('CanvasElementAdded', {
|
||||||
|
control: 'context-menu',
|
||||||
|
type: 'embed-linked-doc',
|
||||||
|
});
|
||||||
|
ctx.track('DocCreated', {
|
||||||
|
control: 'create linked doc',
|
||||||
|
type: 'embed-linked-doc',
|
||||||
|
});
|
||||||
|
ctx.track('LinkedDocCreated', {
|
||||||
|
control: 'create linked doc',
|
||||||
|
type: 'embed-linked-doc',
|
||||||
|
other: 'new doc',
|
||||||
|
});
|
||||||
|
|
||||||
|
notifyDocCreated(ctx.std, ctx.store);
|
||||||
|
};
|
||||||
|
|
||||||
|
create().catch(console.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deleting Group
|
||||||
|
{
|
||||||
|
id: 'e.delete',
|
||||||
|
label: 'Delete',
|
||||||
|
icon: DeleteIcon(),
|
||||||
|
variant: 'destructive',
|
||||||
|
run(ctx) {
|
||||||
|
const models = ctx.getSurfaceModels();
|
||||||
|
if (!models.length) return;
|
||||||
|
|
||||||
|
const edgeless = getEdgelessWith(ctx);
|
||||||
|
if (!edgeless) return;
|
||||||
|
|
||||||
|
ctx.store.captureSync();
|
||||||
|
|
||||||
|
deleteElements(edgeless, models);
|
||||||
|
|
||||||
|
// Clears
|
||||||
|
ctx.select('surface');
|
||||||
|
ctx.reset();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const satisfies ToolbarActions;
|
||||||
|
|
||||||
|
function reorderElements(
|
||||||
|
ctx: ToolbarContext,
|
||||||
|
models: GfxModel[],
|
||||||
|
type: ReorderingType
|
||||||
|
) {
|
||||||
|
if (!models.length) return;
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
const index = ctx.gfx.layer.getReorderedIndex(model, type);
|
||||||
|
|
||||||
|
// block should be updated in transaction
|
||||||
|
if (model instanceof GfxBlockElementModel) {
|
||||||
|
ctx.store.transact(() => {
|
||||||
|
model.index = index;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
model.index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRefreshableModel(model: GfxModel) {
|
||||||
|
return (
|
||||||
|
model instanceof AttachmentBlockModel ||
|
||||||
|
model instanceof BookmarkBlockModel ||
|
||||||
|
model instanceof ImageBlockModel ||
|
||||||
|
isExternalEmbedModel(model)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRefreshableBlock(block: BlockComponent | null) {
|
||||||
|
return (
|
||||||
|
!!block &&
|
||||||
|
(block instanceof AttachmentBlockComponent ||
|
||||||
|
block instanceof BookmarkBlockComponent ||
|
||||||
|
block instanceof ImageBlockComponent ||
|
||||||
|
isExternalEmbedBlockComponent(block))
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
|
|||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
import { EdgelessRootBlockComponent, type ShapeToolOption } from '../..';
|
import type { ShapeToolOption } from '../..';
|
||||||
import { ShapeComponentConfig } from '../../components/toolbar/shape/shape-menu-config';
|
import { ShapeComponentConfig } from '../../components/toolbar/shape/shape-menu-config';
|
||||||
import { mountShapeTextEditor } from '../../utils/text';
|
import { mountShapeTextEditor } from '../../utils/text';
|
||||||
import { LINE_STYLE_LIST } from './consts';
|
import { LINE_STYLE_LIST } from './consts';
|
||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
createMindmapStyleActionMenu,
|
createMindmapStyleActionMenu,
|
||||||
} from './mindmap';
|
} from './mindmap';
|
||||||
import { createTextActions } from './text-common';
|
import { createTextActions } from './text-common';
|
||||||
import { renderMenu } from './utils';
|
import { getEdgelessWith, renderMenu } from './utils';
|
||||||
|
|
||||||
export const builtinShapeToolbarConfig = {
|
export const builtinShapeToolbarConfig = {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -318,15 +318,8 @@ export const builtinShapeToolbarConfig = {
|
|||||||
const model = ctx.getCurrentModelByType(ShapeElementModel);
|
const model = ctx.getCurrentModelByType(ShapeElementModel);
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
|
|
||||||
const rootModel = ctx.store.root;
|
const edgeless = getEdgelessWith(ctx);
|
||||||
if (!rootModel) return;
|
if (!edgeless) return;
|
||||||
|
|
||||||
// TODO(@fundon): it should be simple
|
|
||||||
const edgeless = ctx.view.getBlock(rootModel.id);
|
|
||||||
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
|
||||||
console.error('edgeless view is not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mountShapeTextEditor(model, edgeless);
|
mountShapeTextEditor(model, edgeless);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import type { ToolbarContext } from '@blocksuite/affine-shared/services';
|
||||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
|
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
|
import { EdgelessRootBlockComponent } from '../..';
|
||||||
import type { Menu, MenuItem } from './types';
|
import type { Menu, MenuItem } from './types';
|
||||||
|
|
||||||
export function renderCurrentMenuItemWith<T, F extends keyof MenuItem<T>>(
|
export function renderCurrentMenuItemWith<T, F extends keyof MenuItem<T>>(
|
||||||
@@ -60,3 +62,17 @@ export function renderMenuItems<T>(
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(@fundon): it should be simple
|
||||||
|
export function getEdgelessWith(ctx: ToolbarContext) {
|
||||||
|
const rootModel = ctx.store.root;
|
||||||
|
if (!rootModel) return;
|
||||||
|
|
||||||
|
const edgeless = ctx.view.getBlock(rootModel.id);
|
||||||
|
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
|
||||||
|
console.error('edgeless view is not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return edgeless;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
offset,
|
offset,
|
||||||
shift,
|
shift,
|
||||||
} from '@floating-ui/dom';
|
} from '@floating-ui/dom';
|
||||||
import { html, render, type TemplateResult } from 'lit';
|
import { html, render } from 'lit';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { join } from 'lit/directives/join.js';
|
import { join } from 'lit/directives/join.js';
|
||||||
import { keyed } from 'lit/directives/keyed.js';
|
import { keyed } from 'lit/directives/keyed.js';
|
||||||
@@ -223,32 +223,31 @@ export function renderToolbar(
|
|||||||
context,
|
context,
|
||||||
renderMenuActionItem
|
renderMenuActionItem
|
||||||
);
|
);
|
||||||
// if (moreMenuItems.length) {
|
if (moreMenuItems.length) {
|
||||||
// TODO(@fundon): edgeless case needs to be considered
|
const key = `${context.getCurrentModel()?.id}`;
|
||||||
const key = `${context.getCurrentModel()?.id}`;
|
|
||||||
|
|
||||||
primaryActionGroup.push({
|
primaryActionGroup.push({
|
||||||
id: 'more',
|
id: 'more',
|
||||||
content: html`${keyed(
|
content: html`${keyed(
|
||||||
`${flavour}:${key}`,
|
`${flavour}:${key}`,
|
||||||
html`
|
html`
|
||||||
<editor-menu-button
|
<editor-menu-button
|
||||||
class="more-menu"
|
class="more-menu"
|
||||||
.contentPadding="${'8px'}"
|
.contentPadding="${'8px'}"
|
||||||
.button=${html`
|
.button=${html`
|
||||||
<editor-icon-button aria-label="More" .tooltip="${'More'}">
|
<editor-icon-button aria-label="More" .tooltip="${'More'}">
|
||||||
${MoreVerticalIcon()}
|
${MoreVerticalIcon()}
|
||||||
</editor-icon-button>
|
</editor-icon-button>
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div data-size="large" data-orientation="vertical">
|
<div data-size="large" data-orientation="vertical">
|
||||||
${join(moreMenuItems, renderToolbarSeparator('horizontal'))}
|
${join(moreMenuItems, renderToolbarSeparator('horizontal'))}
|
||||||
</div>
|
</div>
|
||||||
</editor-menu-button>
|
</editor-menu-button>
|
||||||
`
|
`
|
||||||
)}`,
|
)}`,
|
||||||
});
|
});
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@@ -264,20 +263,18 @@ function renderActions(
|
|||||||
) {
|
) {
|
||||||
return actions
|
return actions
|
||||||
.map(action => {
|
.map(action => {
|
||||||
let content: TemplateResult | null = null;
|
|
||||||
if ('content' in action) {
|
if ('content' in action) {
|
||||||
if (typeof action.content === 'function') {
|
if (typeof action.content === 'function') {
|
||||||
content = action.content(context);
|
return action.content(context);
|
||||||
} else {
|
} else {
|
||||||
content = action.content ?? null;
|
return action.content ?? null;
|
||||||
}
|
}
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('actions' in action && action.actions.length) {
|
if ('actions' in action && action.actions.length) {
|
||||||
const combined = combine(action.actions, context);
|
const combined = combine(action.actions, context);
|
||||||
|
|
||||||
if (!combined.length) return content;
|
if (!combined.length) return null;
|
||||||
|
|
||||||
const ordered = orderBy(combined, ['id', 'score'], ['asc', 'asc']);
|
const ordered = orderBy(combined, ['id', 'score'], ['asc', 'asc']);
|
||||||
|
|
||||||
@@ -301,7 +298,7 @@ function renderActions(
|
|||||||
return render(action, context);
|
return render(action, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return null;
|
||||||
})
|
})
|
||||||
.filter(action => action !== null);
|
.filter(action => action !== null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user