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;