refactor(editor): improve implementation of lit adapter (#12101)

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

- **New Features**
  - Improved mobile experience by disabling certain toolbars and slash menu features on mobile devices.
  - Introduced new modular extension classes for editor and view customization, enabling more flexible configuration of themes, AI features, and editor enhancements.
  - Added clipboard adapter configurations for a wide range of data types, improving clipboard compatibility.
  - Added a new theme extension specifically for preview scenarios.
  - Provided new hooks for block scope management in document modules.

- **Refactor**
  - Streamlined editor extension setup, consolidating options and reducing complexity for better maintainability.
  - Reorganized mobile-specific extension exports for clearer usage.
  - Refined React-to-Lit rendering API by introducing a typed alias and updating related function signatures.
  - Simplified extension registration by splitting monolithic view extension into separate common and editor view extensions.

- **Bug Fixes**
  - Corrected naming inconsistencies in internal effect tracking.

- **Chores**
  - Updated type exports and documentation comments for improved clarity and consistency.
  - Removed unused or redundant exports and functions to clean up the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-01 14:29:11 +00:00
parent e0308c5815
commit 41d404f7f8
26 changed files with 638 additions and 575 deletions
+3 -1
View File
@@ -41,7 +41,6 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
FlavourExtension('affine:code'),
CodeBlockHighlighter,
BlockViewExtension('affine:code', literal`affine-code`),
codeToolbarWidget,
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
CodeKeymapExtension,
...getCodeClipboardExtensions(),
@@ -50,5 +49,8 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
]);
if (!this.isMobile(context.scope)) {
context.register(codeToolbarWidget);
}
}
}
@@ -208,21 +208,21 @@ it('should effect only run once', () => {
const manager = new ViewExtensionManager([ViewExt1]);
expect(ViewExt1.effectRunned).toBe(false);
expect(ViewExt2.effectRunned).toBe(false);
expect(ViewExt1.effectRan).toBe(false);
expect(ViewExt2.effectRan).toBe(false);
manager.get('page');
expect(ViewExt1.effectRunned).toBe(true);
expect(ViewExt2.effectRunned).toBe(false);
expect(ViewExt1.effectRan).toBe(true);
expect(ViewExt2.effectRan).toBe(false);
expect(effect1).toHaveBeenCalledTimes(1);
expect(effect2).toHaveBeenCalledTimes(0);
manager.get('edgeless');
expect(ViewExt1.effectRunned).toBe(true);
expect(ViewExt2.effectRunned).toBe(false);
expect(ViewExt1.effectRan).toBe(true);
expect(ViewExt2.effectRan).toBe(false);
expect(effect1).toHaveBeenCalledTimes(1);
expect(effect2).toHaveBeenCalledTimes(0);
@@ -64,7 +64,7 @@ export class ViewExtensionProvider<
* Static flag to ensure effect is only run once per provider class
* @internal
*/
static effectRunned = false;
static effectRan = false;
/**
* Override this method to implement one-time initialization logic for the provider.
@@ -94,11 +94,29 @@ export class ViewExtensionProvider<
);
};
/**
* Check if the scope is preview
* @param scope - The scope to check
* @returns True if the scope is preview, false otherwise
*/
isPreview = (scope: ViewScope) => {
return scope === 'preview-page' || scope === 'preview-edgeless';
};
/**
* Check if the scope is mobile
* @param scope - The scope to check
* @returns True if the scope is mobile, false otherwise
*/
isMobile = (scope: ViewScope) => {
return scope === 'mobile-page' || scope === 'mobile-edgeless';
};
override setup(context: ViewExtensionContext, options?: Options) {
super.setup(context, options);
const constructer = this.constructor as typeof ViewExtensionProvider;
if (!constructer.effectRunned) {
constructer.effectRunned = true;
const constructor = this.constructor as typeof ViewExtensionProvider;
if (!constructor.effectRan) {
constructor.effectRan = true;
this.effect();
}
}
@@ -0,0 +1,65 @@
import {
AttachmentAdapter,
ClipboardAdapter,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import { ClipboardAdapterConfigExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: ClipboardAdapter.MIME,
adapter: ClipboardAdapter,
priority: 100,
});
const NotionClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/_notion-text-production',
adapter: NotionTextAdapter,
priority: 95,
});
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 90,
});
const imageClipboardConfigs = [
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].map(mimeType => {
return ClipboardAdapterConfigExtension({
mimeType,
adapter: ImageAdapter,
priority: 80,
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,
});
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: '*/*',
adapter: AttachmentAdapter,
priority: 60,
});
export const clipboardConfigs: ExtensionType[] = [
SnapshotClipboardConfig,
NotionClipboardConfig,
HtmlClipboardConfig,
...imageClipboardConfigs,
PlainTextClipboardConfig,
AttachmentClipboardConfig,
];
+1 -65
View File
@@ -3,14 +3,6 @@ import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import {
AttachmentAdapter,
ClipboardAdapter,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
AutoClearSelectionService,
DefaultOpenDocExtension,
@@ -25,67 +17,11 @@ import {
ThemeService,
ToolbarRegistryExtension,
} from '@blocksuite/affine-shared/services';
import { ClipboardAdapterConfigExtension } from '@blocksuite/std';
import { InteractivityManager, ToolController } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { clipboardConfigs } from './clipboard';
import { effects } from './effects';
const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: ClipboardAdapter.MIME,
adapter: ClipboardAdapter,
priority: 100,
});
const NotionClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/_notion-text-production',
adapter: NotionTextAdapter,
priority: 95,
});
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 90,
});
const imageClipboardConfigs = [
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].map(mimeType => {
return ClipboardAdapterConfigExtension({
mimeType,
adapter: ImageAdapter,
priority: 80,
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,
});
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: '*/*',
adapter: AttachmentAdapter,
priority: 60,
});
export const clipboardConfigs: ExtensionType[] = [
SnapshotClipboardConfig,
NotionClipboardConfig,
HtmlClipboardConfig,
...imageClipboardConfigs,
PlainTextClipboardConfig,
AttachmentClipboardConfig,
];
export class FoundationViewExtension extends ViewExtensionProvider {
override name = 'foundation';
@@ -6,7 +6,7 @@ import {
} from '@blocksuite/affine-model';
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/std';
import { Extension, type ExtensionType } from '@blocksuite/store';
import { Extension } from '@blocksuite/store';
import { type Signal, signal } from '@preact/signals-core';
import {
type AffineCssVariables,
@@ -25,14 +25,6 @@ export interface ThemeExtension {
getEdgelessTheme?: (docId?: string) => Signal<ColorScheme>;
}
export function OverrideThemeExtension(service: ThemeExtension): ExtensionType {
return {
setup: di => {
di.override(ThemeExtensionIdentifier, () => service);
},
};
}
export const ThemeProvider = createIdentifier<ThemeService>(
'AffineThemeProvider'
);
@@ -78,6 +70,7 @@ export class ThemeService extends Extension {
*
* @param color - A color value.
* @param fallback - If color value processing fails, it will be used as a fallback.
* @param theme - Target theme, default is the current theme.
* @returns - A color property string.
*
* @example
@@ -112,6 +105,7 @@ export class ThemeService extends Extension {
* @param color - A color value.
* @param fallback - If color value processing fails, it will be used as a fallback.
* @param real - If true, it returns the computed style.
* @param theme - Target theme, default is the current theme.
* @returns - A color property string.
*
* @example
@@ -16,6 +16,7 @@ export class SlashMenuViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) {
super.setup(context);
if (this.isMobile(context.scope)) return;
context.register(SlashMenuExtension);
}
}
@@ -16,6 +16,7 @@ export class ToolbarViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) {
super.setup(context);
if (this.isMobile(context.scope)) return;
context.register(toolbarWidget);
}
}