Skip to content

Commit f3a4525

Browse files
committed
Refactored and abstracted a common interface to allow native as well
1 parent 359bbcb commit f3a4525

File tree

3 files changed

+219
-41
lines changed

3 files changed

+219
-41
lines changed

src/components/view/context-menu.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as React from 'react';
2+
import { HTMLContextMenu } from './html-context-menu';
3+
export type ClickEventType = React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement> | React.KeyboardEvent<HTMLElement> | KeyboardEvent;
4+
export interface ContextItemClicked {
5+
(e:ClickEventType, props:object): void;
6+
}
7+
export interface ContextMenu {
8+
CreateContextMenu(items: BaseContextMenuItem[]): any;
9+
//AttachMenuToItem(domElem: any, props: any): JSX.Element;
10+
GetContextMenuHandler(props: any) : any;
11+
menu: JSX.Element | null;
12+
}
13+
let GetNewContextMenu : ContextMenuGenerator = (component_id? : string) => {
14+
throw new Error('NewContextMenu Initializer not defined');
15+
}
16+
export {GetNewContextMenu}
17+
18+
interface ContextMenuGenerator {
19+
(component_id? : string) : ContextMenu;
20+
}
21+
22+
export function SetNewContextMenuGenerator( gen : ContextMenuGenerator ){
23+
GetNewContextMenu = gen;
24+
}
25+
SetNewContextMenuGenerator( (component_id? : string) => new HTMLContextMenu(component_id) );
26+
export class BaseContextMenuItem {
27+
hidden? = () => false;
28+
static auto_comp_id=4567;
29+
component_id:string;
30+
constructor(component_id:string|null=null){
31+
if (! component_id)
32+
component_id = "ConMenuItemID" + BaseContextMenuItem.auto_comp_id++;
33+
this.component_id = component_id;
34+
}
35+
}
36+
export class SeparatorMenuItem extends BaseContextMenuItem {
37+
}
38+
export interface ContextMenuItemArgs extends TitledMenuItemArgs {
39+
onClick: ContextItemClicked;
40+
}
41+
export interface TitledMenuItemArgs {
42+
disabled?(): boolean;
43+
hidden?(): boolean;
44+
component_id?:string;
45+
title: string;
46+
47+
}
48+
export interface SubMenuItemArgs extends TitledMenuItemArgs {
49+
sub_items: BaseContextMenuItem[];
50+
}
51+
52+
53+
export class TitledContextMenuItem extends BaseContextMenuItem {
54+
disabled = () => this.is_disabled;
55+
title: string;
56+
is_hidden: boolean;
57+
is_disabled: boolean;
58+
constructor(props:TitledMenuItemArgs|null=null){
59+
super(props?.component_id);
60+
this.title = "";
61+
this.is_hidden = false;
62+
this.is_disabled = false;
63+
if (props)
64+
Object.assign(this, props);
65+
}
66+
67+
hidden = () => this.is_hidden;
68+
Hide(hide=true) {
69+
this.is_hidden=hide;
70+
}
71+
Disable(disable=true){
72+
this.is_disabled = disable;
73+
}
74+
75+
}
76+
export class SubMenuItem extends TitledContextMenuItem {
77+
sub_items?: BaseContextMenuItem[];
78+
constructor(props:SubMenuItemArgs|null=null){
79+
super(props);
80+
}
81+
}
82+
export class ContextMenuItem extends TitledContextMenuItem {
83+
constructor(props:ContextMenuItemArgs|null=null){
84+
super(props);
85+
}
86+
onClick(e: ClickEventType, props:any) { };
87+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Menu, Item, Separator, Submenu, useContextMenu, PredicateParams, ItemProps, ItemParams } from "react-contexify";
2+
import * as React from 'react';
3+
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
4+
import { ContextMenu, SubMenuItem, TitledContextMenuItem, ClickEventType, BaseContextMenuItem, ContextMenuItem, SeparatorMenuItem } from "./context-menu";
5+
6+
7+
8+
type ItemData = any;
9+
10+
export class HTMLContextMenu implements ContextMenu {
11+
constructor(component_id? : string) {
12+
this.menu = null;
13+
this.items = null;
14+
if (! component_id)
15+
component_id = "ContextMenu" + HTMLContextMenu.auto_id++;
16+
this.component_id = component_id;
17+
}
18+
static auto_id=4567;
19+
component_id : string;
20+
menu: JSX.Element | null;
21+
ContextMenuItemClicked = ({ id, event, props, data, triggerEvent }: ItemParams<ItemProps, ItemData>) {
22+
let item = this.GetContextMenuItemById(this.items, id);
23+
if (item && item instanceof ContextMenuItem)
24+
item.onClick(event, props);
25+
}
26+
27+
28+
protected GetContextMenuItemById(items: BaseContextMenuItem[] | null | undefined, id: string | undefined): BaseContextMenuItem | null {
29+
if (!items)
30+
return null;
31+
let item = items?.find(i => i.component_id === id);
32+
if (item == null) {
33+
items?.forEach(sub_item => {
34+
if (sub_item instanceof SubMenuItem)
35+
return this.GetContextMenuItemById(sub_item.sub_items, id);
36+
});
37+
}
38+
if (!item)
39+
return null;
40+
return item;
41+
}
42+
43+
items: BaseContextMenuItem[] | null;
44+
CreateContextMenu(items: BaseContextMenuItem[]) {
45+
this.items = items;
46+
this.menu = this.CreateMenuComponent(items);
47+
return this.menu;
48+
}
49+
GetContextMenuHandler(props: any) {
50+
return (e: ClickEventType) => this.displayMenu(e, props);
51+
}
52+
// AttachMenuToItem(domElem: any, props: any) {
53+
// domElem.setState( {onContextMenu: );
54+
// //would need to ues clonelem
55+
// return domElem;
56+
// }
57+
displayMenu(e: ClickEventType, props: object) {
58+
useContextMenu({
59+
id: this.component_id
60+
}).show({ event: e, props: props });
61+
}
62+
63+
IsDisabled = ({ id, triggerEvent, props, data }: PredicateParams<ItemProps, ItemData>) => {
64+
let item = this.GetContextMenuItemById(this.items, id);
65+
if (item && item instanceof TitledContextMenuItem) //not sure this works
66+
return item.disabled();
67+
return false;
68+
}
69+
IsHidden = ({ id, triggerEvent, props, data }: PredicateParams<ItemProps, ItemData>) => {
70+
let item = this.GetContextMenuItemById(this.items, id);
71+
if (item && item instanceof TitledContextMenuItem) //not sure this works
72+
return item.hidden();
73+
return false;
74+
}
75+
GetItem(item: BaseContextMenuItem) {
76+
if (item instanceof SubMenuItem) {
77+
return <Submenu id={item.component_id} label={item.title}>
78+
{item.sub_items?.map((item) => this.GetItem(item))}
79+
</Submenu>;
80+
}
81+
else if (item instanceof ContextMenuItem)
82+
return <Item disabled={this.IsDisabled} hidden={this.IsHidden} id={item.component_id} key={item.component_id} onClick={this.ContextMenuItemClicked}>{item.title}</Item>;
83+
else if (item instanceof SeparatorMenuItem)
84+
return <Separator key={item.component_id} />
85+
}
86+
CreateMenuComponent(items: BaseContextMenuItem[]) {
87+
88+
return (<>
89+
<Menu id={this.component_id}>
90+
{items.map((item) => this.GetItem(item))}
91+
</Menu>
92+
</>);
93+
94+
95+
}
96+
97+
98+
}
99+

