Skip to content

Commit

Permalink
Implemented a React context menu. #43
Browse files Browse the repository at this point in the history
  • Loading branch information
MKHenson committed Aug 15, 2016
1 parent 839ce15 commit 0deb639
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 0 deletions.
179 changes: 179 additions & 0 deletions lib/gui/context-menu/context-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
module Animate {

export interface IReactContextMenuItem {
onSelect? : (item: IReactContextMenuItem) => void;
tag?: any;
label: string;
prefix?: JSX.Element;
image?: string;
items?: IReactContextMenuItem[];
}

export interface IReactContextMenuProps {
x: number;
y: number;
className?: string;
items? : IReactContextMenuItem[];
_closing? : () => void;
}

export interface IReactContextMenuState {
activeItems: IReactContextMenuItem[];
}

/**
* A React component for showing a context menu.
* Simply call ReactContextMenu.show and provide the IReactContextMenuItem items to show
*/
export class ReactContextMenu extends React.Component<IReactContextMenuProps, IReactContextMenuState> {
private static _menuCount: number = 0;
private static _menus : { [id: number]: {
menu : HTMLElement,
jsx : JSX.Element
}} = {};

static defaultProps: IReactContextMenuProps = {
items: [],
x: 0,
y: 0
};

private _mouseUpProxy: any;

/**
* Creates a context menu instance
*/
constructor(props : IReactContextMenuProps) {
super(props);

this._mouseUpProxy = this.onMouseUp.bind(this);
this.state = {
activeItems : []
}
}

/**
* When we click on a menu item
*/
private onMouseDown(e: React.MouseEvent, item : IReactContextMenuItem) {
e.preventDefault();
item.onSelect && item.onSelect(item);
this.props._closing();
}

/**
* Draws each of the submenu items
*/
private drawMenuItems( item : IReactContextMenuItem, level : number, index : number ) : JSX.Element {
let children : JSX.Element;
let prefix : JSX.Element;

if (item.items) {
children = <div className="sub-menu">
{
item.items.map((item, index) => {
return this.drawMenuItems(item, level + 1, index);
})
}
</div>
}

if (item.prefix)
prefix = item.prefix;
else if (item.image)
prefix = <img src={item.image} />;

return <div
onMouseDown={(e) => { this.onMouseDown(e, item); }}
key={`menu-item-${level}-${index}`}
className={"menu-item" + ( prefix ? ' has-prefix' : '' )}>
<div className="menu-item-button">{prefix} {item.label}</div>
{children}
</div>
}

/**
* Creates the component elements
* @returns {JSX.Element}
*/
render(): JSX.Element {
let controlBox : JSX.Element;

return <div ref="context" className={"context-menu " + (this.props.className || '')}>
{
this.props.items.map((item, index) => {
return this.drawMenuItems(item, 0, index);
})
}
</div>;
}

/**
* When the mouse is up we remove the dragging event listeners
*/
private onMouseUp(e: MouseEvent) {
this.props._closing();
window.removeEventListener('mouseup', this._mouseUpProxy);
}

/**
* When the component is mounted
*/
componentDidMount() {
window.addEventListener('mouseup', this._mouseUpProxy);
let elm = ReactDOM.findDOMNode(this.refs['context']) as HTMLElement;
let x = this.props.x;
let y = this.props.y;

x = ( elm.offsetWidth + x > document.body.offsetWidth ? x - elm.offsetWidth : x );
y = ( elm.offsetHeight + y > document.body.offsetHeight ? document.body.offsetHeight - elm.offsetHeight : y );

elm.style.left = x + "px";
elm.style.top = y + "px";
}

/**
* Called when the component is to be removed
*/
componentWillUnmount() {
window.removeEventListener('mouseup', this._mouseUpProxy);
}

/**
* Shows a React context menu component to the user
* @param {IReactContextMenuProps} props The properties to use for the context menu component
*/
static show(props : IReactContextMenuProps) {
let id = ReactContextMenu._menuCount + 1;
let contextView = document.createElement("div");
contextView.className = "context-view";
ReactContextMenu._menuCount = id;

props._closing = () => {
ReactContextMenu._menus[id].menu.remove();
ReactDOM.unmountComponentAtNode( ReactContextMenu._menus[id].menu );
ReactContextMenu._menus[id] = null;
};

let component = React.createElement<IReactContextMenuProps>(ReactContextMenu, props);
ReactContextMenu._menus[id] = {
jsx: component,
menu : contextView
};

// Add the tooltip to the dom
document.body.appendChild( contextView );
ReactDOM.render( component, contextView );
return ReactContextMenu._menuCount;
}

/**
* Hides/Removes a context menu component by id
* @param {number} id
*/
static hide(id : number) {
ReactDOM.unmountComponentAtNode( ReactContextMenu._menus[id].menu );
ReactContextMenu._menus[id] = null;
}
}
}
58 changes: 58 additions & 0 deletions lib/gui/context-menu/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.context-view {
position: absolute;
top: 0;
left: 0;
}

.context-menu {
position: absolute;
top: 200px;
left: 200px;

.menu-item {
position: relative;
min-width: 50px;
white-space: nowrap;
cursor: pointer;

&.first {
margin: 0;
}

&:hover > .sub-menu {
opacity: 1;
visibility: visible;
}

&.has-prefix .menu-item-button {
padding: 0 10px 0 0;
}

&.has-prefix .fa {
padding: 10px;
margin: 0 5px 0 0;
background: shade( $tooltip-bg, 20 );
}

&:hover .menu-item-button {
background: shade( $tooltip-bg, 15 );
}
}

.menu-item-button {
padding: 10px;
background: $tooltip-bg;
color: $tooltip-color;
transition: background 0.5s;
box-shadow: 2px 2px 2px rgba(0,0,0,.2);
}

.sub-menu {
transition: opacity 0.5s;
position: absolute;
top: 0;
left: 100%;
opacity: 0;
visibility: hidden;
}
}

0 comments on commit 0deb639

Please sign in to comment.