import * as React from 'react'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import './RadialMenu.scss'; import { RadialMenuItem, RadialMenuProps } from './RadialMenuItem'; @observer export class RadialMenu extends React.Component { static Instance: RadialMenu; static readonly buffer = 20; constructor(props: any) { super(props); makeObservable(this); RadialMenu.Instance = this; } @observable private _mouseX: number = -1; @observable private _mouseY: number = -1; @observable private _shouldDisplay: boolean = false; @observable private _mouseDown: boolean = false; private _reactionDisposer?: IReactionDisposer; public used: boolean = false; catchTouch = (te: React.TouchEvent) => { te.stopPropagation(); te.preventDefault(); }; @action onPointerDown = (e: PointerEvent) => { this._mouseDown = true; this._mouseX = e.clientX; this._mouseY = e.clientY; this.used = false; document.addEventListener('pointermove', this.onPointerMove); }; @observable private _closest: number = -1; @action onPointerMove = (e: PointerEvent) => { const curX = e.clientX; const curY = e.clientY; const deltX = this._mouseX - curX; const deltY = this._mouseY - curY; const scale = Math.hypot(deltY, deltX); if (scale < 150 && scale > 50) { const rad = Math.atan2(deltY, deltX) + Math.PI; let closest = 0; let closestval = 999999999; for (let x = 0; x < this._items.length; x++) { const curmin = (x / this._items.length) * 2 * Math.PI; if (rad - curmin < closestval && rad - curmin > 0) { closestval = rad - curmin; closest = x; } } this._closest = closest; } else { this._closest = -1; } }; @action onPointerUp = (e: PointerEvent) => { this.used = true; this._mouseDown = false; const curX = e.clientX; const curY = e.clientY; if (this._mouseX !== curX || this._mouseY !== curY) { this._shouldDisplay = false; } this._shouldDisplay && (this._display = true); document.removeEventListener('pointermove', this.onPointerMove); if (this._closest !== -1 && this._items?.length > this._closest) { this._items[this._closest].event(); } }; componentWillUnmount() { document.removeEventListener('pointerdown', this.onPointerDown); document.removeEventListener('pointerup', this.onPointerUp); this._reactionDisposer && this._reactionDisposer(); } @action componentDidMount = () => { document.addEventListener('pointerdown', this.onPointerDown); document.addEventListener('pointerup', this.onPointerUp); this.previewcircle(); this._reactionDisposer = reaction( () => this._shouldDisplay, () => this._shouldDisplay && !this._mouseDown && runInAction(() => (this._display = true)) ); }; componentDidUpdate = () => { this.previewcircle(); }; @observable private _pageX: number = 0; @observable private _pageY: number = 0; @observable _display: boolean = false; @observable private _yRelativeToTop: boolean = true; @observable private _width: number = 0; @observable private _height: number = 0; getItems() { return this._items; } @action addItem(item: RadialMenuProps) { if (this._items.indexOf(item) === -1) { this._items.push(item); } } @observable private _items: Array = []; @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 this._mouseX = x; this._mouseY = y; this._shouldDisplay = true; }; // @computed // get pageX() { // const x = this._pageX; // if (x < 0) { // return 0; // } // const width = this._width; // if (x + width > window.innerWidth - RadialMenu.buffer) { // return window.innerWidth - RadialMenu.buffer - width; // } // return x; // } // @computed // get pageY() { // const y = this._pageY; // if (y < 0) { // return 0; // } // const height = this._height; // if (y + height > window.innerHeight - RadialMenu.buffer) { // return window.innerHeight - RadialMenu.buffer - height; // } // return y; // } @computed get menuItems() { return this._items.map((item, index) => ); } @action closeMenu = () => { this.clearItems(); this._display = false; this._shouldDisplay = false; }; @action openMenu = (x: number, y: number) => { this._pageX = x; this._pageY = y; this._shouldDisplay; this._display = true; }; @action clearItems() { this._items = []; } previewcircle() { if (document.getElementById('newCanvas') !== null) { const c: any = document.getElementById('newCanvas'); if (c.getContext) { const ctx = c.getContext('2d'); ctx.beginPath(); ctx.arc(150, 150, 50, 0, 2 * Math.PI); ctx.fillStyle = 'white'; ctx.fill(); ctx.font = '12px Arial'; ctx.fillStyle = 'black'; ctx.textAlign = 'center'; let description = ''; if (this._closest !== -1) { description = this._items[this._closest].description; } if (description.length > 15) { description = description.slice(0, 12); description += '...'; } ctx.fillText(description, 150, 150, 90); } } } render() { if (!this._display) { return null; } const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } : { left: this._pageX - 130, top: this._pageY - 130 }; return (
{' '} Your browser does not support the HTML5 canvas tag. {this.menuItems}
); } }