Skip to content

Commit

Permalink
Support MixedPlugin (#2917)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Jan 7, 2025
1 parent 85eebb6 commit 0bc2578
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 3 deletions.
34 changes: 31 additions & 3 deletions packages/roosterjs-editor-adapter/lib/corePlugins/BridgePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IgnoredPluginNames } from '../editor/IgnoredPluginNames';
import { newEventToOldEvent, oldEventToNewEvent } from '../editor/utils/eventConverter';
import type {
EditorPlugin as LegacyEditorPlugin,
PluginEvent as LegacyPluginEvent,
ContextMenuProvider as LegacyContextMenuProvider,
IEditor as ILegacyEditor,
ExperimentalFeatures,
Expand All @@ -14,6 +15,7 @@ import type {
DarkColorHandler,
} from 'roosterjs-editor-types';
import type { ContextMenuProvider, IEditor, PluginEvent } from 'roosterjs-content-model-types';
import type { MixedPlugin } from '../publicTypes/MixedPlugin';

const ExclusivelyHandleEventPluginKey = '__ExclusivelyHandleEventPlugin';
const OldEventKey = '__OldEventFromNewEvent';
Expand Down Expand Up @@ -97,7 +99,13 @@ export class BridgePlugin implements ContextMenuProvider<any> {
initialize(editor: IEditor) {
const outerEditor = this.onInitialize(this.createEditorCore(editor));

this.legacyPlugins.forEach(plugin => plugin.initialize(outerEditor));
this.legacyPlugins.forEach(plugin => {
plugin.initialize(outerEditor);

if (isMixedPlugin(plugin)) {
plugin.initializeV9(editor);
}
});
}

/**
Expand All @@ -122,9 +130,9 @@ export class BridgePlugin implements ContextMenuProvider<any> {
const exclusivelyHandleEventPlugin = this.cacheGetExclusivelyHandlePlugin(event);

if (exclusivelyHandleEventPlugin) {
exclusivelyHandleEventPlugin.onPluginEvent?.(oldEvent);
this.handleEvent(exclusivelyHandleEventPlugin, oldEvent, event);
} else {
this.legacyPlugins.forEach(plugin => plugin.onPluginEvent?.(oldEvent));
this.legacyPlugins.forEach(plugin => this.handleEvent(plugin, oldEvent, event));
}

Object.assign(event, oldEventToNewEvent(oldEvent, event));
Expand Down Expand Up @@ -164,6 +172,10 @@ export class BridgePlugin implements ContextMenuProvider<any> {
if (plugin.willHandleEventExclusively?.(oldEvent)) {
return plugin;
}

if (isMixedPlugin(plugin) && plugin.willHandleEventExclusivelyV9?.(event)) {
return plugin;
}
}
}

Expand All @@ -185,6 +197,18 @@ export class BridgePlugin implements ContextMenuProvider<any> {
contextMenuProviders: this.contextMenuProviders,
};
}

private handleEvent(
plugin: LegacyEditorPlugin,
oldEvent: LegacyPluginEvent,
newEvent: PluginEvent
) {
plugin.onPluginEvent?.(oldEvent);

if (isMixedPlugin(plugin)) {
plugin.onPluginEventV9?.(newEvent);
}
}
}

/**
Expand All @@ -200,3 +224,7 @@ function isContextMenuProvider(
): source is LegacyContextMenuProvider<any> {
return !!(<LegacyContextMenuProvider<any>>source)?.getContextMenuItems;
}

function isMixedPlugin(plugin: LegacyEditorPlugin): plugin is MixedPlugin {
return !!(plugin as MixedPlugin).initializeV9;
}
1 change: 1 addition & 0 deletions packages/roosterjs-editor-adapter/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { EditorAdapterOptions } from './publicTypes/EditorAdapterOptions';
export { BeforePasteAdapterEvent } from './publicTypes/BeforePasteAdapterEvent';
export { MixedPlugin } from './publicTypes/MixedPlugin';

export { EditorAdapter } from './editor/EditorAdapter';

Expand Down
35 changes: 35 additions & 0 deletions packages/roosterjs-editor-adapter/lib/publicTypes/MixedPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { PluginEvent, IEditor } from 'roosterjs-content-model-types';
import type { EditorPlugin } from 'roosterjs-editor-types';

/**
* Represents a mixed version plugin that can handle both v8 and v9 events.
* This is not commonly used, but just for transitioning from v8 to v9 plugins
*/
export interface MixedPlugin extends EditorPlugin {
/**
* The first method that editor will call to a plugin when editor is initializing.
* It will pass in the editor instance, plugin should take this chance to save the
* editor reference so that it can call to any editor method or format API later.
* @param editor The editor object
*/

initializeV9: (editor: IEditor) => void;

/**
* Check if the plugin should handle the given event exclusively.
* Handle an event exclusively means other plugin will not receive this event in
* onPluginEvent method.
* If two plugins will return true in willHandleEventExclusively() for the same event,
* the final result depends on the order of the plugins are added into editor
* @param event The event to check:
*/
willHandleEventExclusivelyV9?: (event: PluginEvent) => boolean;

/**
* Core method for a plugin. Once an event happens in editor, editor will call this
* method of each plugin to handle the event as long as the event is not handled
* exclusively by another plugin.
* @param event The event to handle:
*/
onPluginEventV9?: (event: PluginEvent) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -511,4 +511,101 @@ describe('BridgePlugin', () => {

expect(disposeSpy).toHaveBeenCalledTimes(2);
});

it('MixedPlugin', () => {
const initializeV8Spy = jasmine.createSpy('initializeV8');
const initializeV9Spy = jasmine.createSpy('initializeV9');
const onPluginEventV8Spy = jasmine.createSpy('onPluginEventV8');
const onPluginEventV9Spy = jasmine.createSpy('onPluginEventV9');
const willHandleEventExclusivelyV8Spy = jasmine.createSpy('willHandleEventExclusivelyV8');
const willHandleEventExclusivelyV9Spy = jasmine.createSpy('willHandleEventExclusivelyV9');
const disposeSpy = jasmine.createSpy('dispose');

const mockedPlugin = {
initialize: initializeV8Spy,
initializeV9: initializeV9Spy,
onPluginEvent: onPluginEventV8Spy,
onPluginEventV9: onPluginEventV9Spy,
willHandleEventExclusively: willHandleEventExclusivelyV8Spy,
willHandleEventExclusivelyV9: willHandleEventExclusivelyV9Spy,
dispose: disposeSpy,
getName: () => '',
} as any;
const mockedEditor = {} as any;
const onInitializeSpy = jasmine.createSpy('onInitialize').and.returnValue(mockedEditor);
const plugin = new BridgePlugin.BridgePlugin(onInitializeSpy, [mockedPlugin]);

expect(initializeV8Spy).not.toHaveBeenCalled();
expect(initializeV9Spy).not.toHaveBeenCalled();
expect(onPluginEventV8Spy).not.toHaveBeenCalled();
expect(onPluginEventV9Spy).not.toHaveBeenCalled();
expect(disposeSpy).not.toHaveBeenCalled();
expect(onInitializeSpy).not.toHaveBeenCalled();
expect(willHandleEventExclusivelyV8Spy).not.toHaveBeenCalled();
expect(willHandleEventExclusivelyV9Spy).not.toHaveBeenCalled();

const mockedZoomScale = 'ZOOM' as any;
const calculateZoomScaleSpy = jasmine
.createSpy('calculateZoomScale')
.and.returnValue(mockedZoomScale);
const mockedColorManager = 'COLOR' as any;
const mockedInnerDarkColorHandler = 'INNERCOLOR' as any;
const mockedInnerEditor = {
getDOMHelper: () => ({
calculateZoomScale: calculateZoomScaleSpy,
}),
getColorManager: () => mockedInnerDarkColorHandler,
} as any;

const createDarkColorHandlerSpy = spyOn(
DarkColorHandler,
'createDarkColorHandler'
).and.returnValue(mockedColorManager);

plugin.initialize(mockedInnerEditor);

expect(onInitializeSpy).toHaveBeenCalledWith({
customData: {},
experimentalFeatures: [],
sizeTransformer: jasmine.anything(),
darkColorHandler: mockedColorManager,
edit: 'edit',
contextMenuProviders: [],
} as any);
expect(createDarkColorHandlerSpy).toHaveBeenCalledWith(mockedInnerDarkColorHandler);
expect(initializeV8Spy).toHaveBeenCalledTimes(1);
expect(initializeV9Spy).toHaveBeenCalledTimes(1);
expect(disposeSpy).not.toHaveBeenCalled();
expect(initializeV8Spy).toHaveBeenCalledWith(mockedEditor);
expect(initializeV9Spy).toHaveBeenCalledWith(mockedInnerEditor);
expect(onPluginEventV8Spy).not.toHaveBeenCalled();
expect(onPluginEventV9Spy).not.toHaveBeenCalled();
expect(willHandleEventExclusivelyV8Spy).not.toHaveBeenCalled();
expect(willHandleEventExclusivelyV9Spy).not.toHaveBeenCalled();

plugin.onPluginEvent({
eventType: 'editorReady',
addedBlockElements: [],
removedBlockElements: [],
});

expect(onPluginEventV8Spy).toHaveBeenCalledTimes(1);
expect(onPluginEventV9Spy).toHaveBeenCalledTimes(1);
expect(onPluginEventV8Spy).toHaveBeenCalledWith({
eventType: PluginEventType.EditorReady,
eventDataCache: undefined,
});
expect(onPluginEventV9Spy).toHaveBeenCalledWith({
eventType: 'editorReady',
addedBlockElements: [],
removedBlockElements: [],
eventDataCache: undefined,
});
expect(willHandleEventExclusivelyV8Spy).toHaveBeenCalledTimes(1);
expect(willHandleEventExclusivelyV9Spy).toHaveBeenCalledTimes(1);

plugin.dispose();

expect(disposeSpy).toHaveBeenCalledTimes(1);
});
});

0 comments on commit 0bc2578

Please sign in to comment.