mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 08:17:10 +08:00
feat(editor): rich text package (#10689)
This PR performs a significant architectural refactoring by extracting rich text functionality into a dedicated package. Here are the key changes: 1. **New Package Creation** - Created a new package `@blocksuite/affine-rich-text` to house rich text related functionality - Moved rich text components, utilities, and types from `@blocksuite/affine-components` to this new package 2. **Dependency Updates** - Updated multiple block packages to include the new `@blocksuite/affine-rich-text` as a direct dependency: - block-callout - block-code - block-database - block-edgeless-text - block-embed - block-list - block-note - block-paragraph 3. **Import Path Updates** - Refactored all imports that previously referenced rich text functionality from `@blocksuite/affine-components/rich-text` to now use `@blocksuite/affine-rich-text` - Updated imports for components like: - DefaultInlineManagerExtension - RichText types and interfaces - Text manipulation utilities (focusTextModel, textKeymap, etc.) - Reference node components and providers 4. **Build Configuration Updates** - Added references to the new rich text package in the `tsconfig.json` files of all affected packages - Maintained workspace dependencies using the `workspace:*` version specifier The primary motivation appears to be: 1. Better separation of concerns by isolating rich text functionality 2. Improved maintainability through more modular package structure 3. Clearer dependencies between packages 4. Potential for better tree-shaking and bundle optimization This is primarily an architectural improvement that should make the codebase more maintainable and better organized.
This commit is contained in:
116
blocksuite/affine/rich-text/src/hooks.ts
Normal file
116
blocksuite/affine/rich-text/src/hooks.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { isStrictUrl } from '@blocksuite/affine-shared/utils';
|
||||
import type {
|
||||
BeforeinputHookCtx,
|
||||
CompositionEndHookCtx,
|
||||
HookContext,
|
||||
} from '@blocksuite/inline';
|
||||
|
||||
const EDGE_IGNORED_ATTRIBUTES = ['code', 'link'] as const;
|
||||
const GLOBAL_IGNORED_ATTRIBUTES = [] as const;
|
||||
|
||||
const autoIdentifyLink = (ctx: HookContext<AffineTextAttributes>) => {
|
||||
// auto identify link only on pressing space
|
||||
if (ctx.data !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
// space is typed at the end of link, remove the link attribute on typed space
|
||||
if (ctx.attributes?.link) {
|
||||
if (ctx.inlineRange.index === ctx.inlineEditor.yText.length) {
|
||||
delete ctx.attributes['link'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const lineInfo = ctx.inlineEditor.getLine(ctx.inlineRange.index);
|
||||
if (!lineInfo) {
|
||||
return;
|
||||
}
|
||||
const { line, lineIndex, rangeIndexRelatedToLine } = lineInfo;
|
||||
|
||||
if (lineIndex !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const verifyData = line.vTextContent
|
||||
.slice(0, rangeIndexRelatedToLine)
|
||||
.split(' ');
|
||||
|
||||
const verifyStr = verifyData[verifyData.length - 1];
|
||||
|
||||
const isUrl = isStrictUrl(verifyStr);
|
||||
|
||||
if (!isUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = ctx.inlineRange.index - verifyStr.length;
|
||||
|
||||
ctx.inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: verifyStr.length,
|
||||
},
|
||||
{
|
||||
link: verifyStr,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function handleExtendedAttributes(
|
||||
ctx:
|
||||
| BeforeinputHookCtx<AffineTextAttributes>
|
||||
| CompositionEndHookCtx<AffineTextAttributes>
|
||||
) {
|
||||
const { data, inlineEditor, inlineRange } = ctx;
|
||||
const deltas = inlineEditor.getDeltasByInlineRange(inlineRange);
|
||||
// eslint-disable-next-line sonarjs/no-collapsible-if
|
||||
if (data && data.length > 0 && data !== '\n') {
|
||||
if (
|
||||
// cursor is in the between of two deltas
|
||||
(deltas.length > 1 ||
|
||||
// cursor is in the end of line or in the middle of a delta
|
||||
(deltas.length === 1 && inlineRange.index !== 0)) &&
|
||||
!inlineEditor.isEmbed(deltas[0][0]) // embeds should not be extended
|
||||
) {
|
||||
// each new text inserted by inline editor will not contain any attributes,
|
||||
// but we want to keep the attributes of previous text or current text where the cursor is in
|
||||
// here are two cases:
|
||||
// 1. aaa**b|bb**ccc --input 'd'--> aaa**bdbb**ccc, d should extend the bold attribute
|
||||
// 2. aaa**bbb|**ccc --input 'd'--> aaa**bbbd**ccc, d should extend the bold attribute
|
||||
const { attributes } = deltas[0][0];
|
||||
if (
|
||||
deltas.length !== 1 ||
|
||||
inlineRange.index === inlineEditor.yText.length
|
||||
) {
|
||||
// `EDGE_IGNORED_ATTRIBUTES` is which attributes should be ignored in case 2
|
||||
EDGE_IGNORED_ATTRIBUTES.forEach(attr => {
|
||||
delete attributes?.[attr];
|
||||
});
|
||||
}
|
||||
|
||||
// `GLOBAL_IGNORED_ATTRIBUTES` is which attributes should be ignored in case 1, 2
|
||||
GLOBAL_IGNORED_ATTRIBUTES.forEach(attr => {
|
||||
delete attributes?.[attr];
|
||||
});
|
||||
|
||||
ctx.attributes = attributes ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export const onVBeforeinput = (
|
||||
ctx: BeforeinputHookCtx<AffineTextAttributes>
|
||||
) => {
|
||||
handleExtendedAttributes(ctx);
|
||||
autoIdentifyLink(ctx);
|
||||
};
|
||||
|
||||
export const onVCompositionEnd = (
|
||||
ctx: CompositionEndHookCtx<AffineTextAttributes>
|
||||
) => {
|
||||
handleExtendedAttributes(ctx);
|
||||
};
|
||||
Reference in New Issue
Block a user