p5.js-web-editor/client/components/Nav.jsx

615 lines
21 KiB
React
Raw Normal View History

import PropTypes from 'prop-types';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
2016-06-23 22:29:55 +00:00
import { Link } from 'react-router';
2017-07-17 21:09:46 +00:00
import InlineSVG from 'react-inlinesvg';
2017-08-28 15:19:10 +00:00
import classNames from 'classnames';
import * as IDEActions from '../modules/IDE/actions/ide';
2017-07-17 21:09:46 +00:00
import { metaKeyName, } from '../utils/metaKey';
2017-09-01 17:38:40 +00:00
2017-07-17 21:09:46 +00:00
const triangleUrl = require('../images/down-filled-triangle.svg');
const logoUrl = require('../images/p5js-logo-small.svg');
2016-06-23 22:29:55 +00:00
2018-12-11 21:21:37 +00:00
const __process = (typeof global !== 'undefined' ? global : window).process;
class Nav extends React.PureComponent {
2017-08-28 15:19:10 +00:00
constructor(props) {
super(props);
this.state = {
dropdownOpen: 'none'
};
2017-08-28 21:54:39 +00:00
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.clearHideTimeout = this.clearHideTimeout.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentWillMount() {
document.addEventListener('mousedown', this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick, false);
2017-08-28 15:19:10 +00:00
}
2017-08-28 21:54:39 +00:00
setDropdown(dropdown) {
this.setState({
dropdownOpen: dropdown
});
2017-07-19 21:35:25 +00:00
}
handleClick(e) {
if (this.node.contains(e.target)) {
return;
}
this.handleClickOutside();
}
handleClickOutside() {
this.setState({
dropdownOpen: 'none'
});
}
2017-08-28 15:19:10 +00:00
toggleDropdown(dropdown) {
if (this.state.dropdownOpen === 'none') {
this.setState({
dropdownOpen: dropdown
});
} else {
this.setState({
dropdownOpen: 'none'
});
}
}
2017-08-28 21:54:39 +00:00
isUserOwner() {
return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
}
handleFocus(dropdown) {
this.clearHideTimeout();
this.setDropdown(dropdown);
}
clearHideTimeout() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
handleBlur() {
this.timer = setTimeout(this.setDropdown.bind(this, 'none'), 10);
}
render() {
2017-08-28 15:19:10 +00:00
const navDropdownState = {
file: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'file'
}),
edit: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'edit'
}),
sketch: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'sketch'
}),
help: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'help'
}),
account: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'account'
})
};
return (
2018-05-05 00:43:31 +00:00
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
2018-04-03 00:58:21 +00:00
<ul className="nav__items-left" title="project-menu">
<li className="nav__item-logo">
2017-07-17 21:09:46 +00:00
<InlineSVG src={logoUrl} alt="p5.js logo" />
</li>
2017-08-28 15:19:10 +00:00
<li className={navDropdownState.file}>
<button
onClick={this.toggleDropdown.bind(this, 'file')}
2017-08-28 21:54:39 +00:00
onBlur={this.handleBlur}
onFocus={this.clearHideTimeout}
2017-08-28 15:19:10 +00:00
>
2017-07-25 19:35:18 +00:00
<span className="nav__item-header">File</span>
2017-10-11 16:56:44 +00:00
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
2017-07-17 21:09:46 +00:00
</button>
<ul className="nav__dropdown">
<button
onClick={this.toggleDropdown.bind(this, 'file')}
className="nav__dropdown-heading"
>
2017-07-25 19:35:18 +00:00
<span>File</span>
<InlineSVG src={triangleUrl} />
</button>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
2017-07-17 21:09:46 +00:00
<button
onClick={() => {
if (!this.props.unsavedChanges) {
this.props.newProject();
} else if (this.props.warnIfUnsavedChanges()) {
this.props.newProject();
}
2017-10-11 16:56:44 +00:00
this.setDropdown('none');
2017-07-17 21:09:46 +00:00
}}
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
2017-07-17 21:09:46 +00:00
>
New
</button>
</li>
2018-12-11 21:21:37 +00:00
{ __process.env.LOGIN_ENABLED && (!this.props.project.owner || this.isUserOwner()) &&
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
<button
onClick={() => {
if (this.props.user.authenticated) {
this.props.saveProject();
} else {
this.props.showErrorModal('forceAuthentication');
}
2017-10-11 16:56:44 +00:00
this.setDropdown('none');
2017-07-19 21:35:25 +00:00
}}
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
2017-07-19 21:35:25 +00:00
>
2017-07-17 21:09:46 +00:00
Save
<span className="nav__keyboard-shortcut">{metaKeyName}+s</span>
2017-07-17 21:09:46 +00:00
</button>
2017-07-19 21:35:25 +00:00
</li> }
{ this.props.project.id && this.props.user.authenticated &&
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.cloneProject();
this.setDropdown('none');
}}
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
>
2017-07-17 21:09:46 +00:00
Duplicate
</button>
2017-07-19 21:35:25 +00:00
</li> }
{ this.props.project.id &&
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.showShareModal();
this.setDropdown('none');
}}
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
>
2017-07-19 21:35:25 +00:00
Share
2017-07-17 21:09:46 +00:00
</button>
2017-07-19 21:35:25 +00:00
</li> }
{ this.props.project.id &&
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.autosaveProject();
2017-10-11 16:56:44 +00:00
this.props.exportProjectAsZip(this.props.project.id);
this.setDropdown('none');
}}
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
>
2017-07-17 21:09:46 +00:00
Download
</button>
2017-07-19 21:35:25 +00:00
</li> }
{ this.props.user.authenticated &&
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<Link
to={`/${this.props.user.username}/sketches`}
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2017-08-28 21:54:39 +00:00
>
2017-07-19 21:35:25 +00:00
Open
</Link>
</li> }
{ __process.env.EXAMPLES_ENABLED &&
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
<Link
to="/p5/sketches"
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'file')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2017-07-19 21:35:25 +00:00
>
Examples
</Link>
</li> }
2017-07-17 21:09:46 +00:00
</ul>
</li>
2017-08-28 15:19:10 +00:00
<li className={navDropdownState.edit}>
2017-08-28 21:54:39 +00:00
<button
onClick={this.toggleDropdown.bind(this, 'edit')}
onBlur={this.handleBlur}
onFocus={this.clearHideTimeout}
>
2017-07-25 19:35:18 +00:00
<span className="nav__item-header">Edit</span>
2017-10-11 16:56:44 +00:00
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
2017-07-17 21:09:46 +00:00
</button>
<ul className="nav__dropdown">
<button
onClick={this.toggleDropdown.bind(this, 'edit')}
className="nav__dropdown-heading"
>
2017-07-25 19:35:18 +00:00
<span>Edit</span>
<InlineSVG src={triangleUrl} />
</button>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
2017-09-01 16:40:15 +00:00
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.cmController.tidyCode();
this.setDropdown('none');
}}
2017-09-01 16:40:15 +00:00
onFocus={this.handleFocus.bind(this, 'edit')}
onBlur={this.handleBlur}
>
Tidy Code
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
2017-09-01 16:40:15 +00:00
</button>
2017-07-17 21:09:46 +00:00
</li>
2017-07-25 19:35:18 +00:00
<li className="nav__dropdown-item">
2017-09-01 16:40:15 +00:00
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.cmController.showFind();
this.setDropdown('none');
}}
2017-09-01 16:40:15 +00:00
onFocus={this.handleFocus.bind(this, 'edit')}
onBlur={this.handleBlur}
>
Find
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
</button>
</li>
<li className="nav__dropdown-item">
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.cmController.findNext();
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'edit')}
onBlur={this.handleBlur}
>
Find Next
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
</button>
</li>
<li className="nav__dropdown-item">
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.cmController.findPrev();
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'edit')}
onBlur={this.handleBlur}
>
Find Previous
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
2017-09-01 16:40:15 +00:00
</button>
2017-07-17 21:09:46 +00:00
</li>
</ul>
</li>
2017-08-28 15:19:10 +00:00
<li className={navDropdownState.sketch}>
2017-08-28 21:54:39 +00:00
<button
onClick={this.toggleDropdown.bind(this, 'sketch')}
onBlur={this.handleBlur}
onFocus={this.clearHideTimeout}
>
2017-07-25 19:35:18 +00:00
<span className="nav__item-header">Sketch</span>
2017-10-11 16:56:44 +00:00
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
2017-07-17 21:09:46 +00:00
</button>
<ul className="nav__dropdown">
<button
onClick={this.toggleDropdown.bind(this, 'sketch')}
className="nav__dropdown-heading"
>
2017-07-25 19:35:18 +00:00
<span>Sketch</span>
<InlineSVG src={triangleUrl} />
</button>
<li className="nav__dropdown-item">
<button
onClick={() => {
this.props.newFile();
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'sketch')}
onBlur={this.handleBlur}
>
Add File
</button>
</li>
<li className="nav__dropdown-item">
<button
onClick={() => {
this.props.newFolder();
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'sketch')}
onBlur={this.handleBlur}
>
Add Folder
</button>
</li>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.startSketch();
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'sketch')}
onBlur={this.handleBlur}
>
Run
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
</button>
2017-07-17 21:09:46 +00:00
</li>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.stopSketch();
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'sketch')}
onBlur={this.handleBlur}
>
Stop
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
</button>
2017-07-17 21:09:46 +00:00
</li>
<li className="nav__dropdown-item">
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.setAllAccessibleOutput(true);
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'sketch')}
onBlur={this.handleBlur}
>
Start Accessible
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+1</span>
</button>
</li>
<li className="nav__dropdown-item">
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.setAllAccessibleOutput(false);
this.setDropdown('none');
}}
onFocus={this.handleFocus.bind(this, 'sketch')}
onBlur={this.handleBlur}
>
Stop Accessible
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+2</span>
</button>
</li>
2017-07-17 21:09:46 +00:00
</ul>
</li>
2017-08-28 15:19:10 +00:00
<li className={navDropdownState.help}>
2017-08-28 21:54:39 +00:00
<button
onClick={this.toggleDropdown.bind(this, 'help')}
onBlur={this.handleBlur}
onFocus={this.clearHideTimeout}
>
2018-02-09 21:32:06 +00:00
<span className="nav__item-header">Help & Feedback</span>
2017-10-11 16:56:44 +00:00
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
2017-07-17 21:09:46 +00:00
</button>
<ul className="nav__dropdown">
<button
onClick={this.toggleDropdown.bind(this, 'help')}
className="nav__dropdown-heading"
>
2018-02-09 21:32:06 +00:00
<span>Help & Feedback</span>
2017-07-25 19:35:18 +00:00
<InlineSVG src={triangleUrl} />
</button>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
2017-10-11 16:56:44 +00:00
<button
2018-05-12 00:07:29 +00:00
onFocus={this.handleFocus.bind(this, 'help')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.showKeyboardShortcutModal();
this.setDropdown('none');
}}
>
Keyboard Shortcuts
</button>
2017-07-17 21:09:46 +00:00
</li>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
<a
href="https://p5js.org/reference/"
target="_blank"
rel="noopener noreferrer"
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'help')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2018-05-05 00:22:39 +00:00
>Reference
</a>
2017-07-17 21:09:46 +00:00
</li>
2017-07-19 21:35:25 +00:00
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<Link
to="/about"
onFocus={this.handleFocus.bind(this, 'help')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2017-08-28 21:54:39 +00:00
>
2017-07-19 21:35:25 +00:00
About
</Link>
2017-07-17 21:09:46 +00:00
</li>
2018-02-09 21:32:06 +00:00
<li className="nav__dropdown-item">
<Link
to="/feedback"
onFocus={this.handleFocus.bind(this, 'help')}
onBlur={this.handleBlur}
onClick={this.setDropdown.bind(this, 'none')}
>
Feedback
</Link>
</li>
2017-07-17 21:09:46 +00:00
</ul>
</li>
</ul>
2018-12-11 21:21:37 +00:00
{ __process.env.LOGIN_ENABLED && !this.props.user.authenticated &&
2017-07-25 19:35:18 +00:00
<ul className="nav__items-right" title="user-menu">
<li className="nav__item">
<p>
<Link to="/login">Log in</Link>
<span className="nav__item-spacer">or</span>
<Link to="/signup">Sign up</Link>
</p>
</li>
</ul>}
2018-12-11 21:21:37 +00:00
{ __process.env.LOGIN_ENABLED && this.props.user.authenticated &&
2017-07-25 19:35:18 +00:00
<ul className="nav__items-right" title="user-menu">
<li className="nav__item">
<span>Hello, {this.props.user.username}!</span>
</li>
<span className="nav__item-spacer">|</span>
2017-08-28 15:19:10 +00:00
<li className={navDropdownState.account}>
<button
className="nav__item-header"
onClick={this.toggleDropdown.bind(this, 'account')}
2017-08-28 21:54:39 +00:00
onBlur={this.handleBlur}
onFocus={this.clearHideTimeout}
2017-08-28 15:19:10 +00:00
>
My Account
</button>
2017-10-11 16:56:44 +00:00
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
2017-07-25 19:35:18 +00:00
<ul className="nav__dropdown">
<button
onClick={this.toggleDropdown.bind(this, 'account')}
className="nav__dropdown-heading"
>
2017-08-28 15:19:10 +00:00
<span>My Account</span>
2017-07-25 19:35:18 +00:00
<InlineSVG src={triangleUrl} />
</button>
2017-07-25 19:35:18 +00:00
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<Link
to={`/${this.props.user.username}/sketches`}
onFocus={this.handleFocus.bind(this, 'account')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2017-08-28 21:54:39 +00:00
>
2017-07-25 19:35:18 +00:00
My sketches
</Link>
</li>
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<Link
to="/assets"
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'account')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2017-08-28 21:54:39 +00:00
>
2017-07-25 19:35:18 +00:00
My assets
</Link>
</li>
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<Link
to="/account"
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'account')}
onBlur={this.handleBlur}
2017-10-11 16:56:44 +00:00
onClick={this.setDropdown.bind(this, 'none')}
2017-08-28 21:54:39 +00:00
>
2017-07-25 19:35:18 +00:00
Settings
</Link>
</li>
<li className="nav__dropdown-item">
2017-08-28 21:54:39 +00:00
<button
2017-10-11 16:56:44 +00:00
onClick={() => {
this.props.logoutUser();
this.setDropdown('none');
}}
2017-08-28 21:54:39 +00:00
onFocus={this.handleFocus.bind(this, 'account')}
onBlur={this.handleBlur}
>
2017-07-25 19:35:18 +00:00
Log out
</button>
</li>
</ul>
</li>
</ul> }
{/*
<div className="nav__announce">
This is a preview version of the editor, that has not yet been officially released.
2017-06-06 02:33:32 +00:00
It is in development, you can report bugs <a
href="https://github.com/processing/p5.js-web-editor/issues"
target="_blank"
rel="noopener noreferrer"
>here</a>.
Please use with caution.
</div>
*/}
</nav>
);
}
2016-06-23 22:29:55 +00:00
}
Nav.propTypes = {
newProject: PropTypes.func.isRequired,
saveProject: PropTypes.func.isRequired,
autosaveProject: PropTypes.func.isRequired,
2016-07-15 17:11:50 +00:00
exportProjectAsZip: PropTypes.func.isRequired,
2016-07-15 17:36:33 +00:00
cloneProject: PropTypes.func.isRequired,
user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired,
username: PropTypes.string,
id: PropTypes.string
2016-08-17 22:35:15 +00:00
}).isRequired,
project: PropTypes.shape({
id: PropTypes.string,
owner: PropTypes.shape({
id: PropTypes.string
})
2016-08-28 00:46:20 +00:00
}),
2016-08-28 01:52:00 +00:00
logoutUser: PropTypes.func.isRequired,
showShareModal: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
warnIfUnsavedChanges: PropTypes.func.isRequired,
2017-09-01 16:40:15 +00:00
showKeyboardShortcutModal: PropTypes.func.isRequired,
cmController: PropTypes.shape({
tidyCode: PropTypes.func,
showFind: PropTypes.func,
findNext: PropTypes.func,
findPrev: PropTypes.func
}),
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
setAllAccessibleOutput: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired
};
Nav.defaultProps = {
project: {
id: undefined,
owner: undefined
2017-09-01 16:40:15 +00:00
},
cmController: {}
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
Object.assign(
{},
IDEActions
),
dispatch
);
}
export default withRouter(connect(() => ({}), mapDispatchToProps)(Nav));
2019-02-10 00:45:29 +00:00
export { Nav as NavComponent };