- /*jshint es3: true, globalstrict: true, indent: 4 */
-
- /**
- Provides the `Y.Map` data structure.
-
- @module gallery-sm-map
- @main gallery-sm-map
- **/
-
- /**
- An ordered hash map data structure with an interface and behavior similar to
- (but not exactly the same as) [ECMAScript 6 Maps][es6-maps].
-
- [es6-maps]:http://people.mozilla.org/~jorendorff/es6-draft.html#sec-15.14
-
- @class Map
- @constructor
- @param {Array[]|Map} [entries] Array or Map of entries to add to this map. If an
- array, then each entry should itself be an array in which the first item is
- the key and the second item is the value for that entry.
-
- @param {Object} [options] Options.
-
- @param {Boolean} [options.autoStamp=false] If `true`, objects used as keys
- will be automatically stamped with a unique id as the value of the
- property defined by the `objectIdName` option ("_yuid" by default) if
- that property isn't already set. This will result in much faster lookups
- for object keys.
-
- @param {String} [options.objectIdName="_yuid"] Name of a property whose
- string value should be used as the unique key when present on an object
- that's given as a key. This will significantly speed up lookups of
- object-based keys that define this property.
- **/
-
- "use strict";
-
- var emptyObject = {},
- isNative = Y.Lang._isNative,
- nativeObjectCreate = isNative(Object.create),
- protoSlice = Array.prototype.slice,
- sizeIsGetter = isNative(Object.defineProperty) && Y.UA.ie !== 8;
-
- function YMap(entries, options) {
- // Allow options as only param.
- if (arguments.length === 1 && !('length' in entries)
- && typeof entries.entries !== 'function') {
-
- options = entries;
- entries = null;
- }
-
- if (options) {
- this._mapOptions = Y.merge(YMap.prototype._mapOptions, options);
- }
-
- this.clear();
-
- if (entries) {
- if (!Y.Lang.isArray(entries)) {
- if (typeof entries.entries === 'function') {
- // It quacks like a map!
- entries = entries.entries();
- } else {
- // Assume it's an array-like object.
- entries = protoSlice.call(entries);
- }
- }
-
- var entry;
-
- for (var i = 0, len = entries.length; i < len; ++i) {
- entry = entries[i];
-
- if (entry.length !== 2) {
- throw new TypeError('Invalid map entry: ' + entry.toString());
- }
-
- this.set(entry[0], entry[1]);
- }
- }
- }
-
- // In modern browsers, the `size` property is a non-enumerable getter on the
- // prototype, as specified in ES6. In older browsers (mainly IE<9), we just
- // manually update a plain old instance property.
- if (sizeIsGetter) {
- Object.defineProperty(YMap.prototype, 'size', {
- configurable: true,
-
- get: function () {
- return this._mapKeys.length;
- }
- });
- }
-
- Y.mix(YMap.prototype, {
- // -- Public Properties ----------------------------------------------------
-
- /**
- The number of entries in this map.
-
- @property {Number} size
- @default 0
- @readOnly
- **/
-
- // -- Protected Properties -------------------------------------------------
-
- /**
- Whether or not the internal key index is in need of reindexing.
-
- Rather than reindexing immediately whenever it becomes necessary, we use
- this flag to allow on-demand indexing the first time an up-to-date index is
- actually needed.
-
- This makes multiple `remove()` operations significantly faster, at the
- expense of a single reindex operation the next time a key is looked up.
-
- @property {Boolean} _isIndexStale
- @protected
- **/
-
- /**
- Internal array of the keys in this map.
-
- @property {Array} _mapKeys
- @protected
- **/
-
- /**
- Internal index mapping string keys to their indices in the `_mapKeys` array.
-
- @property {Object} _mapKeyIndices
- @protected
- **/
-
- /**
- Internal index mapping object key ids to their indices in the `_mapKeys`
- array. This is separate from `_mapKeyIndices` in order to prevent collisions
- between object key ids and string keys.
-
- @property {Object} _mapObjectIndices
- @protected
- **/
-
- /**
- Options that affect the functionality of this map.
-
- @property {Object} _mapOptions
- @protected
- **/
- _mapOptions: {
- objectIdName: '_yuid'
- },
-
- /**
- Internal array of the values in this map.
-
- @property {Array} _mapValues
- @protected
- **/
-
- // -- Public Methods -------------------------------------------------------
-
- /**
- Deletes all entries from this map.
-
- @method clear
- @chainable
- **/
- clear: function () {
- this._mapKeys = [];
- this._mapValues = [];
-
- this._reindexMap();
- this._updateMapSize();
-
- return this;
- },
-
- /**
- Returns a new map comprised of this map's entries combined with those of the
- maps or arrays passed as arguments. Does not alter this map or those given
- as arguments in any way.
-
- Entries in later (rightmost) arguments will take precedence over entries in
- earlier (leftmost) arguments if their keys are the same.
-
- This method also accepts arrays of entries in lieu of actual Y.Map
- instances.
-
- The returned map will be created using the same options and constructor as
- this map.
-
- @method concat
- @param {Array[]|Map} [maps*] Zero or more maps or entry arrays to
- concatenate into the resulting map.
- @return {Map} New map containing the concatenated values of this map and all
- arguments.
- **/
- concat: function () {
- var map = new this.constructor(this, this._mapOptions);
- return arguments.length ? map.merge.apply(map, arguments) : map;
- },
-
- /**
- Executes the given _callback_ function on each entry in this map.
-
- To halt iteration early, return `false` from the callback.
-
- @method each
- @param {Function} callback Callback function.
- @param {Mixed} callback.value Value being iterated.
- @param {Mixed} callback.key Key being iterated.
- @param {Map} callback.map Reference to this map.
- @param {Object} [thisObj] `this` object to use when calling _callback_.
- @chainable
- @see forEach
- **/
- each: function (callback, thisObj) {
- var entries = this.entries(),
- entry;
-
- for (var i = 0, len = entries.length; i < len; ++i) {
- entry = entries[i];
-
- if (callback.call(thisObj, entry[1], entry[0], this) === false) {
- break;
- }
- }
-
- return this;
- },
-
- /**
- Returns an array of all the entries in this map. Each entry is an array with
- two items, the first being a key and the second a value associated with that
- key.
-
- @method entries
- @return {Array} Array of entries.
- **/
- entries: function () {
- var entries = [],
- mapKeys = this._mapKeys,
- mapValues = this._mapValues;
-
- for (var i = 0, len = mapKeys.length; i < len; ++i) {
- entries.push([mapKeys[i], mapValues[i]]);
- }
-
- return entries;
- },
-
- /**
- Returns the value associated with the given _key_, or _default_ if the key
- isn't found.
-
- @method get
- @param {Mixed} key Key to look up.
- @param {Mixed} [defaultValue] Default value to return if _key_ isn't found.
- @return {Mixed} Value associated with the given _key_, or _default_ if the
- key isn't found.
- **/
- get: function (key, defaultValue) {
- var i = this._indexOfKey(key);
- return i < 0 ? defaultValue : this._mapValues[i];
- },
-
- /**
- Returns `true` if _key_ exists in this map, `false` otherwise.
-
- @method has
- @param {Mixed} key Key to look up.
- @return {Boolean} `true` if _key_ exists in this map, `false` otherwise.
- **/
- has: function (key) {
- return this._indexOfKey(key) >= 0;
- },
-
- /**
- Returns an array of all the keys in this map.
-
- @method keys
- @return {Array} Array of keys.
- **/
- keys: function () {
- return protoSlice.call(this._mapKeys);
- },
-
- /**
- Merges the entries from one or more other maps or entry arrays into this
- map. Entries in later (rightmost) arguments will take precedence over
- entries in earlier (leftmost) arguments if their keys are the same.
-
- This method also accepts arrays of entries in lieu of actual Y.Map
- instances, so the following operations have the same result:
-
- // This...
- map.merge(new Y.Map([['a', 'apple'], ['b', 'bear']]));
-
- // ...has the same result as this...
- map.merge([['a', 'apple'], ['b', 'bear']]);
-
- @method merge
- @param {Array[]|Map} maps* One or more maps or entry arrays to merge into
- this map.
- @chainable
- **/
- merge: function () {
- var maps = protoSlice.call(arguments),
-
- entries,
- entry,
- i,
- len,
- map;
-
- while ((map = maps.shift())) {
- entries = typeof map.entries === 'function' ? map.entries() : map;
-
- for (i = 0, len = entries.length; i < len; ++i) {
- entry = entries[i];
-
- if (entry.length !== 2) {
- throw new TypeError('Invalid map entry: ' + entry.toString());
- }
-
- this.set(entry[0], entry[1]);
- }
- }
-
- return this;
- },
-
- /**
- Deletes the entry with the given _key_.
-
- @method remove
- @param {Mixed} key Key to delete.
- @return {Boolean} `true` if the key existed and was deleted, `false`
- otherwise.
- **/
- remove: function (key) {
- var i = this._indexOfKey(key);
-
- if (i < 0) {
- return false;
- }
-
- this._removeMapEntry(i);
- this._updateMapSize();
-
- return true;
- },
-
- /**
- Sets the value of the entry with the given _key_. If the key already exists,
- its value will be overwritten; otherwise it will be created.
-
- The _key_ may be any JavaScript value (including both primitives and
- objects), but string keys will allow fast lookups, whereas non-string keys
- may result in slower lookups.
-
- @method set
- @param {Mixed} key Key to set.
- @param {Mixed} value Value to set.
- @chainable
- **/
- set: function (key, value) {
- var i = this._indexOfKey(key);
-
- if (i < 0) {
- i = this._mapKeys.length;
- }
-
- this._mapKeys[i] = key;
- this._mapValues[i] = value;
-
- this._indexMapKey(i, key);
- this._updateMapSize();
-
- return this;
- },
-
- /**
- Returns an array of all the values in this map.
-
- @method values
- @return {Array} Array of values.
- **/
- values: function () {
- return protoSlice.call(this._mapValues);
- },
-
- // -- Protected Methods ----------------------------------------------------
-
- /**
- Indexes the given _key_ to enable faster lookups.
-
- @method _indexMapKey
- @param {Number} index Numerical index of the key in the internal `_mapKeys`
- array.
- @param {Mixed} key Key to index.
- @protected
- **/
- _indexMapKey: function (index, key) {
- var objectIdName = this._mapOptions.objectIdName;
-
- if (typeof key === 'string') {
- if (nativeObjectCreate || this._isSafeKey(key)) {
- this._mapKeyIndices[key] = index;
- }
- } else if (objectIdName && key && typeof key === 'object') {
- if (!key[objectIdName] && this._mapOptions.autoStamp) {
- try {
- key[objectIdName] = Y.guid();
- } catch (ex) {}
- }
-
- if (key[objectIdName]
- && (nativeObjectCreate || this._isSafeKey(key[objectIdName]))) {
-
- this._mapObjectIndices[key[objectIdName]] = index;
- }
- }
- },
-
- /**
- Returns the numerical index of the entry with the given _key_, or `-1` if
- not found.
-
- This is a very efficient operation with string keys, but may be slower with
- non-string keys.
-
- @method _indexOfKey
- @param {Mixed} key Key to look up.
- @return {Number} Index of the entry with the given _key_, or `-1` if not
- found.
- @protected
- **/
- _indexOfKey: function (key) {
- var objectIdName = this._mapOptions.objectIdName,
- i;
-
- // Reindex the map if the index is stale.
- if (this._isIndexStale) {
- this._reindexMap();
- }
-
- // If the key is a string, do a fast hash lookup for the index.
- if (typeof key === 'string') {
- if (nativeObjectCreate || this._isSafeKey(key)) {
- i = this._mapKeyIndices[key];
- return i >= 0 ? i : -1;
- }
-
- // If the key is an object but has an objectIdName property, do a fast
- // hash lookup for the index of the object key.
- } else if (objectIdName && key !== null && key[objectIdName]) {
- if (nativeObjectCreate || this._isSafeKey(key[objectIdName])) {
- i = this._mapObjectIndices[key[objectIdName]];
-
- // Return the index if found. If not, we'll fall back to a slow
- // lookup. Even if the object has an id property, it may not be
- // indexed by that property in this Map.
- if (i >= 0) {
- return i;
- }
- }
- }
-
- // Resort to a slow O(n) lookup.
- var keys = this._mapKeys,
- same = this._sameValueZero,
- len;
-
- for (i = 0, len = keys.length; i < len; ++i) {
- if (same(keys[i], key)) {
- return i;
- }
- }
-
- return -1;
- },
-
- /**
- Returns `true` if the given string _key_ is safe to use in environments that
- don't support `Object.create()`.
-
- @method _isSafeKey
- @param {String} key Key to check.
- @return {Boolean} `true` if the key is safe.
- @protected
- **/
- _isSafeKey: function (key) {
- return !(key === 'prototype' || key in emptyObject);
- },
-
- /**
- Reindexes all the keys in this map.
-
- @method _reindexMap
- @protected
- **/
- _reindexMap: function () {
- var mapKeys = this._mapKeys;
-
- this._mapKeyIndices = nativeObjectCreate ? Object.create(null) : {};
- this._mapObjectIndices = nativeObjectCreate ? Object.create(null) : {};
-
- for (var i = 0, len = mapKeys.length; i < len; ++i) {
- this._indexMapKey(i, mapKeys[i]);
- }
-
- this._isIndexStale = false;
- },
-
- /**
- Removes the entry at the given _index_ from internal arrays.
-
- This method does not update the `size` property.
-
- @method _removeMapEntry
- @param {Number} index Index of the entry to remove.
- @protected
- **/
- _removeMapEntry: function (index) {
- if (index === this._mapKeys.length - 1) {
- this._mapKeys.pop();
- this._mapValues.pop();
- } else {
- this._mapKeys.splice(index, 1);
- this._mapValues.splice(index, 1);
-
- this._isIndexStale = true;
- }
- },
-
- /**
- Returns `true` if the two given values are the same value, `false`
- otherwise.
-
- This is an implementation of the [ES6 SameValueZero][es6-svz] comparison
- algorithm. It's more correct than `===` in that it considers `NaN` to be the
- same as `NaN`.
-
- Note that `0` and `-0` are considered the same by this algorithm.
-
- [es6-svz]: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-9.2.4
-
- @method _sameValueZero
- @param {Mixed} a First value to compare.
- @param {Mixed} b Second value to compare.
- @return {Boolean} `true` if _a_ and _b_ are the same value, `false`
- otherwise.
- @protected
- **/
- _sameValueZero: function (a, b) {
- return a === b || (a !== a && b !== b);
- },
-
- /**
- Updates the value of the `size` property in old browsers. In ES5 browsers
- this is a noop, since the `size` property is a getter.
-
- @method _updateMapSize
- @protected
- **/
- _updateMapSize: sizeIsGetter ? function () {} : function () {
- this.size = this._mapKeys.length;
- }
- }, true);
-
- /**
- Alias for `remove()`.
-
- @method delete
- @see remove
- **/
- YMap.prototype['delete'] = YMap.prototype.remove;
-
- /**
- Alias for `each()`.
-
- @method forEach
- @see each
- **/
- YMap.prototype.forEach = YMap.prototype.each;
-
- /**
- Returns a JSON-serializable representation of this map.
-
- This is effectively an alias for `entries()`, but could be overridden to
- return a customized representation.
-
- @method toJSON
- @return {Array} JSON-serializable array of entries in this map.
- **/
- YMap.prototype.toJSON = YMap.prototype.entries;
-
- Y.Map = YMap;
-
-