aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/ContextMenu.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/ContextMenu.tsx')
-rw-r--r--src/client/views/ContextMenu.tsx159
1 files changed, 119 insertions, 40 deletions
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 1133f70a1..69692dbb8 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,11 +1,12 @@
import React = require("react");
-import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem";
+import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem";
import { observable, action, computed } from "mobx";
import { observer } from "mobx-react";
import "./ContextMenu.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons';
+import Measure from "react-measure";
library.add(faSearch);
library.add(faCircle);
@@ -14,23 +15,21 @@ library.add(faCircle);
export class ContextMenu extends React.Component {
static Instance: ContextMenu;
- @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
+ @observable private _items: Array<ContextMenuProps> = [];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
@observable private _display: boolean = false;
@observable private _searchString: string = "";
// afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be
@observable private _yRelativeToTop: boolean = true;
+ @observable selectedIndex = -1;
- private _searchRef = React.createRef<HTMLInputElement>();
-
- private ref: React.RefObject<HTMLDivElement>;
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
constructor(props: Readonly<{}>) {
super(props);
- this.ref = React.createRef();
-
ContextMenu.Instance = this;
}
@@ -50,23 +49,42 @@ export class ContextMenu extends React.Component {
return this._items;
}
+ static readonly buffer = 20;
+ get pageX() {
+ const x = this._pageX;
+ if (x < 0) {
+ return 0;
+ }
+ const width = this._width;
+ if (x + width > window.innerWidth - ContextMenu.buffer) {
+ return window.innerWidth - ContextMenu.buffer - width;
+ }
+ return x;
+ }
+
+ get pageY() {
+ const y = this._pageY;
+ if (y < 0) {
+ return 0;
+ }
+ const height = this._height;
+ if (y + height > window.innerHeight - ContextMenu.buffer) {
+ return window.innerHeight - ContextMenu.buffer - height;
+ }
+ return y;
+ }
+
@action
displayMenu(x: number, y: number) {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
- let maxX = window.innerWidth - 150;
- let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30);
- this._pageX = x > maxX ? maxX : x;
- this._pageY = y > maxY ? maxY : y;
+ this._pageX = x;
+ this._pageY = y;
this._searchString = "";
this._display = true;
-
- if (this._searchRef.current) {
- this._searchRef.current.focus();
- }
}
@action
@@ -75,73 +93,134 @@ export class ContextMenu extends React.Component {
this._display = false;
}
- @computed get filteredItems() {
+ @computed get filteredItems(): (OriginalMenuProps | string[])[] {
const searchString = this._searchString.toLowerCase().split(" ");
const matches = (descriptions: string[]): boolean => {
- return searchString.every(s => descriptions.some(desc => desc.includes(s)));
+ return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s)));
};
- const createGroupHeader = (contents: any) => {
- return (
- <div className="contextMenu-group">
- <div className="contextMenu-description">{contents}</div>
- </div>
- );
- };
- const createItem = (item: ContextMenuProps) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />;
- const flattenItems = (items: ContextMenuProps[], groupFunc: (contents: any) => JSX.Element, getPath: () => string[]) => {
- let eles: JSX.Element[] = [];
+ const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => {
+ let eles: (OriginalMenuProps | string[])[] = [];
+ const leaves: OriginalMenuProps[] = [];
for (const item of items) {
- const description = item.description.toLowerCase();
- const path = [...getPath(), description];
+ const description = item.description;
+ const path = groupFunc(description);
if ("subitems" in item) {
- const children = flattenItems(item.subitems, contents => groupFunc(<>{item.description} -> {contents}</>), () => path);
+ const children = flattenItems(item.subitems, name => [...groupFunc(description), name]);
if (children.length || matches(path)) {
- eles.push(groupFunc(item.description));
+ eles.push(path);
eles = eles.concat(children);
}
} else {
if (!matches(path)) {
continue;
}
- eles.push(createItem(item));
+ leaves.push(item);
}
}
+ eles = [...leaves, ...eles];
+
return eles;
};
- return flattenItems(this._items, createGroupHeader, () => []);
+ return flattenItems(this._items, name => [name]);
+ }
+
+ @computed get flatItems(): OriginalMenuProps[] {
+ return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[];
+ }
+
+ @computed get filteredViews() {
+ const createGroupHeader = (contents: any) => {
+ return (
+ <div className="contextMenu-group">
+ <div className="contextMenu-description">{contents}</div>
+ </div>
+ );
+ };
+ const createItem = (item: ContextMenuProps, selected: boolean) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} selected={selected} />;
+ let itemIndex = 0;
+ return this.filteredItems.map(value => {
+ if (Array.isArray(value)) {
+ return createGroupHeader(value.join(" -> "));
+ } else {
+ return createItem(value, itemIndex++ === this.selectedIndex);
+ }
+ });
}
@computed get menuItems() {
if (!this._searchString) {
return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />);
}
- return this.filteredItems;
+ return this.filteredViews;
}
render() {
if (!this._display) {
return null;
}
- let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY } :
- { left: this._pageX, bottom: this._pageY };
+ let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
+ { left: this.pageX, bottom: this.pageY };
- return (
- <div className="contextMenu-cont" style={style} ref={this.ref}>
+ console.log(this._pageX);
+ console.log(this.pageX);
+ console.log();
+
+ const contents = (
+ <>
<span>
<span className="icon-background">
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} ref={this._searchRef} autoFocus />
+ <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
</span>
{this.menuItems}
- </div>
+ </>
+ );
+ return (
+ <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}>
+ {({ measureRef }) => (
+ <div className="contextMenu-cont" style={style} ref={measureRef}>
+ {contents}
+ </div>
+ )
+ }
+ </Measure>
);
}
@action
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "ArrowDown") {
+ if (this.selectedIndex < this.flatItems.length - 1) {
+ this.selectedIndex++;
+ }
+ e.preventDefault();
+ } else if (e.key === "ArrowUp") {
+ if (this.selectedIndex > 0) {
+ this.selectedIndex--;
+ }
+ e.preventDefault();
+ } else if (e.key === "Enter") {
+ const item = this.flatItems[this.selectedIndex];
+ item.event();
+ this.closeMenu();
+ }
+ }
+
+ @action
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this._searchString = e.target.value;
+ if (!this._searchString) {
+ this.selectedIndex = -1;
+ }
+ else {
+ if (this.selectedIndex === -1) {
+ this.selectedIndex = 0;
+ } else {
+ this.selectedIndex = Math.min(this.flatItems.length - 1, this.selectedIndex);
+ }
+ }
}
} \ No newline at end of file