Context menus are special. There should never be more than one context menu open at any time. They're also special because it can be opened from anywhere. Try the demo to get an idea of how this looks when put together.
To solve our global problem, we'll create a mixin which wraps a private event emitter.
var menuEvents = new events.EventEmitter();
var ContextMenuMixin = {
// this.openContextMenu(['foo', 'bar'], (err, choice) => void)
openContextMenu: function(options, callback){
menuEvents.emit('open', {
options: options,
callback: callback
});
},
closeContextMenu: function(){
menuEvents.emit('close');
}
};
Now for the component, we need to do a few things. Here's the initialization part. Just binding to some events, and lightweight mouse tracking.
var mouse = {x: 0, y: 0};
var updateMouse = function(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
};
var ContextMenu = React.createClass({
getInitialState: function(){
return {options: null, callback: null};
},
componentDidMount: function(){
menuEvents.addListener('open', this.handleOpenEvent);
menuEvents.addListener('close', this.closeMenu);
addEventListener('mousemove', updateMouse);
},
These event handlers are very simple. handleOpenEvent just stores the event payload and mouse position in state, which effectively locks in the mouse position until it's opened next. And the counterpart simply resets the state, and calls the callback with an error.
handleOpenEvent: function(payload){
this.setState(_.merge({}, payload, mouse));
},
closeMenu: function(){
if (this.state.callback) {
this.replaceState(this.getInitialState());
this.state.callback(new Error('no selection made'));
}
},
And finally, we render a list of options passed to the event, and we create click handlers for each.
render: function(){
if (!this.state.options) {
return <div />
}
var style = {
left: this.state.x,
top: this.state.y,
position: 'fixed'
};
return (
<div className="react-contextmenu" style={style}>
<ul className="react-contextmenu-options">
{this.state.options.map(function(x, i){
return <li key={i}
onClick={this.makeClickHandler(x)}>
{x}
</li>
}, this)}
</ul>
</div>
);
},
makeClickHandler: function(option){
return function(){
if (this.state.callback) {
this.state.callback(null, option);
this.replaceState(this.getInitialState());
}
}.bind(this);
}