Supprt right-click context menu

This commit is contained in:
Jordan Eldredge 2018-05-05 14:25:05 -07:00
parent 56717b84b3
commit ca4fef2db0
8 changed files with 157 additions and 81 deletions

View file

@ -1,6 +1,8 @@
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import ContextMenuWrapper from "./ContextMenuWrapper";
import MainContextMenu from "./MainWindow/MainContextMenu";
import WindowManager from "./WindowManager";
import MainWindow from "./MainWindow";
import PlaylistWindow from "./PlaylistWindow";
@ -51,7 +53,9 @@ const App = ({
return (
<div role="application" id="webamp">
<Skin />
<WindowManager windows={windows} container={container} />
<ContextMenuWrapper Contents={MainContextMenu}>
<WindowManager windows={windows} container={container} />
</ContextMenuWrapper>
</div>
);
};

View file

@ -66,7 +66,7 @@ Node.propTypes = {
label: PropTypes.string.isRequired
};
class ContextMenu extends React.Component {
export class ContextMenu extends React.Component {
render() {
const {
children,
@ -87,71 +87,3 @@ class ContextMenu extends React.Component {
);
}
}
export class ContextMenuTarget extends React.Component {
constructor(props) {
super(props);
this.state = { selected: false };
this._handleHandleClick = this._handleHandleClick.bind(this);
this._handleGlobalClick = this._handleGlobalClick.bind(this);
}
componentDidMount() {
document.addEventListener("click", this._handleGlobalClick);
}
componentWillUnmount() {
document.removeEventListener("click", this._handleGlobalClick);
}
_handleHandleClick() {
this.setState({ selected: !this.state.selected });
}
_handleGlobalClick(e) {
if (
this.state.selected &&
// Not sure how, but it's possible for this to get called when handleNode is null/undefined.
// https://sentry.io/share/issue/2066cd79f21e4f279791319f4d2ea35d/
this.handleNode &&
!this.handleNode.contains(e.target)
) {
this.setState({ selected: false });
}
}
render() {
const { handle, children, top, bottom, ...passThroughProps } = this.props;
const rect = this.handleNode
? this.handleNode.getBoundingClientRect()
: { top: 0, left: 0 };
return (
<div {...passThroughProps}>
<div
className="handle"
style={{ width: "100%", height: "100%" }}
ref={node => (this.handleNode = node)}
onClick={this._handleHandleClick}
>
{handle}
</div>
<ContextMenu
selected={this.state.selected}
offsetTop={rect.top}
offsetLeft={rect.left}
top={top}
bottom={bottom}
>
{children}
</ContextMenu>
</div>
);
}
}
ContextMenuTarget.propTypes = {
children: PropTypes.any.isRequired,
handle: PropTypes.any.isRequired,
top: PropTypes.bool,
bottom: PropTypes.bool
};

View file

@ -0,0 +1,71 @@
import React from "react";
import PropTypes from "prop-types";
import { ContextMenu } from "./ContextMenu";
export default class ContextMenuTarget extends React.Component {
constructor(props) {
super(props);
this.state = { selected: false };
this._handleHandleClick = this._handleHandleClick.bind(this);
this._handleGlobalClick = this._handleGlobalClick.bind(this);
}
componentDidMount() {
document.addEventListener("click", this._handleGlobalClick);
}
componentWillUnmount() {
document.removeEventListener("click", this._handleGlobalClick);
}
_handleHandleClick() {
this.setState({ selected: !this.state.selected });
}
_handleGlobalClick(e) {
if (
this.state.selected &&
// Not sure how, but it's possible for this to get called when handleNode is null/undefined.
// https://sentry.io/share/issue/2066cd79f21e4f279791319f4d2ea35d/
this.handleNode &&
!this.handleNode.contains(e.target)
) {
this.setState({ selected: false });
}
}
render() {
const { handle, children, top, bottom, ...passThroughProps } = this.props;
const rect = this.handleNode
? this.handleNode.getBoundingClientRect()
: { top: 0, left: 0 };
return (
<div {...passThroughProps}>
<div
className="handle"
style={{ width: "100%", height: "100%" }}
ref={node => (this.handleNode = node)}
onClick={this._handleHandleClick}
>
{handle}
</div>
<ContextMenu
selected={this.state.selected}
offsetTop={rect.top}
offsetLeft={rect.left}
top={top}
bottom={bottom}
>
{children}
</ContextMenu>
</div>
);
}
}
ContextMenuTarget.propTypes = {
children: PropTypes.any.isRequired,
handle: PropTypes.any.isRequired,
top: PropTypes.bool,
bottom: PropTypes.bool
};

View file

@ -0,0 +1,64 @@
import React from "react";
import PropTypes from "prop-types";
import { ContextMenu } from "./ContextMenu";
export default class ContextMenuWraper extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: false,
offsetTop: null,
offsetLeft: null
};
this._handleRightClick = this._handleRightClick.bind(this);
this._handleGlobalClick = this._handleGlobalClick.bind(this);
}
_handleGlobalClick() {
this.setState({
selected: false,
offsetTop: null,
offsetLeft: null
});
document.removeEventListener("click", this._handleGlobalClick);
}
_handleRightClick(e) {
const { pageX, pageY } = e;
this.setState({
selected: true,
offsetTop: pageY,
offsetLeft: pageX
});
// Even if you right click multiple times before closeing,
// we should only end up with one global listener.
document.addEventListener("click", this._handleGlobalClick);
e.preventDefault();
e.stopPropagation();
}
render() {
const { children, Contents, ...passThroughProps } = this.props;
return (
<div
onContextMenu={this._handleRightClick}
style={{ width: "100%", height: "100%" }}
{...passThroughProps}
>
<ContextMenu
selected={this.state.selected}
offsetTop={this.state.offsetTop}
offsetLeft={this.state.offsetLeft}
>
<Contents />
</ContextMenu>
{children}
</div>
);
}
}
ContextMenuWraper.propTypes = {
children: PropTypes.any.isRequired,
Contents: PropTypes.any.isRequired
};

