import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StrCast } from '../../fields/Types';
import { SettingsManager } from '../util/SettingsManager';
import './ContextMenu.scss';
import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextMenuItem';
import { ObservableReactComponent } from './ObservableReactComponent';
@observer
export class ContextMenu extends ObservableReactComponent<{}> {
static Instance: ContextMenu;
private _ignoreUp = false;
private _reactionDisposer?: IReactionDisposer;
private _defaultPrefix: string = '';
private _defaultItem: ((name: string) => void) | undefined;
private _onDisplay?: () => void = undefined;
@observable.shallow _items: ContextMenuProps[] = [];
@observable _pageX: number = 0;
@observable _pageY: number = 0;
@observable _display: boolean = false;
@observable _searchString: string = '';
@observable _showSearch: boolean = false;
// 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 _yRelativeToTop: boolean = true;
@observable _selectedIndex = -1;
@observable _width: number = 0;
@observable _height: number = 0;
@observable _mouseX: number = -1;
@observable _mouseY: number = -1;
@observable _shouldDisplay: boolean = false;
constructor(props: any) {
super(props);
makeObservable(this);
ContextMenu.Instance = this;
}
public setIgnoreEvents(ignore: boolean) {
this._ignoreUp = ignore;
}
@action
onPointerDown = (e: PointerEvent) => {
this._mouseX = e.clientX;
this._mouseY = e.clientY;
};
@action
onPointerUp = (e: PointerEvent) => {
if (e.button !== 2 && !e.ctrlKey) return;
const curX = e.clientX;
const curY = e.clientY;
if (this._ignoreUp) {
this._ignoreUp = false;
return;
}
if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) {
this._shouldDisplay = false;
}
if (this._shouldDisplay) {
if (this._onDisplay) {
this._onDisplay();
} else {
this._display = true;
}
}
};
componentWillUnmount() {
document.removeEventListener('pointerdown', this.onPointerDown, true);
document.removeEventListener('pointerup', this.onPointerUp);
this._reactionDisposer?.();
}
componentDidMount() {
document.addEventListener('pointerdown', this.onPointerDown, true);
document.addEventListener('pointerup', this.onPointerUp);
}
@action
clearItems() {
this._items.length = 0;
this._defaultPrefix = '';
this._defaultItem = undefined;
}
findByDescription = (target: string, toLowerCase = false) =>
this._items.find(menuItem =>
(toLowerCase ? menuItem.description.toLowerCase() : menuItem.description) === target); // prettier-ignore
@action
addItem(item: ContextMenuProps) {
!this._items.includes(item) && this._items.push(item);
}
@action
moveAfter(item: ContextMenuProps, after?: ContextMenuProps) {
const curInd = this._items.findIndex(i => i.description === item.description);
this._items.splice(curInd, 1);
const afterInd = after && this.findByDescription(after.description) ? this._items.findIndex(i => i.description === after.description) : this._items.length;
this._items.splice(afterInd, 0, item);
}
@action
setDefaultItem(prefix: string, item: (name: string) => void) {
this._defaultPrefix = prefix;
this._defaultItem = item;
}
static readonly buffer = 20;
get pageX() {
return this._pageX + this._width > window.innerWidth - ContextMenu.buffer ? window.innerWidth - ContextMenu.buffer - this._width : Math.max(0, this._pageX);
}
get pageY() {
return this._pageY + this._height > window.innerHeight - ContextMenu.buffer ? window.innerHeight - ContextMenu.buffer - this._height : Math.max(0, this._pageY);
}
@action
displayMenu = (x: number, y: number, initSearch = '', showSearch = false, onDisplay?: () => void) => {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
this._showSearch = showSearch;
this._pageX = x;
this._pageY = y;
this._searchString = initSearch;
this._shouldDisplay = true;
this._onDisplay = onDisplay;
this._display = !onDisplay;
};
@action
closeMenu = () => {
const wasOpen = this._display;
this.clearItems();
this._display = false;
this._shouldDisplay = false;
return wasOpen;
};
@computed get filteredItems(): (OriginalMenuProps | string[])[] {
const searchString = this._searchString.toLowerCase().split(' ');
const matches = (descriptions: string[]): boolean => {
return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s)));
};
const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => {
let eles: (OriginalMenuProps | string[])[] = [];
const leaves: OriginalMenuProps[] = [];
for (const item of items) {
const description = item.description;
const path = groupFunc(description);
if ('subitems' in item) {
const children = flattenItems(item.subitems, name => [...groupFunc(description), name]);
if (children.length || matches(path)) {
eles.push(path);
eles = eles.concat(children);
}
} else {
if (!matches(path)) {
continue;
}
leaves.push(item);
}
}
eles = [...leaves, ...eles];
return eles;
};
return flattenItems(this._items.slice(), name => [name]);
}
@computed get flatItems(): OriginalMenuProps[] {
return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[];
}
@computed get menuItems() {
if (!this._searchString) {
return this._items.map((item, ind) =>