Skip to content

Commit 5675eff

Browse files
committed
Strongly type property info make context menu generic around this.
Pass property info to disable/hidden functions as well
1 parent e75e27a commit 5675eff

File tree

3 files changed

+97
-79
lines changed

3 files changed

+97
-79
lines changed

src/components/view/context-menu.tsx

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
11
import * as React from 'react';
2-
import { HTMLContextMenu } from './html-context-menu';
2+
33
export type ClickEventType = React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement> | React.KeyboardEvent<HTMLElement> | KeyboardEvent;
4-
export interface ContextItemClicked {
5-
(e: ClickEventType, props: object): void;
4+
export interface ContextItemClicked<PropType> {
5+
(e: ClickEventType, props: PropType): void;
66
}
7-
export interface ContextMenu {
8-
CreateContextMenu(items: BaseContextMenuItem[]): void;
7+
export abstract class ContextMenu<PropType> {
8+
abstract SetContextMenuItems(items: BaseContextMenuItem<PropType>[]): void;
9+
abstract AppendContextMenuItem(item: BaseContextMenuItem<PropType>): void;
10+
//abstract CreateMenuItem();
911
//AttachMenuToItem(domElem: any, props: any): JSX.Element;
10-
GetContextMenuHandler(props: any): React.MouseEventHandler<HTMLDivElement>;
11-
menu: JSX.Element | null;
12+
abstract GetContextMenuHandler(props: PropType): React.MouseEventHandler<HTMLDivElement>;
13+
abstract GetMenuComponent(): JSX.Element | null;
14+
15+
/* These helper functions are to support generic inferring so you don't have to specify it multiple times */
16+
NewSubMenuItem(args: SubMenuItemArgs<PropType>) {
17+
return new SubMenuItem(args);
18+
}
19+
NewMenuItem(args: ContextMenuItemArgs<PropType>) {
20+
return new ContextMenuItem(args);
21+
}
22+
NewSeparatorItem() {
23+
return new SeparatorMenuItem();
24+
}
25+
1226
}
1327
let GetNewContextMenu: ContextMenuGenerator = (component_id?: string) => {
1428
throw new Error('NewContextMenu Initializer not defined');
29+
1530
}
31+
1632
export { GetNewContextMenu }
1733

1834
interface ContextMenuGenerator {
19-
(component_id?: string): ContextMenu;
35+
<PropType>(component_id?: string): ContextMenu<PropType>;
2036
}
2137

2238
export function SetNewContextMenuGenerator(gen: ContextMenuGenerator) {
2339
GetNewContextMenu = gen;
2440
}
25-
SetNewContextMenuGenerator((component_id?: string) => new HTMLContextMenu(component_id));
26-
export class BaseContextMenuItem {
27-
hidden?= () => false;
41+
42+
export class BaseContextMenuItem<PropType> {
43+
hidden?= (props: PropType) => false;
2844
static auto_comp_id = 4567;
2945
component_id: string;
3046
constructor(component_id: string | null = null) {
@@ -33,28 +49,28 @@ export class BaseContextMenuItem {
3349
this.component_id = component_id;
3450
}
3551
}
36-
export class SeparatorMenuItem extends BaseContextMenuItem { }
37-
export interface ContextMenuItemArgs extends TitledMenuItemArgs {
38-
onClick: ContextItemClicked;
52+
export class SeparatorMenuItem<PropType> extends BaseContextMenuItem<PropType> { }
53+
export interface ContextMenuItemArgs<PropType> extends TitledMenuItemArgs<PropType> {
54+
onClick: ContextItemClicked<PropType>;
3955
}
40-
export interface TitledMenuItemArgs {
41-
disabled?(): boolean;
42-
hidden?(): boolean;
56+
export interface TitledMenuItemArgs<PropType> {
57+
disabled?(props: PropType): boolean;
58+
hidden?(props: PropType): boolean;
4359
component_id?: string;
4460
title: string;
4561

4662
}
47-
export interface SubMenuItemArgs extends TitledMenuItemArgs {
48-
sub_items: BaseContextMenuItem[];
63+
export interface SubMenuItemArgs<PropType> extends TitledMenuItemArgs<PropType> {
64+
sub_items: BaseContextMenuItem<PropType>[];
4965
}
5066

5167

52-
export class TitledContextMenuItem extends BaseContextMenuItem {
53-
disabled = () => this.is_disabled;
68+
export class TitledContextMenuItem<PropType> extends BaseContextMenuItem<PropType> {
69+
disabled = (props: PropType) => this.is_disabled;
5470
title: string;
5571
is_hidden: boolean;
5672
is_disabled: boolean;
57-
constructor(props: TitledMenuItemArgs | null = null) {
73+
constructor(props: TitledMenuItemArgs<PropType> | null = null) {
5874
super(props?.component_id);
5975
this.title = "";
6076
this.is_hidden = false;
@@ -63,7 +79,7 @@ export class TitledContextMenuItem extends BaseContextMenuItem {
6379
Object.assign(this, props);
6480
}
6581

66-
hidden = () => this.is_hidden;
82+
hidden = (props: PropType) => this.is_hidden;
6783
Hide(hide = true) {
6884
this.is_hidden = hide;
6985
}
@@ -72,15 +88,16 @@ export class TitledContextMenuItem extends BaseContextMenuItem {
7288
}
7389

7490
}
75-
export class SubMenuItem extends TitledContextMenuItem {
76-
sub_items?: BaseContextMenuItem[];
77-
constructor(props: SubMenuItemArgs | null = null) {
91+
export class SubMenuItem<PropType> extends TitledContextMenuItem<PropType> {
92+
sub_items?: BaseContextMenuItem<PropType>[];
93+
constructor(props: SubMenuItemArgs<PropType> | null = null) {
7894
super(props);
7995
}
8096
}
81-
export class ContextMenuItem extends TitledContextMenuItem {
82-
constructor(props: ContextMenuItemArgs | null = null) {
97+
export class ContextMenuItem<PropType> extends TitledContextMenuItem<PropType> {
98+
constructor(props: ContextMenuItemArgs<PropType> | null = null) {
8399
super(props);
84100
}
85-
onClick(e: ClickEventType, props: any) { };
101+
onClick(e: ClickEventType, props: PropType) { };
86102
}
103+
Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { Menu, Item, Separator, Submenu, useContextMenu, PredicateParams, ItemProps, ItemParams } from "react-contexify";
1+
import { Menu, Item, Separator, Submenu, useContextMenu, PredicateParams, ItemParams } from "react-contexify";
22
import * as React from 'react';
3-
import { ContextMenu, SubMenuItem, TitledContextMenuItem, ClickEventType, BaseContextMenuItem, ContextMenuItem, SeparatorMenuItem } from "./context-menu";
3+
import { ContextMenu, SubMenuItem, TitledContextMenuItem, ClickEventType, BaseContextMenuItem, ContextMenuItem, SeparatorMenuItem, SetNewContextMenuGenerator } from "./context-menu";
44

55
type ItemData = any;
66

7-
export class HTMLContextMenu implements ContextMenu {
7+
export class HTMLContextMenu<PropType> extends ContextMenu<PropType> {
88
constructor(component_id?: string) {
9+
super();
910
this.menu = null;
10-
this.items = null;
11+
this.items = [];
1112
if (!component_id)
1213
component_id = "ContextMenu" + HTMLContextMenu.auto_id++;
1314
this.component_id = component_id;
@@ -16,62 +17,71 @@ export class HTMLContextMenu implements ContextMenu {
1617
component_id: string;
1718
menu: JSX.Element | null;
1819

19-
ContextMenuItemClicked({ id, event, props, data, triggerEvent }: ItemParams<ItemProps, ItemData>){
20+
ContextMenuItemClicked = ({ id, event, props, data, triggerEvent }: ItemParams<PropType, ItemData>) => {
2021

2122
let item = this.GetContextMenuItemById(this.items, id);
2223
if (item && item instanceof ContextMenuItem)
2324
item.onClick(event, props);
25+
else
26+
console.log("Unable to find sub item on context menu something likely wrong");
2427
}
2528

2629

27-
protected GetContextMenuItemById(items: BaseContextMenuItem[] | null | undefined, id: string | undefined): BaseContextMenuItem | null {
30+
protected GetContextMenuItemById(items: BaseContextMenuItem<PropType>[] | null | undefined, id: string | undefined): BaseContextMenuItem<PropType> | null {
2831
if (!items)
2932
return null;
3033
let item = items?.find(i => i.component_id === id);
31-
if (item == null) {
32-
items?.forEach(sub_item => {
34+
if (item == null && items) {
35+
for (let sub_item of items) {
3336
if (sub_item instanceof SubMenuItem)
3437
return this.GetContextMenuItemById(sub_item.sub_items, id);
35-
});
38+
}
3639
}
3740
if (!item)
3841
return null;
3942
return item;
4043
}
4144

42-
items: BaseContextMenuItem[] | null;
43-
CreateContextMenu(items: BaseContextMenuItem[]) {
45+
items: BaseContextMenuItem<PropType>[];
46+
SetContextMenuItems(items: BaseContextMenuItem<PropType>[]) {
4447
this.items = items;
45-
this.menu = this.CreateMenuComponent(items);
48+
this.menu = null;
49+
}
50+
AppendContextMenuItem(item: BaseContextMenuItem<PropType>) {
51+
this.items.push(item);
52+
}
53+
GetMenuComponent() {
54+
if (this.menu == null)
55+
this.menu = this.CreateMenuComponent(this.items);
4656
return this.menu;
4757
}
48-
GetContextMenuHandler(props: any) {
58+
GetContextMenuHandler(props: PropType) {
4959
return (e: ClickEventType) => this.displayMenu(e, props);
5060
}
5161
// AttachMenuToItem(domElem: any, props: any) {
5262
// domElem.setState( {onContextMenu: );
5363
// //would need to ues clonelem
5464
// return domElem;
5565
// }
56-
displayMenu(e: ClickEventType, props: object) {
57-
useContextMenu({
58-
id: this.component_id
59-
}).show({ event: e, props: props });
66+
displayMenu(e: ClickEventType, props: PropType) {
67+
let selection = window.getSelection();
68+
if (selection == null || selection.toString().length < 2)//if text is selected use native menu, note if text is selected but its not what we right click on this will still work as that text is deselected prior to this call
69+
useContextMenu({ id: this.component_id }).show({ event: e, props: props });
6070
}
6171

62-
IsDisabled = ({ id, triggerEvent, props, data }: PredicateParams<ItemProps, ItemData>) => {
72+
IsDisabled = ({ id, triggerEvent, props, data }: PredicateParams<PropType, ItemData>) => {
6373
let item = this.GetContextMenuItemById(this.items, id);
6474
if (item && item instanceof TitledContextMenuItem)
65-
return item.disabled();
75+
return item.disabled(props);
6676
return false;
6777
}
68-
IsHidden = ({ id, triggerEvent, props, data }: PredicateParams<ItemProps, ItemData>) => {
78+
IsHidden = ({ id, triggerEvent, props, data }: PredicateParams<PropType, ItemData>) => {
6979
let item = this.GetContextMenuItemById(this.items, id);
7080
if (item && item instanceof TitledContextMenuItem)
71-
return item.hidden();
81+
return item.hidden(props);
7282
return false;
7383
}
74-
GetItem(item: BaseContextMenuItem) {
84+
GetItem(item: BaseContextMenuItem<PropType>) {
7585
if (item instanceof SubMenuItem) {
7686
return <Submenu id={item.component_id} label={item.title}>
7787
{item.sub_items?.map((item) => this.GetItem(item))}
@@ -82,7 +92,7 @@ export class HTMLContextMenu implements ContextMenu {
8292
else if (item instanceof SeparatorMenuItem)
8393
return <Separator key={item.component_id} />
8494
}
85-
CreateMenuComponent(items: BaseContextMenuItem[]) {
95+
CreateMenuComponent(items: BaseContextMenuItem<PropType>[]) {
8696

8797
return (<>
8898
<Menu id={this.component_id}>
@@ -92,4 +102,6 @@ export class HTMLContextMenu implements ContextMenu {
92102

93103
}
94104

95-
}
105+
}
106+
SetNewContextMenuGenerator(<PropType,>(component_id?: string) => new HTMLContextMenu<PropType>(component_id));
107+
export function noop() { }

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

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
99
import { styled } from '../../styles'
1010
import { ArrowIcon, Icon, WarningIcon } from '../../icons';
1111

12-
import { ContextMenuItem, ClickEventType, BaseContextMenuItem, SubMenuItem, GetNewContextMenu } from './context-menu';
12+
import { GetNewContextMenu } from './context-menu';
13+
import {HTMLContextMenu,noop} from './html-context-menu';
14+
15+
noop();
1316

1417
import {
1518
CollectedEvent,
@@ -359,32 +362,18 @@ const EventRow = observer((props: EventRowProps) => {
359362
}
360363
});
361364

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-
365+
interface EventClickedData {
366+
exchange : HttpExchange;
368367
}
369-
let aMenu = GetNewContextMenu();
368+
let aMenu = GetNewContextMenu<EventClickedData>();
370369
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);
370+
aMenu.AppendContextMenuItem(aMenu.NewMenuItem ({ title: "Toggle Pin", disabled: () => (cntr++ % 2) == 0 , onClick: (e, props) => runInAction(() => props.exchange.pinned = !props.exchange.pinned)}));
371+
aMenu.AppendContextMenuItem(aMenu.NewSubMenuItem({title: "Copy",sub_items: [ aMenu.NewMenuItem( {
372+
title: "Decoded Body", onClick: (e, props) => {
373+
if (props.exchange && props.exchange.hasResponseBody() && props.exchange.response.body)
374+
props.exchange.response.body.decodedPromise.then(val => { copyToClipboard(UTF8Decoder.decode(val)) });
375+
}
376+
})] }) );
388377

389378
const ExchangeRow = observer(({
390379
index,
@@ -789,7 +778,7 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
789778
}</Observer>
790779
}</AutoSizer>
791780
}
792-
{aMenu.menu}
781+
{aMenu.GetMenuComponent()}
793782
</ListContainer>;
794783
}
795784

0 commit comments

Comments
 (0)