UNPKG

victory-core

Version:
306 lines (294 loc) 12.6 kB
import React from "react"; import defaults from "lodash/defaults"; import isEmpty from "lodash/isEmpty"; import pick from "lodash/pick"; import isEqual from "react-fast-compare"; import { VictoryTransition } from "../victory-transition/victory-transition"; import { difference } from "./collection"; import * as Events from "./events"; import { isFunction, isNil } from "./helpers"; // DISCLAIMER: // This file is not currently tested, and it is first on the list of files // to refactor in our current refactoring effort. Please do not make changes // to this file without manual testing and/or refactoring and adding tests. const datumHasXandY = datum => { return !isNil(datum._x) && !isNil(datum._y); }; // used for checking state changes. Expected components can be passed in via options const defaultComponents = [{ name: "parent", index: "parent" }, { name: "data" }, { name: "labels" }]; /** * These methods will be implemented by the Mixin, * and are accessible to the Wrapped Component. * * To make your Wrapped Component type-safe, use "interface merging" like so: * @example * interface MyComponent extends EventsMixinClass<MyProps> {} * class MyComponent extends React.Component<MyProps> { ... } */ /** * These fields are calculated by the Mixin */ /** * These are the common roles that we care about internally. */ /** * A component can have any "role", * but there are certain ones that we actually care about internally */ /** * Static component fields used by Victory for common behavior */ /** * This represents the class itself, including static fields */ export function addEvents(WrappedComponent, options) { if (options === void 0) { options = {}; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type // @ts-expect-error "TS2545: A mixin class must have a constructor with a single rest parameter of type 'any[]'." class AddEventsMixin extends WrappedComponent { constructor(props) { super(props); this.cacheValues(this.getCalculatedValues(props)); } state = {}; getEventState = Events.getEventState.bind(this); getScopedEvents = Events.getScopedEvents.bind(this); getEvents = (p, target, eventKey) => { return Events.getEvents.call(this, p, target, eventKey, this.getScopedEvents); }; externalMutations = this.getExternalMutations(this.props); calculatedState = this.getStateChanges(this.props); globalEvents = {}; prevGlobalEventKeys = []; boundGlobalEvents = {}; shouldComponentUpdate(nextProps) { const externalMutations = this.getExternalMutations(nextProps); // @ts-expect-error "Property 'animating' does not exist on type EventMixinCommonProps" const animating = this.props.animating || this.props.animate; const newMutation = !isEqual(externalMutations, this.externalMutations); if (animating || newMutation) { this.cacheValues(this.getCalculatedValues(nextProps)); this.externalMutations = externalMutations; this.applyExternalMutations(nextProps, externalMutations); return true; } const calculatedState = this.getStateChanges(nextProps); if (!isEqual(this.calculatedState, calculatedState)) { this.cacheValues(this.getCalculatedValues(nextProps)); return true; } if (!isEqual(this.props, nextProps)) { this.cacheValues(this.getCalculatedValues(nextProps)); return true; } return false; } componentDidMount() { const globalEventKeys = Object.keys(this.globalEvents); globalEventKeys.forEach(key => this.addGlobalListener(key)); this.prevGlobalEventKeys = globalEventKeys; } componentDidUpdate(prevProps) { const calculatedState = this.getStateChanges(prevProps); this.calculatedState = calculatedState; const globalEventKeys = Object.keys(this.globalEvents); const removedGlobalEventKeys = difference(this.prevGlobalEventKeys, globalEventKeys); removedGlobalEventKeys.forEach(key => this.removeGlobalListener(key)); const addedGlobalEventKeys = difference(globalEventKeys, this.prevGlobalEventKeys); addedGlobalEventKeys.forEach(key => this.addGlobalListener(key)); this.prevGlobalEventKeys = globalEventKeys; } componentWillUnmount() { this.prevGlobalEventKeys.forEach(key => this.removeGlobalListener(key)); } addGlobalListener(key) { const boundListener = event => { const listener = this.globalEvents[key]; return listener && listener(Events.emulateReactEvent(event)); }; this.boundGlobalEvents[key] = boundListener; window.addEventListener(Events.getGlobalEventNameFromKey(key), boundListener); } removeGlobalListener(key) { window.removeEventListener(Events.getGlobalEventNameFromKey(key), this.boundGlobalEvents[key]); } // compile all state changes from own and parent state. Order doesn't matter, as any state // state change should trigger a re-render getStateChanges(props) { if (!this.hasEvents) { return {}; } const getState = (key, type) => { const result = defaults({}, this.getEventState(key, type), this.getSharedEventState(key, type)); return isEmpty(result) ? undefined : result; }; const components = options.components || defaultComponents; const stateChanges = components.map(component => { if (!props.standalone && component.name === "parent") { // don't check for changes on parent props for non-standalone components return undefined; } return component.index !== undefined ? getState(component.index, component.name) : this.dataKeys.map(key => getState(key, component.name)).filter(Boolean); }).filter(Boolean); return stateChanges; } applyExternalMutations(props, externalMutations) { if (!isEmpty(externalMutations)) { const callbacks = props.externalEventMutations.reduce((memo, mutation) => isFunction(mutation.callback) ? memo.concat(mutation.callback) : memo, []); const compiledCallbacks = callbacks.length ? () => { callbacks.forEach(c => c()); } : undefined; this.setState(externalMutations, compiledCallbacks); } } getCalculatedValues(props) { const { sharedEvents } = props; const components = WrappedComponent.expectedComponents; const componentEvents = Events.getComponentEvents(props, components); const getSharedEventState = sharedEvents && isFunction(sharedEvents.getEventState) ? sharedEvents.getEventState : () => undefined; const baseProps = this.getBaseProps(props, getSharedEventState); const dataKeys = Object.keys(baseProps).filter(key => key !== "parent"); const hasEvents = props.events || props.sharedEvents || componentEvents; const events = this.getAllEvents(props); return { componentEvents, getSharedEventState, baseProps, dataKeys, hasEvents, events }; } getExternalMutations(props) { const { sharedEvents, externalEventMutations } = props; return isEmpty(externalEventMutations) || sharedEvents ? undefined : Events.getExternalMutations(externalEventMutations, this.baseProps, this.state); } cacheValues(obj) { Object.keys(obj).forEach(key => { this[key] = obj[key]; }); } getBaseProps(props, getSharedEventState) { const getSharedEventStateFunction = getSharedEventState || this.getSharedEventState.bind(this); const sharedParentState = getSharedEventStateFunction("parent", "parent"); const parentState = this.getEventState("parent", "parent"); const baseParentProps = defaults({}, parentState, sharedParentState); const parentPropsList = baseParentProps.parentControlledProps; const parentProps = parentPropsList ? pick(baseParentProps, parentPropsList) : {}; const modifiedProps = defaults({}, parentProps, props); return typeof WrappedComponent.getBaseProps === "function" ? WrappedComponent.getBaseProps(modifiedProps) : {}; } getAllEvents(props) { if (Array.isArray(this.componentEvents)) { return Array.isArray(props.events) ? this.componentEvents.concat(...props.events) : this.componentEvents; } return props.events; } getComponentProps(component, type, index) { const name = this.props.name || WrappedComponent.role; const key = this.dataKeys && this.dataKeys[index] || index; const id = `${name}-${type}-${key}`; const baseProps = this.baseProps[key] && this.baseProps[key][type] || this.baseProps[key]; if (!baseProps && !this.hasEvents) { return undefined; } const currentProps = component && typeof component === "object" && "props" in component ? component.props : undefined; if (this.hasEvents) { const baseEvents = this.getEvents(this.props, type, key); const componentProps = defaults({ index, key: id }, this.getEventState(key, type), this.getSharedEventState(key, type), currentProps, baseProps, { id }); const events = defaults({}, Events.getPartialEvents(baseEvents, key, componentProps), componentProps.events); return Object.assign({}, componentProps, { events }); } return defaults({ index, key: id }, currentProps, baseProps, { id }); } renderContainer(component, children) { const isContainer = component.type && component.type.role === "container"; const parentProps = isContainer ? this.getComponentProps(component, "parent", "parent") : {}; if (parentProps.events) { this.globalEvents = Events.getGlobalEvents(parentProps.events); parentProps.events = Events.omitGlobalEvents(parentProps.events); } return /*#__PURE__*/React.cloneElement(component, parentProps, children); } animateComponent(props, defaultAnimationWhitelist) { const animationWhitelist = typeof props.animate === "object" && props.animate?.animationWhitelist || defaultAnimationWhitelist; const Comp = this.constructor; return /*#__PURE__*/React.createElement(VictoryTransition, { animate: props.animate, animationWhitelist: animationWhitelist }, /*#__PURE__*/React.createElement(Comp, props)); } // Used by `VictoryLine` and `VictoryArea` renderContinuousData(props) { const { dataComponent, labelComponent, groupComponent } = props; const dataKeys = this.dataKeys.filter(value => value !== "all"); const labelComponents = dataKeys.reduce((memo, key) => { let newMemo = memo; const labelProps = this.getComponentProps(labelComponent, "labels", key); if (labelProps && labelProps.text !== undefined && labelProps.text !== null) { newMemo = newMemo.concat( /*#__PURE__*/React.cloneElement(labelComponent, labelProps)); } return newMemo; }, []); const dataProps = this.getComponentProps(dataComponent, "data", "all"); const children = [/*#__PURE__*/React.cloneElement(dataComponent, dataProps), ...labelComponents]; return this.renderContainer(groupComponent, children); } renderData(props, shouldRenderDatum) { if (shouldRenderDatum === void 0) { shouldRenderDatum = datumHasXandY; } const { dataComponent, labelComponent, groupComponent } = props; const dataComponents = this.dataKeys.reduce((validDataComponents, _dataKey, index) => { const dataProps = this.getComponentProps(dataComponent, "data", index); if (shouldRenderDatum(dataProps.datum)) { validDataComponents.push( /*#__PURE__*/React.cloneElement(dataComponent, dataProps)); } return validDataComponents; }, []); const labelComponents = this.dataKeys.map((_dataKey, index) => { const labelProps = this.getComponentProps(labelComponent, "labels", index); if (labelProps.text !== undefined && labelProps.text !== null) { return /*#__PURE__*/React.cloneElement(labelComponent, labelProps); } return undefined; }).filter(Boolean); const children = [...dataComponents, ...labelComponents]; return this.renderContainer(groupComponent, children); } } return AddEventsMixin; }