UNPKG

@hms-dbmi-bgm/react-workflow-viz

Version:

React component for visualizing CWL-like workflows and provenance graphs.

537 lines (463 loc) 23.5 kB
'use strict'; 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)" }; }