diff options
| author | yipstanley <stanley_yip@brown.edu> | 2019-06-24 15:59:22 -0400 |
|---|---|---|
| committer | yipstanley <stanley_yip@brown.edu> | 2019-06-24 15:59:22 -0400 |
| commit | a99639e9073ea154728ff9676c41af646791f4cd (patch) | |
| tree | 027ef2ae68f00432c99cf1caf6ad4d9c9547b5d8 /src/client/views/ContextMenu.tsx | |
| parent | 65288e33b49404d21012323fcb53fefb3f646fbf (diff) | |
| parent | d475b19e9ba7bc8870ec7bc1e10b5cc88decea0b (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into pdf_snippet
Diffstat (limited to 'src/client/views/ContextMenu.tsx')
| -rw-r--r-- | src/client/views/ContextMenu.tsx | 159 |
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 |
