mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-31 17:19:56 +08:00
feat(editor): add "Copy as Markdown" option in context & export menus (#14705)
- Allow users to select text and copy it as Markdown via the context menu - Add "Copy as Markdown" under Export menu to copy entire document to clipboard Fixes #12983 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added "Copy as Markdown" to the toolbar clipboard More menu for selected content. * Added "Copy as Markdown" to the page export menu to copy entire pages as Markdown. * **Behavior** * Export flow now returns success/failure so the UI shows a dedicated success or error notification for clipboard exports. * **Localization** * Added strings for "Copy as Markdown" and "Copied as Markdown". <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Whitewater <me@waterwater.moe> Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com>
This commit is contained in:
+102
-2
@@ -44,7 +44,10 @@ import {
|
||||
EmbedSyncedDocModel,
|
||||
SurfaceRefBlockSchema,
|
||||
} from '@blocksuite/affine/model';
|
||||
import { getSelectedModelsCommand } from '@blocksuite/affine/shared/commands';
|
||||
import {
|
||||
draftSelectedModelsCommand,
|
||||
getSelectedModelsCommand,
|
||||
} from '@blocksuite/affine/shared/commands';
|
||||
import { ImageSelection } from '@blocksuite/affine/shared/selection';
|
||||
import {
|
||||
ActionPlacement,
|
||||
@@ -71,11 +74,12 @@ import {
|
||||
GfxBlockElementModel,
|
||||
GfxPrimitiveElementModel,
|
||||
} from '@blocksuite/affine/std/gfx';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import { type ExtensionType, Slice } from '@blocksuite/affine/store';
|
||||
import {
|
||||
CopyAsImgaeIcon,
|
||||
CopyIcon,
|
||||
EditIcon,
|
||||
ExportToMarkdownIcon,
|
||||
LinkIcon,
|
||||
OpenInNewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
@@ -87,6 +91,7 @@ import { keyed } from 'lit/directives/keyed.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { getContentFromSlice } from '../../../utils/markdown-utils';
|
||||
import { openDocActions } from '../../editor-view/open-doc';
|
||||
import { copyAsImage, createCopyAsPngMenuItem } from './copy-as-image';
|
||||
|
||||
@@ -120,6 +125,12 @@ export function createToolbarMoreMenuConfig(framework: FrameworkProvider) {
|
||||
0,
|
||||
createCopyAsPngMenuItem(framework)
|
||||
);
|
||||
|
||||
clipboardGroup.items.splice(
|
||||
copyIndex + 1,
|
||||
0,
|
||||
createCopyAsMarkdownMenuItem(framework)
|
||||
);
|
||||
}
|
||||
|
||||
return groups;
|
||||
@@ -209,6 +220,55 @@ function createCopyLinkToBlockMenuItem(
|
||||
};
|
||||
}
|
||||
|
||||
function createCopyAsMarkdownMenuItem(
|
||||
_framework: FrameworkProvider,
|
||||
item = {
|
||||
icon: ExportToMarkdownIcon({ width: '20', height: '20' }),
|
||||
label: I18n['com.affine.export.copy-markdown'](),
|
||||
type: 'copy-as-markdown',
|
||||
when: (ctx: MenuContext) => {
|
||||
if (ctx.isEmpty()) return false;
|
||||
return true;
|
||||
},
|
||||
}
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
action: (ctx: MenuContext) => {
|
||||
void (async () => {
|
||||
const { std } = ctx;
|
||||
const [ok, commandCtx] = std.command
|
||||
.chain()
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.run();
|
||||
const draftedModels = commandCtx.draftedModels;
|
||||
if (!ok || !draftedModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
const models = await draftedModels;
|
||||
if (models.length > 0) {
|
||||
const slice = Slice.fromModels(std.store, models);
|
||||
const markdown = await getContentFromSlice(
|
||||
std.host,
|
||||
slice,
|
||||
'markdown'
|
||||
);
|
||||
if (markdown) {
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
toast(std.host, I18n['com.affine.export.copied-as-markdown']());
|
||||
}
|
||||
}
|
||||
})()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
ctx.close();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createToolbarMoreMenuConfigV2(baseUrl?: string) {
|
||||
return {
|
||||
actions: [
|
||||
@@ -228,6 +288,46 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
|
||||
copyAsImage(std);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copy-as-markdown',
|
||||
label: I18n['com.affine.export.copy-markdown'](),
|
||||
icon: ExportToMarkdownIcon(),
|
||||
when: ({ gfx, flags, isPageMode }) => {
|
||||
if (flags.isHovering()) return false;
|
||||
if (isPageMode) return true;
|
||||
return gfx.selection.selectedElements.length > 0;
|
||||
},
|
||||
run: ({ std }) => {
|
||||
void (async () => {
|
||||
const [ok, ctx] = std.command
|
||||
.chain()
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.run();
|
||||
const draftedModels = ctx.draftedModels;
|
||||
if (!ok || !draftedModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
const models = await draftedModels;
|
||||
if (models.length > 0) {
|
||||
const slice = Slice.fromModels(std.store, models);
|
||||
const markdown = await getContentFromSlice(
|
||||
std.host,
|
||||
slice,
|
||||
'markdown'
|
||||
);
|
||||
if (markdown) {
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
toast(
|
||||
std.host,
|
||||
I18n['com.affine.export.copied-as-markdown']()
|
||||
);
|
||||
}
|
||||
}
|
||||
})().catch(console.error);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copy-link-to-block',
|
||||
label: 'Copy link to block',
|
||||
|
||||
Reference in New Issue
Block a user