/**
* @module mixing
*/
/*jshint latedef:nofunc*/
var defaultSettings;
if (! Array.isArray) {
Array.isArray = function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
}
function prepareFieldList(fieldList, value) {
var map, nI, regexp, sType;
if (Array.isArray(fieldList)) {
if (fieldList.length > 0) {
map = {};
nI = fieldList.length;
do {
map[ fieldList[--nI] ] = value || null;
} while(nI);
}
}
else {
sType = typeof fieldList;
if (sType === "string" || sType === "symbol") {
map = {};
map[fieldList] = value || null;
}
else if (sType === "object") {
if (fieldList instanceof RegExp) {
regexp = fieldList;
}
else {
map = fieldList;
}
}
}
return {map: map, regexp: regexp};
}
function copy(destination, source, propName, settings) {
/*jshint laxbreak:true*/
var propValue = source[propName],
sPropString = propName.toString(),
bFuncProp, change, otherNameMap, sType, value;
function getParam() {
return {
field: propName,
value: propValue,
targetValue: destination[propName],
target: destination,
source: source
};
}
if ((! settings.ownProperty || source.hasOwnProperty(propName))
&& (! settings.copyMap || (propName in settings.copyMap))
&& (! settings.copyRegExp || settings.copyRegExp.test(sPropString))
&& (! settings.exceptions || ! settings.exceptions[propName])
&& (! settings.exceptRegExp || ! settings.exceptRegExp.test(sPropString))
&& (! settings.filter || settings.filter.call(null, getParam()))
/* jshint -W122 */
&& (! settings.filterRegExp || settings.filterRegExp.test(typeof propValue === "symbol" ? propValue.toString() : propValue))) {
/* jshint +W122 */
otherNameMap = settings.otherNameMap;
if (otherNameMap && (propName in otherNameMap)) {
propName = otherNameMap[propName];
}
sType = typeof propValue;
// If recursive mode and field's value is an object
if (settings.recursive && propValue && sType === "object" && (value = destination[propName]) && typeof value === "object"
&& (! Array.isArray(propValue) || settings.mixFromArray) && (! Array.isArray(value) || settings.mixToArray)) {
mixing(value, propValue,
settings.mixFromArray
? mixing({oneSource: true}, settings)
: settings);
}
else {
bFuncProp = (sType === "function");
if ((! bFuncProp || settings.copyFunc)
&& (! (propName in destination)
|| ((value = settings.overwrite)
&& ( typeof value !== "function" || value(getParam()) )
&& ( ! (value instanceof RegExp) || value.test(propName) ) ))) {
if (settings.changeFunc) {
propValue = settings.changeFunc.call(null, getParam());
}
else if ((change = settings.change) && (propName in change)) {
propValue = change[propName];
}
if (bFuncProp && settings.funcToProto) {
destination.constructor.prototype[propName] = propValue;
}
else {
destination[propName] = propValue;
}
}
}
}
}
/**
* Copy/add all fields and functions from source objects into the target object.
* As a result the target object may be modified.
*
* @param {Object | Function} destination
* The target object into which fields and functions will be copied.
* @param {Array | Object} source
* Array of source objects or just one object whose contents will be copied.
* If a source is a falsy value (e.g. `null` or `undefined`), the source will be skipped.
* @param {Object} [settings]
* Operation settings. Fields are names of settings, their values are the corresponding values of settings.
* The following settings are being supported.
* <table>
* <tr>
* <th>Name</th><th>Type</th><th>Default value</th><th>Description</th>
* </tr>
* <tr>
* <td>`copyFunc`</td>
* <td>`Boolean`</td>
* <td>`true`</td>
* <td>Should functions be copied?</td>
* </tr>
* <tr>
* <td>`funcToProto`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>
* Should functions be copied into `prototype` of the target object's `constructor`
* (i.e. into `destination.constructor.prototype`)?
* <br>
* If `false` then functions will be copied directly into the target object.
* </td>
* </tr>
* <tr>
* <td>`processSymbol`</td>
* <td>`Boolean`</td>
* <td>`true`</td>
* <td>Should symbol property keys (i.e. fields whose name is a symbol) be processed?</td>
* </tr>
* <tr>
* <td>`overwrite`</td>
* <td>`Boolean | Function | RegExp`</td>
* <td>`false`</td>
* <td>
* Specifies whether a field/function should be overwritten when it exists in the target object.
* <br>
* If `true` then any existent field will be overwritten in the target object.
* <br>
* Function or regular expression can be used to select fields that should be overwritten.
* <br>
* If a regular expression is passed, only those fields will be overwritten
* whose names are matching the regular expression.
* <br>
* If specified function returns `true` for a field,
* the field will be overwritten in the target object.
* An object with contextual data is passed into the function (see details below).
* </td>
* </tr>
* <tr>
* <td>`recursive`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>
* Should this function be called recursively when field's value of the target and source object is an object?
* <br>
* If `true` then object fields from the target and source objects will be mixed by using this function
* with the same settings.
* <br>
* This option has no dependency with `overwrite` setting and has priority over it.
* </td>
* </tr>
* <tr>
* <td>`mixFromArray`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>
* Should contents of a field of the source object be copied when the field's value is an array?
* <br>
* Will be used only when `recursive` setting has `true` value.
* </td>
* </tr>
* <tr>
* <td>`mixToArray`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>
* Should contents of a field of the source object be copied into a field of the target object
* when the latest field's value is an array?
* <br>
* Will be used only when `recursive` setting has `true` value.
* </td>
* </tr>
* <tr>
* <td>`mixArray`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>
* Sets default value for `mixFromArray` and `mixToArray` settings.
* </td>
* </tr>
* <tr>
* <td>`oneSource`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>
* Indicates that array that is passed as `source` parameter should be interpreted
* directly as copied object instead of list of source objects.
* </td>
* </tr>
* <tr>
* <td>`ownProperty`</td>
* <td>`Boolean`</td>
* <td>`false`</td>
* <td>Should only own properties of the source object be copied in the target object?</td>
* </tr>
* <tr>
* <td>`copy`</td>
* <td>`Array | Object | RegExp | String | Symbol`</td>
* <td>`""` (empty string)</td>
* <td>
* Array, object, regular expression or string/symbol that defines names of fields/functions that should be copied.
* <br>
* If an object is passed then his fields determine copied elements.
* If a regular expression is passed, then field names matching the regular expression will be copied.
* If a string/symbol is passed then it is name of the only copied field.
* </td>
* </tr>
* <tr>
* <td>`except`</td>
* <td>`Array | Object | RegExp | String | Symbol`</td>
* <td>`""` (empty string)</td>
* <td>
* Array, object, regular expression or string/symbol that defines names of fields/functions that shouldn't be copied.
* <br>
* If an object is passed then his fields with true values determine non-copied elements.
* If a regular expression is passed, then field names matching the regular expression will not be copied.
* If a string/symbol is passed then it is name of the only non-copied field.
* </td>
* </tr>
* <tr>
* <td>`filter`</td>
* <td>`Function | RegExp`</td>
* <td>`null`</td>
* <td>
* Function or regular expression that can be used to select elements that should be copied.
* <br>
* If regular expression is passed, only those fields will be copied whose values are matching regular expression.
* <br>
* If specified function returns `true` for a field,
* the field will be copied in the target object.
* <br>
* An object with contextual data is passed into function (see details below).
* </td>
* </tr>
* <tr>
* <td>`otherName`</td>
* <td>`Object`</td>
* <td>`null`</td>
* <td>
* Defines "renaming table" for copied elements.
* <br>
* Fields of the table are names from a source object, values are the corresponding names in the target object.
* <br>
* For example, the call
* <br>
* <code>
* mixing({}, {field: 1, func: "no-func"}, {otherName: {"field": "prop", "func": "method"}})
* </code>
* <br>
* will return the following object
* <br>
* `{prop: 1, method: "no-func"}`
* </td>
* </tr>
* <tr>
* <td>`change`</td>
* <td>`Function | Object`</td>
* <td>`null`</td>
* <td>
* Function or object that gives ability to change values that should be copied.
* <br>
* If an object is passed then his fields determine new values for copied elements.
* <br>
* If a function is passed then value returned by the function for a field will be copied into the target object
* instead of original field's value.
* <br>
* An object with contextual data is passed into function (see details below).
* </td>
* </tr>
* </table>
* An object having the following fields is passed into `overwrite`, `filter` and `change` function:
* <ul>
* <li>`field` - field name
* <li>`value` - field value from the source object
* <li>`targetValue` - field value from the target object
* <li>`target` - reference to the target object
* <li>`source` - reference to the source object
* </ul>
* Default values of settings can be redefined by {@link module:mixing.setSettings setSettings} method.
* <br>
* `copy`, `except` and `filter` settings can be used together.
* In such situation a field will be copied only when the field satisfies to all settings
* (i.e. belongs to copied elements, not in exceptions and conforms to filter).
* @return {Object}
* Modified target object.
* @alias module:mixing
*/
function mixing(destination, source, settings) {
/*jshint boss:true, laxbreak:true*/
var destinationType = typeof destination;
var sourceType = typeof source;
if (destination && (destinationType === "object" || destinationType === "function")
&& source && (sourceType === "object" || sourceType === "function")) {
var obj;
// Prepare parameters
if (typeof settings !== "object" || settings === null) {
settings = defaultSettings || {};
}
else if (defaultSettings) {
obj = defaultSettings;
defaultSettings = null;
settings = mixing({}, [settings, obj]);
defaultSettings = obj;
}
if (! Array.isArray(source) || settings.oneSource) {
source = [source];
}
// Prepare settings
var getOwnPropertySymbols = Object.getOwnPropertySymbols,
getPrototypeOf = Object.getPrototypeOf,
options = {
copyFunc: ("copyFunc" in settings ? settings.copyFunc : true),
funcToProto: Boolean(settings.funcToProto),
processSymbol: ("processSymbol" in settings ? settings.processSymbol : true)
&& typeof getOwnPropertySymbols === "function",
mixFromArray: Boolean( ("mixFromArray" in settings) ? settings.mixFromArray : settings.mixArray ),
mixToArray: Boolean( ("mixToArray" in settings) ? settings.mixToArray : settings.mixArray ),
overwrite: settings.overwrite,
ownProperty: Boolean(settings.ownProperty),
recursive: Boolean(settings.recursive),
otherNameMap: ("otherName" in settings ? settings.otherName : null),
changeFunc: settings.changeFunc,
copyMap: settings.copyMap,
copyRegExp: settings.copyRegExp,
exceptions: settings.exceptions,
exceptRegExp: settings.exceptRegExp,
filterRegExp: settings.filterRegExp
},
bOwnProperty = options.ownProperty,
bProcessSymbol = options.processSymbol,
change = settings.change,
copyList = settings.copy,
exceptList = settings.except,
filter = settings.filter,
nI, nK, nL, nQ, propName;
if (copyList) {
copyList = prepareFieldList(copyList);
options.copyMap = copyList.map;
options.copyRegExp = copyList.regexp;
}
if (exceptList) {
exceptList = prepareFieldList(exceptList, true);
options.exceptions = exceptList.map;
options.exceptRegExp = exceptList.regexp;
}
if (filter) {
options[typeof filter === "object" ? "filterRegExp" : "filter"] = filter;
}
if (change) {
options[typeof change === "function" ? "changeFunc" : "change"] = change;
}
// Copy fields and functions according to settings
for (nI = 0, nL = source.length; nI < nL; nI++) {
if (obj = source[nI]) {
for (propName in obj) {
copy(destination, obj, propName, options);
}
// Process symbol property keys
if (bProcessSymbol) {
exceptList = {};
do {
copyList = getOwnPropertySymbols(obj);
for (nK = 0, nQ = copyList.length; nK < nQ; nK++) {
propName = copyList[nK];
if (! (propName in exceptList)) {
copy(destination, obj, propName, options);
exceptList[propName] = true;
}
}
obj = bOwnProperty
? null
: getPrototypeOf(obj);
} while (obj);
}
}
}
}
return destination;
}
/**
* Copy values of all of the own properties from one or more source objects to the target object
* (similar to `Object.assign`).
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* mixing(destination, Array.prototype.slice.call(arguments, 1), {overwrite: true, ownProperty: true});
* </pre></code>
*
* @param {Object | Function} destination
* The target object into which fields and functions will be copied.
* @param {...Object} source
* An object whose contents will be copied.
* If a source is a falsy value (e.g. `null` or `undefined`), the source will be skipped.
* @return {Object}
* Modified `target` object.
*/
mixing.assign = function(destination) {
return mixing(destination, Array.prototype.slice.call(arguments, 1), {overwrite: true, ownProperty: true});
};
/**
* Change values of fields of given object.
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* mixing(source, source, {change: change, overwrite: true, oneSource: true});
* </pre></code>
*
* @param {Array | Object} source
* An array or an object whose fields should be modified.
* @param {Function | Object} change
* A function or an object that specifies the modification. See {@link module:mixing mixing} for details.
* @return {Object}
* Modified `source` object.
*/
mixing.change = function(source, change) {
return mixing(source, source, {change: change, overwrite: true, oneSource: true});
};
/**
* Make a copy of source object(s).
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* var copy = mixing({}, source, settings);
* </pre></code>
*
* @param {Array | Object} source
* Array of source objects or just one object whose contents will be copied.
* @param {Object} [settings]
* Operation settings. See {@link module:mixing mixing} for details.
* @return {Object}
* Newly created object containing contents of source objects.
*/
mixing.copy = function(source, settings) {
return mixing({}, source, settings);
};
/**
* Copy fields from source object(s) into every object item of given array.
*
* @param {Array} destinationList
* An array whose items should be modified.
* @param {Array | Object} source
* Array of source objects or just one object whose contents will be copied.
* @param {Object} [settings]
* Operation settings that will be applied to every copying. See {@link module:mixing mixing} for details.
* @return {Array}
* Original `destinationList` with possibly modified object items.
*/
mixing.mixToItems = function(destinationList, source, settings) {
for (var nI = 0, nL = destinationList.length; nI < nL; nI++) {
destinationList[nI] = mixing(destinationList[nI], source, settings);
}
return destinationList;
};
/**
* Make a copy of `this` object.
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* var copy = mixing({}, this, settings);
* </pre></code>
* It can be transferred to an object to use as a method.
*
* @param {Object} [settings]
* Operation settings. See {@link module:mixing mixing} for details.
* @return {Object}
* Newly created object containing contents of `this` object.
*/
mixing.clone = function(settings) {
return mixing({}, this, settings);
};
/**
* Filter `this` object.
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* var result = mixing({}, this, {filter: filter});
* </pre></code>
* It can be transferred to an object to use as a method.
*
* @param {Function | Object} filter
* Filter function to select fields or object that represents operation settings including filter function.
* See {@link module:mixing mixing} for details.
* @return {Object}
* Newly created object containing fields of `this` object for which filter function returns true.
*/
mixing.filter = function(filter) {
return mixing({}, this, typeof filter === "function" ? {filter: filter} : filter);
};
/**
* Copy and change values of fields of `this` object.
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* var result = mixing({}, this, {change: change});
* </pre></code>
* It can be transferred to an object to use as a method.
*
* @param {Function | Object} change
* Function to change values of copied fields or object that represents operation settings including change function.
* See {@link module:mixing mixing} for details.
* @return {Object}
* Newly created object containing fields of `this` object with changed values.
*/
mixing.map = function(change) {
return mixing({}, this, typeof change === "function" ? {change: change} : change);
};
/**
* Copy/add all fields and functions from source objects into `this` object.
* As a result `this` object may be modified.
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* mixing(this, source, settings);
* </pre></code>
* It can be transferred to an object to use as a method.
*
* @param {Array | Object} source
* Array of source objects or just one object whose contents will be copied.
* @param {Object} [settings]
* Operation settings. See {@link module:mixing mixing} for details.
* @return {Object}
* Modified `this` object.
*/
mixing.mix = function(source, settings) {
return mixing(this, source, settings);
};
/**
* Change values of fields of `this` object.
* <br>
* This function is a "wrap" for the following code:
* <code><pre>
* mixing.change(this, change);
* </pre></code>
* It can be transferred to an object to use as a method.
*
* @param {Function | Object} change
* A function or an object that specifies the modification. See {@link module:mixing mixing} for details.
* @return {Object}
* Modified `this` object.
*/
mixing.update = function(change) {
return mixing.change(this, change);
};
/**
* Return default settings that were set earlier.
*
* @return {Object | undefined}
* Default settings that were set earlier or `undefined / null` if default settings were not set.
*/
mixing.getSettings = function() {
return defaultSettings;
};
/**
* Set (redefine, reset) default settings that should be used for subsequent {@link module:mixing mixing} calls.
*
* @param {Object | undefined} [settings]
* Default settings that should be used for subsequent {@link module:mixing mixing} calls.
* Initial default values will be used for settings that are not specified in the passed object.
* Pass `undefined`, `null`, non-object or to call without parameter
* to reset default settings to initial values.
* @alias module:mixing.setSettings
*/
mixing.setSettings = function(settings) {
defaultSettings = typeof settings === "object" ? settings : null;
};
module.exports = mixing;