API Docs for: 3.11.0-git
Show:

File: src/sm-map/js/map.js

  1. /*jshint es3: true, globalstrict: true, indent: 4 */
  2.  
  3. /**
  4. Provides the `Y.Map` data structure.
  5.  
  6. @module gallery-sm-map
  7. @main gallery-sm-map
  8. **/
  9.  
  10. /**
  11. An ordered hash map data structure with an interface and behavior similar to
  12. (but not exactly the same as) [ECMAScript 6 Maps][es6-maps].
  13.  
  14. [es6-maps]:http://people.mozilla.org/~jorendorff/es6-draft.html#sec-15.14
  15.  
  16. @class Map
  17. @constructor
  18. @param {Array[]|Map} [entries] Array or Map of entries to add to this map. If an
  19. array, then each entry should itself be an array in which the first item is
  20. the key and the second item is the value for that entry.
  21.  
  22. @param {Object} [options] Options.
  23.  
  24. @param {Boolean} [options.autoStamp=false] If `true`, objects used as keys
  25. will be automatically stamped with a unique id as the value of the
  26. property defined by the `objectIdName` option ("_yuid" by default) if
  27. that property isn't already set. This will result in much faster lookups
  28. for object keys.
  29.  
  30. @param {String} [options.objectIdName="_yuid"] Name of a property whose
  31. string value should be used as the unique key when present on an object
  32. that's given as a key. This will significantly speed up lookups of
  33. object-based keys that define this property.
  34. **/
  35.  
  36. "use strict";
  37.  
  38. var emptyObject = {},
  39. isNative = Y.Lang._isNative,
  40. nativeObjectCreate = isNative(Object.create),
  41. protoSlice = Array.prototype.slice,
  42. sizeIsGetter = isNative(Object.defineProperty) && Y.UA.ie !== 8;
  43.  
  44. function YMap(entries, options) {
  45. // Allow options as only param.
  46. if (arguments.length === 1 && !('length' in entries)
  47. && typeof entries.entries !== 'function') {
  48.  
  49. options = entries;
  50. entries = null;
  51. }
  52.  
  53. if (options) {
  54. this._mapOptions = Y.merge(YMap.prototype._mapOptions, options);
  55. }
  56.  
  57. this.clear();
  58.  
  59. if (entries) {
  60. if (!Y.Lang.isArray(entries)) {
  61. if (typeof entries.entries === 'function') {
  62. // It quacks like a map!
  63. entries = entries.entries();
  64. } else {
  65. // Assume it's an array-like object.
  66. entries = protoSlice.call(entries);
  67. }
  68. }
  69.  
  70. var entry;
  71.  
  72. for (var i = 0, len = entries.length; i < len; ++i) {
  73. entry = entries[i];
  74.  
  75. if (entry.length !== 2) {
  76. throw new TypeError('Invalid map entry: ' + entry.toString());
  77. }
  78.  
  79. this.set(entry[0], entry[1]);
  80. }
  81. }
  82. }
  83.  
  84. // In modern browsers, the `size` property is a non-enumerable getter on the
  85. // prototype, as specified in ES6. In older browsers (mainly IE<9), we just
  86. // manually update a plain old instance property.
  87. if (sizeIsGetter) {
  88. Object.defineProperty(YMap.prototype, 'size', {
  89. configurable: true,
  90.  
  91. get: function () {
  92. return this._mapKeys.length;
  93. }
  94. });
  95. }
  96.  
  97. Y.mix(YMap.prototype, {
  98. // -- Public Properties ----------------------------------------------------
  99.  
  100. /**
  101. The number of entries in this map.
  102.  
  103. @property {Number} size
  104. @default 0
  105. @readOnly
  106. **/
  107.  
  108. // -- Protected Properties -------------------------------------------------
  109.  
  110. /**
  111. Whether or not the internal key index is in need of reindexing.
  112.  
  113. Rather than reindexing immediately whenever it becomes necessary, we use
  114. this flag to allow on-demand indexing the first time an up-to-date index is
  115. actually needed.
  116.  
  117. This makes multiple `remove()` operations significantly faster, at the
  118. expense of a single reindex operation the next time a key is looked up.
  119.  
  120. @property {Boolean} _isIndexStale
  121. @protected
  122. **/
  123.  
  124. /**
  125. Internal array of the keys in this map.
  126.  
  127. @property {Array} _mapKeys
  128. @protected
  129. **/
  130.  
  131. /**
  132. Internal index mapping string keys to their indices in the `_mapKeys` array.
  133.  
  134. @property {Object} _mapKeyIndices
  135. @protected
  136. **/
  137.  
  138. /**
  139. Internal index mapping object key ids to their indices in the `_mapKeys`
  140. array. This is separate from `_mapKeyIndices` in order to prevent collisions
  141. between object key ids and string keys.
  142.  
  143. @property {Object} _mapObjectIndices
  144. @protected
  145. **/
  146.  
  147. /**
  148. Options that affect the functionality of this map.
  149.  
  150. @property {Object} _mapOptions
  151. @protected
  152. **/
  153. _mapOptions: {
  154. objectIdName: '_yuid'
  155. },
  156.  
  157. /**
  158. Internal array of the values in this map.
  159.  
  160. @property {Array} _mapValues
  161. @protected
  162. **/
  163.  
  164. // -- Public Methods -------------------------------------------------------
  165.  
  166. /**
  167. Deletes all entries from this map.
  168.  
  169. @method clear
  170. @chainable
  171. **/
  172. clear: function () {
  173. this._mapKeys = [];
  174. this._mapValues = [];
  175.  
  176. this._reindexMap();
  177. this._updateMapSize();
  178.  
  179. return this;
  180. },
  181.  
  182. /**
  183. Returns a new map comprised of this map's entries combined with those of the
  184. maps or arrays passed as arguments. Does not alter this map or those given
  185. as arguments in any way.
  186.  
  187. Entries in later (rightmost) arguments will take precedence over entries in
  188. earlier (leftmost) arguments if their keys are the same.
  189.  
  190. This method also accepts arrays of entries in lieu of actual Y.Map
  191. instances.
  192.  
  193. The returned map will be created using the same options and constructor as
  194. this map.
  195.  
  196. @method concat
  197. @param {Array[]|Map} [maps*] Zero or more maps or entry arrays to
  198. concatenate into the resulting map.
  199. @return {Map} New map containing the concatenated values of this map and all
  200. arguments.
  201. **/
  202. concat: function () {
  203. var map = new this.constructor(this, this._mapOptions);
  204. return arguments.length ? map.merge.apply(map, arguments) : map;
  205. },
  206.  
  207. /**
  208. Executes the given _callback_ function on each entry in this map.
  209.  
  210. To halt iteration early, return `false` from the callback.
  211.  
  212. @method each
  213. @param {Function} callback Callback function.
  214. @param {Mixed} callback.value Value being iterated.
  215. @param {Mixed} callback.key Key being iterated.
  216. @param {Map} callback.map Reference to this map.
  217. @param {Object} [thisObj] `this` object to use when calling _callback_.
  218. @chainable
  219. @see forEach
  220. **/
  221. each: function (callback, thisObj) {
  222. var entries = this.entries(),
  223. entry;
  224.  
  225. for (var i = 0, len = entries.length; i < len; ++i) {
  226. entry = entries[i];
  227.  
  228. if (callback.call(thisObj, entry[1], entry[0], this) === false) {
  229. break;
  230. }
  231. }
  232.  
  233. return this;
  234. },
  235.  
  236. /**
  237. Returns an array of all the entries in this map. Each entry is an array with
  238. two items, the first being a key and the second a value associated with that
  239. key.
  240.  
  241. @method entries
  242. @return {Array} Array of entries.
  243. **/
  244. entries: function () {
  245. var entries = [],
  246. mapKeys = this._mapKeys,
  247. mapValues = this._mapValues;
  248.  
  249. for (var i = 0, len = mapKeys.length; i < len; ++i) {
  250. entries.push([mapKeys[i], mapValues[i]]);
  251. }
  252.  
  253. return entries;
  254. },
  255.  
  256. /**
  257. Returns the value associated with the given _key_, or _default_ if the key
  258. isn't found.
  259.  
  260. @method get
  261. @param {Mixed} key Key to look up.
  262. @param {Mixed} [defaultValue] Default value to return if _key_ isn't found.
  263. @return {Mixed} Value associated with the given _key_, or _default_ if the
  264. key isn't found.
  265. **/
  266. get: function (key, defaultValue) {
  267. var i = this._indexOfKey(key);
  268. return i < 0 ? defaultValue : this._mapValues[i];
  269. },
  270.  
  271. /**
  272. Returns `true` if _key_ exists in this map, `false` otherwise.
  273.  
  274. @method has
  275. @param {Mixed} key Key to look up.
  276. @return {Boolean} `true` if _key_ exists in this map, `false` otherwise.
  277. **/
  278. has: function (key) {
  279. return this._indexOfKey(key) >= 0;
  280. },
  281.  
  282. /**
  283. Returns an array of all the keys in this map.
  284.  
  285. @method keys
  286. @return {Array} Array of keys.
  287. **/
  288. keys: function () {
  289. return protoSlice.call(this._mapKeys);
  290. },
  291.  
  292. /**
  293. Merges the entries from one or more other maps or entry arrays into this
  294. map. Entries in later (rightmost) arguments will take precedence over
  295. entries in earlier (leftmost) arguments if their keys are the same.
  296.  
  297. This method also accepts arrays of entries in lieu of actual Y.Map
  298. instances, so the following operations have the same result:
  299.  
  300. // This...
  301. map.merge(new Y.Map([['a', 'apple'], ['b', 'bear']]));
  302.  
  303. // ...has the same result as this...
  304. map.merge([['a', 'apple'], ['b', 'bear']]);
  305.  
  306. @method merge
  307. @param {Array[]|Map} maps* One or more maps or entry arrays to merge into
  308. this map.
  309. @chainable
  310. **/
  311. merge: function () {
  312. var maps = protoSlice.call(arguments),
  313.  
  314. entries,
  315. entry,
  316. i,
  317. len,
  318. map;
  319.  
  320. while ((map = maps.shift())) {
  321. entries = typeof map.entries === 'function' ? map.entries() : map;
  322.  
  323. for (i = 0, len = entries.length; i < len; ++i) {
  324. entry = entries[i];
  325.  
  326. if (entry.length !== 2) {
  327. throw new TypeError('Invalid map entry: ' + entry.toString());
  328. }
  329.  
  330. this.set(entry[0], entry[1]);
  331. }
  332. }
  333.  
  334. return this;
  335. },
  336.  
  337. /**
  338. Deletes the entry with the given _key_.
  339.  
  340. @method remove
  341. @param {Mixed} key Key to delete.
  342. @return {Boolean} `true` if the key existed and was deleted, `false`
  343. otherwise.
  344. **/
  345. remove: function (key) {
  346. var i = this._indexOfKey(key);
  347.  
  348. if (i < 0) {
  349. return false;
  350. }
  351.  
  352. this._removeMapEntry(i);
  353. this._updateMapSize();
  354.  
  355. return true;
  356. },
  357.  
  358. /**
  359. Sets the value of the entry with the given _key_. If the key already exists,
  360. its value will be overwritten; otherwise it will be created.
  361.  
  362. The _key_ may be any JavaScript value (including both primitives and
  363. objects), but string keys will allow fast lookups, whereas non-string keys
  364. may result in slower lookups.
  365.  
  366. @method set
  367. @param {Mixed} key Key to set.
  368. @param {Mixed} value Value to set.
  369. @chainable
  370. **/
  371. set: function (key, value) {
  372. var i = this._indexOfKey(key);
  373.  
  374. if (i < 0) {
  375. i = this._mapKeys.length;
  376. }
  377.  
  378. this._mapKeys[i] = key;
  379. this._mapValues[i] = value;
  380.  
  381. this._indexMapKey(i, key);
  382. this._updateMapSize();
  383.  
  384. return this;
  385. },
  386.  
  387. /**
  388. Returns an array of all the values in this map.
  389.  
  390. @method values
  391. @return {Array} Array of values.
  392. **/
  393. values: function () {
  394. return protoSlice.call(this._mapValues);
  395. },
  396.  
  397. // -- Protected Methods ----------------------------------------------------
  398.  
  399. /**
  400. Indexes the given _key_ to enable faster lookups.
  401.  
  402. @method _indexMapKey
  403. @param {Number} index Numerical index of the key in the internal `_mapKeys`
  404. array.
  405. @param {Mixed} key Key to index.
  406. @protected
  407. **/
  408. _indexMapKey: function (index, key) {
  409. var objectIdName = this._mapOptions.objectIdName;
  410.  
  411. if (typeof key === 'string') {
  412. if (nativeObjectCreate || this._isSafeKey(key)) {
  413. this._mapKeyIndices[key] = index;
  414. }
  415. } else if (objectIdName && key && typeof key === 'object') {
  416. if (!key[objectIdName] && this._mapOptions.autoStamp) {
  417. try {
  418. key[objectIdName] = Y.guid();
  419. } catch (ex) {}
  420. }
  421.  
  422. if (key[objectIdName]
  423. && (nativeObjectCreate || this._isSafeKey(key[objectIdName]))) {
  424.  
  425. this._mapObjectIndices[key[objectIdName]] = index;
  426. }
  427. }
  428. },
  429.  
  430. /**
  431. Returns the numerical index of the entry with the given _key_, or `-1` if
  432. not found.
  433.  
  434. This is a very efficient operation with string keys, but may be slower with
  435. non-string keys.
  436.  
  437. @method _indexOfKey
  438. @param {Mixed} key Key to look up.
  439. @return {Number} Index of the entry with the given _key_, or `-1` if not
  440. found.
  441. @protected
  442. **/
  443. _indexOfKey: function (key) {
  444. var objectIdName = this._mapOptions.objectIdName,
  445. i;
  446.  
  447. // Reindex the map if the index is stale.
  448. if (this._isIndexStale) {
  449. this._reindexMap();
  450. }
  451.  
  452. // If the key is a string, do a fast hash lookup for the index.
  453. if (typeof key === 'string') {
  454. if (nativeObjectCreate || this._isSafeKey(key)) {
  455. i = this._mapKeyIndices[key];
  456. return i >= 0 ? i : -1;
  457. }
  458.  
  459. // If the key is an object but has an objectIdName property, do a fast
  460. // hash lookup for the index of the object key.
  461. } else if (objectIdName && key !== null && key[objectIdName]) {
  462. if (nativeObjectCreate || this._isSafeKey(key[objectIdName])) {
  463. i = this._mapObjectIndices[key[objectIdName]];
  464.  
  465. // Return the index if found. If not, we'll fall back to a slow
  466. // lookup. Even if the object has an id property, it may not be
  467. // indexed by that property in this Map.
  468. if (i >= 0) {
  469. return i;
  470. }
  471. }
  472. }
  473.  
  474. // Resort to a slow O(n) lookup.
  475. var keys = this._mapKeys,
  476. same = this._sameValueZero,
  477. len;
  478.  
  479. for (i = 0, len = keys.length; i < len; ++i) {
  480. if (same(keys[i], key)) {
  481. return i;
  482. }
  483. }
  484.  
  485. return -1;
  486. },
  487.  
  488. /**
  489. Returns `true` if the given string _key_ is safe to use in environments that
  490. don't support `Object.create()`.
  491.  
  492. @method _isSafeKey
  493. @param {String} key Key to check.
  494. @return {Boolean} `true` if the key is safe.
  495. @protected
  496. **/
  497. _isSafeKey: function (key) {
  498. return !(key === 'prototype' || key in emptyObject);
  499. },
  500.  
  501. /**
  502. Reindexes all the keys in this map.
  503.  
  504. @method _reindexMap
  505. @protected
  506. **/
  507. _reindexMap: function () {
  508. var mapKeys = this._mapKeys;
  509.  
  510. this._mapKeyIndices = nativeObjectCreate ? Object.create(null) : {};
  511. this._mapObjectIndices = nativeObjectCreate ? Object.create(null) : {};
  512.  
  513. for (var i = 0, len = mapKeys.length; i < len; ++i) {
  514. this._indexMapKey(i, mapKeys[i]);
  515. }
  516.  
  517. this._isIndexStale = false;
  518. },
  519.  
  520. /**
  521. Removes the entry at the given _index_ from internal arrays.
  522.  
  523. This method does not update the `size` property.
  524.  
  525. @method _removeMapEntry
  526. @param {Number} index Index of the entry to remove.
  527. @protected
  528. **/
  529. _removeMapEntry: function (index) {
  530. if (index === this._mapKeys.length - 1) {
  531. this._mapKeys.pop();
  532. this._mapValues.pop();
  533. } else {
  534. this._mapKeys.splice(index, 1);
  535. this._mapValues.splice(index, 1);
  536.  
  537. this._isIndexStale = true;
  538. }
  539. },
  540.  
  541. /**
  542. Returns `true` if the two given values are the same value, `false`
  543. otherwise.
  544.  
  545. This is an implementation of the [ES6 SameValueZero][es6-svz] comparison
  546. algorithm. It's more correct than `===` in that it considers `NaN` to be the
  547. same as `NaN`.
  548.  
  549. Note that `0` and `-0` are considered the same by this algorithm.
  550.  
  551. [es6-svz]: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-9.2.4
  552.  
  553. @method _sameValueZero
  554. @param {Mixed} a First value to compare.
  555. @param {Mixed} b Second value to compare.
  556. @return {Boolean} `true` if _a_ and _b_ are the same value, `false`
  557. otherwise.
  558. @protected
  559. **/
  560. _sameValueZero: function (a, b) {
  561. return a === b || (a !== a && b !== b);
  562. },
  563.  
  564. /**
  565. Updates the value of the `size` property in old browsers. In ES5 browsers
  566. this is a noop, since the `size` property is a getter.
  567.  
  568. @method _updateMapSize
  569. @protected
  570. **/
  571. _updateMapSize: sizeIsGetter ? function () {} : function () {
  572. this.size = this._mapKeys.length;
  573. }
  574. }, true);
  575.  
  576. /**
  577. Alias for `remove()`.
  578.  
  579. @method delete
  580. @see remove
  581. **/
  582. YMap.prototype['delete'] = YMap.prototype.remove;
  583.  
  584. /**
  585. Alias for `each()`.
  586.  
  587. @method forEach
  588. @see each
  589. **/
  590. YMap.prototype.forEach = YMap.prototype.each;
  591.  
  592. /**
  593. Returns a JSON-serializable representation of this map.
  594.  
  595. This is effectively an alias for `entries()`, but could be overridden to
  596. return a customized representation.
  597.  
  598. @method toJSON
  599. @return {Array} JSON-serializable array of entries in this map.
  600. **/
  601. YMap.prototype.toJSON = YMap.prototype.entries;
  602.  
  603. Y.Map = YMap;
  604.