react-data-menu
Version:
Smart data-driven menu rendered in an overlay
390 lines (310 loc) • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.App = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _lodash = require('lodash');
var _lodash2 = _interopRequireDefault(_lodash);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _AppMenuItems = require('./data/AppMenuItems.js');
var _AppMenuItems2 = _interopRequireDefault(_AppMenuItems);
var _BottomToolbar = require('./components/BottomToolbar.js');
var _BottomToolbar2 = _interopRequireDefault(_BottomToolbar);
var _Circle = require('./components/Circle.js');
var _Circle2 = _interopRequireDefault(_Circle);
var _CircleMenuItems = require('./data/CircleMenuItems.js');
var _CircleMenuItems2 = _interopRequireDefault(_CircleMenuItems);
var _CircleOps = require('./util/CircleOps.js');
var _CircleOps2 = _interopRequireDefault(_CircleOps);
var _LinkRenderer = require('./renderers/LinkRenderer.js');
var _LinkRenderer2 = _interopRequireDefault(_LinkRenderer);
var _Menu = require('./components/Menu.js');
var _Menu2 = _interopRequireDefault(_Menu);
var _MenuEmitter = require('./emitters/MenuEmitter.js');
var _MenuEmitter2 = _interopRequireDefault(_MenuEmitter);
var _Svg = require('./components/Svg.js');
var _Svg2 = _interopRequireDefault(_Svg);
var _TextRotator = require('./components/TextRotator.js');
var _TextRotator2 = _interopRequireDefault(_TextRotator);
var _TopToolbar = require('./components/TopToolbar.js');
var _TopToolbar2 = _interopRequireDefault(_TopToolbar);
var _Colors = require('./util/Colors');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var Emitter = require('raycast-dom').Emitter.default;
require('./styles/main.less');
require('./styles/menu.less');
var rootNode, canvasNode;
//<editor-fold desc="Helper functions">
function isCircle(target) {
return target.id.startsWith('circle');
}
function getCircleId(circleElement) {
return parseInt(circleElement.id.split('-')[1]);
}
//</editor-fold>
var App = exports.App = function (_Component) {
_inherits(App, _Component);
//<editor-fold desc="Constructor">
function App(props) {
_classCallCheck(this, App);
var _this = _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).call(this, props));
_this.state = {
contextMenuVisible: false,
openOnMouseOver: false, // if true, drop-down menu will open on mouse over
circles: [{
x: 200, y: 200, r: 100, color: _Colors.PURPLE
}, {
x: 800, y: 500, r: 150, color: _Colors.ORANGE
}],
hoveredCircleIndex: -1,
selectedCircleIndex: -1
};
_this.onMenuClose = _this.onMenuClose.bind(_this);
_this.onMouseDownInsideOrOutside = _this.onMouseDownInsideOrOutside.bind(_this);
_this.onContextMenuOutside = _this.onContextMenuOutside.bind(_this);
_this.executeCommand = _this.executeCommand.bind(_this);
// 1. Raycast Emitter subscription
Emitter.getInstance().connect({
onMouseOver: _this.onMouseOver.bind(_this), // circle mouse over
onMouseOut: _this.onMouseOut.bind(_this), // circle mouse out
onMouseMove: _this.onMouseMove.bind(_this), // drawing circles with Alt key
onMouseDown: _this.onMouseDown.bind(_this), // drawing circles with Alt key
onMouseUp: _this.onMouseUp.bind(_this) // stop drawing circles with Alt key
});
// 2. MenuEmitter subscription
_MenuEmitter2.default.getInstance().connect({
onContextMenuOutside: _this.onContextMenuOutside, // show context menu
onMouseDownOutside: _this.onMouseDownInsideOrOutside, // flip openOnMouseOver state
onMouseDownInside: _this.onMouseDownInsideOrOutside, // flip openOnMouseOver state
onMouseUpInside: _this.onMouseUpInside.bind(_this) // flip openOnMouseOver state
});
return _this;
}
//</editor-fold>
//<editor-fold desc="Raycast">
_createClass(App, [{
key: 'onMouseOver',
value: function onMouseOver(ray) {
var circle = ray.intersectsId(_Circle.CIRCLE_ID_PREFIX),
circleId,
circleIndex;
if (circle) {
// circle mouse over
circleId = circle.id;
circleIndex = parseInt(circleId.split(_Circle.CIRCLE_ID_PREFIX)[1]);
this.setState({
hoveredCircleIndex: circleIndex
});
}
}
}, {
key: 'onMouseOut',
value: function onMouseOut(ray) {
var circle = ray.intersectsId(_Circle.CIRCLE_ID_PREFIX);
if (circle) {
// circle mouse over
this.setState({
hoveredCircleIndex: -1
});
}
}
}, {
key: 'onMouseDown',
value: function onMouseDown(ray) {
var self = this;
if (!ray.intersects(canvasNode)) {
return;
}
this.setState({
mouseIsDown: true,
mousePosition: ray.position
}, function () {
if (ray.e.altKey) {
if (ray.e.shiftKey) {
self.executeCommand('clear'); // Alt + Shift + click = clear
} else if (ray.intersects(canvasNode)) {
self.executeCommand('new-circle'); // Alt + click = new circle
}
}
});
}
}, {
key: 'onMouseUp',
value: function onMouseUp() {
this.setState({
mouseIsDown: false
});
}
}, {
key: 'onMouseMove',
value: function onMouseMove(ray) {
var self = this;
if (!ray.e.altKey || !this.state.mouseIsDown || !ray.intersects(canvasNode)) {
return;
}
this.setState({
mousePosition: ray.position
}, function () {
self.executeCommand('new-circle'); // Alt + mouse move = new circle
});
}
}, {
key: 'onMouseDownInsideOrOutside',
value: function onMouseDownInsideOrOutside() {
this.setState({
openOnMouseOver: false
});
}
}, {
key: 'onMouseUpInside',
value: function onMouseUpInside() {
this.setState({
openOnMouseOver: false
});
}
/**
* Fires on contextmenu or tap-and-hold outside of the current menu
* @param ray Ray
*/
}, {
key: 'onContextMenuOutside',
value: function onContextMenuOutside(ray) {
var target = ray.target;
this.setState({
openOnMouseOver: false
});
if (ray.intersects(rootNode)) {
// cancel default on the app root (not the web page - GitHub link must work)
ray.preventDefault();
}
if (!ray.intersects(canvasNode)) {
return; // we're interested only in canvas clicks
}
// cancel default menu on canvas
ray.preventDefault();
if (isCircle(target)) {
// circle clicked
this.selectCircle(target);
this.showContextMenu(ray, this.circleContextMenuItems);
} else {
// canvas clicked
this.setState({
selectedCircleIndex: -1
});
this.showContextMenu(ray, this.appContextMenuItems);
}
}
/**
* Fires on menu close
* We would accomplish the same effect by subscribing to dispatched directly, instead of the Menu
*/
}, {
key: 'onMenuClose',
value: function onMenuClose() {
this.setState({
contextMenuVisible: false,
selectedCircleIndex: -1
});
}
//</editor-fold>
//<editor-fold desc="Show context menu">
}, {
key: 'showContextMenu',
value: function showContextMenu(ray, items) {
this.setState({
contextMenuVisible: true,
menuPosition: ray.position,
items: items
});
}
//</editor-fold>
//<editor-fold desc="Circles & commands">
}, {
key: 'selectCircle',
value: function selectCircle(circleElement) {
this.state.selectedCircleIndex = getCircleId(circleElement);
}
}, {
key: 'executeCommand',
value: function executeCommand(command) {
var position = this.state.mouseIsDown ? this.state.mousePosition : this.state.menuPosition,
circles = _CircleOps2.default.executeCommand(command, this.state.circles, this.state.selectedCircleIndex, position);
this.setState({ circles: circles });
}
//</editor-fold>
//<editor-fold desc="React">
}, {
key: 'render',
value: function render() {
var self = this,
index = 0,
commonToolbarProps = {
openOnMouseOver: this.state.openOnMouseOver,
renderers: {
'link': _LinkRenderer2.default
},
onOpen: function onOpen() {
self.setState({
// let's have the Mac-like behaviour
// once the first drop-down was opened by clicking, consequent drop-downs open on mouse-over
openOnMouseOver: true
});
}
},
circles = this.state.circles.map(function (item) {
var id = _Circle.CIRCLE_ID_PREFIX + index,
circle = _react2.default.createElement(_Circle2.default, _extends({}, item, {
id: id,
key: id,
strokeColor: 'white',
hovered: self.state.hoveredCircleIndex === index,
selected: self.state.selectedCircleIndex === index }));
index++;
return circle;
}),
menu = this.state.contextMenuVisible ? _react2.default.createElement(_Menu2.default, { items: this.state.items,
position: this.state.menuPosition,
onClose: this.onMenuClose }) : null;
return _react2.default.createElement(
'div',
{ ref: 'root' },
_react2.default.createElement(_TopToolbar2.default, commonToolbarProps),
_react2.default.createElement(
'div',
{ ref: 'canvas', className: 'container' },
_react2.default.createElement(
_Svg2.default,
{ width: '100%', height: '100%' },
circles
),
_react2.default.createElement(_TextRotator2.default, null)
),
menu,
_react2.default.createElement(_BottomToolbar2.default, commonToolbarProps)
);
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
var self = this;
function binder() {
var _self$executeCommand;
return (_self$executeCommand = self.executeCommand).bind.apply(_self$executeCommand, [self].concat(Array.prototype.slice.call(arguments)));
}
this.circleContextMenuItems = new _CircleMenuItems2.default(binder);
this.appContextMenuItems = new _AppMenuItems2.default(binder);
rootNode = _reactDom2.default.findDOMNode(this.refs.root);
canvasNode = _reactDom2.default.findDOMNode(this.refs.canvas);
}
//</editor-fold>
}]);
return App;
}(_react.Component);