Skip to content

Commit 97251ed

Browse files
Support passing event to getContextMenuItems when plugin is a MixedPlugin (#3188)
* feat: enhance context menu handling to support V9 providers with event parameter * fix: enhance isV9ContextMenuProvider to check for mixed plugins * refactor: simplify spyOn usage for DarkColorHandler in BridgePlugin tests * fix: enhance isV9ContextMenuProvider to validate V9 provider signature * fix: update context menu provider check to use isMixedPluginProvider
1 parent 07951f4 commit 97251ed

File tree

2 files changed

+328
-12
lines changed

2 files changed

+328
-12
lines changed

packages/roosterjs-editor-adapter/lib/corePlugins/BridgePlugin.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,28 @@ export class BridgePlugin implements ContextMenuProvider<any> {
153153
* @param target Target node that triggered a ContextMenu event
154154
* @returns An array of context menu items, or null means no items needed
155155
*/
156-
getContextMenuItems(target: Node): any[] {
156+
getContextMenuItems(target: Node, event?: Event): any[] {
157157
const allItems: any[] = [];
158158

159159
this.contextMenuProviders.forEach(provider => {
160-
const items = provider.getContextMenuItems(target) ?? [];
161-
if (items?.length > 0) {
162-
if (allItems.length > 0) {
163-
allItems.push(null);
160+
if (isMixedPluginProvider(provider)) {
161+
const items = provider.getContextMenuItems(target, event) ?? [];
162+
if (items?.length > 0) {
163+
if (allItems.length > 0) {
164+
allItems.push(null);
165+
}
166+
167+
allItems.push(...items);
168+
}
169+
} else {
170+
const items = provider.getContextMenuItems(target) ?? [];
171+
if (items?.length > 0) {
172+
if (allItems.length > 0) {
173+
allItems.push(null);
174+
}
175+
176+
allItems.push(...items);
164177
}
165-
166-
allItems.push(...items);
167178
}
168179
});
169180

@@ -220,6 +231,15 @@ export class BridgePlugin implements ContextMenuProvider<any> {
220231
}
221232
}
222233

234+
/**
235+
* Check if a provider is a V9 context menu provider
236+
* @param provider The provider to check
237+
* @returns True if the provider is a V9 context menu provider, false otherwise
238+
*/
239+
function isMixedPluginProvider(provider: any): provider is ContextMenuProvider<any> {
240+
return isMixedPlugin(provider);
241+
}
242+
223243
/**
224244
* @internal Export for test only. This function is only used for compatibility from older build
225245

packages/roosterjs-editor-adapter/test/corePlugins/BridgePluginTest.ts

Lines changed: 301 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ describe('BridgePlugin', () => {
255255
mockedPlugin2,
256256
]);
257257

258-
spyOn(eventConverter, 'newEventToOldEvent').and.callFake(newEvent => {
258+
spyOn(eventConverter, 'newEventToOldEvent').and.callFake((newEvent: any) => {
259259
return ('NEW_' + newEvent) as any;
260260
});
261261

@@ -277,10 +277,10 @@ describe('BridgePlugin', () => {
277277

278278
it('onPluginEvent without exclusive handling', () => {
279279
const initializeSpy = jasmine.createSpy('initialize');
280-
const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1').and.callFake(event => {
280+
const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1').and.callFake((event: any) => {
281281
event.data = 'plugin1';
282282
});
283-
const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2').and.callFake(event => {
283+
const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2').and.callFake((event: any) => {
284284
event.data = 'plugin2';
285285
});
286286
const disposeSpy = jasmine.createSpy('dispose');
@@ -305,7 +305,7 @@ describe('BridgePlugin', () => {
305305
mockedPlugin2,
306306
]);
307307

308-
spyOn(eventConverter, 'newEventToOldEvent').and.callFake(newEvent => {
308+
spyOn(eventConverter, 'newEventToOldEvent').and.callFake((newEvent: any) => {
309309
return {
310310
eventType: 'old_' + newEvent.eventType,
311311
} as any;
@@ -390,7 +390,7 @@ describe('BridgePlugin', () => {
390390
mockedPlugin2,
391391
]);
392392

393-
spyOn(eventConverter, 'newEventToOldEvent').and.callFake(newEvent => {
393+
spyOn(eventConverter, 'newEventToOldEvent').and.callFake((newEvent: any) => {
394394
return {
395395
eventType: 'old_' + newEvent.eventType,
396396
eventDataCache: newEvent.eventDataCache,
@@ -534,6 +534,302 @@ describe('BridgePlugin', () => {
534534
expect(disposeSpy).toHaveBeenCalledTimes(2);
535535
});
536536

537+
it('Context Menu provider with V9 providers', () => {
538+
const initializeSpy = jasmine.createSpy('initialize');
539+
const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1');
540+
const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2');
541+
const onPluginEventSpy3 = jasmine.createSpy('onPluginEvent3');
542+
const disposeSpy = jasmine.createSpy('dispose');
543+
const queryElementsSpy = jasmine.createSpy('queryElement').and.returnValue([]);
544+
545+
// V8 style context menu provider (1 argument)
546+
const getContextMenuItemsSpyV8 = jasmine
547+
.createSpy('getContextMenuItems V8')
548+
.and.returnValue(['item1', 'item2']);
549+
550+
// V9 style context menu provider (2 arguments) - create function with proper length
551+
const getContextMenuItemsSpyV9 = jasmine
552+
.createSpy('getContextMenuItems V9')
553+
.and.returnValue(['item3', 'item4']);
554+
// Override length property to simulate V9 function signature
555+
Object.defineProperty(getContextMenuItemsSpyV9, 'length', { value: 2 });
556+
557+
const mockedPluginV8 = {
558+
initialize: initializeSpy,
559+
onPluginEvent: onPluginEventSpy1,
560+
dispose: disposeSpy,
561+
getContextMenuItems: getContextMenuItemsSpyV8,
562+
getName: () => '',
563+
} as any;
564+
565+
const mockedPluginV9 = {
566+
initialize: initializeSpy,
567+
onPluginEvent: onPluginEventSpy2,
568+
dispose: disposeSpy,
569+
getContextMenuItems: getContextMenuItemsSpyV9,
570+
getName: () => '',
571+
initializeV9: jasmine.createSpy('initializeV9'),
572+
} as any;
573+
574+
const mockedPluginRegular = {
575+
initialize: initializeSpy,
576+
onPluginEvent: onPluginEventSpy3,
577+
dispose: disposeSpy,
578+
getName: () => '',
579+
} as any;
580+
581+
const mockedEditor = {
582+
queryElements: queryElementsSpy,
583+
} as any;
584+
585+
const onInitializeSpy = jasmine.createSpy('onInitialize').and.returnValue(mockedEditor);
586+
const plugin = new BridgePlugin.BridgePlugin(onInitializeSpy, [
587+
mockedPluginV8,
588+
mockedPluginV9,
589+
mockedPluginRegular,
590+
]);
591+
592+
const mockedZoomScale = 'ZOOM' as any;
593+
const calculateZoomScaleSpy = jasmine
594+
.createSpy('calculateZoomScale')
595+
.and.returnValue(mockedZoomScale);
596+
const mockedColorManager = 'COLOR' as any;
597+
const mockedInnerEditor = {
598+
getDOMHelper: () => ({
599+
calculateZoomScale: calculateZoomScaleSpy,
600+
}),
601+
getColorManager: () => mockedColorManager,
602+
getEnvironment: () => {
603+
return {
604+
domToModelSettings: {
605+
customized: {},
606+
},
607+
};
608+
},
609+
} as any;
610+
const mockedDarkColorHandler = 'COLOR' as any;
611+
spyOn(DarkColorHandler, 'createDarkColorHandler').and.returnValue(mockedDarkColorHandler);
612+
613+
plugin.initialize(mockedInnerEditor);
614+
615+
expect(onInitializeSpy).toHaveBeenCalledWith({
616+
customData: {},
617+
experimentalFeatures: [],
618+
sizeTransformer: jasmine.anything(),
619+
darkColorHandler: mockedDarkColorHandler,
620+
edit: 'edit',
621+
contextMenuProviders: [mockedPluginV8, mockedPluginV9],
622+
} as any);
623+
624+
const mockedNode = 'NODE' as any;
625+
const mockedEvent = 'EVENT' as any;
626+
627+
// Test that V9 provider receives both arguments, V8 provider receives only target
628+
const items = plugin.getContextMenuItems(mockedNode, mockedEvent);
629+
630+
expect(items).toEqual(['item1', 'item2', null, 'item3', 'item4']);
631+
expect(getContextMenuItemsSpyV8).toHaveBeenCalledWith(mockedNode);
632+
expect(getContextMenuItemsSpyV9).toHaveBeenCalledWith(mockedNode, mockedEvent);
633+
634+
plugin.dispose();
635+
636+
expect(disposeSpy).toHaveBeenCalledTimes(3);
637+
});
638+
639+
it('Context Menu provider with empty results', () => {
640+
const initializeSpy = jasmine.createSpy('initialize');
641+
const disposeSpy = jasmine.createSpy('dispose');
642+
const queryElementsSpy = jasmine.createSpy('queryElement').and.returnValue([]);
643+
644+
// V8 provider returning empty array
645+
const getContextMenuItemsSpyV8Empty = jasmine
646+
.createSpy('getContextMenuItems V8 Empty')
647+
.and.returnValue([]);
648+
649+
// V9 provider returning null
650+
const getContextMenuItemsSpyV9Null = jasmine
651+
.createSpy('getContextMenuItems V9 Null')
652+
.and.returnValue(null);
653+
Object.defineProperty(getContextMenuItemsSpyV9Null, 'length', { value: 2 });
654+
655+
// V9 provider returning items
656+
const getContextMenuItemsSpyV9Items = jasmine
657+
.createSpy('getContextMenuItems V9 Items')
658+
.and.returnValue(['item1']);
659+
Object.defineProperty(getContextMenuItemsSpyV9Items, 'length', { value: 2 });
660+
661+
const mockedPluginV8Empty = {
662+
initialize: initializeSpy,
663+
dispose: disposeSpy,
664+
getContextMenuItems: getContextMenuItemsSpyV8Empty,
665+
getName: () => '',
666+
} as any;
667+
668+
const mockedPluginV9Null = {
669+
initialize: initializeSpy,
670+
dispose: disposeSpy,
671+
getContextMenuItems: getContextMenuItemsSpyV9Null,
672+
getName: () => '',
673+
initializeV9: jasmine.createSpy('initializeV9'),
674+
} as any;
675+
676+
const mockedPluginV9Items = {
677+
initialize: initializeSpy,
678+
dispose: disposeSpy,
679+
getContextMenuItems: getContextMenuItemsSpyV9Items,
680+
getName: () => '',
681+
initializeV9: jasmine.createSpy('initializeV9'),
682+
} as any;
683+
684+
const mockedEditor = {
685+
queryElements: queryElementsSpy,
686+
} as any;
687+
688+
const onInitializeSpy = jasmine.createSpy('onInitialize').and.returnValue(mockedEditor);
689+
const plugin = new BridgePlugin.BridgePlugin(onInitializeSpy, [
690+
mockedPluginV8Empty,
691+
mockedPluginV9Null,
692+
mockedPluginV9Items,
693+
]);
694+
695+
const mockedZoomScale = 'ZOOM' as any;
696+
const calculateZoomScaleSpy = jasmine
697+
.createSpy('calculateZoomScale')
698+
.and.returnValue(mockedZoomScale);
699+
const mockedColorManager = 'COLOR' as any;
700+
const mockedInnerEditor = {
701+
getDOMHelper: () => ({
702+
calculateZoomScale: calculateZoomScaleSpy,
703+
}),
704+
getColorManager: () => mockedColorManager,
705+
getEnvironment: () => {
706+
return {
707+
domToModelSettings: {
708+
customized: {},
709+
},
710+
};
711+
},
712+
} as any;
713+
const mockedDarkColorHandler = 'COLOR' as any;
714+
spyOn(DarkColorHandler, 'createDarkColorHandler').and.returnValue(mockedDarkColorHandler);
715+
716+
plugin.initialize(mockedInnerEditor);
717+
718+
const mockedNode = 'NODE' as any;
719+
const mockedEvent = 'EVENT' as any;
720+
721+
// Only the provider with items should contribute to the result
722+
const items = plugin.getContextMenuItems(mockedNode, mockedEvent);
723+
724+
expect(items).toEqual(['item1']);
725+
expect(getContextMenuItemsSpyV8Empty).toHaveBeenCalledWith(mockedNode);
726+
expect(getContextMenuItemsSpyV9Null).toHaveBeenCalledWith(mockedNode, mockedEvent);
727+
expect(getContextMenuItemsSpyV9Items).toHaveBeenCalledWith(mockedNode, mockedEvent);
728+
729+
plugin.dispose();
730+
});
731+
732+
it('isV9ContextMenuProvider detection', () => {
733+
const initializeSpy = jasmine.createSpy('initialize');
734+
const disposeSpy = jasmine.createSpy('dispose');
735+
736+
// Function with 1 parameter (V8 style)
737+
const getContextMenuItemsV8 = jasmine
738+
.createSpy('getContextMenuItems V8')
739+
.and.returnValue(['item1']);
740+
Object.defineProperty(getContextMenuItemsV8, 'length', { value: 1 });
741+
742+
// Function with 2 parameters (V9 style)
743+
const getContextMenuItemsV9 = jasmine
744+
.createSpy('getContextMenuItems V9')
745+
.and.returnValue(['item2']);
746+
Object.defineProperty(getContextMenuItemsV9, 'length', { value: 2 });
747+
748+
// Function with 0 parameters
749+
const getContextMenuItemsZero = jasmine
750+
.createSpy('getContextMenuItems Zero')
751+
.and.returnValue(['item3']);
752+
Object.defineProperty(getContextMenuItemsZero, 'length', { value: 0 });
753+
754+
// Function with 3 parameters
755+
const getContextMenuItemsThree = jasmine
756+
.createSpy('getContextMenuItems Three')
757+
.and.returnValue(['item4']);
758+
Object.defineProperty(getContextMenuItemsThree, 'length', { value: 3 });
759+
760+
const mockedPluginV8 = {
761+
initialize: initializeSpy,
762+
dispose: disposeSpy,
763+
getContextMenuItems: getContextMenuItemsV8,
764+
getName: () => 'V8Plugin',
765+
} as any;
766+
767+
const mockedPluginV9 = {
768+
initialize: initializeSpy,
769+
dispose: disposeSpy,
770+
getContextMenuItems: getContextMenuItemsV9,
771+
getName: () => 'V9Plugin',
772+
initializeV9: jasmine.createSpy('initializeV9'),
773+
} as any;
774+
775+
const mockedPluginZero = {
776+
initialize: initializeSpy,
777+
dispose: disposeSpy,
778+
getContextMenuItems: getContextMenuItemsZero,
779+
getName: () => 'ZeroPlugin',
780+
} as any;
781+
782+
const mockedPluginThree = {
783+
initialize: initializeSpy,
784+
dispose: disposeSpy,
785+
getContextMenuItems: getContextMenuItemsThree,
786+
getName: () => 'ThreePlugin',
787+
} as any;
788+
789+
const mockedEditor = {} as any;
790+
const onInitializeSpy = jasmine.createSpy('onInitialize').and.returnValue(mockedEditor);
791+
const plugin = new BridgePlugin.BridgePlugin(onInitializeSpy, [
792+
mockedPluginV8,
793+
mockedPluginV9,
794+
mockedPluginZero,
795+
mockedPluginThree,
796+
]);
797+
798+
const mockedInnerEditor = {
799+
getDOMHelper: () => ({
800+
calculateZoomScale: () => 1,
801+
}),
802+
getColorManager: () => 'COLOR',
803+
getEnvironment: () => {
804+
return {
805+
domToModelSettings: {
806+
customized: {},
807+
},
808+
};
809+
},
810+
} as any;
811+
812+
spyOn(DarkColorHandler, 'createDarkColorHandler').and.returnValue('COLOR' as any);
813+
814+
plugin.initialize(mockedInnerEditor);
815+
816+
const mockedNode = 'NODE' as any;
817+
const mockedEvent = 'EVENT' as any;
818+
819+
const items = plugin.getContextMenuItems(mockedNode, mockedEvent);
820+
821+
// V8 plugins should be called with only target, V9 plugin should be called with both target and event
822+
expect(getContextMenuItemsV8).toHaveBeenCalledWith(mockedNode);
823+
expect(getContextMenuItemsV9).toHaveBeenCalledWith(mockedNode, mockedEvent);
824+
expect(getContextMenuItemsZero).toHaveBeenCalledWith(mockedNode);
825+
expect(getContextMenuItemsThree).toHaveBeenCalledWith(mockedNode);
826+
827+
// Only V9 plugin (length === 2) should receive the event parameter
828+
expect(items).toEqual(['item1', null, 'item2', null, 'item3', null, 'item4']);
829+
830+
plugin.dispose();
831+
});
832+
537833
it('MixedPlugin', () => {
538834
const initializeV8Spy = jasmine.createSpy('initializeV8');
539835
const initializeV9Spy = jasmine.createSpy('initializeV9');

0 commit comments

Comments
 (0)