UNPKG

@gravityforms/utils

Version:
208 lines (187 loc) 6.72 kB
import defaultIsMergeableObject from './is-mergeable-object'; import isFunction from './is-function'; /** * @module deepMerge * @description Merge two or more objects deeply. * */ function emptyTarget( val ) { // eslint-disable-line jsdoc/require-jsdoc return Array.isArray( val ) ? [] : {}; } function cloneUnlessOtherwiseSpecified( value, options ) { // eslint-disable-line jsdoc/require-jsdoc return ( options.clone !== false && options.isMergeableObject( value ) ) ? deepMerge( emptyTarget( value ), value, options ) : value; } function defaultArrayMerge( target, source, options ) { // eslint-disable-line jsdoc/require-jsdoc return target.concat( source ).map( function( element ) { return cloneUnlessOtherwiseSpecified( element, options ); } ); } function combineArrayMerge( target, source, options ) { // eslint-disable-line jsdoc/require-jsdoc const destination = target.slice(); source.forEach( ( item, index ) => { if ( typeof destination[ index ] === 'undefined' ) { destination[ index ] = options.cloneUnlessOtherwiseSpecified( item, options ); } else if ( options.isMergeableObject( item ) ) { destination[ index ] = deepMerge( target[ index ], item, options ); } else if ( target.indexOf( item ) === -1 ) { destination.push( item ); } } ); return destination; } function getArrayMergeType( options ) { // eslint-disable-line jsdoc/require-jsdoc let arrayMergeType = defaultArrayMerge; if ( options.arrayMerge === 'combine' ) { arrayMergeType = combineArrayMerge; } else if ( isFunction( options.arrayMerge ) ) { arrayMergeType = options.arrayMerge; } return arrayMergeType; } function getMergeFunction( key, options ) { // eslint-disable-line jsdoc/require-jsdoc if ( ! options.customMerge ) { return deepMerge; } const customMerge = options.customMerge( key ); return typeof customMerge === 'function' ? customMerge : deepMerge; } function getEnumerableOwnPropertySymbols( target ) { // eslint-disable-line jsdoc/require-jsdoc return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols( target ).filter( function( symbol ) { return target.propertyIsEnumerable( symbol ); // eslint-disable-line } ) : []; } function getKeys( target ) { // eslint-disable-line jsdoc/require-jsdoc return Object.keys( target ).concat( getEnumerableOwnPropertySymbols( target ) ); } function propertyIsOnObject( object, property ) { // eslint-disable-line jsdoc/require-jsdoc try { return property in object; } catch ( _ ) { return false; } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe( target, key ) { // eslint-disable-line jsdoc/require-jsdoc return propertyIsOnObject( target, key ) && // Properties are safe to merge if they don't exist in the target yet, ! ( Object.hasOwnProperty.call( target, key ) && // unsafe if they exist up the prototype chain, Object.propertyIsEnumerable.call( target, key ) ); // and also unsafe if they're nonenumerable. } function mergeObject( target, source, options ) { // eslint-disable-line jsdoc/require-jsdoc const destination = {}; if ( options.isMergeableObject( target ) ) { getKeys( target ).forEach( function( key ) { destination[ key ] = cloneUnlessOtherwiseSpecified( target[ key ], options ); } ); } getKeys( source ).forEach( function( key ) { if ( propertyIsUnsafe( target, key ) ) { return; } if ( propertyIsOnObject( target, key ) && options.isMergeableObject( source[ key ] ) ) { destination[ key ] = getMergeFunction( key, options )( target[ key ], source[ key ], options ); } else { destination[ key ] = cloneUnlessOtherwiseSpecified( source[ key ], options ); } } ); return destination; } /** * @description Merge two objects x and y deeply, returning a new merged object with the elements from both x and y. * If an element at the same key is present for both x and y, the value from y will appear in the result. * Merging creates a new object, so that neither x or y is modified. * Note: By default, arrays are merged by concatenating them. * * Taken and adapted from https://www.npmjs.com/package/deepmerge * * @since 1.0.0 * * @param {Array|object} target The original array or object to merge to. * @param {Array|object} source The new array or object that will get priority when keys match. * @param {object} options Options object. * * @requires isFunction * @requires defaultIsMergeableObject * * @return {Array|object} Returns a new array or object, after merging target and source. * * @example * import { deepMerge } from "@gravityforms/utils"; * * function Example() { * const target = { * foo: { bar: 3 }, * array: [ 1, 2, 3 ] * }; * * const source = { * foo: { baz: 4, bar: 5 }, * quux: 5, * array: [ 4, 5, 6 ], * }; * * const output = deepMerge( target, source ); * } * */ function deepMerge( target, source, options = {} ) { options.arrayMerge = getArrayMergeType( options ); options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject; // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; const sourceIsArray = Array.isArray( source ); const targetIsArray = Array.isArray( target ); const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; if ( ! sourceAndTargetTypesMatch ) { return cloneUnlessOtherwiseSpecified( source, options ); } else if ( sourceIsArray ) { return options.arrayMerge( target, source, options ); } return mergeObject( target, source, options ); } /** * @function all * @description Merges an array of arrays or objects using `deepMerge` into one array or object. * * @param {Array} array The array of arrays or objects to deep merge. * @param {object} options Options object. * * @return {Array|object} Returns a new array or object, after merging all items in the array. * * @example * import { deepMerge } from "@gravityforms/utils"; * * function Example() { * const array = [ * { * foo: { bar: 3 }, * array: [ 1, 2, 3 ], * }, * { * foo: { baz: 4, bar: 5 }, * quux: 5, * array: [ 4, 5, 6 ], * }, * { * foo: { baz: 1 }, * array: [ 7, 8, 9 ], * }, * ]; * * const output = deepMerge.all( array ); * } * */ deepMerge.all = function deepMergeAll( array, options ) { if ( ! Array.isArray( array ) ) { throw new Error( 'first argument should be an array' ); } return array.reduce( function( prev, next ) { return deepMerge( prev, next, options ); }, {} ); }; export default deepMerge;