View file

@ -1,7 +1,8 @@
import React from "react";
import { connect } from "react-redux";
import { openEqfFileDialog, downloadPreset } from "../../actionCreators";
import { ContextMenuTarget, Node } from "../ContextMenu";
import { Node } from "../ContextMenu";
import ContextMenuTarget from "../ContextMenuTarget";
const PresetsContextMenu = props => (
<ContextMenuTarget top id="presets-context" handle={<div id="presets" />}>

View file

@ -1,6 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import ClickedDiv from "../ClickedDiv";
import {
close,
setSkinFromUrl,
@ -16,14 +15,10 @@ import {
} from "../../actionTypes";
import { getGenWindows } from "../../selectors";
import { LOAD_STYLE } from "../../constants";
import { ContextMenuTarget, Hr, Node, Parent, LinkNode } from "../ContextMenu";
import { Hr, Node, Parent, LinkNode } from "../ContextMenu";
const MainContextMenu = props => (
<ContextMenuTarget
id="option-context"
bottom
handle={<ClickedDiv id="option" title="Winamp Menu" />}
>
<React.Fragment>
<LinkNode
href="https://github.com/captbaritone/webamp"
target="_blank"
@ -90,7 +85,7 @@ const MainContextMenu = props => (
</Parent>
<Hr />
<Node onClick={props.close} label="Exit" />
</ContextMenuTarget>
</React.Fragment>
);
const mapStateToProps = state => ({

View file

@ -14,6 +14,8 @@ import DropTarget from "../DropTarget";
import MiniTime from "../MiniTime";
import { SET_FOCUSED_WINDOW } from "../../actionTypes";
import ClickedDiv from "../ClickedDiv";
import ContextMenuTarget from "../ContextMenuTarget";
import ActionButtons from "./ActionButtons";
import MainBalance from "./MainBalance";
import Close from "./Close";
@ -90,7 +92,13 @@ export class MainWindow extends React.Component {
className="selected title-bard draggable"
onDoubleClick={this.props.toggleMainWindowShadeMode}
>
<MainContextMenu filePickers={filePickers} />
<ContextMenuTarget
id="option-context"
bottom
handle={<ClickedDiv id="option" title="Winamp Menu" />}
>
<MainContextMenu filePickers={filePickers} />
</ContextMenuTarget>
{mainShade && <MiniTime />}
<Minimize />
<Shade />

View file

@ -7,7 +7,8 @@ import {
downloadHtmlPlaylist
} from "../../actionCreators";
import { ContextMenuTarget, Hr, Node } from "../ContextMenu";
import { Hr, Node } from "../ContextMenu";
import ContextMenuTarget from "../ContextMenuTarget";
import PlaylistMenu from "./PlaylistMenu";
/* eslint-disable no-alert */