@hms-dbmi-bgm/react-workflow-viz
Version:
React component for visualizing CWL-like workflows and provenance graphs.
537 lines (463 loc) • 23.5 kB
JavaScript
;
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ScaleControls = exports.ScaleController = void 0;
exports.scaledStyle = scaledStyle;
var _react = _interopRequireDefault(require("react"));
var _utilities = require("../utilities");
var _excluded = ["children", "initialScale", "scale", "setScale", "minScale"];
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function (o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function (o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/**
* @deprecated Moved most of the functionality into Graph
*/
var ScaleController = /*#__PURE__*/function (_React$PureComponent) {
_inherits(ScaleController, _React$PureComponent);
var _super = _createSuper(ScaleController);
function ScaleController(props) {
var _this;
_classCallCheck(this, ScaleController);
_this = _super.call(this, props);
_this.handleWheelMove = _this.handleWheelMove.bind(_assertThisInitialized(_this));
_this.handleInnerContainerMounted = _this.handleInnerContainerMounted.bind(_assertThisInitialized(_this));
_this.handleInnerContainerWillUnmount = _this.handleInnerContainerWillUnmount.bind(_assertThisInitialized(_this));
_this.innerElemReference = null;
return _this;
}
_createClass(ScaleController, [{
key: "componentDidMount",
value: function componentDidMount() {// const {
// containerWidth,
// containerHeight,
// minScale: propMinScale,
// maxScale,
// graphWidth,
// graphHeight,
// zoomToExtentsOnMount = true
// } = this.props;
// if (typeof containerWidth !== "number" || typeof containerHeight !== "number") {
// // Maybe will become set in componentDidUpdate later.
// return false;
// }
// if (isNaN(containerWidth) || isNaN(containerHeight)) {
// throw new Error("Width or height is NaN.");
// }
// const minScaleUnbounded = Math.min(
// (containerWidth / graphWidth),
// (containerHeight / graphHeight)
// );
// // Decrease by 5% for scrollbars, etc.
// const nextMinScale = Math.floor(
// Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded))
// * 95) / 100;
// const retObj = { minScale: nextMinScale };
// // First time that we've gotten dimensions -- set scale to fit.
// // Also, if nextMinScale > scale or we had scale === minScale before.
// // TODO: Maybe do this onMount also
// if (zoomToExtentsOnMount) {
// retObj.scale = nextMinScale;
// }
// requestAnimationFrame(() => {
// this.setState(retObj);
// });
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate() {
var _this$props = this.props,
enableMouseWheelZoom = _this$props.enableMouseWheelZoom,
enablePinchZoom = _this$props.enablePinchZoom,
zoomToExtentsOnMount = _this$props.zoomToExtentsOnMount,
containerWidth = _this$props.containerWidth,
containerHeight = _this$props.containerHeight,
graphWidth = _this$props.graphWidth,
graphHeight = _this$props.graphHeight,
propMinScale = _this$props.minScale,
maxScale = _this$props.maxScale; // const { scale, minScale: stateMinScale } = this.state;
// const {
// enableMouseWheelZoom: pastWheelEnabled,
// enablePinchZoom: pastPinchEnabled,
// containerWidth: pastWidth,
// containerHeight: pastHeight,
// graphWidth: pastGraphWidth,
// graphHeight: pastGraphHeight
// } = pastProps;
// // Remove or attach listeners if needed.
// const listenNow = (enableMouseWheelZoom || enablePinchZoom);
// const listenBefore = (pastWheelEnabled || pastPinchEnabled);
// if (this.innerElemReference){
// if (listenNow && !listenBefore){
// this.innerElemReference.addEventListener("wheel", this.handleWheelMove, { "passive": false, "capture": true });
// } else if (!listenNow && listenBefore){
// this.innerElemReference.removeEventListener("wheel", this.handleWheelMove);
// }
// }
// // Update minScale (& possibly scale itself)
// // We read `pastState` here before updating set
// // vs using functional updater because want to avoid
// // React's state change queuing mechanisms / reading
// // most accurate prev value not important.
// if (containerWidth !== pastWidth ||
// containerHeight !== pastHeight ||
// graphWidth !== pastGraphWidth ||
// graphHeight !== pastGraphHeight
// ){
// const minScaleUnbounded = Math.min(
// (containerWidth / graphWidth),
// (containerHeight / graphHeight)
// );
// // Decrease by 5% for scrollbars, etc.
// const nextMinScale = Math.floor(
// Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded))
// * 95) / 100;
// const retObj = { minScale: nextMinScale };
// // First time that we've gotten dimensions -- set scale to fit.
// // Also, if nextMinScale > scale or we had scale === minScale before.
// // TODO: Maybe do this onMount also
// if (nextMinScale > scale || stateMinScale === scale || (zoomToExtentsOnMount && (!pastHeight || !pastWidth))) {
// retObj.scale = nextMinScale;
// }
// requestAnimationFrame(() => {
// this.setState(retObj);
// });
// }
}
/**
* If `enableMouseWheelZoom` or `enablePinchZoom` are enabled, will
* zoom in response to mouse wheel events or ctrl+mousewheel & touchpad
* pinch events, respectively.
*/
}, {
key: "handleWheelMove",
value: function handleWheelMove(evt) {
var deltaY = evt.deltaY,
deltaX = evt.deltaX,
ctrlKey = evt.ctrlKey;
var _this$props2 = this.props,
enableMouseWheelZoom = _this$props2.enableMouseWheelZoom,
enablePinchZoom = _this$props2.enablePinchZoom,
scale = _this$props2.scale,
setScale = _this$props2.setScale;
if (!enableMouseWheelZoom && !enablePinchZoom) {
return false;
} // Chrome registers touchpad-based pinching as scrollwheel with ctrlKey.
if (enablePinchZoom && !enableMouseWheelZoom && !ctrlKey) {
return false;
}
if (!enablePinchZoom && enableMouseWheelZoom && ctrlKey) {
return false;
}
if (enableMouseWheelZoom && Math.abs(deltaX) > 0) {
// Not perfect --
// Make sure is mousewheel and not bidirectional touchpad,
// for which we might still wanna allow left/right movement.
return false;
}
evt.preventDefault();
evt.stopPropagation(); // `ctrlKey=true` implies 2nd ctrl key being pressed -or- touchpad pinching
var deltaMultiplier = ctrlKey ? 0.01 : 0.0005; // React uses own state change queuing system, which guessing
// gets bypassed w. raf, so below line might work better, since
// `state.scale` unchanged.. (vs. functional updater)
setScale(scale - deltaY * deltaMultiplier);
}
}, {
key: "handleInnerContainerMounted",
value: function handleInnerContainerMounted(innerElem) {
var _this$props3 = this.props,
onMount = _this$props3.onMount,
enableMouseWheelZoom = _this$props3.enableMouseWheelZoom,
enablePinchZoom = _this$props3.enablePinchZoom;
if (typeof onMount === "function") {
onMount.apply(void 0, arguments);
}
this.innerElemReference = innerElem; // We need to listen to `wheel` events directly (not thru React)
// as react doesn't handle these (it seems / afaik).
if (enableMouseWheelZoom || enablePinchZoom) {
// Chrome & some other browsers set passive:true by default.
innerElem.addEventListener("wheel", this.handleWheelMove, {
"passive": false,
"capture": true
});
}
}
}, {
key: "handleInnerContainerWillUnmount",
value: function handleInnerContainerWillUnmount(innerElem) {
var onWillUnmount = this.props.onWillUnmount;
if (typeof onMount === "function") {
onWillUnmount.apply(void 0, arguments);
}
if (this.innerElemReference === null) {
console.error("No inner elem, exiting");
return;
}
if (this.innerElemReference !== innerElem) {
throw new Error("Inner elem is different, exiting");
}
this.innerElemReference.removeEventListener("wheel", this.handleWheelMove);
this.innerElemReference = null;
}
}, {
key: "render",
value: function render() {
var _this$props4 = this.props,
children = _this$props4.children,
_this$props4$initialS = _this$props4.initialScale,
initialScale = _this$props4$initialS === void 0 ? null : _this$props4$initialS,
scale = _this$props4.scale,
setScale = _this$props4.setScale,
minScale = _this$props4.minScale,
passProps = _objectWithoutProperties(_this$props4, _excluded);
var childProps = _objectSpread(_objectSpread({}, passProps), {}, {
scale: scale || 1,
minScale: minScale,
setScale: setScale,
onMount: this.handleInnerContainerMounted,
onWillUnmount: this.handleInnerContainerWillUnmount
});
return _react["default"].Children.map(children, function (child) {
return child && /*#__PURE__*/_react["default"].cloneElement(child, childProps);
});
}
}]);
return ScaleController;
}(_react["default"].PureComponent);
/**
* Component which provides UI for adjusting scale and
* calls `ScaleController`'s `setScale` function.
*
* Uses `requestAnimationFrame` (`raf`) for smooth and performant
* zooming transitions.
*
* To assert whether `raf` makes a meaningful difference, try to comment out
* the `raf`-related lines in `onSliderChange` method (except for `setScale(nextVal)`)
* and compare performance/smoothness :-D
*
* React _does_ seem to use requestAnimationFrame under the hood but maybe only
* for batched updates, as animation frames aren't always requested (Chrome dev
* tools > performance > profiling).
*
* We're getting performance gain from using `raf` in onSliderChange potentially
* because we're listening to `SyntheticEvent`s passed in from React element, which
* may be throttled or deferred until after state changes (vs native events).
*/
exports.ScaleController = ScaleController;
_defineProperty(ScaleController, "defaultProps", {
minScale: 0.01,
maxScale: 1.25,
initialScale: 1,
zoomToExtentsOnMount: true
});
var ScaleControls = /*#__PURE__*/function (_React$PureComponent2) {
_inherits(ScaleControls, _React$PureComponent2);
var _super2 = _createSuper(ScaleControls);
function ScaleControls(props) {
var _this2;
_classCallCheck(this, ScaleControls);
_this2 = _super2.call(this, props);
_this2.onZoomOutDown = _this2.onZoomOutDown.bind(_assertThisInitialized(_this2));
_this2.onZoomOutUp = _this2.onZoomOutUp.bind(_assertThisInitialized(_this2));
_this2.onZoomInDown = _this2.onZoomInDown.bind(_assertThisInitialized(_this2));
_this2.onZoomInUp = _this2.onZoomInUp.bind(_assertThisInitialized(_this2));
_this2.cancelAnimationFrame = _this2.cancelAnimationFrame.bind(_assertThisInitialized(_this2));
_this2.onSliderChange = _this2.onSliderChange.bind(_assertThisInitialized(_this2));
_this2.state = {
zoomOutPressed: false,
zoomInPressed: false
};
_this2.nextAnimationFrame = null;
return _this2;
}
_createClass(ScaleControls, [{
key: "cancelAnimationFrame",
value: function cancelAnimationFrame() {
if (this.nextAnimationFrame !== null) {
(0, _utilities.cancelAnimationFrame)(this.nextAnimationFrame);
this.nextAnimationFrame = null;
}
}
}, {
key: "onZoomOutDown",
value: function onZoomOutDown(evt) {
var _this3 = this;
evt.preventDefault();
evt.stopPropagation();
var _this$props5 = this.props,
setScale = _this$props5.setScale,
scaleChangeInterval = _this$props5.scaleChangeInterval,
scaleChangeDownFactor = _this$props5.scaleChangeDownFactor,
initScale = _this$props5.scale;
this.setState({
zoomOutPressed: true
}, function () {
var start = Date.now(); //const diff = (scaleChangeDownFactor * initScale) - initScale;
var performZoom = function () {
var _this3$props = _this3.props,
scale = _this3$props.scale,
minScale = _this3$props.minScale;
if (scale <= minScale) {
// Button becomes disabled so `onZoomOutUp` is not guaranteed to be called.
_this3.setState({
zoomOutPressed: false
});
_this3.nextAnimationFrame = null;
return;
}
setScale( //initScale + (diff * Math.floor((Date.now() - start) / scaleChangeInterval))
initScale * Math.pow(scaleChangeDownFactor, Math.floor((Date.now() - start) / scaleChangeInterval)));
_this3.nextAnimationFrame = (0, _utilities.requestAnimationFrame)(performZoom);
};
_this3.nextAnimationFrame = (0, _utilities.requestAnimationFrame)(performZoom);
});
}
}, {
key: "onZoomOutUp",
value: function onZoomOutUp(evt) {
evt.preventDefault();
evt.stopPropagation();
this.cancelAnimationFrame();
this.setState({
zoomOutPressed: false
});
}
}, {
key: "onZoomInDown",
value: function onZoomInDown(evt) {
var _this4 = this;
evt.preventDefault();
evt.stopPropagation();
var _this$props6 = this.props,
setScale = _this$props6.setScale,
scaleChangeInterval = _this$props6.scaleChangeInterval,
scaleChangeUpFactor = _this$props6.scaleChangeUpFactor,
initScale = _this$props6.scale;
this.setState({
zoomInPressed: true
}, function () {
var start = Date.now(); //const diff = (scaleChangeUpFactor * initScale) - initScale;
var performZoom = function () {
var _this4$props = _this4.props,
scale = _this4$props.scale,
maxScale = _this4$props.maxScale;
if (scale >= maxScale) {
// Button becomes disabled so `onZoomOutUp` is not guaranteed to be called.
_this4.setState({
zoomInPressed: false
});
_this4.nextAnimationFrame = null;
return;
}
setScale( //initScale + (diff * Math.floor((Date.now() - start) / scaleChangeInterval))
initScale * Math.pow(scaleChangeUpFactor, Math.floor((Date.now() - start) / scaleChangeInterval)));
_this4.nextAnimationFrame = (0, _utilities.requestAnimationFrame)(performZoom);
};
_this4.nextAnimationFrame = (0, _utilities.requestAnimationFrame)(performZoom);
});
}
}, {
key: "onZoomInUp",
value: function onZoomInUp(evt) {
evt.preventDefault();
evt.stopPropagation();
this.cancelAnimationFrame();
this.setState({
zoomInPressed: false
});
}
}, {
key: "onSliderChange",
value: function onSliderChange(evt) {
var _this5 = this;
evt.preventDefault();
evt.stopPropagation();
var setScale = this.props.setScale;
var nextVal = parseFloat(evt.target.value);
this.cancelAnimationFrame();
this.nextAnimationFrame = (0, _utilities.requestAnimationFrame)(function () {
setScale(nextVal);
_this5.nextAnimationFrame = null;
});
}
}, {
key: "render",
value: function render() {
var _this$props7 = this.props,
scale = _this$props7.scale,
setScale = _this$props7.setScale,
minScale = _this$props7.minScale,
maxScale = _this$props7.maxScale;
if (typeof setScale !== "function" || typeof scale !== "number" || isNaN(scale)) {
return null;
}
return /*#__PURE__*/_react["default"].createElement("div", {
className: "zoom-controls-container"
}, /*#__PURE__*/_react["default"].createElement("div", {
className: "zoom-buttons-row"
}, /*#__PURE__*/_react["default"].createElement("button", {
type: "button",
className: "zoom-btn zoom-out",
onMouseDown: this.onZoomOutDown,
onMouseUp: this.onZoomOutUp,
onTouchStart: this.onZoomOutDown,
onTouchEnd: this.onZoomOutUp,
disabled: minScale >= scale
}, /*#__PURE__*/_react["default"].createElement("i", {
className: "icon icon-fw fa-fw icon-search-minus fa-search-minus fas"
})), /*#__PURE__*/_react["default"].createElement("div", {
className: "zoom-value no-user-select"
}, Math.round(scale * 100), /*#__PURE__*/_react["default"].createElement("i", {
className: "icon icon-fw fa-fw icon-percentage fa-percentage fas small"
})), /*#__PURE__*/_react["default"].createElement("button", {
type: "button",
className: "zoom-btn zoom-in",
onMouseDown: this.onZoomInDown,
onMouseUp: this.onZoomInUp,
onTouchStart: this.onZoomInDown,
onTouchEnd: this.onZoomInUp,
disabled: maxScale <= scale
}, /*#__PURE__*/_react["default"].createElement("i", {
className: "icon icon-fw fa-fw icon-search-plus fa-search-plus fas"
}))), /*#__PURE__*/_react["default"].createElement("div", {
className: "zoom-slider"
}, /*#__PURE__*/_react["default"].createElement("input", {
type: "range",
min: minScale,
max: maxScale,
value: scale,
step: 0.01,
onChange: this.onSliderChange
})));
}
}]);
return ScaleControls;
}(_react["default"].PureComponent);
exports.ScaleControls = ScaleControls;
_defineProperty(ScaleControls, "defaultProps", {
scaleChangeInterval: 15,
// milliseconds
scaleChangeUpFactor: 1.025,
scaleChangeDownFactor: 0.975
});
function scaledStyle(graphHeight, graphWidth, scale) {
return {
width: (0, _utilities.roundScaled)(graphWidth, scale),
height: (0, _utilities.roundScaled)(graphHeight, scale),
transform: "scale3d(" + scale + "," + scale + ",1)"
};
}