import React from "react"; import defaults from "lodash/defaults"; import isEmpty from "lodash/isEmpty"; import fromPairs from "lodash/fromPairs"; import isEqual from "react-fast-compare"; import stringify from "json-stringify-safe"; import { Collection, EventCallbackInterface, EventMixinCalculatedValues, EventPropTypeInterface, Events, Helpers, StringOrNumberOrCallback, TimerContext, } from "victory-core"; export type VictorySharedEventsProps = { children?: React.ReactElement | React.ReactElement[]; container?: React.ReactElement; groupComponent?: React.ReactElement; events?: EventPropTypeInterface[]; eventKey?: StringOrNumberOrCallback; externalEventMutations?: EventCallbackInterface< string | string[], string | number | (string | number)[] >[]; }; // 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. // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface VictorySharedEvents extends EventMixinCalculatedValues {} export class VictorySharedEvents extends React.Component { static displayName = "VictorySharedEvents"; static role = "shared-event-wrapper"; static contextType = TimerContext; static defaultProps = { groupComponent: , }; getScopedEvents; getEventState; baseProps; sharedEventsCache; globalEvents; prevGlobalEventKeys; boundGlobalEvents; constructor(props: VictorySharedEventsProps) { super(props); this.getScopedEvents = Events.getScopedEvents.bind(this); this.getEventState = Events.getEventState.bind(this); this.state = this.state || {}; this.sharedEventsCache = {}; this.globalEvents = {}; this.prevGlobalEventKeys = []; this.boundGlobalEvents = {}; this.baseProps = this.getBaseProps(props); } shouldComponentUpdate(nextProps) { if (!isEqual(this.props, nextProps)) { this.baseProps = this.getBaseProps(nextProps); const externalMutations = this.getExternalMutations( nextProps, this.baseProps, ); this.applyExternalMutations(nextProps, externalMutations); } return true; } componentDidMount() { const globalEventKeys = Object.keys(this.globalEvents); globalEventKeys.forEach((key) => this.addGlobalListener(key)); this.prevGlobalEventKeys = globalEventKeys; } componentDidUpdate() { const globalEventKeys = Object.keys(this.globalEvents); const removedGlobalEventKeys = Collection.difference( this.prevGlobalEventKeys, globalEventKeys, ); removedGlobalEventKeys.forEach((key) => this.removeGlobalListener(key)); const addedGlobalEventKeys = Collection.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], ); } getAllEvents(props) { const components = ["container", "groupComponent"]; const componentEvents = Events.getComponentEvents(props, components); if (Array.isArray(componentEvents)) { return Array.isArray(props.events) ? componentEvents.concat(...props.events) : componentEvents; } return props.events; } applyExternalMutations(props, externalMutations) { if (!isEmpty(externalMutations)) { const callbacks = props.externalEventMutations.reduce( (memo, mutation) => Helpers.isFunction(mutation.callback) ? memo.concat(mutation.callback) : memo, [], ); const compiledCallbacks = callbacks.length ? () => { callbacks.forEach((c) => c()); } : undefined; this.setState(externalMutations, compiledCallbacks); } } getExternalMutations(props, baseProps) { return !isEmpty(props.externalEventMutations) ? Events.getExternalMutationsWithChildren( props.externalEventMutations, baseProps, this.state, Object.keys(baseProps), ) : undefined; } cacheSharedEvents(name, sharedEvents, cacheValues) { this.sharedEventsCache[name] = [sharedEvents, cacheValues]; } getCachedSharedEvents(name, cacheValues) { const [sharedEvents, prevCacheValues] = this.sharedEventsCache[name] || []; if (sharedEvents && isEqual(cacheValues, prevCacheValues)) { return sharedEvents; } return undefined; } getBaseProps(props) { const { container } = props; const children = React.Children.toArray(this.props.children); const childBaseProps = this.getBasePropsFromChildren(children); const parentBaseProps = container ? container.props : {}; return Object.assign({}, childBaseProps, { parent: parentBaseProps }); } getBasePropsFromChildren(childComponents) { const iteratee = (child, childName) => { if (child.type && Helpers.isFunction(child.type.getBaseProps)) { const baseProps = child.props && child.type.getBaseProps(child.props); return baseProps ? [[childName, baseProps]] : null; } return null; }; const baseProps = Helpers.reduceChildren(childComponents, iteratee); return fromPairs(baseProps); } getNewChildren(props, baseProps) { const { events, eventKey } = props; const alterChildren = (children, childNames) => { return children.reduce((memo, child, index) => { if (child.props.children) { const newChildren = React.Children.toArray(child.props.children); const names = childNames.slice(index, index + newChildren.length); const results = React.cloneElement( child, child.props, alterChildren(newChildren, names), ); return memo.concat(results); } else if ( childNames[index] !== "parent" && child.type && Helpers.isFunction(child.type.getBaseProps) ) { const name = child.props.name || childNames[index]; const childEvents = Array.isArray(events) && events.filter((event) => { if (event.target === "parent") { return false; } return Array.isArray(event.childName) ? event.childName.indexOf(name) > -1 : event.childName === name || event.childName === "all"; }); const sharedEventsCacheValues = [ name, baseProps, childEvents, stringify(this.state[name]), ]; const sharedEvents = this.getCachedSharedEvents( name, sharedEventsCacheValues, ) || { events: childEvents, // partially apply child name and baseProps, getEvents: (evts, target) => this.getScopedEvents(evts, target, name, baseProps), // partially apply child name getEventState: (key, target) => this.getEventState(key, target, name), }; this.cacheSharedEvents(name, sharedEvents, sharedEventsCacheValues); return memo.concat( React.cloneElement( child, Object.assign( { key: `events-${name}`, sharedEvents, eventKey, name }, child.props, ), ), ); } return memo.concat(child); }, []); }; const childNames = Object.keys(baseProps); const childComponents = React.Children.toArray(props.children); return alterChildren(childComponents, childNames); } getContainer(props, baseProps, events) { const children = this.getNewChildren(props, baseProps); const parents = Array.isArray(events) ? events.filter((event) => event.target === "parent") : []; const sharedEvents = parents.length > 0 ? { events: parents, // partially apply childName (null) and baseProps, getEvents: (evts, target) => this.getScopedEvents(evts, target, null, baseProps), getEventState: this.getEventState, } : null; const container = props.container || props.groupComponent; const role = container.type && container.type.role; const containerProps = container.props || {}; const boundGetEvents = Events.getEvents.bind(this); const parentEvents = sharedEvents && boundGetEvents({ sharedEvents }, "parent"); const parentProps = defaults( {}, this.getEventState("parent", "parent"), containerProps, baseProps.parent, { children }, ); const containerEvents = defaults( {}, Events.getPartialEvents(parentEvents, "parent", parentProps), containerProps.events, ); this.globalEvents = Events.getGlobalEvents(containerEvents); const localEvents = Events.omitGlobalEvents(containerEvents); return role === "container" ? React.cloneElement( container, Object.assign({}, parentProps, { events: localEvents }), ) : React.cloneElement(container, localEvents, children); } render(): React.ReactElement { const events = this.getAllEvents(this.props); if (events) { return this.getContainer(this.props, this.baseProps, events); } return React.cloneElement(this.props.container as React.ReactElement, { children: this.props.children, }); } }