refactor(editor): surface-ref slash menu config extension (#10681)

This commit is contained in:
L-Sun
2025-03-07 09:05:03 +00:00
parent d37868d97a
commit 83b05aca4d
11 changed files with 293 additions and 293 deletions
@@ -17,9 +17,10 @@
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@blocksuite/icons": "^2.2.4",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
@@ -0,0 +1,75 @@
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import {
type SlashMenuActionItem,
type SlashMenuConfig,
SlashMenuConfigExtension,
} from '@blocksuite/affine-widget-slash-menu';
import { FrameIcon, GroupingIcon } from '@blocksuite/icons/lit';
import { insertSurfaceRefBlockCommand } from '../commands';
import { EdgelessTooltip } from './tooltips';
const surfaceRefSlashMenuConfig: SlashMenuConfig = {
items: ({ std }) => {
let index = 0;
const surfaceModel = getSurfaceBlock(std.store);
if (!surfaceModel) return [];
const frameModels = std.store
.getBlocksByFlavour('affine:frame')
.map(block => block.model as FrameBlockModel);
const frameItems = frameModels.map<SlashMenuActionItem>(frameModel => ({
name: 'Frame: ' + frameModel.title,
icon: FrameIcon(),
group: `5_Document Group & Frame@${index++}`,
tooltip: {
figure: EdgelessTooltip,
caption: 'Edgeless',
},
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertSurfaceRefBlockCommand, {
reference: frameModel.id,
place: 'after',
removeEmptyLine: true,
})
.run();
},
}));
const groupElements = surfaceModel.getElementsByType('group');
const groupItems = groupElements.map<SlashMenuActionItem>(group => ({
name: 'Group: ' + group.title.toString(),
icon: GroupingIcon(),
group: `5_Document Group & Frame@${index++}`,
tooltip: {
figure: EdgelessTooltip,
caption: 'Edgeless',
},
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertSurfaceRefBlockCommand, {
reference: group.id,
place: 'after',
removeEmptyLine: true,
})
.run();
},
}));
return [...frameItems, ...groupItems];
},
};
export const SurfaceRefSlashMenuConfigExtension = SlashMenuConfigExtension(
'affine:surface-ref',
surfaceRefSlashMenuConfig
);
@@ -6,6 +6,8 @@ import {
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { SurfaceRefSlashMenuConfigExtension } from './configs/slash-menu';
export const surfaceRefToolbarWidget = WidgetViewExtension(
'affine:surface-ref',
'surfaceToolbar',
@@ -16,6 +18,7 @@ export const PageSurfaceRefBlockSpec: ExtensionType[] = [
FlavourExtension('affine:surface-ref'),
BlockViewExtension('affine:surface-ref', literal`affine-surface-ref`),
surfaceRefToolbarWidget,
SurfaceRefSlashMenuConfigExtension,
];
export const EdgelessSurfaceRefBlockSpec: ExtensionType[] = [
@@ -24,4 +27,5 @@ export const EdgelessSurfaceRefBlockSpec: ExtensionType[] = [
'affine:surface-ref',
literal`affine-edgeless-surface-ref`
),
SurfaceRefSlashMenuConfigExtension,
];
@@ -11,6 +11,7 @@
{ "path": "../components" },
{ "path": "../model" },
{ "path": "../shared" },
{ "path": "../widget-slash-menu" },
{ "path": "../../framework/block-std" },
{ "path": "../../framework/global" },
{ "path": "../../framework/inline" },
@@ -13,8 +13,6 @@
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-block-surface-ref": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
+208 -277
View File
@@ -1,15 +1,9 @@
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
import { toast } from '@blocksuite/affine-components/toast';
import type {
FrameBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import {
getInlineEditorByModel,
insertContent,
} from '@blocksuite/affine-rich-text';
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { createDefaultDoc } from '@blocksuite/affine-shared/utils';
import {
@@ -18,8 +12,6 @@ import {
CopyIcon,
DeleteIcon,
DualLinkIcon,
FrameIcon,
GroupingIcon,
LinkedPageIcon,
NowIcon,
PlusIcon,
@@ -31,289 +23,228 @@ import type { DeltaInsert } from '@blocksuite/inline';
import { Slice, Text } from '@blocksuite/store';
import { slashMenuToolTips } from './tooltips';
import type { SlashMenuActionItem, SlashMenuConfig } from './types';
import type { SlashMenuConfig } from './types';
import { formatDate, formatTime } from './utils';
// TODO(@L-Sun): This counter temporarily added variables for refactoring.
let index = 0;
export const defaultSlashMenuConfig: SlashMenuConfig = {
disableWhen: ({ model }) => {
return model.flavour === 'affine:code';
},
items: ({ std, model }) => [
{
name: 'New Doc',
description: 'Start a new document.',
icon: PlusIcon(),
tooltip: slashMenuToolTips['New Doc'],
group: `3_Page@${index++}`,
when: ({ model }) =>
model.doc.schema.flavourSchemaMap.has('affine:embed-linked-doc'),
action: ({ std, model }) => {
const newDoc = createDefaultDoc(std.host.doc.workspace);
insertContent(std.host, model, REFERENCE_NODE, {
reference: {
type: 'LinkedPage',
pageId: newDoc.id,
},
});
items: () => {
const now = new Date();
const tomorrow = new Date();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
tomorrow.setDate(tomorrow.getDate() + 1);
return [
{
name: 'New Doc',
description: 'Start a new document.',
icon: PlusIcon(),
tooltip: slashMenuToolTips['New Doc'],
group: '3_Page@0',
when: ({ model }) =>
model.doc.schema.flavourSchemaMap.has('affine:embed-linked-doc'),
action: ({ std, model }) => {
const newDoc = createDefaultDoc(std.host.doc.workspace);
insertContent(std.host, model, REFERENCE_NODE, {
reference: {
type: 'LinkedPage',
pageId: newDoc.id,
},
});
},
},
},
{
name: 'Linked Doc',
description: 'Link to another document.',
icon: LinkedPageIcon(),
tooltip: slashMenuToolTips['Linked Doc'],
searchAlias: ['dual link'],
group: `3_Page@${index++}`,
when: ({ std, model }) => {
const root = model.doc.root;
if (!root) return false;
const linkedDocWidget = std.view.getWidget(
'affine-linked-doc-widget',
root.id
);
if (!linkedDocWidget) return false;
{
name: 'Linked Doc',
description: 'Link to another document.',
icon: LinkedPageIcon(),
tooltip: slashMenuToolTips['Linked Doc'],
searchAlias: ['dual link'],
group: '3_Page@1',
when: ({ std, model }) => {
const root = model.doc.root;
if (!root) return false;
const linkedDocWidget = std.view.getWidget(
'affine-linked-doc-widget',
root.id
);
if (!linkedDocWidget) return false;
return model.doc.schema.flavourSchemaMap.has('affine:embed-linked-doc');
},
action: ({ model, std }) => {
const root = model.doc.root;
if (!root) return;
const linkedDocWidget = std.view.getWidget(
'affine-linked-doc-widget',
root.id
);
if (!linkedDocWidget) return;
// TODO(@L-Sun): make linked-doc-widget as extension
// @ts-expect-error same as above
const triggerKey = linkedDocWidget.config.triggerKeys[0];
insertContent(std.host, model, triggerKey);
const inlineEditor = getInlineEditorByModel(std.host, model);
// Wait for range to be updated
inlineEditor?.slots.inlineRangeSync.once(() => {
return model.doc.schema.flavourSchemaMap.has(
'affine:embed-linked-doc'
);
},
action: ({ model, std }) => {
const root = model.doc.root;
if (!root) return;
const linkedDocWidget = std.view.getWidget(
'affine-linked-doc-widget',
root.id
);
if (!linkedDocWidget) return;
// TODO(@L-Sun): make linked-doc-widget as extension
// @ts-expect-error same as above
linkedDocWidget.show({ addTriggerKey: true });
});
},
},
const triggerKey = linkedDocWidget.config.triggerKeys[0];
// ---------------------------------------------------------
...(() => {
const { host } = std;
insertContent(std.host, model, triggerKey);
const surfaceModel = getSurfaceBlock(host.doc);
if (!surfaceModel) return [];
const parent = host.doc.getParent(model);
if (!parent) return [];
const frameModels = host.doc
.getBlocksByFlavour('affine:frame')
.map(block => block.model as FrameBlockModel);
const frameItems = frameModels.map<SlashMenuActionItem>(frameModel => ({
name: 'Frame: ' + frameModel.title,
icon: FrameIcon(),
group: `5_Document Group & Frame@${index++}`,
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertSurfaceRefBlockCommand, {
reference: frameModel.id,
place: 'after',
removeEmptyLine: true,
})
.run();
},
}));
const groupElements = surfaceModel.getElementsByType('group');
const groupItems = groupElements.map<SlashMenuActionItem>(group => ({
name: 'Group: ' + group.title.toString(),
icon: GroupingIcon(),
group: `5_Document Group & Frame@${index++}`,
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertSurfaceRefBlockCommand, {
reference: group.id,
place: 'after',
removeEmptyLine: true,
})
.run();
},
}));
return [...frameItems, ...groupItems];
})(),
// ---------------------------------------------------------
...((): SlashMenuActionItem[] => {
const now = new Date();
const tomorrow = new Date();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
tomorrow.setDate(tomorrow.getDate() + 1);
return [
{
name: 'Today',
icon: TodayIcon(),
tooltip: slashMenuToolTips['Today'],
description: formatDate(now),
group: `6_Date@${index++}`,
action: ({ std, model }) => {
insertContent(std.host, model, formatDate(now));
},
},
{
name: 'Tomorrow',
icon: TomorrowIcon(),
tooltip: slashMenuToolTips['Tomorrow'],
description: formatDate(tomorrow),
group: `6_Date@${index++}`,
action: ({ std, model }) => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
insertContent(std.host, model, formatDate(tomorrow));
},
},
{
name: 'Yesterday',
icon: YesterdayIcon(),
tooltip: slashMenuToolTips['Yesterday'],
description: formatDate(yesterday),
group: `6_Date@${index++}`,
action: ({ std, model }) => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
insertContent(std.host, model, formatDate(yesterday));
},
},
{
name: 'Now',
icon: NowIcon(),
tooltip: slashMenuToolTips['Now'],
description: formatTime(now),
group: `6_Date@${index++}`,
action: ({ std, model }) => {
insertContent(std.host, model, formatTime(now));
},
},
];
})(),
// ---------------------------------------------------------
// { groupName: 'Actions' },
{
name: 'Move Up',
description: 'Shift this line up.',
icon: ArrowUpBigIcon(),
tooltip: slashMenuToolTips['Move Up'],
group: `8_Actions@${index++}`,
action: ({ std, model }) => {
const { host } = std;
const previousSiblingModel = host.doc.getPrev(model);
if (!previousSiblingModel) return;
const parentModel = host.doc.getParent(previousSiblingModel);
if (!parentModel) return;
host.doc.moveBlocks([model], parentModel, previousSiblingModel, true);
},
},
{
name: 'Move Down',
description: 'Shift this line down.',
icon: ArrowDownBigIcon(),
tooltip: slashMenuToolTips['Move Down'],
group: `8_Actions@${index++}`,
action: ({ std, model }) => {
const { host } = std;
const nextSiblingModel = host.doc.getNext(model);
if (!nextSiblingModel) return;
const parentModel = host.doc.getParent(nextSiblingModel);
if (!parentModel) return;
host.doc.moveBlocks([model], parentModel, nextSiblingModel, false);
},
},
{
name: 'Copy',
description: 'Copy this line to clipboard.',
icon: CopyIcon(),
tooltip: slashMenuToolTips['Copy'],
group: `8_Actions@${index++}`,
action: ({ std, model }) => {
const slice = Slice.fromModels(std.store, [model]);
std.clipboard
.copy(slice)
.then(() => {
toast(std.host, 'Copied to clipboard');
})
.catch(e => {
console.error(e);
const inlineEditor = getInlineEditorByModel(std.host, model);
// Wait for range to be updated
inlineEditor?.slots.inlineRangeSync.once(() => {
// TODO(@L-Sun): make linked-doc-widget as extension
// @ts-expect-error same as above
linkedDocWidget.show({ addTriggerKey: true });
});
},
},
},
{
name: 'Duplicate',
description: 'Create a duplicate of this line.',
icon: DualLinkIcon(),
tooltip: slashMenuToolTips['Copy'],
group: `8_Actions@${index++}`,
action: ({ std, model }) => {
if (!model.text || !(model.text instanceof Text)) {
console.error("Can't duplicate a block without text");
return;
}
const { host } = std;
const parent = host.doc.getParent(model);
if (!parent) {
console.error(
'Failed to duplicate block! Parent not found: ' +
model.id +
'|' +
model.flavour
);
return;
}
const index = parent.children.indexOf(model);
{
name: 'Today',
icon: TodayIcon(),
tooltip: slashMenuToolTips['Today'],
description: formatDate(now),
group: '6_Date@0',
action: ({ std, model }) => {
insertContent(std.host, model, formatDate(now));
},
},
{
name: 'Tomorrow',
icon: TomorrowIcon(),
tooltip: slashMenuToolTips['Tomorrow'],
description: formatDate(tomorrow),
group: '6_Date@1',
action: ({ std, model }) => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
insertContent(std.host, model, formatDate(tomorrow));
},
},
{
name: 'Yesterday',
icon: YesterdayIcon(),
tooltip: slashMenuToolTips['Yesterday'],
description: formatDate(yesterday),
group: '6_Date@2',
action: ({ std, model }) => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
insertContent(std.host, model, formatDate(yesterday));
},
},
{
name: 'Now',
icon: NowIcon(),
tooltip: slashMenuToolTips['Now'],
description: formatTime(now),
group: '6_Date@3',
action: ({ std, model }) => {
insertContent(std.host, model, formatTime(now));
},
},
{
name: 'Move Up',
description: 'Shift this line up.',
icon: ArrowUpBigIcon(),
tooltip: slashMenuToolTips['Move Up'],
group: '8_Actions@0',
action: ({ std, model }) => {
const { host } = std;
const previousSiblingModel = host.doc.getPrev(model);
if (!previousSiblingModel) return;
// TODO add clone model util
host.doc.addBlock(
model.flavour as never,
{
type: (model as ParagraphBlockModel).type,
text: new Text(model.text.toDelta() as DeltaInsert[]),
// @ts-expect-error FIXME: ts error
checked: model.checked,
},
host.doc.getParent(model),
index
);
const parentModel = host.doc.getParent(previousSiblingModel);
if (!parentModel) return;
host.doc.moveBlocks([model], parentModel, previousSiblingModel, true);
},
},
},
{
name: 'Delete',
description: 'Remove this line permanently.',
searchAlias: ['remove'],
icon: DeleteIcon(),
tooltip: slashMenuToolTips['Delete'],
group: `8_Actions@${index++}`,
action: ({ std, model }) => {
std.host.doc.deleteBlock(model);
{
name: 'Move Down',
description: 'Shift this line down.',
icon: ArrowDownBigIcon(),
tooltip: slashMenuToolTips['Move Down'],
group: '8_Actions@1',
action: ({ std, model }) => {
const { host } = std;
const nextSiblingModel = host.doc.getNext(model);
if (!nextSiblingModel) return;
const parentModel = host.doc.getParent(nextSiblingModel);
if (!parentModel) return;
host.doc.moveBlocks([model], parentModel, nextSiblingModel, false);
},
},
},
],
{
name: 'Copy',
description: 'Copy this line to clipboard.',
icon: CopyIcon(),
tooltip: slashMenuToolTips['Copy'],
group: '8_Actions@2',
action: ({ std, model }) => {
const slice = Slice.fromModels(std.store, [model]);
std.clipboard
.copy(slice)
.then(() => {
toast(std.host, 'Copied to clipboard');
})
.catch(e => {
console.error(e);
});
},
},
{
name: 'Duplicate',
description: 'Create a duplicate of this line.',
icon: DualLinkIcon(),
tooltip: slashMenuToolTips['Copy'],
group: '8_Actions@3',
action: ({ std, model }) => {
if (!model.text || !(model.text instanceof Text)) {
console.error("Can't duplicate a block without text");
return;
}
const { host } = std;
const parent = host.doc.getParent(model);
if (!parent) {
console.error(
'Failed to duplicate block! Parent not found: ' +
model.id +
'|' +
model.flavour
);
return;
}
const index = parent.children.indexOf(model);
// TODO add clone model util
host.doc.addBlock(
model.flavour as never,
{
type: (model as ParagraphBlockModel).type,
text: new Text(model.text.toDelta() as DeltaInsert[]),
// @ts-expect-error FIXME: ts error
checked: model.checked,
},
host.doc.getParent(model),
index
);
},
},
{
name: 'Delete',
description: 'Remove this line permanently.',
searchAlias: ['remove'],
icon: DeleteIcon(),
tooltip: slashMenuToolTips['Delete'],
group: '8_Actions@4',
action: ({ std, model }) => {
std.host.doc.deleteBlock(model);
},
},
];
},
};
@@ -1,7 +1,6 @@
import type { SlashMenuTooltip } from '../types';
import { CopyTooltip } from './copy';
import { DeleteTooltip } from './delete';
import { EdgelessTooltip } from './edgeless';
import { LinkDocTooltip } from './link-doc';
import { MoveDownTooltip } from './move-down';
import { MoveUpTooltip } from './move-up';
@@ -61,9 +60,4 @@ export const slashMenuToolTips: Record<string, SlashMenuTooltip> = {
figure: DeleteTooltip,
caption: 'Delete',
},
'Group & Frame': {
figure: EdgelessTooltip,
caption: 'Edgeless',
},
};
@@ -7,8 +7,6 @@
},
"include": ["./src"],
"references": [
{ "path": "../block-surface" },
{ "path": "../block-surface-ref" },
{ "path": "../components" },
{ "path": "../rich-text" },
{ "path": "../shared" },
+1 -2
View File
@@ -301,6 +301,7 @@ export const PackageList = [
'blocksuite/affine/components',
'blocksuite/affine/model',
'blocksuite/affine/shared',
'blocksuite/affine/widget-slash-menu',
'blocksuite/framework/block-std',
'blocksuite/framework/global',
'blocksuite/framework/inline',
@@ -495,8 +496,6 @@ export const PackageList = [
location: 'blocksuite/affine/widget-slash-menu',
name: '@blocksuite/affine-widget-slash-menu',
workspaceDependencies: [
'blocksuite/affine/block-surface',
'blocksuite/affine/block-surface-ref',
'blocksuite/affine/components',
'blocksuite/affine/rich-text',
'blocksuite/affine/shared',
+2 -3
View File
@@ -2665,9 +2665,10 @@ __metadata:
"@blocksuite/affine-components": "workspace:*"
"@blocksuite/affine-model": "workspace:*"
"@blocksuite/affine-shared": "workspace:*"
"@blocksuite/affine-widget-slash-menu": "workspace:*"
"@blocksuite/block-std": "workspace:*"
"@blocksuite/global": "workspace:*"
"@blocksuite/icons": "npm:^2.2.3"
"@blocksuite/icons": "npm:^2.2.4"
"@blocksuite/inline": "workspace:*"
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
@@ -3040,8 +3041,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@blocksuite/affine-widget-slash-menu@workspace:blocksuite/affine/widget-slash-menu"
dependencies:
"@blocksuite/affine-block-surface": "workspace:*"
"@blocksuite/affine-block-surface-ref": "workspace:*"
"@blocksuite/affine-components": "workspace:*"
"@blocksuite/affine-rich-text": "workspace:*"
"@blocksuite/affine-shared": "workspace:*"