-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented a React context menu. #43
- Loading branch information
Showing
2 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |