UNPKG

victory-core

Version:
515 lines (472 loc) 17.4 kB
import React from "react"; import isEmpty from "lodash/isEmpty"; import pickBy from "lodash/pickBy"; import omitBy from "lodash/omitBy"; import uniq from "lodash/uniq"; import type { EventMixinCalculatedValues } from "./add-events"; import { isFunction } from "./helpers"; const GLOBAL_EVENT_REGEX = /^onGlobal(.*)$/; type ComponentEventKey = string | number; export interface ComponentEvent { target?: "parent" | string; eventKey?: ComponentEventKey | ComponentEventKey[]; eventHandlers: ComponentEventHandlers; } // Normally we'd use Template Literal Types, but we're avoiding it to maximize TS compatibility with TS < 4.1 export type ComponentEventName = string; // `on${Capitalize<string>}`; export interface ComponentEventHandlers { [k: ComponentEventName]: ComponentEventHandler; } export type ComponentEventHandler = ( evt: React.SyntheticEvent, childProps: unknown, eventKey: ComponentEventKey, eventName: ComponentEventName, ) => UpdatedProps; export type UpdatedProps = any; interface ComponentWithEvents extends EventMixinCalculatedValues { state; setState; } /* Returns all own and shared events that should be attached to a single target element, * i.e. an individual bar specified by target: "data", eventKey: [index]. * Returned events are scoped to the appropriate state. Either that of the component itself * (i.e. VictoryBar) in the case of own events, or that of the parent component * (i.e. VictoryChart) in the case of shared events */ // eslint-disable-next-line max-params export function getEvents( this: ComponentWithEvents, props, target?, eventKey?, // eslint-disable-next-line no-shadow getScopedEvents?, ) { // Returns all events that apply to a particular target element const getEventsByTarget = (events: Array<ComponentEvent>) => { const getSelectedEvents = () => { const targetEvents = events.reduce((memo, event) => { if (event.target !== undefined) { const matchesTarget = Array.isArray(event.target) ? event.target.includes(target) : `${event.target}` === `${target}`; return matchesTarget ? memo.concat(event) : memo; } return memo.concat(event); }, [] as ComponentEvent[]); if (eventKey !== undefined && target !== "parent") { return targetEvents.filter((obj) => { const targetKeys = obj.eventKey; const useKey = (key) => (key ? `${key}` === `${eventKey}` : true); return Array.isArray(targetKeys) ? targetKeys.some((k) => useKey(k)) : useKey(targetKeys); }); } return targetEvents; }; const selectedEvents = getSelectedEvents(); return ( Array.isArray(selectedEvents) && selectedEvents.reduce( (memo, event) => { return event ? Object.assign(memo, event.eventHandlers) : memo; }, {} as ComponentEvent["eventHandlers"], ) ); }; /* Returns all events from props and defaultEvents from components. Events handlers * specified in props will override handlers for the same event if they are also * specified in defaultEvents of a sub-component */ const getAllEvents = () => { // Mandatory usage: `getEvents.bind(this)` if (Array.isArray(this.componentEvents)) { return Array.isArray(props.events) ? this.componentEvents.concat(...props.events) : this.componentEvents; } return props.events; }; const allEvents = getAllEvents(); const ownEvents = allEvents && isFunction(getScopedEvents) ? getScopedEvents(getEventsByTarget(allEvents), target) : undefined; if (!props.sharedEvents) { return ownEvents; } const getSharedEvents = props.sharedEvents.getEvents; const sharedEvents = props.sharedEvents.events && getSharedEvents(getEventsByTarget(props.sharedEvents.events), target); return Object.assign({}, sharedEvents, ownEvents); } /* Returns a modified events object where each event handler is replaced by a new * function that calls the original handler and then calls setState with the return * of the original event handler assigned to state property that maps to the target * element. */ // eslint-disable-next-line max-params export function getScopedEvents( this: ComponentWithEvents, events, namespace, childType, baseProps, ) { if (isEmpty(events)) { return {}; } // Mandatory usage: `getScopedEvents.bind(this)` const newBaseProps = baseProps || this.baseProps; // returns the original base props or base state of a given target element const getTargetProps = (identifier, type) => { const { childName, target, key } = identifier; const baseType = type === "props" ? newBaseProps : this.state || {}; const base = childName === undefined || childName === null || !baseType[childName] ? baseType : baseType[childName]; return key === "parent" ? base.parent : base[key] && base[key][target]; }; // Returns the state object with the mutation caused by a given eventReturn // applied to the appropriate property on the state object const parseEvent = (eventReturn, eventKey) => { const childNames = namespace === "parent" ? eventReturn.childName : eventReturn.childName || childType; const target = eventReturn.target || namespace; // returns all eventKeys to modify for a targeted childName const getKeys = (childName) => { if (target === "parent") { return "parent"; } if (eventReturn.eventKey === "all") { return newBaseProps[childName] ? Object.keys(newBaseProps[childName]).filter( (value) => value !== "parent", ) : Object.keys(newBaseProps).filter((value) => value !== "parent"); } else if (eventReturn.eventKey === undefined && eventKey === "parent") { return newBaseProps[childName] ? Object.keys(newBaseProps[childName]) : Object.keys(newBaseProps); } return eventReturn.eventKey !== undefined ? eventReturn.eventKey : eventKey; }; // returns the state object with mutated props applied for a single key const getMutationObject = (key, childName) => { const baseState = this.state || {}; if (!isFunction(eventReturn.mutation)) { return baseState; } const mutationTargetProps = getTargetProps( { childName, key, target }, "props", ); const mutationTargetState = getTargetProps( { childName, key, target }, "state", ); const mutatedProps = eventReturn.mutation( Object.assign({}, mutationTargetProps, mutationTargetState), newBaseProps, ); const childState = baseState[childName] || {}; const filterState = (state) => { if (state[key] && state[key][target]) { delete state[key][target]; } if (state[key] && !Object.keys(state[key]).length) { delete state[key]; } return state; }; const extendState = (state) => { return target === "parent" ? Object.assign(state, { [key]: Object.assign(state[key] || {}, mutatedProps), }) : Object.assign(state, { [key]: Object.assign(state[key] || {}, { [target]: mutatedProps, }), }); }; const updateState = (state) => { return mutatedProps ? extendState(state) : filterState(state); }; return childName !== undefined && childName !== null ? Object.assign(baseState, { [childName]: updateState(childState) }) : updateState(baseState); }; // returns entire mutated state for a given childName const getReturnByChild = (childName) => { const mutationKeys = getKeys(childName); return Array.isArray(mutationKeys) ? mutationKeys.reduce((memo, key) => { return Object.assign(memo, getMutationObject(key, childName)); }, {}) : getMutationObject(mutationKeys, childName); }; // returns an entire mutated state for all children const allChildNames = childNames === "all" ? Object.keys(newBaseProps).filter((value) => value !== "parent") : childNames; return Array.isArray(allChildNames) ? allChildNames.reduce((memo, childName) => { return Object.assign(memo, getReturnByChild(childName)); }, {}) : getReturnByChild(allChildNames); }; // Parses an array of event returns into a single state mutation const parseEventReturn = (eventReturn, eventKey) => { return Array.isArray(eventReturn) ? eventReturn.reduce( (memo, props) => Object.assign({}, memo, parseEvent(props, eventKey)), {}, ) : parseEvent(eventReturn, eventKey); }; const compileCallbacks = (eventReturn) => { const getCallback = (obj) => isFunction(obj.callback) && obj.callback; const callbacks = Array.isArray(eventReturn) ? eventReturn.map((evtObj) => getCallback(evtObj)) : [getCallback(eventReturn)]; const callbackArray = callbacks.filter((callback) => callback !== false); return callbackArray.length ? () => callbackArray.forEach((callback) => callback()) : undefined; }; // A function that calls a particular event handler, parses its return // into a state mutation, and calls setState // eslint-disable-next-line max-params const onEvent = (evt, childProps, eventKey, eventName) => { const eventReturn = events[eventName](evt, childProps, eventKey, this); if (!isEmpty(eventReturn)) { const callbacks = compileCallbacks(eventReturn); this.setState(parseEventReturn(eventReturn, eventKey), callbacks); } }; // returns a new events object with enhanced event handlers return Object.keys(events).reduce((memo, event) => { memo[event] = onEvent; return memo; }, {}); } /* * Returns a partially applied event handler for a specific target element * This allows event handlers to have access to props controlling each element */ export function getPartialEvents( events: ComponentEventHandlers, eventKey: ComponentEventKey, childProps: unknown, ): PartialEvents { if (!events) return {}; return Object.keys(events).reduce((memo, eventName) => { const appliedEvent = (evt) => events[eventName](evt, childProps, eventKey, eventName); memo[eventName] = appliedEvent; return memo; }, {} as PartialEvents); } export interface PartialEvents { [eventName: ComponentEventName]: (evt: React.SyntheticEvent) => UpdatedProps; } /* Returns the property of the state object corresponding to event changes for * a particular element */ // eslint-disable-next-line max-params export function getEventState( this: ComponentWithEvents, eventKey: ComponentEventKey, namespace: string, childType?: string, ) { // Mandatory usage: `getEventState.bind(this)` const state = this.state || {}; if (!childType) { return eventKey === "parent" ? (state[eventKey] && state[eventKey][namespace]) || state[eventKey] : state[eventKey] && state[eventKey][namespace]; } return ( state[childType] && state[childType][eventKey] && state[childType][eventKey][namespace] ); } /** * Returns a set of all mutations for shared events * * @param {Array} mutations an array of mutations objects * @param {Object} baseProps an object that describes all props for children of VictorySharedEvents * @param {Object} baseState an object that describes state for children of VictorySharedEvents * @param {Array} childNames an array of childNames * * @return {Object} a object describing all mutations for VictorySharedEvents */ // eslint-disable-next-line max-params export function getExternalMutationsWithChildren( mutations, baseProps = {}, baseState = {}, childNames, ) { return childNames.reduce((memo, childName) => { const childState = baseState[childName]; const mutation = getExternalMutations( mutations, baseProps[childName], baseState[childName], childName, ); memo[childName] = mutation ? mutation : childState; return pickBy(memo, (v) => !isEmpty(v)); }, {}); } /** * Returns a set of all mutations for a component * * @param {Array} mutations an array of mutations objects * @param {Object} baseProps a props object (scoped to a childName when used by shared events) * @param {Object} baseState a state object (scoped to a childName when used by shared events) * @param {String} childName an optional childName * * @return {Object} a object describing mutations for a given component */ // eslint-disable-next-line max-params export function getExternalMutations( mutations, baseProps = {}, baseState = {}, childName?, ) { const eventKeys = Object.keys(baseProps); return eventKeys.reduce((memo, eventKey) => { const keyState = baseState[eventKey] || {}; const keyProps = baseProps[eventKey] || {}; if (eventKey === "parent") { const identifier = { eventKey, target: "parent" }; const mutation = getExternalMutation( mutations, keyProps, keyState, identifier, ); memo[eventKey] = mutation !== undefined ? Object.assign({}, keyState, mutation) : keyState; } else { // use keys from both state and props so that elements not intially included in baseProps // will be used. (i.e. labels) const targets = uniq(Object.keys(keyProps).concat(Object.keys(keyState))); memo[eventKey] = targets.reduce((m, target) => { const identifier = { eventKey, target, childName }; const mutation = getExternalMutation( mutations, keyProps[target], keyState[target], identifier, ); m[target] = mutation !== undefined ? Object.assign({}, keyState[target], mutation) : keyState[target]; return pickBy(m, (v) => !isEmpty(v)); }, {}); } return pickBy(memo, (v) => !isEmpty(v)); }, {}); } /** * Returns a set of mutations for a particular element given scoped baseProps and baseState * * @param {Array} mutations an array of mutations objects * @param {Object} baseProps a props object (scoped the element specified by the identifier) * @param {Object} baseState a state object (scoped the element specified by the identifier) * @param {Object} identifier { eventKey, target, childName } * * @return {Object | undefined} a object describing mutations for a given element, or undefined */ // eslint-disable-next-line max-params export function getExternalMutation( mutations, baseProps, baseState, identifier, ) { const filterMutations = (mutation, type) => { if (typeof mutation[type] === "string") { return mutation[type] === "all" || mutation[type] === identifier[type]; } else if (Array.isArray(mutation[type])) { // coerce arrays to strings before matching const stringArray = mutation[type].map((m) => `${m}`); return stringArray.includes(identifier[type]); } return false; }; let scopedMutations = Array.isArray(mutations) ? mutations : [mutations]; if (identifier.childName) { scopedMutations = mutations.filter((m) => filterMutations(m, "childName")); } // find any mutation objects that match the target const targetMutations = scopedMutations.filter((m) => filterMutations(m, "target"), ); if (isEmpty(targetMutations)) { return undefined; } const keyMutations = targetMutations.filter((m) => filterMutations(m, "eventKey"), ); if (isEmpty(keyMutations)) { return undefined; } return keyMutations.reduce((memo, curr) => { const mutationFunction = curr && isFunction(curr.mutation) ? curr.mutation : () => undefined; const currentMutation = mutationFunction( Object.assign({}, baseProps, baseState), ); return Object.assign({}, memo, currentMutation); }, {}); } /* Returns an array of defaultEvents from sub-components of a given component. * i.e. any static `defaultEvents` on `labelComponent` will be returned */ export function getComponentEvents(props, components) { const events = Array.isArray(components) && components.reduce((memo, componentName) => { const component = props[componentName]; const defaultEvents = component && component.type && component.type.defaultEvents; const componentEvents = isFunction(defaultEvents) ? defaultEvents(component.props) : defaultEvents; return Array.isArray(componentEvents) ? memo.concat(...componentEvents) : memo; }, [] as ComponentEvent[]); return events && events.length ? events : undefined; } export function getGlobalEventNameFromKey(key) { const match = key.match(GLOBAL_EVENT_REGEX); return match && match[1] && match[1].toLowerCase(); } export const getGlobalEvents = (events) => pickBy(events, (_, key) => GLOBAL_EVENT_REGEX.test(key)); export const omitGlobalEvents = (events) => omitBy(events, (_, key) => GLOBAL_EVENT_REGEX.test(key)); export const emulateReactEvent = (event) => Object.assign(event, { nativeEvent: event });