objects.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. define([
  6. 'ko',
  7. 'jquery',
  8. 'underscore'
  9. ], function (ko, $, _) {
  10. 'use strict';
  11. var primitives = [
  12. 'undefined',
  13. 'boolean',
  14. 'number',
  15. 'string'
  16. ];
  17. /**
  18. * Sets nested property of a specified object.
  19. * @private
  20. *
  21. * @param {Object} parent - Object to look inside for the properties.
  22. * @param {Array} path - Splitted path the property.
  23. * @param {*} value - Value of the last property in 'path' array.
  24. * returns {*} New value for the property.
  25. */
  26. function setNested(parent, path, value) {
  27. var last = path.pop(),
  28. len = path.length,
  29. pi = 0,
  30. part = path[pi];
  31. for (; pi < len; part = path[++pi]) {
  32. if (!_.isObject(parent[part])) {
  33. parent[part] = {};
  34. }
  35. parent = parent[part];
  36. }
  37. if (typeof parent[last] === 'function') {
  38. parent[last](value);
  39. } else {
  40. parent[last] = value;
  41. }
  42. return value;
  43. }
  44. /**
  45. * Retrieves value of a nested property.
  46. * @private
  47. *
  48. * @param {Object} parent - Object to look inside for the properties.
  49. * @param {Array} path - Splitted path the property.
  50. * @returns {*} Value of the property.
  51. */
  52. function getNested(parent, path) {
  53. var exists = true,
  54. len = path.length,
  55. pi = 0;
  56. for (; pi < len && exists; pi++) {
  57. parent = parent[path[pi]];
  58. if (typeof parent === 'undefined') {
  59. exists = false;
  60. }
  61. }
  62. if (exists) {
  63. if (ko.isObservable(parent)) {
  64. parent = parent();
  65. }
  66. return parent;
  67. }
  68. }
  69. /**
  70. * Removes property from a specified object.
  71. * @private
  72. *
  73. * @param {Object} parent - Object from which to remove property.
  74. * @param {Array} path - Splitted path to the property.
  75. */
  76. function removeNested(parent, path) {
  77. var field = path.pop();
  78. parent = getNested(parent, path);
  79. if (_.isObject(parent)) {
  80. delete parent[field];
  81. }
  82. }
  83. return {
  84. /**
  85. * Retrieves or defines objects' property by a composite path.
  86. *
  87. * @param {Object} data - Container for the properties specified in path.
  88. * @param {String} path - Objects' properties divided by dots.
  89. * @param {*} [value] - New value for the last property.
  90. * @returns {*} Returns value of the last property in chain.
  91. *
  92. * @example
  93. * utils.nested({}, 'one.two', 3);
  94. * => { one: {two: 3} }
  95. */
  96. nested: function (data, path, value) {
  97. var action = arguments.length > 2 ? setNested : getNested;
  98. path = path ? path.split('.') : [];
  99. return action(data, path, value);
  100. },
  101. /**
  102. * Removes nested property from an object.
  103. *
  104. * @param {Object} data - Data source.
  105. * @param {String} path - Path to the property e.g. 'one.two.three'
  106. */
  107. nestedRemove: function (data, path) {
  108. path = path.split('.');
  109. removeNested(data, path);
  110. },
  111. /**
  112. * Flattens objects' nested properties.
  113. *
  114. * @param {Object} data - Object to flatten.
  115. * @param {String} [separator='.'] - Objects' keys separator.
  116. * @returns {Object} Flattened object.
  117. *
  118. * @example Example with a default separator.
  119. * utils.flatten({one: { two: { three: 'value'} }});
  120. * => { 'one.two.three': 'value' };
  121. *
  122. * @example Example with a custom separator.
  123. * utils.flatten({one: { two: { three: 'value'} }}, '=>');
  124. * => {'one=>two=>three': 'value'};
  125. */
  126. flatten: function (data, separator, parent, result) {
  127. separator = separator || '.';
  128. result = result || {};
  129. _.each(data, function (node, name) {
  130. if (parent) {
  131. name = parent + separator + name;
  132. }
  133. typeof node === 'object' ?
  134. this.flatten(node, separator, name, result) :
  135. result[name] = node;
  136. }, this);
  137. return result;
  138. },
  139. /**
  140. * Opposite operation of the 'flatten' method.
  141. *
  142. * @param {Object} data - Previously flattened object.
  143. * @param {String} [separator='.'] - Keys separator.
  144. * @returns {Object} Object with nested properties.
  145. *
  146. * @example Example using custom separator.
  147. * utils.unflatten({'one=>two': 'value'}, '=>');
  148. * => {
  149. * one: { two: 'value' }
  150. * };
  151. */
  152. unflatten: function (data, separator) {
  153. var result = {};
  154. separator = separator || '.';
  155. _.each(data, function (value, nodes) {
  156. nodes = nodes.split(separator);
  157. setNested(result, nodes, value);
  158. });
  159. return result;
  160. },
  161. /**
  162. * Same operation as 'flatten' method,
  163. * but returns objects' keys wrapped in '[]'.
  164. *
  165. * @param {Object} data - Object that should be serialized.
  166. * @returns {Object} Serialized data.
  167. *
  168. * @example
  169. * utils.serialize({one: { two: { three: 'value'} }});
  170. * => { 'one[two][three]': 'value' }
  171. */
  172. serialize: function (data) {
  173. var result = {};
  174. data = this.flatten(data);
  175. _.each(data, function (value, keys) {
  176. keys = this.serializeName(keys);
  177. value = _.isUndefined(value) ? '' : value;
  178. result[keys] = value;
  179. }, this);
  180. return result;
  181. },
  182. /**
  183. * Performs deep extend of specified objects.
  184. *
  185. * @returns {Object|Array} Extended object.
  186. */
  187. extend: function () {
  188. var args = _.toArray(arguments);
  189. args.unshift(true);
  190. return $.extend.apply($, args);
  191. },
  192. /**
  193. * Performs a deep clone of a specified object.
  194. *
  195. * @param {(Object|Array)} data - Data that should be copied.
  196. * @returns {Object|Array} Cloned object.
  197. */
  198. copy: function (data) {
  199. var result = data,
  200. isArray = Array.isArray(data),
  201. placeholder;
  202. if (this.isObject(data) || isArray) {
  203. placeholder = isArray ? [] : {};
  204. result = this.extend(placeholder, data);
  205. }
  206. return result;
  207. },
  208. /**
  209. * Performs a deep clone of a specified object.
  210. * Doesn't save links to original object.
  211. *
  212. * @param {*} original - Object to clone
  213. * @returns {*}
  214. */
  215. hardCopy: function (original) {
  216. if (original === null || typeof original !== 'object') {
  217. return original;
  218. }
  219. return JSON.parse(JSON.stringify(original));
  220. },
  221. /**
  222. * Removes specified nested properties from the target object.
  223. *
  224. * @param {Object} target - Object whose properties should be removed.
  225. * @param {(...String|Array|Object)} list - List that specifies properties to be removed.
  226. * @returns {Object} Modified object.
  227. *
  228. * @example Basic usage
  229. * var obj = {a: {b: 2}, c: 'a'};
  230. *
  231. * omit(obj, 'a.b');
  232. * => {'a.b': 2};
  233. * obj => {a: {}, c: 'a'};
  234. *
  235. * @example Various syntaxes that would return same result
  236. * omit(obj, ['a.b', 'c']);
  237. * omit(obj, 'a.b', 'c');
  238. * omit(obj, {'a.b': true, 'c': true});
  239. */
  240. omit: function (target, list) {
  241. var removed = {},
  242. ignored = list;
  243. if (this.isObject(list)) {
  244. ignored = [];
  245. _.each(list, function (value, key) {
  246. if (value) {
  247. ignored.push(key);
  248. }
  249. });
  250. } else if (_.isString(list)) {
  251. ignored = _.toArray(arguments).slice(1);
  252. }
  253. _.each(ignored, function (path) {
  254. var value = this.nested(target, path);
  255. if (!_.isUndefined(value)) {
  256. removed[path] = value;
  257. this.nestedRemove(target, path);
  258. }
  259. }, this);
  260. return removed;
  261. },
  262. /**
  263. * Checks if provided value is a plain object.
  264. *
  265. * @param {*} value - Value to be checked.
  266. * @returns {Boolean}
  267. */
  268. isObject: function (value) {
  269. var objProto = Object.prototype;
  270. return typeof value == 'object' ?
  271. objProto.toString.call(value) === '[object Object]' :
  272. false;
  273. },
  274. /**
  275. *
  276. * @param {*} value
  277. * @returns {Boolean}
  278. */
  279. isPrimitive: function (value) {
  280. return value === null || ~primitives.indexOf(typeof value);
  281. },
  282. /**
  283. * Iterates over obj props/array elems recursively, applying action to each one
  284. *
  285. * @param {Object|Array} data - Data to be iterated.
  286. * @param {Function} action - Callback to be called with each item as an argument.
  287. * @param {Number} [maxDepth=7] - Max recursion depth.
  288. */
  289. forEachRecursive: function (data, action, maxDepth) {
  290. maxDepth = typeof maxDepth === 'number' && !isNaN(maxDepth) ? maxDepth - 1 : 7;
  291. if (!_.isFunction(action) || _.isFunction(data) || maxDepth < 0) {
  292. return;
  293. }
  294. if (!_.isObject(data)) {
  295. action(data);
  296. return;
  297. }
  298. _.each(data, function (value) {
  299. this.forEachRecursive(value, action, maxDepth);
  300. }, this);
  301. action(data);
  302. },
  303. /**
  304. * Maps obj props/array elems recursively
  305. *
  306. * @param {Object|Array} data - Data to be iterated.
  307. * @param {Function} action - Callback to transform each item.
  308. * @param {Number} [maxDepth=7] - Max recursion depth.
  309. *
  310. * @returns {Object|Array}
  311. */
  312. mapRecursive: function (data, action, maxDepth) {
  313. var newData;
  314. maxDepth = typeof maxDepth === 'number' && !isNaN(maxDepth) ? maxDepth - 1 : 7;
  315. if (!_.isFunction(action) || _.isFunction(data) || maxDepth < 0) {
  316. return data;
  317. }
  318. if (!_.isObject(data)) {
  319. return action(data);
  320. }
  321. if (_.isArray(data)) {
  322. newData = _.map(data, function (item) {
  323. return this.mapRecursive(item, action, maxDepth);
  324. }, this);
  325. return action(newData);
  326. }
  327. newData = _.mapObject(data, function (val, key) {
  328. if (data.hasOwnProperty(key)) {
  329. return this.mapRecursive(val, action, maxDepth);
  330. }
  331. return val;
  332. }, this);
  333. return action(newData);
  334. },
  335. /**
  336. * Removes empty(in common sence) obj props/array elems
  337. *
  338. * @param {*} data - Data to be cleaned.
  339. * @returns {*}
  340. */
  341. removeEmptyValues: function (data) {
  342. if (!_.isObject(data)) {
  343. return data;
  344. }
  345. if (_.isArray(data)) {
  346. return data.filter(function (item) {
  347. return !this.isEmptyObj(item);
  348. }, this);
  349. }
  350. return _.omit(data, this.isEmptyObj.bind(this));
  351. },
  352. /**
  353. * Checks that argument of any type is empty in common sence:
  354. * empty string, string with spaces only, object without own props, empty array, null or undefined
  355. *
  356. * @param {*} val - Value to be checked.
  357. * @returns {Boolean}
  358. */
  359. isEmptyObj: function (val) {
  360. return _.isObject(val) && _.isEmpty(val) ||
  361. this.isEmpty(val) ||
  362. val && val.trim && this.isEmpty(val.trim());
  363. }
  364. };
  365. });