UNPKG

victory-core

Version:
375 lines (365 loc) 15.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getInitialTransitionState = getInitialTransitionState; exports.getTransitionPropsFactory = getTransitionPropsFactory; var _react = _interopRequireDefault(require("react")); var _defaults = _interopRequireDefault(require("lodash/defaults")); var _identity = _interopRequireDefault(require("lodash/identity")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function getDatumKey(datum, idx) { return (datum.key || idx).toString(); } function getKeyedData(data) { return data.reduce((keyedData, datum, idx) => { const key = getDatumKey(datum, idx); keyedData[key] = datum; return keyedData; }, {}); } function getKeyedDataDifference(a, b) { let hasDifference = false; const difference = Object.keys(a).reduce((_difference, key) => { if (!(key in b)) { hasDifference = true; _difference[key] = true; } return _difference; }, {}); return hasDifference && difference; } /** * Calculate which data-points exist in oldData and not nextData - * these are the `exiting` data-points. Also calculate which * data-points exist in nextData and not oldData - these are the * `entering` data-points. * * @param {Array} oldData this.props.data Array * @param {Array} nextData this.props.data Array * * @return {Object} Object with `entering` and `exiting` properties. * entering[datum.key] will be true if the data is * entering, and similarly for `exiting`. */ function getNodeTransitions(oldData, nextData) { const oldDataKeyed = oldData && getKeyedData(oldData); const nextDataKeyed = nextData && getKeyedData(nextData); return { entering: oldDataKeyed && getKeyedDataDifference(nextDataKeyed, oldDataKeyed), exiting: nextDataKeyed && getKeyedDataDifference(oldDataKeyed, nextDataKeyed) }; } function getChildData(child) { if (child.type && child.type.getData) { return child.type.getData(child.props); } return child.props && child.props.data || false; } /** * If a parent component has animation enabled, calculate the transitions * for any data of any child component that supports data transitions. * Data transitions are defined as any two datasets where data nodes exist * in the first set and not the second, in the second and not the first, * or both. * * @param {Children} oldChildren this.props.children from old props * @param {Children} nextChildren this.props.children from next props * * @return {Object} Object with the following properties: * - nodesWillExit * - nodesWillEnter * - childrenTransitions * - nodesShouldEnter */ function getInitialTransitionState(oldChildren, nextChildren) { let nodesWillExit = false; let nodesWillEnter = false; const getTransition = (oldChild, newChild) => { if (!newChild || oldChild.type !== newChild.type) { return {}; } const { entering, exiting } = getNodeTransitions(getChildData(oldChild), getChildData(newChild)) || {}; nodesWillExit = nodesWillExit || !!exiting; nodesWillEnter = nodesWillEnter || !!entering; return { entering: entering || false, exiting: exiting || false }; }; const getTransitionsFromChildren = (old, next) => { return old.map((child, idx) => { if (child && child.props && child.props.children && next[idx]) { return getTransitionsFromChildren(_react.default.Children.toArray(old[idx].props.children), _react.default.Children.toArray(next[idx].props.children)); } // get Transition entering and exiting nodes return getTransition(child, next[idx]); }); }; const childrenTransitions = getTransitionsFromChildren(_react.default.Children.toArray(oldChildren), _react.default.Children.toArray(nextChildren)); return { nodesWillExit, nodesWillEnter, childrenTransitions, // TODO: This may need to be refactored for the following situation. // The component receives new props, and the data provided // is a perfect match for the previous data and domain except // for new nodes. In this case, we wouldn't want a delay before // the new nodes appear. nodesShouldEnter: false }; } function getInitialChildProps(animate, data) { const after = animate.onEnter && animate.onEnter.after ? animate.onEnter.after : _identity.default; return { data: data.map((datum, idx) => Object.assign({}, datum, after(datum, idx, data))) }; } // eslint-disable-next-line max-params function getChildBeforeLoad(animate, child, data, cb) { const newAnimate = Object.assign({}, animate, { onEnd: cb }); if (newAnimate && newAnimate.onLoad && !newAnimate.onLoad.duration) { return { animate: newAnimate, data }; } const before = newAnimate.onLoad && newAnimate.onLoad.before ? newAnimate.onLoad.before : _identity.default; // If nodes need to exit, transform them with the provided onLoad.before function. const newData = data.map((datum, idx) => { return Object.assign({}, datum, before(datum, idx, data)); }); return { animate: newAnimate, data: newData, clipWidth: 0 }; } function getChildOnLoad(animate, data, cb) { const newAnimate = Object.assign({}, animate, { onEnd: cb }); let newData = data; if (newAnimate && newAnimate.onLoad && !newAnimate.onLoad.duration) { return { animate, data }; } const after = animate.onLoad && animate.onLoad.after ? animate.onLoad.after : _identity.default; // If nodes need to exit, transform them with the provided onLoad.after function. newData = data.map((datum, idx) => { return Object.assign({}, datum, after(datum, idx, data)); }); return { animate: newAnimate, data: newData }; } // eslint-disable-next-line max-params function getChildPropsOnExit(animate, child, data, exitingNodes, cb) { // Whether or not _this_ child has exiting nodes, we want the exit- // transition for all children to have the same duration, delay, etc. const onExit = animate && animate.onExit; const newAnimate = Object.assign({}, animate, onExit); let newData = data; if (exitingNodes) { // After the exit transition occurs, trigger the animations for // nodes that are neither exiting nor entering. animate.onEnd = cb; const before = animate.onExit && animate.onExit.before ? animate.onExit.before : _identity.default; // If nodes need to exit, transform them with the provided onExit.before function. newData = data.map((datum, idx) => { const key = (datum.key || idx).toString(); return exitingNodes[key] ? Object.assign({}, datum, before(datum, idx, data)) : datum; }); } return { animate: newAnimate, data: newData }; } // eslint-disable-next-line max-params function getChildPropsBeforeEnter(animate, child, data, enteringNodes, cb) { let newAnimate = animate; let newData = data; if (enteringNodes) { // Perform a normal animation here, except - when it finishes - trigger // the transition for entering nodes. newAnimate = Object.assign({}, animate, { onEnd: cb }); const before = animate.onEnter && animate.onEnter.before ? animate.onEnter.before : _identity.default; // We want the entering nodes to be included in the transition target // domain. However, we may not want these nodes to be displayed initially, // so perform the `onEnter.before` transformation on each node. newData = data.map((datum, idx) => { const key = (datum.key || idx).toString(); return enteringNodes[key] ? Object.assign({}, datum, before(datum, idx, data)) : datum; }); } return { animate: newAnimate, data: newData }; } // eslint-disable-next-line max-params function getChildPropsOnEnter(animate, data, enteringNodes, cb) { // Whether or not _this_ child has entering nodes, we want the entering- // transition for all children to have the same duration, delay, etc. const onEnter = animate && animate.onEnter; const newAnimate = Object.assign({}, animate, onEnter); let newData = data; if (enteringNodes) { // Old nodes have been transitioned to their new values, and the // domain should encompass the nodes that will now enter. So perform // the `onEnter.after` transformation on each node. newAnimate.onEnd = cb; const after = newAnimate.onEnter && newAnimate.onEnter.after ? newAnimate.onEnter.after : _identity.default; newData = data.map((datum, idx) => { const key = getDatumKey(datum, idx); return enteringNodes[key] ? Object.assign({}, datum, after(datum, idx, data)) : datum; }); } return { animate: newAnimate, data: newData }; } /** * getTransitionPropsFactory - putting the Java in JavaScript. This will return a * function that returns prop transformations for a child, given that child's props * and its index in the parent's children array. * * In particular, this will include an `animate` object that is set appropriately * so that each child will be synchronized for each stage of a transition * animation. It will also include a transformed `data` object, where each datum * is transformed by `animate.onExit` and `animate.onEnter` `before` and `after` * functions. * * @param {Object} props `this.props` for the parent component. * @param {Object} state `this.state` for the parent component. * @param {Function} setState Function that, when called, will `this.setState` on * the parent component with the provided object. * * @return {Function} Child-prop transformation function. */ function getTransitionPropsFactory(props, state, setState) { const nodesWillExit = state && state.nodesWillExit; const nodesWillEnter = state && state.nodesWillEnter; const nodesShouldEnter = state && state.nodesShouldEnter; const nodesShouldLoad = state && state.nodesShouldLoad; const nodesDoneLoad = state && state.nodesDoneLoad; const childrenTransitions = state && state.childrenTransitions || []; const transitionDurations = { enter: props.animate && props.animate.onEnter && props.animate.onEnter.duration, exit: props.animate && props.animate.onExit && props.animate.onExit.duration, load: props.animate && props.animate.onLoad && props.animate.onLoad.duration, move: props.animate && props.animate.duration }; const onLoad = (child, data, animate) => { if (nodesShouldLoad) { return getChildOnLoad(animate, data, () => { setState({ nodesShouldLoad: false, nodesDoneLoad: true }); }); } return getChildBeforeLoad(animate, child, data, () => { setState({ nodesDoneLoad: true }); }); }; // eslint-disable-next-line max-params const onExit = (nodes, child, data, animate) => { return getChildPropsOnExit(animate, child, data, nodes, () => { setState({ nodesWillExit: false }); }); }; // eslint-disable-next-line max-params const onEnter = (nodes, child, data, animate) => { if (nodesShouldEnter) { return getChildPropsOnEnter(animate, data, nodes, () => { setState({ nodesWillEnter: false }); }); } return getChildPropsBeforeEnter(animate, child, data, nodes, () => { setState({ nodesShouldEnter: true }); }); }; const getChildTransitionDuration = function (child, type) { const animate = child.props.animate; if (!child.type) { return {}; } const defaultTransitions = child.props && child.props.polar ? child.type.defaultPolarTransitions || child.type.defaultTransitions : child.type.defaultTransitions; if (defaultTransitions) { const animationDuration = animate[type] && animate[type].duration; return animationDuration !== undefined ? animationDuration : defaultTransitions[type] && defaultTransitions[type].duration; } return {}; }; return function getTransitionProps(child, index) { const data = getChildData(child) || []; const animate = (0, _defaults.default)({}, props.animate, child.props.animate); const defaultTransitions = child.props.polar ? child.type.defaultPolarTransitions || child.type.defaultTransitions : child.type.defaultTransitions; animate.onExit = (0, _defaults.default)({}, animate.onExit, defaultTransitions && defaultTransitions.onExit); animate.onEnter = (0, _defaults.default)({}, animate.onEnter, defaultTransitions && defaultTransitions.onEnter); animate.onLoad = (0, _defaults.default)({}, animate.onLoad, defaultTransitions && defaultTransitions.onLoad); const childTransitions = childrenTransitions[index] || childrenTransitions[0]; if (!nodesDoneLoad) { // should do onLoad animation const load = transitionDurations.load !== undefined ? transitionDurations.load : getChildTransitionDuration(child, "onLoad"); const animation = { duration: load }; return onLoad(child, data, Object.assign({}, animate, animation)); } else if (nodesWillExit) { const exitingNodes = childTransitions && childTransitions.exiting; const exit = transitionDurations.exit !== undefined ? transitionDurations.exit : getChildTransitionDuration(child, "onExit"); // if nodesWillExit, but this child has no exiting nodes, set a delay instead of a duration const animation = exitingNodes ? { duration: exit } : { delay: exit }; return onExit(exitingNodes, child, data, Object.assign({}, animate, animation)); } else if (nodesWillEnter) { const enteringNodes = childTransitions && childTransitions.entering; const enter = transitionDurations.enter !== undefined ? transitionDurations.enter : getChildTransitionDuration(child, "onEnter"); const move = transitionDurations.move !== undefined ? transitionDurations.move : child.props.animate && child.props.animate.duration; const animation = { duration: nodesShouldEnter && enteringNodes ? enter : move }; return onEnter(enteringNodes, child, data, Object.assign({}, animate, animation)); } else if (!state && animate && animate.onExit) { // This is the initial render, and nodes may enter when props change. Because // animation interpolation is determined by old- and next- props, data may need // to be augmented with certain properties. // // For example, it may be desired that exiting nodes go from `opacity: 1` to // `opacity: 0`. Without setting this on a per-datum basis, the interpolation // might go from `opacity: undefined` to `opacity: 0`, which would result in // interpolated `opacity: NaN` values. // return getInitialChildProps(animate, data); } return { animate, data }; }; }