aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/ContextMenuItem.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-09-02 09:26:37 -0400
committerbobzel <zzzman@gmail.com>2024-09-02 09:26:37 -0400
commitcda69e48361fce8d71a4dc66edd9dd976a27f52d (patch)
tree82b9a1a5967ae88a9534f89f7eaed3aeb289652f /src/client/views/ContextMenuItem.tsx
parentc01828308714874589d1f60c33ca59df4c656c0c (diff)
parenta958577d4c27b276aa37484e3f895e196138b17c (diff)
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'src/client/views/ContextMenuItem.tsx')
-rw-r--r--src/client/views/ContextMenuItem.tsx180
1 files changed, 54 insertions, 126 deletions
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 21faad0c1..5b4eb704b 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -8,163 +8,91 @@ import { SnappingManager } from '../util/SnappingManager';
import { UndoManager } from '../util/UndoManager';
import { ObservableReactComponent } from './ObservableReactComponent';
-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;
-}
-
-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;
-}
-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>;
}
}