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.tsx92
1 files changed, 70 insertions, 22 deletions
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 1133f70a1..59a0de2a0 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,5 +1,5 @@
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";
@@ -14,13 +14,14 @@ 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>();
@@ -75,49 +76,67 @@ 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() {
@@ -133,7 +152,7 @@ export class ContextMenu extends React.Component {
<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} ref={this._searchRef} autoFocus />
</span>
{this.menuItems}
</div>
@@ -141,7 +160,36 @@ export class ContextMenu extends React.Component {
}
@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