mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
703 lines
21 KiB
TypeScript
703 lines
21 KiB
TypeScript
import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment';
|
|
import { insertImagesCommand } from '@blocksuite/affine-block-image';
|
|
import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex';
|
|
import { focusBlockEnd } from '@blocksuite/affine-block-note';
|
|
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
|
|
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
|
|
import { insertTableBlockCommand } from '@blocksuite/affine-block-table';
|
|
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
|
|
import {
|
|
getInlineEditorByModel,
|
|
insertContent,
|
|
insertInlineLatex,
|
|
textConversionConfigs,
|
|
textFormatConfigs,
|
|
} from '@blocksuite/affine-components/rich-text';
|
|
import { toast } from '@blocksuite/affine-components/toast';
|
|
import type {
|
|
FrameBlockModel,
|
|
ParagraphBlockModel,
|
|
} from '@blocksuite/affine-model';
|
|
import {
|
|
getSelectedModelsCommand,
|
|
getTextSelectionCommand,
|
|
} from '@blocksuite/affine-shared/commands';
|
|
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
|
import {
|
|
FeatureFlagService,
|
|
FileSizeLimitService,
|
|
TelemetryProvider,
|
|
} from '@blocksuite/affine-shared/services';
|
|
import {
|
|
createDefaultDoc,
|
|
findAncestorModel,
|
|
openFileOrFiles,
|
|
} from '@blocksuite/affine-shared/utils';
|
|
import {
|
|
ArrowDownBigIcon,
|
|
ArrowUpBigIcon,
|
|
CopyIcon,
|
|
DeleteIcon,
|
|
DualLinkIcon,
|
|
ExportToPdfIcon,
|
|
FigmaDuotoneIcon,
|
|
FileIcon,
|
|
FontIcon,
|
|
FrameIcon,
|
|
GithubDuotoneIcon,
|
|
GroupingIcon,
|
|
HeadingsIcon,
|
|
ImageIcon,
|
|
LinkedPageIcon,
|
|
LinkIcon,
|
|
LoomLogoDuotoneIcon,
|
|
NowIcon,
|
|
PlusIcon,
|
|
TableIcon,
|
|
TeXIcon,
|
|
TodayIcon,
|
|
TomorrowIcon,
|
|
YesterdayIcon,
|
|
YoutubeDuotoneIcon,
|
|
} from '@blocksuite/icons/lit';
|
|
import type { DeltaInsert } from '@blocksuite/inline';
|
|
import { Slice, Text } from '@blocksuite/store';
|
|
|
|
import { slashMenuToolTips } from './tooltips';
|
|
import type { SlashMenuActionItem, SlashMenuConfig } from './types';
|
|
import {
|
|
createConversionItem,
|
|
createTextFormatItem,
|
|
formatDate,
|
|
formatTime,
|
|
insideEdgelessText,
|
|
tryRemoveEmptyLine,
|
|
} from './utils';
|
|
|
|
// TODO(@L-Sun): This counter temporarily added variables for refactoring.
|
|
let index = 0;
|
|
|
|
export const defaultSlashMenuConfig: SlashMenuConfig = {
|
|
disableWhen: ({ model }) => {
|
|
return (
|
|
['affine:code'].includes(model.flavour) ||
|
|
!!findAncestorModel(
|
|
model,
|
|
ancestor => ancestor.flavour === 'affine:callout'
|
|
)
|
|
);
|
|
},
|
|
items: [
|
|
// TODO(@L-Sun): move this to rich-text when it has been remove from blocksuite/affine-components
|
|
...textConversionConfigs
|
|
.filter(i => i.type && ['h1', 'h2', 'h3', 'text'].includes(i.type))
|
|
.map(config => createConversionItem(config, `0_Basic@${index++}`)),
|
|
{
|
|
name: 'Other Headings',
|
|
icon: HeadingsIcon(),
|
|
group: `0_Basic@${index++}`,
|
|
subMenu: textConversionConfigs
|
|
.filter(i => i.type && ['h4', 'h5', 'h6'].includes(i.type))
|
|
.map(config => createConversionItem(config)),
|
|
},
|
|
...textConversionConfigs
|
|
.filter(i => i.flavour === 'affine:code')
|
|
.map(config => createConversionItem(config, `0_Basic@${index++}`)),
|
|
|
|
...textConversionConfigs
|
|
.filter(i => i.type && ['divider', 'quote'].includes(i.type))
|
|
.map(
|
|
config =>
|
|
({
|
|
...createConversionItem(config, `0_Basic@${index++}`),
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has(config.flavour) &&
|
|
!insideEdgelessText(model),
|
|
}) satisfies SlashMenuActionItem
|
|
),
|
|
|
|
{
|
|
name: 'Callout',
|
|
description: 'Let your words stand out.',
|
|
icon: FontIcon(),
|
|
searchAlias: ['callout'],
|
|
group: `0_Basic@${index++}`,
|
|
when: ({ model }) => {
|
|
return model.doc.get(FeatureFlagService).getFlag('enable_callout');
|
|
},
|
|
action: ({ model, std }) => {
|
|
const { doc } = model;
|
|
const parent = doc.getParent(model);
|
|
if (!parent) return;
|
|
|
|
const index = parent.children.indexOf(model);
|
|
if (index === -1) return;
|
|
const calloutId = doc.addBlock('affine:callout', {}, parent, index + 1);
|
|
if (!calloutId) return;
|
|
const paragraphId = doc.addBlock('affine:paragraph', {}, calloutId);
|
|
if (!paragraphId) return;
|
|
std.host.updateComplete
|
|
.then(() => {
|
|
const paragraph = std.view.getBlock(paragraphId);
|
|
if (!paragraph) return;
|
|
std.command.exec(focusBlockEnd, {
|
|
focusBlock: paragraph,
|
|
});
|
|
})
|
|
.catch(console.error);
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'Inline equation',
|
|
group: `0_Basic@${index++}`,
|
|
description: 'Create a equation block.',
|
|
icon: TeXIcon(),
|
|
searchAlias: ['inlineMath, inlineEquation', 'inlineLatex'],
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getTextSelectionCommand)
|
|
.pipe(insertInlineLatex)
|
|
.run();
|
|
},
|
|
},
|
|
|
|
// ---------------------------------------------------------
|
|
// { groupName: 'List' },
|
|
...textConversionConfigs
|
|
.filter(i => i.flavour === 'affine:list')
|
|
.map(config => createConversionItem(config, `1_List@${index++}`)),
|
|
|
|
// ---------------------------------------------------------
|
|
// { groupName: 'Style' },
|
|
...textFormatConfigs
|
|
.filter(i => !['Code', 'Link'].includes(i.name))
|
|
.map(config => createTextFormatItem(config, `2_Style@${index++}`)),
|
|
|
|
{
|
|
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,
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
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;
|
|
|
|
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(() => {
|
|
// TODO(@L-Sun): make linked-doc-widget as extension
|
|
// @ts-expect-error same as above
|
|
linkedDocWidget.show({ addTriggerKey: true });
|
|
});
|
|
},
|
|
},
|
|
|
|
// ---------------------------------------------------------
|
|
// { groupName: 'Content & Media' },
|
|
{
|
|
name: 'Table',
|
|
description: 'Create a simple table.',
|
|
icon: TableIcon(),
|
|
tooltip: slashMenuToolTips['Table View'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) => !insideEdgelessText(model),
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertTableBlockCommand, {
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.pipe(({ insertedTableBlockId }) => {
|
|
if (insertedTableBlockId) {
|
|
const telemetry = std.getOptional(TelemetryProvider);
|
|
telemetry?.track('BlockCreated', {
|
|
blockType: 'affine:table',
|
|
});
|
|
}
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
name: 'Image',
|
|
description: 'Insert an image.',
|
|
icon: ImageIcon(),
|
|
tooltip: slashMenuToolTips['Image'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:image'),
|
|
action: ({ std }) => {
|
|
const [success, ctx] = std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertImagesCommand, { removeEmptyLine: true })
|
|
.run();
|
|
|
|
if (success) ctx.insertedImageIds.catch(console.error);
|
|
},
|
|
},
|
|
{
|
|
name: 'Link',
|
|
description: 'Add a bookmark for reference.',
|
|
icon: LinkIcon(),
|
|
tooltip: slashMenuToolTips['Link'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:bookmark'),
|
|
action: ({ std, model }) => {
|
|
const { host } = std;
|
|
const parentModel = host.doc.getParent(model);
|
|
if (!parentModel) {
|
|
return;
|
|
}
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
toggleEmbedCardCreateModal(
|
|
host,
|
|
'Links',
|
|
'The added link will be displayed as a card view.',
|
|
{ mode: 'page', parentModel, index }
|
|
)
|
|
.then(() => {
|
|
tryRemoveEmptyLine(model);
|
|
})
|
|
.catch(console.error);
|
|
},
|
|
},
|
|
{
|
|
name: 'Attachment',
|
|
description: 'Attach a file to document.',
|
|
icon: FileIcon(),
|
|
tooltip: slashMenuToolTips['Attachment'],
|
|
searchAlias: ['file'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:attachment'),
|
|
action: ({ std, model }) => {
|
|
(async () => {
|
|
const file = await openFileOrFiles();
|
|
if (!file) return;
|
|
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
|
await addSiblingAttachmentBlocks(
|
|
std.host,
|
|
[file],
|
|
maxFileSize,
|
|
model
|
|
);
|
|
tryRemoveEmptyLine(model);
|
|
})().catch(console.error);
|
|
},
|
|
},
|
|
{
|
|
name: 'PDF',
|
|
description: 'Upload a PDF to document.',
|
|
icon: ExportToPdfIcon(),
|
|
tooltip: slashMenuToolTips['PDF'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:attachment'),
|
|
action: ({ std, model }) => {
|
|
(async () => {
|
|
const file = await openFileOrFiles();
|
|
if (!file) return;
|
|
|
|
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
|
|
|
await addSiblingAttachmentBlocks(
|
|
std.host,
|
|
[file],
|
|
maxFileSize,
|
|
model,
|
|
'after',
|
|
true
|
|
);
|
|
tryRemoveEmptyLine(model);
|
|
})().catch(console.error);
|
|
},
|
|
},
|
|
{
|
|
name: 'YouTube',
|
|
description: 'Embed a YouTube video.',
|
|
icon: YoutubeDuotoneIcon(),
|
|
tooltip: slashMenuToolTips['YouTube'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:embed-youtube'),
|
|
action: ({ std, model }) => {
|
|
(async () => {
|
|
const { host } = std;
|
|
const parentModel = host.doc.getParent(model);
|
|
if (!parentModel) {
|
|
return;
|
|
}
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
host,
|
|
'YouTube',
|
|
'The added YouTube video link will be displayed as an embed view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
tryRemoveEmptyLine(model);
|
|
})().catch(console.error);
|
|
},
|
|
},
|
|
{
|
|
name: 'GitHub',
|
|
description: 'Link to a GitHub repository.',
|
|
icon: GithubDuotoneIcon(),
|
|
tooltip: slashMenuToolTips['Github'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:embed-github'),
|
|
action: ({ std, model }) => {
|
|
(async () => {
|
|
const { host } = std;
|
|
const parentModel = host.doc.getParent(model);
|
|
if (!parentModel) {
|
|
return;
|
|
}
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
host,
|
|
'GitHub',
|
|
'The added GitHub issue or pull request link will be displayed as a card view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
tryRemoveEmptyLine(model);
|
|
})().catch(console.error);
|
|
},
|
|
},
|
|
// TODO: X Twitter
|
|
|
|
{
|
|
name: 'Figma',
|
|
description: 'Embed a Figma document.',
|
|
icon: FigmaDuotoneIcon(),
|
|
tooltip: slashMenuToolTips['Figma'],
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:embed-figma'),
|
|
action: ({ std, model }) => {
|
|
(async () => {
|
|
const { host } = std;
|
|
const parentModel = host.doc.getParent(model);
|
|
if (!parentModel) {
|
|
return;
|
|
}
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
host,
|
|
'Figma',
|
|
'The added Figma link will be displayed as an embed view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
tryRemoveEmptyLine(model);
|
|
})().catch(console.error);
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'Loom',
|
|
icon: LoomLogoDuotoneIcon(),
|
|
group: `4_Content & Media@${index++}`,
|
|
when: ({ model }) =>
|
|
model.doc.schema.flavourSchemaMap.has('affine:embed-loom'),
|
|
action: ({ std, model }) => {
|
|
(async () => {
|
|
const { host } = std;
|
|
const parentModel = host.doc.getParent(model);
|
|
if (!parentModel) {
|
|
return;
|
|
}
|
|
const index = parentModel.children.indexOf(model) + 1;
|
|
await toggleEmbedCardCreateModal(
|
|
host,
|
|
'Loom',
|
|
'The added Loom video link will be displayed as an embed view.',
|
|
{ mode: 'page', parentModel, index }
|
|
);
|
|
tryRemoveEmptyLine(model);
|
|
})().catch(console.error);
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'Equation',
|
|
description: 'Create a equation block.',
|
|
icon: TeXIcon(),
|
|
searchAlias: ['mathBlock, equationBlock', 'latexBlock'],
|
|
group: `4_Content & Media@${index++}`,
|
|
action: ({ std }) => {
|
|
std.command
|
|
.chain()
|
|
.pipe(getSelectedModelsCommand)
|
|
.pipe(insertLatexBlockCommand, {
|
|
place: 'after',
|
|
removeEmptyLine: true,
|
|
})
|
|
.run();
|
|
},
|
|
},
|
|
|
|
// TODO(@L-Sun): Linear
|
|
|
|
// ---------------------------------------------------------
|
|
({ std, model }) => {
|
|
const { host } = std;
|
|
|
|
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];
|
|
},
|
|
|
|
// ---------------------------------------------------------
|
|
() => {
|
|
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);
|
|
});
|
|
},
|
|
},
|
|
{
|
|
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);
|
|
|
|
// 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@${index++}`,
|
|
action: ({ std, model }) => {
|
|
std.host.doc.deleteBlock(model);
|
|
},
|
|
},
|
|
],
|
|
};
|