/** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ /** * @api */ define([ 'jquery', 'underscore' ], function ($, _) { 'use strict'; var privateData = new WeakMap(); /** * Extracts private item storage associated * with a provided registry instance. * * @param {Object} container * @returns {Object} */ function getItems(container) { return privateData.get(container).items; } /** * Extracts private requests array associated * with a provided registry instance. * * @param {Object} container * @returns {Array} */ function getRequests(container) { return privateData.get(container).requests; } /** * Wrapper function used for convenient access to the elements. * See 'async' method for examples of usage and comparison * with a regular 'get' method. * * @param {(String|Object|Function)} name - Key of the requested element. * @param {Registry} registry - Instance of a registry * where to search for the element. * @param {(Function|String)} [method] - Optional callback function * or a name of the elements' method which * will be invoked when element is available in registry. * @returns {*} */ function async(name, registry, method) { var args = _.toArray(arguments).slice(3); if (_.isString(method)) { registry.get(name, function (component) { component[method].apply(component, args); }); } else if (_.isFunction(method)) { registry.get(name, method); } else if (!args.length) { return registry.get(name); } } /** * Checks that every property of the query object * is present and equal to the corresponding * property in target object. * Note that non-strict comparison is used. * * @param {Object} query - Query object. * @param {Object} target - Target object. * @returns {Boolean} */ function compare(query, target) { var matches = true, index, keys, key; if (!_.isObject(query) || !_.isObject(target)) { return false; } keys = Object.getOwnPropertyNames(query); index = keys.length; while (matches && index--) { key = keys[index]; /* eslint-disable eqeqeq */ if (target[key] != query[key]) { matches = false; } /* eslint-enable eqeqeq */ } return matches; } /** * Explodes incoming string into object if * string is defined as a set of key = value pairs. * * @param {(String|*)} query - String to be processed. * @returns {Object|*} Either created object or an unmodified incoming * value if conversion was not possible. * @example Sample conversions. * 'key = value, key2 = value2' * => {key: 'value', key2: 'value2'} */ function explode(query) { var result = {}, index, data; if (typeof query !== 'string' || !~query.indexOf('=')) { return query; } query = query.split(','); index = query.length; while (index--) { data = query[index].split('='); result[data[0].trim()] = data[1].trim(); } return result; } /** * Extracts items from the provided data object * which matches specified search criteria. * * @param {Object} data - Data object where to perform a lookup. * @param {(String|Object|Function)} query - Search criteria. * @param {Boolean} findAll - Flag that defines whether to * search for all applicable items or to stop on a first found entry. * @returns {Array|Object|*} */ function find(data, query, findAll) { var iterator, item; query = explode(query); if (typeof query === 'string') { item = data[query]; if (findAll) { return item ? [item] : []; } return item; } iterator = !_.isFunction(query) ? compare.bind(null, query) : query; return findAll ? _.filter(data, iterator) : _.find(data, iterator); } /** * @constructor */ function Registry() { var data = { items: {}, requests: [] }; this._updateRequests = _.debounce(this._updateRequests.bind(this), 10); privateData.set(this, data); } Registry.prototype = { constructor: Registry, /** * Retrieves item from registry which matches specified search criteria. * * @param {(Object|String|Function|Array)} query - Search condition (see examples). * @param {Function} [callback] - Callback that will be invoked when * all of the requested items are available. * @returns {*} * * @example Requesting item by it's name. * var obj = {index: 'test', sample: true}; * * registry.set('first', obj); * registry.get('first') === obj; * => true * * @example Requesting item with a specific properties. * registry.get('sample = 1, index = test') === obj; * => true * registry.get('sample = 0, index = foo') === obj; * => false * * @example Declaring search criteria as an object. * registry.get({sample: true}) === obj; * => true; * * @example Providing custom search handler. * registry.get(function (item) { return item.sample === true; }) === obj; * => true * * @example Sample asynchronous request declaration. * registry.get('index = test', function (item) {}); * * @example Requesting multiple elements. * registry.set('second', {index: 'test2'}); * registry.get(['first', 'second'], function (first, second) {}); */ get: function (query, callback) { if (typeof callback !== 'function') { return find(getItems(this), query); } this._addRequest(query, callback); }, /** * Sets provided item to the registry. * * @param {String} id - Item's identifier. * @param {*} item - Item's data. * returns {Registry} Chainable. */ set: function (id, item) { getItems(this)[id] = item; this._updateRequests(); return this; }, /** * Removes specified item from registry. * Note that search query is not applicable. * * @param {String} id - Item's identifier. * @returns {Registry} Chainable. */ remove: function (id) { delete getItems(this)[id]; return this; }, /** * Retrieves a collection of elements that match * provided search criteria. * * @param {(Object|String|Function)} query - Search query. * See 'get' method for the syntax examples. * @returns {Array} Found elements. */ filter: function (query) { return find(getItems(this), query, true); }, /** * Checks that at least one element in collection * matches provided search criteria. * * @param {(Object|String|Function)} query - Search query. * See 'get' method for the syntax examples. * @returns {Boolean} */ has: function (query) { return !!this.get(query); }, /** * Checks that registry contains a provided item. * * @param {*} item - Item to be checked. * @returns {Boolean} */ contains: function (item) { return _.contains(getItems(this), item); }, /** * Extracts identifier of an item if it's present in registry. * * @param {*} item - Item whose identifier will be extracted. * @returns {String|Undefined} */ indexOf: function (item) { return _.findKey(getItems(this), function (elem) { return item === elem; }); }, /** * Same as a 'get' method except that it returns * a promise object instead of invoking provided callback. * * @param {(String|Function|Object|Array)} query - Search query. * See 'get' method for the syntax examples. * @returns {jQueryPromise} */ promise: function (query) { var defer = $.Deferred(), callback = defer.resolve.bind(defer); this.get(query, callback); return defer.promise(); }, /** * Creates a wrapper function over the provided search query * in order to provide somehow more convenient access to the * registry's items. * * @param {(String|Object|Function)} query - Search criteria. * See 'get' method for the syntax examples. * @returns {Function} * * @example Comparison with a 'get' method on retrieving items. * var module = registry.async('name'); * * module(); * => registry.get('name'); * * @example Asynchronous request. * module(function (component) {}); * => registry.get('name', function (component) {}); * * @example Requesting item and invoking it's method with specified parameters. * module('trigger', true); * => registry.get('name', function (component) { * component.trigger(true); * }); */ async: function (query) { return async.bind(null, query, this); }, /** * Creates new instance of a Registry. * * @returns {Registry} New instance. */ create: function () { return new Registry; }, /** * Adds new request to the queue or resolves it immediately * if all of the required items are available. * * @private * @param {(Object|String|Function|Array)} queries - Search criteria. * See 'get' method for the syntax examples. * @param {Function} callback - Callback that will be invoked when * all of the requested items are available. * @returns {Registry} */ _addRequest: function (queries, callback) { var request; if (!Array.isArray(queries)) { queries = queries ? [queries] : []; } request = { queries: queries.map(explode), callback: callback }; this._canResolve(request) ? this._resolveRequest(request) : getRequests(this).push(request); return this; }, /** * Updates requests list resolving applicable items. * * @private * @returns {Registry} Chainable. */ _updateRequests: function () { getRequests(this) .filter(this._canResolve, this) .forEach(this._resolveRequest, this); return this; }, /** * Resolves provided request invoking it's callback * with items specified in query parameters. * * @private * @param {Object} request - Request object. * @returns {Registry} Chainable. */ _resolveRequest: function (request) { var requests = getRequests(this), items = request.queries.map(this.get, this), index = requests.indexOf(request); request.callback.apply(null, items); if (~index) { requests.splice(index, 1); } return this; }, /** * Checks if provided request can be resolved. * * @private * @param {Object} request - Request object. * @returns {Boolean} */ _canResolve: function (request) { var queries = request.queries; return queries.every(this.has, this); } }; return new Registry; });