mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 00:41:39 +08:00
fix(editor): callout delete merge and slash menu (#13597)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Press Enter inside a callout splits the paragraph at the cursor into a new focused paragraph. - Clicking an empty callout inserts and focuses a new paragraph; emoji menu behavior unchanged. - New command to convert a callout paragraph to callout/selection flow for Backspace handling. - New native API: ShareableContent.isUsingMicrophone(processId). - Bug Fixes - Backspace inside callout paragraphs now merges or deletes text predictably and selects the callout when appropriate. - Style - Callout layout refined: top-aligned content and adjusted emoji spacing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { Text } from '@blocksuite/store';
|
||||
|
||||
export const calloutToParagraphCommand: Command<
|
||||
{
|
||||
id: string;
|
||||
stopCapturing?: boolean;
|
||||
},
|
||||
{
|
||||
success: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
const { id, stopCapturing = true } = ctx;
|
||||
const std = ctx.std;
|
||||
const doc = std.store;
|
||||
const model = doc.getBlock(id)?.model;
|
||||
|
||||
if (!model || !matchModels(model, [ParagraphBlockModel])) return false;
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent || !matchModels(parent, [CalloutBlockModel])) return false;
|
||||
|
||||
if (stopCapturing) std.store.captureSync();
|
||||
|
||||
// Get current block index in callout
|
||||
const currentIndex = parent.children.indexOf(model);
|
||||
const hasText = model.text && model.text.length > 0;
|
||||
|
||||
// Find previous paragraph block in callout
|
||||
let previousBlock = null;
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const sibling = parent.children[i];
|
||||
if (matchModels(sibling, [ParagraphBlockModel])) {
|
||||
previousBlock = sibling;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (previousBlock && hasText) {
|
||||
// Clone current text content before any operations to prevent data loss
|
||||
const currentText = model.text || new Text();
|
||||
|
||||
// Get previous block text and merge index
|
||||
const previousText = previousBlock.text || new Text();
|
||||
const mergeIndex = previousText.length;
|
||||
|
||||
// Apply each delta from cloned current text to previous block to preserve formatting
|
||||
previousText.join(currentText);
|
||||
|
||||
// Remove current block after text has been merged
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
// Focus at merge point in previous block
|
||||
focusTextModel(std, previousBlock.id, mergeIndex);
|
||||
} else if (previousBlock && !hasText) {
|
||||
// Move cursor to end of previous block
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
const previousText = previousBlock.text || new Text();
|
||||
focusTextModel(std, previousBlock.id, previousText.length);
|
||||
} else {
|
||||
// No previous block, select the entire callout
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
std.selection.setGroup('note', [
|
||||
std.selection.create(BlockSelection, {
|
||||
blockId: parent.id,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return next({ success: true });
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command, EditorHost } from '@blocksuite/std';
|
||||
|
||||
export const splitCalloutCommand: Command<{
|
||||
blockId: string;
|
||||
inlineIndex: number;
|
||||
currentBlockId: string;
|
||||
}> = (ctx, next) => {
|
||||
const { blockId, inlineIndex, currentBlockId, std } = ctx;
|
||||
const host = std.host as EditorHost;
|
||||
const doc = host.store;
|
||||
|
||||
const calloutModel = doc.getBlock(blockId)?.model;
|
||||
if (!calloutModel || !matchModels(calloutModel, [CalloutBlockModel])) {
|
||||
console.error(`block ${blockId} is not a callout block`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentModel = doc.getBlock(currentBlockId)?.model;
|
||||
if (!currentModel) {
|
||||
console.error(`current block ${currentBlockId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
doc.captureSync();
|
||||
|
||||
if (matchModels(currentModel, [ParagraphBlockModel])) {
|
||||
// User is in a paragraph within the callout's children
|
||||
const afterText = currentModel.props.text.split(inlineIndex);
|
||||
|
||||
// Update the current paragraph's text to keep only the part before cursor
|
||||
doc.transact(() => {
|
||||
currentModel.props.text.delete(
|
||||
inlineIndex,
|
||||
currentModel.props.text.length - inlineIndex
|
||||
);
|
||||
});
|
||||
|
||||
// Create a new paragraph block after the current one
|
||||
const parent = doc.getParent(currentModel);
|
||||
if (parent) {
|
||||
const currentIndex = parent.children.indexOf(currentModel);
|
||||
const newParagraphId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: afterText,
|
||||
},
|
||||
parent,
|
||||
currentIndex + 1
|
||||
);
|
||||
|
||||
if (newParagraphId) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
focusTextModel(std, newParagraphId);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If current block is not a paragraph, create a new paragraph in callout
|
||||
const newParagraphId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new Text(),
|
||||
},
|
||||
calloutModel
|
||||
);
|
||||
|
||||
if (newParagraphId) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
focusTextModel(std, newParagraphId);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
Reference in New Issue
Block a user