src/components/view/view-event-list.tsx

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
88

99
import { styled } from '../../styles'
1010
import { ArrowIcon, Icon, WarningIcon } from '../../icons';
11+
12+
import { ContextMenuItem, ClickEventType, BaseContextMenuItem, SubMenuItem, GetNewContextMenu } from './context-menu';
13+
1114
import {
1215
CollectedEvent,
1316
HttpExchange,
@@ -33,22 +36,6 @@ import { StatusCode } from '../common/status-code';
3336

3437
import { HEADER_FOOTER_HEIGHT } from './view-event-list-footer';
3538

36-
import {
37-
Menu,
38-
Item,
39-
Separator,
40-
Submenu,
41-
useContextMenu,
42-
ItemProps,
43-
ItemParams,
44-
} from "react-contexify";
45-
const MENU_VIEW_EVENT_ID = "MENU_VIEW_EVENT_LIST";
46-
47-
48-
const { show } = useContextMenu({
49-
id: MENU_VIEW_EVENT_ID
50-
});
51-
type ItemData = any;
5239
const SCROLL_BOTTOM_MARGIN = 5; // If you're in the last 5 pixels of the scroll area, we say you're at the bottom
5340

5441
const EmptyStateOverlay = styled(EmptyState)`
@@ -372,6 +359,33 @@ const EventRow = observer((props: EventRowProps) => {
372359
}
373360
});
374361

362+
interface ExchangeOnClickFunc {
363+
(e: ClickEventType, props: object, exchange: HttpExchange): void;
364+
}
365+
function getExchangeOnClick(func: ExchangeOnClickFunc) {
366+
return (e: ClickEventType, props: object) => func(e, props, (props as any).exchange as HttpExchange);
367+
368+
}
369+
let aMenu = GetNewContextMenu();
370+
let cntr = 0;
371+
let arr: BaseContextMenuItem[] = [
372+
new ContextMenuItem({ title: "Toggle Pin", disabled: () => (cntr++ % 2) == 0 , onClick: getExchangeOnClick((e, props, exchange) => runInAction(() => exchange.pinned = !exchange.pinned)) }),
373+
new SubMenuItem({
374+
title: "Copy",
375+
sub_items: [new ContextMenuItem(
376+
{
377+
title: "Decoded Body", onClick: getExchangeOnClick( (e, props, exchange) => {
378+
if (exchange && exchange.hasResponseBody() && exchange.response.body)
379+
exchange.response.body.decodedPromise.then(val => { copyToClipboard(UTF8Decoder.decode(val)) });
380+
}
381+
)
382+
383+
}) ]
384+
})
385+
386+
];
387+
let addItem = aMenu.CreateContextMenu(arr);
388+
375389
const ExchangeRow = observer(({
376390
index,
377391
isSelected,
@@ -396,7 +410,7 @@ const ExchangeRow = observer(({
396410
aria-rowindex={index + 1}
397411
data-event-id={exchange.id}
398412
tabIndex={isSelected ? 0 : -1}
399-
onContextMenu={e => displayMenu(e,exchange)}
413+
onContextMenu={aMenu.GetContextMenuHandler({ exchange: exchange })}
400414
className={isSelected ? 'selected' : ''}
401415
style={style}
402416
>
@@ -688,22 +702,7 @@ async function copyToClipboard(textToCopy: string) {
688702
console.error("Clipboard copy failure", error);
689703
}
690704
}
691-
const ContextMenuItemClicked = ( { id, event, props, data, triggerEvent }: ItemParams<ItemProps, ItemData> ) => {
692-
let exchange = (props as any).exchange as HttpExchange;
693-
switch(id) {
694-
case "TogglePin":
695-
runInAction(() => exchange.pinned = ! exchange.pinned );
696-
break;
697-
case "DecodedBody":
698-
if (exchange && exchange.hasResponseBody() && exchange.response.body )
699-
exchange.response.body.decodedPromise.then( val => { copyToClipboard(UTF8Decoder.decode(val)) });
700-
break;
701-
}
702-
703-
};
704-
function displayMenu(e: React.MouseEvent, exchange : HttpExchange) {
705-
show({event: e, props: { exchange: exchange } });
706-
}
705+
707706
@observer
708707
export class ViewEventList extends React.Component<ViewEventListProps> {
709708

@@ -790,14 +789,7 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
790789
}</Observer>
791790
}</AutoSizer>
792791
}
793-
794-
<Menu id={MENU_VIEW_EVENT_ID}>
795-
<Item id="TogglePin" onClick={ContextMenuItemClicked}>Toggle Pinned</Item>
796-
<Separator />
797-
<Submenu label="Copy">
798-
<Item id="DecodedBody" onClick={ContextMenuItemClicked}>Decoded Body</Item>
799-
</Submenu>
800-
</Menu>
792+
{aMenu.menu}
801793
</ListContainer>;
802794
}
803795

0 commit comments

Comments
 (0)