Skip to content

Commit e75e27a

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

File tree

3 files changed

+214
-41
lines changed

3 files changed

+214
-41
lines changed

src/components/view/context-menu.tsx

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

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)