diff options
Diffstat (limited to 'src/client/views/ContextMenuItem.tsx')
-rw-r--r-- | src/client/views/ContextMenuItem.tsx | 192 |
1 files changed, 56 insertions, 136 deletions
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 782077fd6..679b108b3 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -5,174 +5,94 @@ import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { SnappingManager } from '../util/SnappingManager'; -import { UndoManager, undoable } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { ObservableReactComponent } from './ObservableReactComponent'; -import { ColorPicker, Type } from 'browndash-components'; -import { DocumentView } from './nodes/DocumentView'; -import { Doc } from '../../fields/Doc'; -import { StrCast } from '../../fields/Types'; -import { ColorResult, SketchPicker } from 'react-color'; -import color from 'color'; -export interface OriginalMenuProps { +export interface ContextMenuProps { + icon: IconProp | JSX.Element; description: string; - event: (stuff?: any) => void; - undoable?: boolean; - icon: IconProp | JSX.Element; // maybe should be optional (icon?) - closeMenu?: () => void; - colorPicker?: boolean; -} - -export interface SubmenuProps { - description: string; - // eslint-disable-next-line no-use-before-define - subitems: ContextMenuProps[]; - noexpand?: boolean; addDivider?: boolean; - icon: IconProp; // maybe should be optional (icon?) closeMenu?: () => void; - colorPicker?: boolean; -} -export type ContextMenuProps = OriginalMenuProps | SubmenuProps; + subitems?: ContextMenuProps[]; + noexpand?: boolean; // whether to render the submenu items as a flyout from this item, or inline in place of this item + + undoable?: boolean; // whether to wrap the event callback in an UndoBatch or not + event?: (stuff?: unknown) => void; +} @observer export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & { selected?: boolean }> { - @observable private _items: Array<ContextMenuProps> = []; - @observable private overItem = false; + static readonly HOVER_TIMEOUT = 100; + _hoverTimeout?: NodeJS.Timeout; + _overPosY = 0; + _overPosX = 0; + @observable _items: ContextMenuProps[] = []; + @observable _overItem = false; - constructor(props: any) { + constructor(props: ContextMenuProps & { selected?: boolean }) { super(props); makeObservable(this); } componentDidMount() { - runInAction(() => { - this._items.length = 0; - }); - if ((this._props as SubmenuProps)?.subitems) { - (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); - } + runInAction(() => this._items.push(...(this._props.subitems ?? []))); } handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { - if ('event' in this._props) { + if (this._props.event) { this._props.closeMenu?.(); - const batch = this._props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this._props.description}`) : undefined; + const batch = this._props.undoable ? UndoManager.StartBatch(`Click Menu item: ${this._props.description}`) : undefined; await this._props.event({ x: e.clientX, y: e.clientY }); batch?.end(); } }; - currentTimeout?: any; - static readonly timeout = 300; - _overPosY = 0; - _overPosX = 0; + setOverItem = (over: boolean) => { + this._hoverTimeout = setTimeout( action(() => { this._overItem = over; }), ContextMenuItem.HOVER_TIMEOUT ); // prettier-ignore + }; + onPointerEnter = (e: React.MouseEvent) => { - if (this.currentTimeout) { - clearTimeout(this.currentTimeout); - this.currentTimeout = undefined; - } - if (this.overItem) { - return; - } + this._hoverTimeout && clearTimeout(this._hoverTimeout); this._overPosY = e.clientY; this._overPosX = e.clientX; - this.currentTimeout = setTimeout( - action(() => { - this.overItem = true; - }), - ContextMenuItem.timeout - ); + !this._overItem && this.setOverItem(true); }; onPointerLeave = () => { - if (this.currentTimeout) { - clearTimeout(this.currentTimeout); - this.currentTimeout = undefined; - } - if (!this.overItem) { - return; - } - this.currentTimeout = setTimeout( - action(() => { - this.overItem = false; - }), - ContextMenuItem.timeout - ); + this._hoverTimeout && clearTimeout(this._hoverTimeout); + this._overItem && this.setOverItem(false); }; - isJSXElement(val: any): val is JSX.Element { - return React.isValidElement(val); - } + renderItem = (submenu: JSX.Element[]) => { + const alignItems = this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center'; + const marginTop = this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : ''; + const marginLeft = window.innerWidth - this._overPosX - 50 > 0 ? '90%' : '20%'; - render() { - if ('event' in this._props) { - return ( - <div className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent} > - {this._props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this._props.icon) ? this._props.icon : <FontAwesomeIcon icon={this._props.icon} size="sm" />}</span> : null} - <div className="contextMenu-description">{this._props.description.replace(':', '')}</div> - <div - className="contextMenu-item-background" - style={{ - background: SnappingManager.userColor, - }} - /> - </div> - ); - } - if ('subitems' in this._props) { - const where = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center'; - const marginTop = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : ''; + return ( + <div className={`contextMenuItem${this._props.selected ? '-Selected' : ''}`} // + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerDown={this.handleEvent} + style={{ alignItems, borderTop: this._props.addDivider ? 'solid 1px' : undefined }} + > + <div className="contextMenuItem-background" style={{ background: SnappingManager.userColor, filter: this._overItem ? 'opacity(0.2)' : '' }} /> + <span className="contextMenuItem-icon" style={{ alignItems: 'center', alignSelf: 'center' }}> + {React.isValidElement(this._props.icon) ? this._props.icon : this._props.icon ? <FontAwesomeIcon icon={this._props.icon as IconProp} size="sm" /> : null} + </span> + <div className="contextMenu-description"> {this._props.description} </div> + {!submenu.length ? null : ( + !this._overItem ? + <FontAwesomeIcon icon="angle-right" size="lg" style={{ position: 'absolute', right: '10px' }} /> : ( + <div className="contextMenu-subMenu-cont" style={{ marginLeft, marginTop, background: SnappingManager.userBackgroundColor }}> + {submenu} + </div> + ) + )} + </div> + ); // prettier-ignore + }; - // here - const submenu = !this.overItem ? null : ( - <div - className="contextMenu-subMenu-cont" - style={{ - marginLeft: window.innerWidth - this._overPosX - 50 > 0 ? '90%' : '20%', - marginTop, - background: SnappingManager.userBackgroundColor, - }}> - {this._items.map(prop => ( - <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} /> - ))} - </div> - ); - if (!(this._props as SubmenuProps).noexpand) { - return ( - <div className="contextMenu-inlineMenu"> - {this._items.map(prop => ( - <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} /> - ))} - </div> - ); - } - return ( - <div - className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} - style={{ alignItems: where, borderTop: this._props.addDivider ? 'solid 1px' : undefined }} - onMouseLeave={this.onPointerLeave} - onMouseEnter={this.onPointerEnter}> - {this._props.icon ? ( - <span className="contextMenu-item-icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: 'center', alignSelf: 'center' }}> - <FontAwesomeIcon icon={this._props.icon} size="sm" /> - </span> - ) : null} - <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}> - {this._props.description} - <FontAwesomeIcon icon="angle-right" size="lg" style={{ position: 'absolute', right: '10px' }} /> - </div> - <div - className="contextMenu-item-background" - style={{ - background: SnappingManager.userColor, - }} - /> - {submenu} - </div> - ); - } - return null; + render() { + const submenu = this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />); + return this.props.event || this._props.noexpand ? this.renderItem(submenu) : <div className="contextMenu-inlineMenu">{submenu}</div>; } -} +}
\ No newline at end of file |