Files
AFFiNE-Mirror/blocksuite/affine/blocks/code/src/clipboard/index.ts
T
DarkSky 07a08e6d4d fix(editor): import & save logic (#15098)
fix #15080
fix #15085
fix #15031
fix #15094


#### PR Dependency Tree


* **PR #15098** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved code-block paste behavior for plain-text insertion
  * Fixed block selection ordering to reflect document model
  * Made table cell formatting resilient to conversion errors
  * Ensured user feature list is consistently returned as an array

* **Refactor**
  * Streamlined authentication session fetch and profile enrichment flow

* **Tests**
  * Added tests for markdown blockquote list preservation
  * Added authentication session validation tests
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-10 22:43:31 +08:00

200 lines
5.4 KiB
TypeScript

import { deleteTextCommand } from '@blocksuite/affine-inline-preset';
import type { RichText } from '@blocksuite/affine-rich-text';
import {
HtmlAdapter,
pasteMiddleware,
PlainTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
getBlockIndexCommand,
getBlockSelectionsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
type BlockStdScope,
Clipboard,
type ClipboardAdapterConfig,
LifeCycleWatcher,
LifeCycleWatcherIdentifier,
StdIdentifier,
TextSelection,
type UIEventHandler,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
export const CodeClipboardAdapterConfigIdentifier =
createIdentifier<ClipboardAdapterConfig>('code-clipboard-adapter-config');
export function CodeClipboardAdapterConfigExtension(
config: ClipboardAdapterConfig
): ExtensionType {
return {
setup: di => {
di.addImpl(
CodeClipboardAdapterConfigIdentifier(config.mimeType),
() => config
);
},
};
}
const PlainTextClipboardConfig = CodeClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: PlainTextAdapter,
priority: 90,
});
const HtmlClipboardConfig = CodeClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 80,
});
export class CodeBlockClipboard extends Clipboard {
static override readonly key = 'code-block-clipboard';
override get _adapters() {
const adapterConfigs = this.std.provider.getAll(
CodeClipboardAdapterConfigIdentifier
);
return Array.from(adapterConfigs.values());
}
}
export class CodeBlockClipboardController extends LifeCycleWatcher {
static override key = 'code-block-clipboard-controller';
private readonly _disposables = new DisposableGroup();
constructor(
std: BlockStdScope,
readonly clipboard: CodeBlockClipboard
) {
super(std);
}
static override setup(di: Container) {
di.add(
this as unknown as {
new (
std: BlockStdScope,
clipboard: CodeBlockClipboard
): CodeBlockClipboardController;
},
[StdIdentifier, CodeBlockClipboard]
);
di.addImpl(LifeCycleWatcherIdentifier(this.key), provider =>
provider.get(this)
);
}
protected _init = () => {
const paste = pasteMiddleware(this.std);
this.clipboard.use(paste);
this._disposables.add({
dispose: () => {
this.clipboard.unuse(paste);
},
});
};
onPaste: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
const textSelection = this.std.selection.find(TextSelection);
const plainText = e.clipboardData
?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n');
const selectedBlockId = textSelection?.from.blockId;
const codeBlock = selectedBlockId
? this.std.store.getBlock(selectedBlockId)?.model
: null;
if (plainText && codeBlock?.flavour === 'affine:code' && selectedBlockId) {
const richText = this.std.view
.getBlock(selectedBlockId)
?.querySelector<RichText>('rich-text');
const inlineEditor = richText?.inlineEditor;
const inlineRange = inlineEditor?.getInlineRange();
if (inlineEditor && inlineRange) {
inlineEditor.insertText(inlineRange, plainText);
inlineEditor.setInlineRange({
index: inlineRange.index + plainText.length,
length: 0,
});
return true;
}
}
this.std.store.captureSync();
this.std.command
.chain()
.try(cmd => [
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
const textSelection = ctx.currentTextSelection;
if (!textSelection) return;
const end = textSelection.to ?? textSelection.from;
next({ currentSelectionPath: end.blockId });
}),
cmd.pipe(getBlockSelectionsCommand).pipe((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
if (!currentBlockSelections) return;
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) return;
next({ currentSelectionPath: blockSelection.blockId });
}),
])
.pipe(getBlockIndexCommand)
.try(cmd => [cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand)])
.pipe((ctx, next) => {
if (!ctx.parentBlock) {
return;
}
this.clipboard
.paste(
e,
this.std.store,
ctx.parentBlock.model.id,
ctx.blockIndex ? ctx.blockIndex + 1 : 1
)
.catch(console.error);
return next();
})
.run();
return true;
};
override mounted() {
this._init();
// add paste event listener for code block
const subscription = this.std.view.viewUpdated.subscribe(
({ type, method, view }) => {
if (type !== 'block' || view.model.flavour !== 'affine:code') return;
if (method === 'add') {
view.handleEvent('paste', this.onPaste);
}
}
);
this._disposables.add(subscription);
}
override unmounted() {
this._disposables.dispose();
}
}
export function getCodeClipboardExtensions(): ExtensionType[] {
return [
PlainTextClipboardConfig,
HtmlClipboardConfig,
CodeBlockClipboard,
CodeBlockClipboardController,
];
}