<commit_before>(function(window)
{
    var Gitana = window.Gitana;

    Gitana.AbstractMap = Gitana.AbstractPersistable.extend(
    /** @lends Gitana.AbstractMap.prototype */
    {
        /**
         * @constructs
         * @augments Gitana.AbstractPersistable
         *
         * @class Abstract base class for a map of Gitana objects
         *                   f
         * @param {Gitana} driver
         * @param [Object] object
         */
        constructor: function(driver, object)
        {
            //////////////////////////////////////////////////////////////////////////////////////////////
            //
            // PRIVILEGED METHODS
            //
            //////////////////////////////////////////////////////////////////////////////////////////////

            // auto-manage a list of keys
            this.__keys = (function() {
                var list = [];
                return function(x) {
                    if (!Gitana.isUndefined(x)) {
                        if (x == 'empty') {
                            while (list.length > 0) { list.shift(); }
                        } else {
                            if (!x && x.length) {
                                for (var i = 0; i < x.length; i++) {
                                    list.push(x[i]);
                                }
                            }
                            else
                            {
                                list.push(x);
                            }
                        }
                    }
                    return list;
                };
            })();

            this.__totalRows = (function() {
                var _totalRows = null;
                return function(totalRows) {
                    if (!Gitana.isUndefined(totalRows)) { _totalRows = totalRows; }
                    return _totalRows;
                }
            })();

            this.__size = (function() {
                var _size = null;
                return function(size) {
                    if (!Gitana.isUndefined(size)) { _size = size; }
                    return _size;
                }
            })();

            this.__offset = (function() {
                var _offset = 0;
                return function(offset) {
                    if (!Gitana.isUndefined(offset) && offset >= 0) { _offset = offset; }
                    return _offset;
                }
            })();

            this.base(driver, object);

            // in case the incoming object is a state-carrying object (like another map)
            if (object)
            {
                this.chainCopyState(object);
            }
        },

        /**
         * Override to include:
         *
         *   __keys
         *   __totalRows
         *   __size
         *   __offset
         *
         * @param otherObject
         */
        chainCopyState: function(otherObject)
        {
            this.base(otherObject);

            // include keys
            if (otherObject.__keys) {
                this.__keys('empty');
                for (var i = 0; i < otherObject.__keys().length; i++)
                {
                    var k = otherObject.__keys()[i];
                    this.__keys().push(k);
                }
            }

            if (otherObject.__totalRows) {
                this.__totalRows(otherObject.__totalRows());
            }
            if (otherObject.__size) {
                this.__size(otherObject.__size());
            }
            if (otherObject.__offset) {
                this.__offset(otherObject.__offset());
            }

        },

        clear: function()
        {
            // clear object properties (but not member functions)
            Gitana.deleteProperties(this, false);

            // empty keys
            this.__keys('empty');
        },

        /**
         * @override
         *
         * Convert the json response object into the things we want to preserve on the object.
         * This should set the "object" property but may choose to set other properties as well.
         *
         * @param response
         */
        handleResponse: function(response)
        {
            this.clear();

            if (response)
            {
                // is it a gitana map?
                if (response.totalRows && response.size && response.offset) {
                    this.__totalRows(response.totalRows());
                    this.__size(response.size());
                    this.__offset(response.offset());
                }
                else
                {
                    // otherwise assume it is a gitana result set
                    this.__totalRows(response["total_rows"]);
                    this.__size(response["size"]);
                    this.__offset(response["offset"]);
                }

                if (response.rows)
                {
                    // parse array
                    if (Gitana.isArray(response.rows))
                    {
                        for (var i = 0; i < response.rows.length; i++)
                        {
                            var o = this.buildObject(response.rows[i]);
                            this[o.getId()] = o;

                            this.__keys().push(o.getId());
                        }
                    }
                    else
                    {
                        // parse object
                        for (var key in response.rows)
                        {
                            if (response.rows.hasOwnProperty(key) && !Gitana.isFunction(response.rows[key]))
                            {
                                var value = response.rows[key];

                                var o = this.buildObject(value);

                                // determine key
                                var k = (o.getId && o.getId());
                                if (!k) {
                                    k = key;
                                }
                                this[k] = o;

                                this.__keys().push(k);
                            }
                        }
                    }
                    this.__size(this.__keys().length);
                }
                else
                {
                    // otherwise, assume it is key/value pairs
                    // it also might be another Gitana Map

                    for (var key in response)
                    {
                        if (response.hasOwnProperty(key) && !Gitana.isFunction(response[key]))
                        {
                            var value = response[key];

                            var o = this.buildObject(value);

                            // determine key
                            var k = (o.getId && o.getId());
                            if (!k) {
                                k = key;
                            }
                            this[k] = o;

                            this[k] = o;

                            this.__keys().push(k);
                        }
                    }
                    this.__size(this.__keys().length);
                }
            }
        },

        /**
         * @abstract
         * ABSTRACT METHOD
         *
         * @param json
         */
        buildObject: function(json)
        {

        },

        get: function(key)
        {
            return this[key];
        },

        asArray: function()
        {
            var array = [];
            for (var i = 0; i < this.__keys().length; i++)
            {
                var k = this.__keys()[i];

                array.push(this[k]);
            }

            return array;
        },

        size: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__size());
                });
            }

            return this.__size();
        },

        offset: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__offset());
                });
            }

            return this.__offset();
        },

        totalRows: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__totalRows());
                });
            }

            return this.__totalRows();
        },

        /**
         * Iterates over the map and fires the callback function in SERIAL for each element in the map.
         * The scope for the callback is the object from the map (i.e. repository object, node object).
         *
         * The arguments to the callback function are (key, value) where value is the same as "this".
         *
         * NOTE: This works against elements in the map in SERIAL.  One at a time.  If you are doing concurrent
         * remote operations for members of the set such that each operation is independent, you may want to use
         * the eachX() method.
         *
         * @chained this
         *
         * @param callback
         */
        each: function(callback)
        {
            return this.then(function() {

                // run functions
                for (var i = 0; i < this.__keys().length; i++)
                {
                    // key and value from the map
                    var key = this.__keys()[i];
                    var value = this[key];

                    // a function that fires our callback
                    // wrap in a closure so that we store the callback and key
                    // note: this = the value wrapped in a chain, so we don't pass in value
                    var f = function(callback, key, index, map)
                    {
                        return function()
                        {
                            callback.call(this, key, this, index);

                            // manually copy resulting value back
                            Gitana.deleteProperties(map[key]);
                            Gitana.copyInto(map[key], this);
                        };

                    }(callback, key, i, this);

                    // create subchain mounted on this chainable and the run function
                    this.subchain(value).then(f);
                }

                return this;
            });
        },

        /**
         * Iterates over the map and fires the callback function in PARALLEL for each element in the map.
         * The scope for the callback is the object from the map (i.e. repository object, node object).
         *
         * The arguments to the callback function are (key, value) where value is the same as "this".
         *
         * NOTE: This works against elements in the map in PARALLEL.  All map members are fired against at the same
         * time on separate timeouts.  There is no guaranteed order for their completion.  If you require serial
         * execution, use the each() method.
         *
         * @chained
         *
         * @param callback
         */
        eachX: function(callback)
        {
            return this.then(function() {

                // create an array of functions that invokes the callback for each element in the array
                var functions = [];
                for (var i = 0; i < this.__keys().length; i++)
                {
                    var key = this.__keys()[i];
                    var value = this[key];

                    var f = function(callback, key, value, index) {

                        return function()
                        {
                            // NOTE: we're running a parallel chain that is managed for us by the Chain then() method.
                            // we can't change the parallel chain but we can subchain from it
                            // in our subchain we run our method
                            // the parallel chain kind of acts like one-hop noop so that we can take over and do our thing
                            this.subchain(value).then(function() {
                                callback.call(this, key, this, index);
                            });
                        };

                    }(callback, key, value, i);

                    functions.push(f);
                }

                // kick off all these functions in parallel
                // adding them to the subchain
                return this.then(functions)

            });
        },

        /**
         * Iterates over the map and applies the callback filter function to each element.
         * It should hand back true if it wants to keep the value and false to remove it.
         *
         * NOTE: the "this" for the callback is the object from the map.
         *
         * @chained
         *
         * @param callback
         */
        filter: function(callback)
        {
            return this.then(function() {

                var keysToKeep = [];
                var keysToRemove = [];

                for (var i = 0; i < this.__keys().length; i++)
                {
                    var key = this.__keys()[i];
                    var object = this[key];

                    var keepIt = callback.call(object);
                    if (keepIt)
                    {
                        keysToKeep.push(key);
                    }
                    else
                    {
                        keysToRemove.push(key);
                    }
                }

                // remove any keys we don't want from the map
                for (var i = 0; i < keysToRemove.length; i++)
                {
                    delete this[keysToRemove[i]];
                }

                // swap keys to keep
                // NOTE: we first clear the keys but we can't use slice(0,0) since that produces a NEW array
                // instead, do this shift trick
                this.__keys('empty');
                for (var i = 0; i < keysToKeep.length; i++)
                {
                    this.__keys().push(keysToKeep[i]);
                }
            });
        },

        /**
         * Applies a comparator to sort the map.
         *
         * If no comparator is applied, the map will be sorted by its modification timestamp (if possible).
         *
         * The comparator can be a string that uses dot-notation to identify a field in the JSON that
         * should be sorted.  (example: "title" or "property1.property2.property3")
         *
         * Finally, the comparator can be a function.  It takes (previousValue, currentValue) and hands back:
         *   -1 if the currentValue is less than the previousValue (should be sorted lower)
         *   0 if they are equivalent
         *   1 if they currentValue is greater than the previousValue (should be sorted higher)
         *
         * @chained
         *
         * @param comparator
         */
        sort: function(comparator)
        {
            return this.then(function() {

                // build a temporary array of values
                var array = [];
                for (var i = 0; i < this.__keys().length; i++) {
                    var key = this.__keys()[i];
                    array.push(this[key]);
                }

                // sort the array
                array.sort(comparator);

                // now reset keys according to the order of the array
                this.__keys("empty");
                for (var i = 0; i < array.length; i++)
                {
                    this.__keys().push(array[i].getId());
                }

            });
        },

        /**
         * Limits the number of elements in the map.
         *
         * @chained
         *
         * @param size
         */
        limit: function(size)
        {
            return this.then(function() {

                var keysToRemove = [];

                if (size > this.__keys().length)
                {
                    // keep everything
                    return;
                }

                // figure out which keys to remove
                for (var i = 0; i < this.__keys().length; i++)
                {
                    if (i >= size)
                    {
                        keysToRemove.push(this.__keys()[i]);
                    }
                }

                // truncate the keys
                // NOTE: we can't use slice here since that produces a new array
                while (this.__keys().length > size)
                {
                    this.__keys().pop();
                }

                // remove any keys to remove from map
                for (var i = 0; i < keysToRemove.length; i++)
                {
                    delete this[keysToRemove[i]];
                }

                // reset the size
                this.__size(this.__keys().length);
            });
        },

        /**
         * Paginates elements in the map.
         *
         * @chained
         *
         * @param pagination
         */
        paginate: function(pagination)
        {
            return this.then(function() {

                var skip = pagination.skip;
                var limit = pagination.limit;

                var keysToRemove = [];

                // figure out which keys to remove
                for (var i = 0; i < this.__keys().length; i++)
                {
                    if (i < skip || i >= skip + limit)
                    {
                        keysToRemove.push(this.__keys()[i]);
                    }
                }

                // truncate the keys
                // NOTE: we can't use slice here since that produces a new array
                while (this.__keys().length > limit + skip)
                {
                    this.__keys().pop();
                }

                // remove any keys to remove from map
                for (var i = 0; i < keysToRemove.length; i++)
                {
                    delete this[keysToRemove[i]];
                }

                // reset the limit
                this.__size(this.__keys().length);
            });
        },

        /**
         * Counts the number of elements in the map and fires it into a callback function.
         */
        count: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__keys().length);
                });
            }

            return this.__keys().length;
        },

        /**
         * Keeps the first element in the map
         */
        keepOne: function(emptyHandler)
        {
            var self = this;

            var chainable = this.buildObject({});

            return this.subchain(chainable).then(function() {

                var chain = this;

                this.subchain(self).then(function() {

                    if (this.__keys().length > 0)
                    {
                        var obj = this[this.__keys()[0]];

                        if (chain.loadFrom)
                        {
                            // for objects, like nodes or branches
                            chain.loadFrom(obj);
                        }
                        else
                        {
                            // non-objects? (i.e. binary or attachment maps)
                            chain.handleResponse(obj);
                        }
                    }
                    else
                    {
                        var err = new Gitana.Error();
                        err.name = "Empty Map";
                        err.message = "The map doesn't have any elements in it";

                        if (emptyHandler)
                        {
                            return emptyHandler.call(self, err);
                        }
                        else
                        {
                            this.error(err);
                        }
                    }

                });
            });
        },

        /**
         * Selects an individual element from the map and continues the chain.
         *
         * @param key
         */
        select: function(key)
        {
            var self = this;

            // what we hand back
            var result = this.subchain(this.buildObject({}));

            // preload some work
            return result.then(function() {

                var chain = this;

                this.subchain(self).then(function() {

                    var obj = this[key];
                    if (!obj)
                    {
                        var err = new Gitana.Error();
                        err.name = "No element with key: " + key;
                        err.message = err.name;

                        this.error(err);

                        return false;
                    }

                    if (result.loadFrom)
                    {
                        // for objects, like nodes or branches
                        chain.loadFrom(obj);
                    }
                    else
                    {
                        // non-objects? (i.e. binary or attachment maps)
                        chain.handleResponse(obj);
                    }

                });
            });
        }

    });

})(window);
<commit_msg>keepOne and select for abstract map works properly<commit_after>(function(window)
{
    var Gitana = window.Gitana;

    Gitana.AbstractMap = Gitana.AbstractPersistable.extend(
    /** @lends Gitana.AbstractMap.prototype */
    {
        /**
         * @constructs
         * @augments Gitana.AbstractPersistable
         *
         * @class Abstract base class for a map of Gitana objects
         *                   f
         * @param {Gitana} driver
         * @param [Object] object
         */
        constructor: function(driver, object)
        {
            //////////////////////////////////////////////////////////////////////////////////////////////
            //
            // PRIVILEGED METHODS
            //
            //////////////////////////////////////////////////////////////////////////////////////////////

            // auto-manage a list of keys
            this.__keys = (function() {
                var list = [];
                return function(x) {
                    if (!Gitana.isUndefined(x)) {
                        if (x == 'empty') {
                            while (list.length > 0) { list.shift(); }
                        } else {
                            if (!x && x.length) {
                                for (var i = 0; i < x.length; i++) {
                                    list.push(x[i]);
                                }
                            }
                            else
                            {
                                list.push(x);
                            }
                        }
                    }
                    return list;
                };
            })();

            this.__totalRows = (function() {
                var _totalRows = null;
                return function(totalRows) {
                    if (!Gitana.isUndefined(totalRows)) { _totalRows = totalRows; }
                    return _totalRows;
                }
            })();

            this.__size = (function() {
                var _size = null;
                return function(size) {
                    if (!Gitana.isUndefined(size)) { _size = size; }
                    return _size;
                }
            })();

            this.__offset = (function() {
                var _offset = 0;
                return function(offset) {
                    if (!Gitana.isUndefined(offset) && offset >= 0) { _offset = offset; }
                    return _offset;
                }
            })();

            this.base(driver, object);

            // in case the incoming object is a state-carrying object (like another map)
            if (object)
            {
                this.chainCopyState(object);
            }
        },

        /**
         * Override to include:
         *
         *   __keys
         *   __totalRows
         *   __size
         *   __offset
         *
         * @param otherObject
         */
        chainCopyState: function(otherObject)
        {
            this.base(otherObject);

            // include keys
            if (otherObject.__keys) {
                this.__keys('empty');
                for (var i = 0; i < otherObject.__keys().length; i++)
                {
                    var k = otherObject.__keys()[i];
                    this.__keys().push(k);
                }
            }

            if (otherObject.__totalRows) {
                this.__totalRows(otherObject.__totalRows());
            }
            if (otherObject.__size) {
                this.__size(otherObject.__size());
            }
            if (otherObject.__offset) {
                this.__offset(otherObject.__offset());
            }

        },

        clear: function()
        {
            // clear object properties (but not member functions)
            Gitana.deleteProperties(this, false);

            // empty keys
            this.__keys('empty');
        },

        /**
         * @override
         *
         * Convert the json response object into the things we want to preserve on the object.
         * This should set the "object" property but may choose to set other properties as well.
         *
         * @param response
         */
        handleResponse: function(response)
        {
            this.clear();

            if (response)
            {
                // is it a gitana map?
                if (response.totalRows && response.size && response.offset) {
                    this.__totalRows(response.totalRows());
                    this.__size(response.size());
                    this.__offset(response.offset());
                }
                else
                {
                    // otherwise assume it is a gitana result set
                    this.__totalRows(response["total_rows"]);
                    this.__size(response["size"]);
                    this.__offset(response["offset"]);
                }

                if (response.rows)
                {
                    // parse array
                    if (Gitana.isArray(response.rows))
                    {
                        for (var i = 0; i < response.rows.length; i++)
                        {
                            var o = this.buildObject(response.rows[i]);
                            this[o.getId()] = o;

                            this.__keys().push(o.getId());
                        }
                    }
                    else
                    {
                        // parse object
                        for (var key in response.rows)
                        {
                            if (response.rows.hasOwnProperty(key) && !Gitana.isFunction(response.rows[key]))
                            {
                                var value = response.rows[key];

                                var o = this.buildObject(value);

                                // determine key
                                var k = (o.getId && o.getId());
                                if (!k) {
                                    k = key;
                                }
                                this[k] = o;

                                this.__keys().push(k);
                            }
                        }
                    }
                    this.__size(this.__keys().length);
                }
                else
                {
                    // otherwise, assume it is key/value pairs
                    // it also might be another Gitana Map

                    for (var key in response)
                    {
                        if (response.hasOwnProperty(key) && !Gitana.isFunction(response[key]))
                        {
                            var value = response[key];

                            var o = this.buildObject(value);

                            // determine key
                            var k = (o.getId && o.getId());
                            if (!k) {
                                k = key;
                            }
                            this[k] = o;

                            this[k] = o;

                            this.__keys().push(k);
                        }
                    }
                    this.__size(this.__keys().length);
                }
            }
        },

        /**
         * @abstract
         * ABSTRACT METHOD
         *
         * @param json
         */
        buildObject: function(json)
        {

        },

        get: function(key)
        {
            return this[key];
        },

        asArray: function()
        {
            var array = [];
            for (var i = 0; i < this.__keys().length; i++)
            {
                var k = this.__keys()[i];

                array.push(this[k]);
            }

            return array;
        },

        size: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__size());
                });
            }

            return this.__size();
        },

        offset: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__offset());
                });
            }

            return this.__offset();
        },

        totalRows: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__totalRows());
                });
            }

            return this.__totalRows();
        },

        /**
         * Iterates over the map and fires the callback function in SERIAL for each element in the map.
         * The scope for the callback is the object from the map (i.e. repository object, node object).
         *
         * The arguments to the callback function are (key, value) where value is the same as "this".
         *
         * NOTE: This works against elements in the map in SERIAL.  One at a time.  If you are doing concurrent
         * remote operations for members of the set such that each operation is independent, you may want to use
         * the eachX() method.
         *
         * @chained this
         *
         * @param callback
         */
        each: function(callback)
        {
            return this.then(function() {

                // run functions
                for (var i = 0; i < this.__keys().length; i++)
                {
                    // key and value from the map
                    var key = this.__keys()[i];
                    var value = this[key];

                    // a function that fires our callback
                    // wrap in a closure so that we store the callback and key
                    // note: this = the value wrapped in a chain, so we don't pass in value
                    var f = function(callback, key, index, map)
                    {
                        return function()
                        {
                            callback.call(this, key, this, index);

                            // manually copy resulting value back
                            Gitana.deleteProperties(map[key]);
                            Gitana.copyInto(map[key], this);
                        };

                    }(callback, key, i, this);

                    // create subchain mounted on this chainable and the run function
                    this.subchain(value).then(f);
                }

                return this;
            });
        },

        /**
         * Iterates over the map and fires the callback function in PARALLEL for each element in the map.
         * The scope for the callback is the object from the map (i.e. repository object, node object).
         *
         * The arguments to the callback function are (key, value) where value is the same as "this".
         *
         * NOTE: This works against elements in the map in PARALLEL.  All map members are fired against at the same
         * time on separate timeouts.  There is no guaranteed order for their completion.  If you require serial
         * execution, use the each() method.
         *
         * @chained
         *
         * @param callback
         */
        eachX: function(callback)
        {
            return this.then(function() {

                // create an array of functions that invokes the callback for each element in the array
                var functions = [];
                for (var i = 0; i < this.__keys().length; i++)
                {
                    var key = this.__keys()[i];
                    var value = this[key];

                    var f = function(callback, key, value, index) {

                        return function()
                        {
                            // NOTE: we're running a parallel chain that is managed for us by the Chain then() method.
                            // we can't change the parallel chain but we can subchain from it
                            // in our subchain we run our method
                            // the parallel chain kind of acts like one-hop noop so that we can take over and do our thing
                            this.subchain(value).then(function() {
                                callback.call(this, key, this, index);
                            });
                        };

                    }(callback, key, value, i);

                    functions.push(f);
                }

                // kick off all these functions in parallel
                // adding them to the subchain
                return this.then(functions)

            });
        },

        /**
         * Iterates over the map and applies the callback filter function to each element.
         * It should hand back true if it wants to keep the value and false to remove it.
         *
         * NOTE: the "this" for the callback is the object from the map.
         *
         * @chained
         *
         * @param callback
         */
        filter: function(callback)
        {
            return this.then(function() {

                var keysToKeep = [];
                var keysToRemove = [];

                for (var i = 0; i < this.__keys().length; i++)
                {
                    var key = this.__keys()[i];
                    var object = this[key];

                    var keepIt = callback.call(object);
                    if (keepIt)
                    {
                        keysToKeep.push(key);
                    }
                    else
                    {
                        keysToRemove.push(key);
                    }
                }

                // remove any keys we don't want from the map
                for (var i = 0; i < keysToRemove.length; i++)
                {
                    delete this[keysToRemove[i]];
                }

                // swap keys to keep
                // NOTE: we first clear the keys but we can't use slice(0,0) since that produces a NEW array
                // instead, do this shift trick
                this.__keys('empty');
                for (var i = 0; i < keysToKeep.length; i++)
                {
                    this.__keys().push(keysToKeep[i]);
                }
            });
        },

        /**
         * Applies a comparator to sort the map.
         *
         * If no comparator is applied, the map will be sorted by its modification timestamp (if possible).
         *
         * The comparator can be a string that uses dot-notation to identify a field in the JSON that
         * should be sorted.  (example: "title" or "property1.property2.property3")
         *
         * Finally, the comparator can be a function.  It takes (previousValue, currentValue) and hands back:
         *   -1 if the currentValue is less than the previousValue (should be sorted lower)
         *   0 if they are equivalent
         *   1 if they currentValue is greater than the previousValue (should be sorted higher)
         *
         * @chained
         *
         * @param comparator
         */
        sort: function(comparator)
        {
            return this.then(function() {

                // build a temporary array of values
                var array = [];
                for (var i = 0; i < this.__keys().length; i++) {
                    var key = this.__keys()[i];
                    array.push(this[key]);
                }

                // sort the array
                array.sort(comparator);

                // now reset keys according to the order of the array
                this.__keys("empty");
                for (var i = 0; i < array.length; i++)
                {
                    this.__keys().push(array[i].getId());
                }

            });
        },

        /**
         * Limits the number of elements in the map.
         *
         * @chained
         *
         * @param size
         */
        limit: function(size)
        {
            return this.then(function() {

                var keysToRemove = [];

                if (size > this.__keys().length)
                {
                    // keep everything
                    return;
                }

                // figure out which keys to remove
                for (var i = 0; i < this.__keys().length; i++)
                {
                    if (i >= size)
                    {
                        keysToRemove.push(this.__keys()[i]);
                    }
                }

                // truncate the keys
                // NOTE: we can't use slice here since that produces a new array
                while (this.__keys().length > size)
                {
                    this.__keys().pop();
                }

                // remove any keys to remove from map
                for (var i = 0; i < keysToRemove.length; i++)
                {
                    delete this[keysToRemove[i]];
                }

                // reset the size
                this.__size(this.__keys().length);
            });
        },

        /**
         * Paginates elements in the map.
         *
         * @chained
         *
         * @param pagination
         */
        paginate: function(pagination)
        {
            return this.then(function() {

                var skip = pagination.skip;
                var limit = pagination.limit;

                var keysToRemove = [];

                // figure out which keys to remove
                for (var i = 0; i < this.__keys().length; i++)
                {
                    if (i < skip || i >= skip + limit)
                    {
                        keysToRemove.push(this.__keys()[i]);
                    }
                }

                // truncate the keys
                // NOTE: we can't use slice here since that produces a new array
                while (this.__keys().length > limit + skip)
                {
                    this.__keys().pop();
                }

                // remove any keys to remove from map
                for (var i = 0; i < keysToRemove.length; i++)
                {
                    delete this[keysToRemove[i]];
                }

                // reset the limit
                this.__size(this.__keys().length);
            });
        },

        /**
         * Counts the number of elements in the map and fires it into a callback function.
         */
        count: function(callback)
        {
            if (callback)
            {
                return this.then(function() {
                    callback.call(this, this.__keys().length);
                });
            }

            return this.__keys().length;
        },

        /**
         * Keeps the first element in the map
         */
        keepOne: function(emptyHandler)
        {
            var self = this;

            var json = {};
            if (this.__keys().length > 0)
            {
                json = this[this.__keys()[0]];
            }

            var chainable = this.buildObject(json);

            return this.subchain(chainable).then(function() {

                var chain = this;

                this.subchain(self).then(function() {

                    if (this.__keys().length > 0)
                    {
                        var obj = this[this.__keys()[0]];

                        if (chain.loadFrom)
                        {
                            // for objects, like nodes or branches
                            chain.loadFrom(obj);
                        }
                        else
                        {
                            // non-objects? (i.e. binary or attachment maps)
                            chain.handleResponse(obj);
                        }
                    }
                    else
                    {
                        var err = new Gitana.Error();
                        err.name = "Empty Map";
                        err.message = "The map doesn't have any elements in it";

                        if (emptyHandler)
                        {
                            return emptyHandler.call(self, err);
                        }
                        else
                        {
                            this.error(err);
                        }
                    }

                });
            });
        },

        /**
         * Selects an individual element from the map and continues the chain.
         *
         * @param key
         */
        select: function(key)
        {
            var self = this;

            var json = {};
            if (this[key])
            {
                json = this[key];
            }

            // what we hand back
            var result = this.subchain(this.buildObject(json));

            // preload some work
            return result.then(function() {

                var chain = this;

                this.subchain(self).then(function() {

                    var obj = this[key];
                    if (!obj)
                    {
                        var err = new Gitana.Error();
                        err.name = "No element with key: " + key;
                        err.message = err.name;

                        this.error(err);

                        return false;
                    }

                    if (result.loadFrom)
                    {
                        // for objects, like nodes or branches
                        chain.loadFrom(obj);
                    }
                    else
                    {
                        // non-objects? (i.e. binary or attachment maps)
                        chain.handleResponse(obj);
                    }

                });
            });
        }

    });

})(window);
<|endoftext|>


<commit_before>(function(window)
{
    var Gitana = window.Gitana;

    Gitana.Job = Gitana.AbstractClusterObject.extend(
    /** @lends Gitana.Job.prototype */
    {
        /**
         * @constructs
         * @augments Gitana.AbstractClusterObject
         *
         * @class Job
         *
         * @param {Gitana.Cluster} cluster
         * @param [Object] object json object (if no callback required for populating)
         */
        constructor: function(cluster, object)
        {
            this.base(cluster, object);

            this.objectType = function() { return "Gitana.Job"; };
        },

        /**
         * @override
         */
        clone: function()
        {
            return new Gitana.Job(this.getCluster(), this);
        },

        /**
         * @override
         */
        getUri: function()
        {
            return "/jobs/" + this.getId();
        },

        /**
         * @returns {String} the type id of the job
         */
        getType: function()
        {
            return this.get("type");
        },

        /**
         * @returns {String} the id of the principal that this job will run as
         */
        getRunAsPrincipalId: function()
        {
            return this.get("runAsPrincipal");
        },

        /**
         * @returns {String} the domain of the principal that this job will run as
         */
        getRunAsPrincipalDomainId: function()
        {
            return this.get("runAsPrincipalDomain");
        },

        /**
         * @returns {String} the state of the job
         */
        getState: function()
        {
            return this.get("state");
        },

        /**
         * @returns {String} the platform id
         */
        getPlatformId: function()
        {
            return this.get("platformId");
        },

        /**
         * @returns {Number} the priority of the job
         */
        getPriority: function()
        {
            return this.get("priority");
        },

        /**
         * @returns {Number} the number of attempts made to run this job
         */
        getAttempts: function()
        {
            return this.get("attempts");
        },

        /**
         * @returns {Object} when the job is scheduled to start (or null)
         */
        getScheduledStartTime: function()
        {
            return this.get("schedule_start_ms");
        },

        /**
         * @returns [Array] array of status log objects
         */
        getLogEntries: function()
        {
            return this.get("log_entries");
        },

        getCurrentThread: function()
        {
            return this.get("current_thread");
        },

        getCurrentServer: function()
        {
            return this.get("current_server");
        },

        getCurrentServerTimeStamp: function()
        {
            return this.get("current_server_timestamp");
        },

        getSubmittedBy: function()
        {
            return this.get("submitted_by");
        },

        getSubmittedTimestamp: function()
        {
            return this.get("submitted_timestamp");
        },

        getStarted: function()
        {
            return this.get("started");
        },

        getStartedBy: function()
        {
            return this.get("started_by");
        },

        getStartedTimestamp: function()
        {
            return this.get("started_timestamp");
        },

        getStopped: function()
        {
            return this.get("stopped");
        },

        getStoppedTimestamp: function()
        {
            return this.get("stopped_timestamp");
        },

        getPaused: function()
        {
            return this.get("paused");
        },

        getPausedBy: function()
        {
            return this.get("paused_by");
        },

        getPausedTimestamp: function()
        {
            return this.get("paused_timestamp");
        },

        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // ATTACHMENTS
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Hands back an attachments map.
         *
         * @chained attachment map
         *
         * @param local
         *
         * @public
         */
        listAttachments: Gitana.Methods.listAttachments(),

        /**
         * Picks off a single attachment
         *
         * @chained attachment
         *
         * @param attachmentId
         */
        attachment: function(attachmentId)
        {
            return this.listAttachments().select(attachmentId);
        },

        /**
         * Creates an attachment.
         *
         * When using this method from within the JS driver, it really only works for text-based content such
         * as JSON or text.
         *
         * @chained attachment
         *
         * @param attachmentId (use null or false for default attachment)
         * @param contentType
         * @param data
         */
        attach: Gitana.Methods.attach(),

        /**
         * Deletes an attachment.
         *
         * @param attachmentId
         */
        unattach: Gitana.Methods.unattach(),

        /**
         * Generates a URI to a preview resource.
         */
        getPreviewUri: Gitana.Methods.getPreviewUri()
    });

})(window);
<commit_msg>change job getType to getJobType and add getType method that returns job so that job references can be properly built<commit_after>(function(window)
{
    var Gitana = window.Gitana;

    Gitana.Job = Gitana.AbstractClusterObject.extend(
    /** @lends Gitana.Job.prototype */
    {
        /**
         * @constructs
         * @augments Gitana.AbstractClusterObject
         *
         * @class Job
         *
         * @param {Gitana.Cluster} cluster
         * @param [Object] object json object (if no callback required for populating)
         */
        constructor: function(cluster, object)
        {
            this.base(cluster, object);

            this.objectType = function() { return "Gitana.Job"; };
        },

        /**
         * @override
         */
        clone: function()
        {
            return new Gitana.Job(this.getCluster(), this);
        },

        /**
         * @override
         */
        getUri: function()
        {
            return "/jobs/" + this.getId();
        },

        /**
         * @returns {String} the type id of the job
         */
        getJobType: function()
        {
            return this.get("type");
        },

        getType: function()
        {
            return Gitana.TypedIDConstants.TYPE_JOB;
        },

        /**
         * @returns {String} the id of the principal that this job will run as
         */
        getRunAsPrincipalId: function()
        {
            return this.get("runAsPrincipal");
        },

        /**
         * @returns {String} the domain of the principal that this job will run as
         */
        getRunAsPrincipalDomainId: function()
        {
            return this.get("runAsPrincipalDomain");
        },

        /**
         * @returns {String} the state of the job
         */
        getState: function()
        {
            return this.get("state");
        },

        /**
         * @returns {String} the platform id
         */
        getPlatformId: function()
        {
            return this.get("platformId");
        },

        /**
         * @returns {Number} the priority of the job
         */
        getPriority: function()
        {
            return this.get("priority");
        },

        /**
         * @returns {Number} the number of attempts made to run this job
         */
        getAttempts: function()
        {
            return this.get("attempts");
        },

        /**
         * @returns {Object} when the job is scheduled to start (or null)
         */
        getScheduledStartTime: function()
        {
            return this.get("schedule_start_ms");
        },

        /**
         * @returns [Array] array of status log objects
         */
        getLogEntries: function()
        {
            return this.get("log_entries");
        },

        getCurrentThread: function()
        {
            return this.get("current_thread");
        },

        getCurrentServer: function()
        {
            return this.get("current_server");
        },

        getCurrentServerTimeStamp: function()
        {
            return this.get("current_server_timestamp");
        },

        getSubmittedBy: function()
        {
            return this.get("submitted_by");
        },

        getSubmittedTimestamp: function()
        {
            return this.get("submitted_timestamp");
        },

        getStarted: function()
        {
            return this.get("started");
        },

        getStartedBy: function()
        {
            return this.get("started_by");
        },

        getStartedTimestamp: function()
        {
            return this.get("started_timestamp");
        },

        getStopped: function()
        {
            return this.get("stopped");
        },

        getStoppedTimestamp: function()
        {
            return this.get("stopped_timestamp");
        },

        getPaused: function()
        {
            return this.get("paused");
        },

        getPausedBy: function()
        {
            return this.get("paused_by");
        },

        getPausedTimestamp: function()
        {
            return this.get("paused_timestamp");
        },

        //////////////////////////////////////////////////////////////////////////////////////////
        //
        // ATTACHMENTS
        //
        //////////////////////////////////////////////////////////////////////////////////////////

        /**
         * Hands back an attachments map.
         *
         * @chained attachment map
         *
         * @param local
         *
         * @public
         */
        listAttachments: Gitana.Methods.listAttachments(),

        /**
         * Picks off a single attachment
         *
         * @chained attachment
         *
         * @param attachmentId
         */
        attachment: function(attachmentId)
        {
            return this.listAttachments().select(attachmentId);
        },

        /**
         * Creates an attachment.
         *
         * When using this method from within the JS driver, it really only works for text-based content such
         * as JSON or text.
         *
         * @chained attachment
         *
         * @param attachmentId (use null or false for default attachment)
         * @param contentType
         * @param data
         */
        attach: Gitana.Methods.attach(),

        /**
         * Deletes an attachment.
         *
         * @param attachmentId
         */
        unattach: Gitana.Methods.unattach(),

        /**
         * Generates a URI to a preview resource.
         */
        getPreviewUri: Gitana.Methods.getPreviewUri()
    });

})(window);
<|endoftext|>


<commit_before>// Copyright 2015-2020, University of Colorado Boulder

/**
 * Returns a map such that map["locale"]["REPO/stringKey"] will be the string value (with fallbacks to English where needed).
 * Loads each string file only once, and only loads the repository/locale combinations necessary.
 */

'use strict';

const _ = require( 'lodash' ); // eslint-disable-line require-statement-match
const assert = require( 'assert' );
const ChipperConstants = require( '../common/ChipperConstants' );
const ChipperStringUtils = require( '../common/ChipperStringUtils' );
const fs = require( 'fs' );
const grunt = require( 'grunt' );
const localeInfo = require( '../data/localeInfo' ); // Locale information
const path = require( 'path' );

/**
 * Load all the required string files into memory, so we don't load them multiple times (for each usage).
 *
 * @param {Array.<string>} reposWithUsedStrings - All of the repos that have 1+ used strings
 * @param {Array.<string>} locales - All supported locales for this build
 * @returns {Object} - maps [repositoryName][locale] => contents of locale string file
 */
const getStringFilesContents = ( reposWithUsedStrings, locales ) => {
  const stringFilesContents = {}; // maps [repositoryName][locale] => contents of locale string file

  reposWithUsedStrings.forEach( repo => {
    stringFilesContents[ repo ] = {};

    /**
     * Adds a locale into our stringFilesContents map.
     *
     * @param {string} locale
     * @param {boolean} isRTL
     */
    const addLocale = ( locale, isRTL ) => {
      // Read optional string file
      const stringsFilename = path.normalize( `../${locale === ChipperConstants.FALLBACK_LOCALE ? '' : 'babel/'}${repo}/${repo}-strings_${locale}.json` );
      let fileContents;
      try {
        fileContents = grunt.file.readJSON( stringsFilename );
      }
      catch( error ) {
        grunt.log.debug( `missing string file: ${stringsFilename}` );
        fileContents = {};
      }

      // Format the string values
      ChipperStringUtils.formatStringValues( fileContents, isRTL );

      stringFilesContents[ repo ][ locale ] = fileContents;
    };

    locales.forEach( locale => {
      assert( localeInfo[ locale ], `unsupported locale: ${locale}` );
      const isRTL = localeInfo[ locale ].direction === 'rtl';

      // Handle fallback locales
      addLocale( locale, isRTL );
      if ( locale.length > 2 ) {
        const middleLocale = locale.slice( 0, 2 );
        if ( !locales.includes( middleLocale ) ) {
          addLocale( middleLocale, isRTL );
        }
      }
    } );
  } );

  return stringFilesContents;
};

/**
 * @param {Array.<string>} locales
 * @param {Array.<string>} phetLibs - Used to check for bad string dependencies
 * @param {Array.<string>} usedModules - relative file path of the module (filename) from the repos root
 *
 * @returns {Object} - map[locale][stringKey] => {string}
 */
module.exports = function( locales, phetLibs, usedModules ) {

  assert( locales.indexOf( ChipperConstants.FALLBACK_LOCALE ) !== -1, 'fallback locale is required' );

  /**
   * For a given locale, return an array of specific locales that we'll use as fallbacks, e.g.
   * 'zh_CN' => [ 'zh_CN', 'zh', 'en' ]
   * 'es' => [ 'es', 'en' ]
   * 'en' => [ 'en' ]
   *
   * @param {string} locale
   * @returns {Array.<string>}
   */
  const localeFallbacks = locale => {
    return [
      ...( locale !== ChipperConstants.FALLBACK_LOCALE ? [ locale ] : [] ), // e.g. 'zh_CN'
      ...( ( locale.length > 2 && locale.slice( 0, 2 ) !== ChipperConstants.FALLBACK_LOCALE ) ? [ locale.slice( 0, 2 ) ] : [] ), // e.g. 'zh'
      ChipperConstants.FALLBACK_LOCALE // e.g. 'en'
    ];
  };

  // Load the file contents of every single JS module that used any strings
  const usedFileContents = usedModules.map( usedModule => fs.readFileSync( `../${usedModule}`, 'utf-8' ) );

  // Compute which repositories contain one more more used strings (since we'll need to load string files for those
  // repositories).
  let reposWithUsedStrings = [];
  usedFileContents.forEach( fileContent => {
    const allImportStatements = fileContent.match( /import [a-zA-Z_$][a-zA-Z0-9_$]*Strings from '[^\n\r]+Strings.js';/g );
    if ( allImportStatements ) {
      reposWithUsedStrings.push( ...allImportStatements.map( importStatement => {
        const importName = importStatement.match( /\/([\w-]+)Strings\.js/ )[ 1 ];

        // un-camel-case
        let repo = importName;
        for ( let i = 0; i < repo.length; i++ ) {
          if ( repo[ i ] >= 'A' && repo[ i ] <= 'Z' ) {
            repo = repo.slice( 0, i ) + '-' + repo[ i ].toLowerCase() + repo.slice( i + 1 );
          }
        }
        return repo;
      } ) );
    }
  } );
  reposWithUsedStrings = _.uniq( reposWithUsedStrings ).filter( repo => {
    return fs.existsSync( `../${repo}/package.json` );
  } );

  // Compute a map of {repo:string} => {requirejsNamepsace:string}, so we can construct full string keys from strings
  // that would be accessing them, e.g. `joistStrings.ResetAllButton.name` => `JOIST/ResetAllButton.name`.
  const requirejsNamespaceMap = {};
  reposWithUsedStrings.forEach( repo => {
    const packageObject = JSON.parse( fs.readFileSync( `../${repo}/package.json`, 'utf-8' ) );
    requirejsNamespaceMap[ repo ] = packageObject.phet.requirejsNamespace;
  } );

  // Load all the required string files into memory, so we don't load them multiple times (for each usage)
  // maps [repositoryName][locale] => contents of locale string file
  const stringFilesContents = getStringFilesContents( reposWithUsedStrings, locales );

  // Initialize our full stringMap object (which will be filled with results and then returned as our string map).
  const stringMap = {};
  locales.forEach( locale => {
    stringMap[ locale ] = {};
  } );

  // combine our strings into [locale][stringKey] map, using the fallback locale where necessary. In regards to nested
  // strings, this data structure doesn't nest. Instead it gets nested string values, and then sets them with the
  // flat key string like `"FRICTION/a11y.some.string.here": { value: 'My Some String' }`
  reposWithUsedStrings.forEach( repo => {

    // Scan all of the files with string module references, scanning for anything that looks like a string access for
    // our repo. This will include the string module reference, e.g. `joistStrings.ResetAllButton.name`, but could also
    // include slightly more (since we're string parsing), e.g. `joistStrings.ResetAllButton.name.length` would be
    // included, even though only part of that is a string access.
    let stringAccesses = [];
    const prefix = `${_.camelCase( repo )}Strings`; // e.g. joistStrings
    usedFileContents.forEach( ( fileContent, i ) => {
      // Only scan files where we can identify an import for it
      if ( fileContent.includes( `import ${prefix} from` ) ) {

        // Look for normal matches, e.g. `joistStrings.` followed by one or more chunks like:
        // .somethingValuely_alphaNum3r1c
        // [ 'aStringInBracketsBecauseOfSpecialCharacters' ]
        //
        // It will also then end on anything that doesn't look like another one of those chunks
        const matches = fileContent.match( new RegExp( `${prefix}(\\.[a-zA-Z_$][a-zA-Z0-9_$]*|\\[ '[^']+' \\])+[^\\.\\[]`, 'g' ) );
        if ( matches ) {
          stringAccesses.push( ...matches.map( match => match.slice( 0, match.length - 1 ) ).filter( m => m !== `${prefix}.get` ) );
        }

        // Look for backup matches, e.g. `joistStrings.get( 'fullString' )`
        const workaroundMatches = fileContent.match( new RegExp( `${prefix}\\.get\\( '([^']+)' \\)`, 'g' ) );
        if ( workaroundMatches ) {
          stringAccesses.push( ...workaroundMatches.map( match => `${prefix}.${match.slice( match.indexOf( '\'' ) + 1, match.lastIndexOf( '\'' ) )}` ) );
        }
      }
    } );

    // Strip off our prefixes, so our stringAccesses will have things like `'ResetAllButton.name'` inside.
    stringAccesses = _.uniq( stringAccesses ).map( str => str.slice( prefix.length ) );

    // Turn each string access into an array of parts, e.g. '.ResetAllButton.name' => [ 'ResetAllButton', 'name' ]
    // or '[ \'A\' ].B[ \'C\' ]' => [ 'A', 'B', 'C' ]
    const stringKeysByParts = stringAccesses.map( access => access.match( /\.[a-zA-Z_$][a-zA-Z0-9_$]*|\[ '[^']+' \]/g ).map( token => {
      return token.startsWith( '.' ) ? token.slice( 1 ) : token.slice( 3, token.length - 3 );
    } ) );

    // Concatenate the string parts for each access into something that looks like a partial string key, e.g.
    // [ 'ResetAllButton', 'name' ] => 'ResetAllButton.name'
    const partialStringKeys = _.uniq( stringKeysByParts.map( parts => parts.join( '.' ) ) ).filter( key => key !== 'js' );

    // For each string key and locale, we'll look up the string entry and fill it into the stringMap
    partialStringKeys.forEach( partialStringKey => {
      locales.forEach( locale => {
        let stringValue = null;
        for ( const fallbackLocale of localeFallbacks( locale ) ) {
          const stringFileContents = stringFilesContents[ repo ][ fallbackLocale ];
          if ( stringFileContents ) {
            stringValue = ChipperStringUtils.getStringFromMap( stringFileContents, partialStringKey );
            if ( stringValue ) {
              break;
            }
          }
        }
        assert( stringValue !== null, `Missing string information for ${repo} ${partialStringKey}` );

        stringMap[ locale ][ `${requirejsNamespaceMap[ repo ]}/${partialStringKey}` ] = stringValue;
      } );
    } );
  } );

  return stringMap;
};<commit_msg>Additional docs for regex in getStringMap, see https://github.com/phetsims/chipper/issues/922<commit_after>// Copyright 2015-2020, University of Colorado Boulder

/**
 * Returns a map such that map["locale"]["REPO/stringKey"] will be the string value (with fallbacks to English where needed).
 * Loads each string file only once, and only loads the repository/locale combinations necessary.
 */

'use strict';

const _ = require( 'lodash' ); // eslint-disable-line require-statement-match
const assert = require( 'assert' );
const ChipperConstants = require( '../common/ChipperConstants' );
const ChipperStringUtils = require( '../common/ChipperStringUtils' );
const fs = require( 'fs' );
const grunt = require( 'grunt' );
const localeInfo = require( '../data/localeInfo' ); // Locale information
const path = require( 'path' );

/**
 * Load all the required string files into memory, so we don't load them multiple times (for each usage).
 *
 * @param {Array.<string>} reposWithUsedStrings - All of the repos that have 1+ used strings
 * @param {Array.<string>} locales - All supported locales for this build
 * @returns {Object} - maps [repositoryName][locale] => contents of locale string file
 */
const getStringFilesContents = ( reposWithUsedStrings, locales ) => {
  const stringFilesContents = {}; // maps [repositoryName][locale] => contents of locale string file

  reposWithUsedStrings.forEach( repo => {
    stringFilesContents[ repo ] = {};

    /**
     * Adds a locale into our stringFilesContents map.
     *
     * @param {string} locale
     * @param {boolean} isRTL
     */
    const addLocale = ( locale, isRTL ) => {
      // Read optional string file
      const stringsFilename = path.normalize( `../${locale === ChipperConstants.FALLBACK_LOCALE ? '' : 'babel/'}${repo}/${repo}-strings_${locale}.json` );
      let fileContents;
      try {
        fileContents = grunt.file.readJSON( stringsFilename );
      }
      catch( error ) {
        grunt.log.debug( `missing string file: ${stringsFilename}` );
        fileContents = {};
      }

      // Format the string values
      ChipperStringUtils.formatStringValues( fileContents, isRTL );

      stringFilesContents[ repo ][ locale ] = fileContents;
    };

    locales.forEach( locale => {
      assert( localeInfo[ locale ], `unsupported locale: ${locale}` );
      const isRTL = localeInfo[ locale ].direction === 'rtl';

      // Handle fallback locales
      addLocale( locale, isRTL );
      if ( locale.length > 2 ) {
        const middleLocale = locale.slice( 0, 2 );
        if ( !locales.includes( middleLocale ) ) {
          addLocale( middleLocale, isRTL );
        }
      }
    } );
  } );

  return stringFilesContents;
};

/**
 * @param {Array.<string>} locales
 * @param {Array.<string>} phetLibs - Used to check for bad string dependencies
 * @param {Array.<string>} usedModules - relative file path of the module (filename) from the repos root
 *
 * @returns {Object} - map[locale][stringKey] => {string}
 */
module.exports = function( locales, phetLibs, usedModules ) {

  assert( locales.indexOf( ChipperConstants.FALLBACK_LOCALE ) !== -1, 'fallback locale is required' );

  /**
   * For a given locale, return an array of specific locales that we'll use as fallbacks, e.g.
   * 'zh_CN' => [ 'zh_CN', 'zh', 'en' ]
   * 'es' => [ 'es', 'en' ]
   * 'en' => [ 'en' ]
   *
   * @param {string} locale
   * @returns {Array.<string>}
   */
  const localeFallbacks = locale => {
    return [
      ...( locale !== ChipperConstants.FALLBACK_LOCALE ? [ locale ] : [] ), // e.g. 'zh_CN'
      ...( ( locale.length > 2 && locale.slice( 0, 2 ) !== ChipperConstants.FALLBACK_LOCALE ) ? [ locale.slice( 0, 2 ) ] : [] ), // e.g. 'zh'
      ChipperConstants.FALLBACK_LOCALE // e.g. 'en'
    ];
  };

  // Load the file contents of every single JS module that used any strings
  const usedFileContents = usedModules.map( usedModule => fs.readFileSync( `../${usedModule}`, 'utf-8' ) );

  // Compute which repositories contain one more more used strings (since we'll need to load string files for those
  // repositories).
  let reposWithUsedStrings = [];
  usedFileContents.forEach( fileContent => {
    // [a-zA-Z_$][a-zA-Z0-9_$] ---- general JS identifiers, first character can't be a number
    // [^\n\r] ---- grab everything except for newlines here, so we get everything
    const allImportStatements = fileContent.match( /import [a-zA-Z_$][a-zA-Z0-9_$]*Strings from '[^\n\r]+Strings.js';/g );
    if ( allImportStatements ) {
      reposWithUsedStrings.push( ...allImportStatements.map( importStatement => {
        // Grabs out the prefix before `Strings.js` (without the leading slash too)
        const importName = importStatement.match( /\/([\w-]+)Strings\.js/ )[ 1 ];

        // un-camel-case
        let repo = importName;
        for ( let i = 0; i < repo.length; i++ ) {
          if ( repo[ i ] >= 'A' && repo[ i ] <= 'Z' ) {
            repo = repo.slice( 0, i ) + '-' + repo[ i ].toLowerCase() + repo.slice( i + 1 );
          }
        }
        return repo;
      } ) );
    }
  } );
  reposWithUsedStrings = _.uniq( reposWithUsedStrings ).filter( repo => {
    return fs.existsSync( `../${repo}/package.json` );
  } );

  // Compute a map of {repo:string} => {requirejsNamepsace:string}, so we can construct full string keys from strings
  // that would be accessing them, e.g. `joistStrings.ResetAllButton.name` => `JOIST/ResetAllButton.name`.
  const requirejsNamespaceMap = {};
  reposWithUsedStrings.forEach( repo => {
    const packageObject = JSON.parse( fs.readFileSync( `../${repo}/package.json`, 'utf-8' ) );
    requirejsNamespaceMap[ repo ] = packageObject.phet.requirejsNamespace;
  } );

  // Load all the required string files into memory, so we don't load them multiple times (for each usage)
  // maps [repositoryName][locale] => contents of locale string file
  const stringFilesContents = getStringFilesContents( reposWithUsedStrings, locales );

  // Initialize our full stringMap object (which will be filled with results and then returned as our string map).
  const stringMap = {};
  locales.forEach( locale => {
    stringMap[ locale ] = {};
  } );

  // combine our strings into [locale][stringKey] map, using the fallback locale where necessary. In regards to nested
  // strings, this data structure doesn't nest. Instead it gets nested string values, and then sets them with the
  // flat key string like `"FRICTION/a11y.some.string.here": { value: 'My Some String' }`
  reposWithUsedStrings.forEach( repo => {

    // Scan all of the files with string module references, scanning for anything that looks like a string access for
    // our repo. This will include the string module reference, e.g. `joistStrings.ResetAllButton.name`, but could also
    // include slightly more (since we're string parsing), e.g. `joistStrings.ResetAllButton.name.length` would be
    // included, even though only part of that is a string access.
    let stringAccesses = [];
    const prefix = `${_.camelCase( repo )}Strings`; // e.g. joistStrings
    usedFileContents.forEach( ( fileContent, i ) => {
      // Only scan files where we can identify an import for it
      if ( fileContent.includes( `import ${prefix} from` ) ) {

        // Look for normal matches, e.g. `joistStrings.` followed by one or more chunks like:
        // .somethingValuely_alphaNum3r1c
        // [ 'aStringInBracketsBecauseOfSpecialCharacters' ]
        //
        // It will also then end on anything that doesn't look like another one of those chunks
        // [a-zA-Z_$][a-zA-Z0-9_$]* ---- this grabs things that looks like valid JS identifiers
        // \\[ '[^']+' \\])+ ---- this grabs things like our second case above
        // [^\\.\\[] ---- matches something at the end that is NOT either of those other two cases
        const matches = fileContent.match( new RegExp( `${prefix}(\\.[a-zA-Z_$][a-zA-Z0-9_$]*|\\[ '[^']+' \\])+[^\\.\\[]`, 'g' ) );
        if ( matches ) {
          stringAccesses.push( ...matches.map( match => match.slice( 0, match.length - 1 ) ).filter( m => m !== `${prefix}.get` ) );
        }

        // Look for backup matches, e.g. `joistStrings.get( 'fullString' )`
        const workaroundMatches = fileContent.match( new RegExp( `${prefix}\\.get\\( '([^']+)' \\)`, 'g' ) );
        if ( workaroundMatches ) {
          stringAccesses.push( ...workaroundMatches.map( match => `${prefix}.${match.slice( match.indexOf( '\'' ) + 1, match.lastIndexOf( '\'' ) )}` ) );
        }
      }
    } );

    // Strip off our prefixes, so our stringAccesses will have things like `'ResetAllButton.name'` inside.
    stringAccesses = _.uniq( stringAccesses ).map( str => str.slice( prefix.length ) );

    // Turn each string access into an array of parts, e.g. '.ResetAllButton.name' => [ 'ResetAllButton', 'name' ]
    // or '[ \'A\' ].B[ \'C\' ]' => [ 'A', 'B', 'C' ]
    // Regex grabs either `.identifier` or `[ 'text' ]`.
    const stringKeysByParts = stringAccesses.map( access => access.match( /\.[a-zA-Z_$][a-zA-Z0-9_$]*|\[ '[^']+' \]/g ).map( token => {
      return token.startsWith( '.' ) ? token.slice( 1 ) : token.slice( 3, token.length - 3 );
    } ) );

    // Concatenate the string parts for each access into something that looks like a partial string key, e.g.
    // [ 'ResetAllButton', 'name' ] => 'ResetAllButton.name'
    const partialStringKeys = _.uniq( stringKeysByParts.map( parts => parts.join( '.' ) ) ).filter( key => key !== 'js' );

    // For each string key and locale, we'll look up the string entry and fill it into the stringMap
    partialStringKeys.forEach( partialStringKey => {
      locales.forEach( locale => {
        let stringValue = null;
        for ( const fallbackLocale of localeFallbacks( locale ) ) {
          const stringFileContents = stringFilesContents[ repo ][ fallbackLocale ];
          if ( stringFileContents ) {
            stringValue = ChipperStringUtils.getStringFromMap( stringFileContents, partialStringKey );
            if ( stringValue ) {
              break;
            }
          }
        }
        assert( stringValue !== null, `Missing string information for ${repo} ${partialStringKey}` );

        stringMap[ locale ][ `${requirejsNamespaceMap[ repo ]}/${partialStringKey}` ] = stringValue;
      } );
    } );
  } );

  return stringMap;
};<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado

/*
 * A bucket that can be used to store spherical objects.  Manages the addition
 * and removal of the spheres, and stacks them as the are added, and manages
 * the stack as spheres are removed.
 *
 * This expects the spheres to have certain properties, please inspect the
 * code to understand the "contract" between the bucket and the spheres.
 *
 * @author John Blanco
 */
define( function( require ) {
  'use strict';
  var Vector2 = require( 'DOT/Vector2' );
  var Inheritance = require( 'PHETCOMMON/util/Inheritance' );
  var Bucket = require( 'PHETCOMMON/model/Bucket' );

  function SphereBucket( options ) {
    Bucket.call( this, options );
    this.particleRadius = options.particleRadius || 10;
    this.yOffset = -this.particleRadius / 2; // Empirically determined, for positioning particles inside the bucket.
    this.particles = [];
  }

  // Inherit from base type.
  Inheritance.inheritPrototype( SphereBucket, Bucket );

  SphereBucket.prototype.addParticleFirstOpen = function( particle, animate ) {
    particle.destination = this.getFirstOpenLocation();
    if ( !animate ) {
      particle.position = particle.destination;
    }
    this.particles.push( particle );
    var thisBucket = this;
    particle.once( 'change:userControlled', function( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } );
  };

  SphereBucket.prototype.addParticleNearestOpen = function( particle, animate ) {
    particle.destination = this.getNearestOpenLocation( particle.position );
    if ( !animate ) {
      particle.position = particle.destination;
    }
    this.particles.push( particle );
    var thisBucket = this;
    particle.once( 'change:userControlled', function( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } );
  };

  SphereBucket.prototype.removeParticle = function( particle ) {
    if ( this.particles.indexOf( particle ) === -1 ) {
      console.log( "Error: Attempt to remove particle not contained in bucket, ignoring." );
      return;
    }
    this.particles = _.without( this.particles, particle );
    this.relayoutBucketParticles();
  };

  SphereBucket.prototype.isPositionOpen = function( position ) {
    var positionOpen = true;
    for ( var i = 0; i < this.particles.length; i++ ) {
      var particle = this.particles[ i ];
      if ( particle.position.equals( position ) ) {
        positionOpen = false;
        break;
      }
    }
    return positionOpen;
  };

  SphereBucket.prototype.getFirstOpenLocation = function() {
    var openLocation = Vector2.ZERO;
    var usableWidth = this.size.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = this.particleRadius * 2;
    var numParticlesInLayer = Math.floor( usableWidth / ( this.particleRadius * 2 ) );
    var row = 0;
    var positionInLayer = 0;
    var found = false;
    while ( !found ) {
      var testLocation = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                      this.getYPositionForLayer( row ) );
      if ( this.isPositionOpen( testLocation ) ) {
        // We found a location that is open.
        openLocation = testLocation;
        found = true;
        continue;
      }
      else {
        positionInLayer++;
        if ( positionInLayer >= numParticlesInLayer ) {
          // Move to the next layer.
          row++;
          positionInLayer = 0;
          numParticlesInLayer--;
          offsetFromBucketEdge += this.particleRadius;
          if ( numParticlesInLayer === 0 ) {
            // This algorithm doesn't handle the situation where
            // more particles are added than can be stacked into
            // a pyramid of the needed size, but so far it hasn't
            // needed to.  If this requirement changes, the
            // algorithm will need to change too.
            numParticlesInLayer = 1;
            offsetFromBucketEdge -= this.particleRadius;
          }
        }
      }
    }
    return openLocation;
  };

  SphereBucket.prototype.getLayerForYPosition = function( yPosition ) {
    return Math.abs( Math.round( ( yPosition - ( this.position.y + this.yOffset ) ) / ( this.particleRadius * 2 * 0.866 ) ) );
  };

  /*
   * Get the nearest open location to the provided current location.  This
   * is used for particle stacking.
   *
   * @param location
   * @return
   */
  SphereBucket.prototype.getNearestOpenLocation = function( position ) {
    // Determine the highest occupied layer.  The bottom layer is 0.
    var highestOccupiedLayer = 0;
    var self = this;
    _.each( this.particles, function( particle ) {
      var layer = self.getLayerForYPosition( particle.position.y );
      if ( layer > highestOccupiedLayer ) {
        highestOccupiedLayer = layer;
      }
    } );

    // Make a list of all open locations in the occupied layers.
    var openLocations = [];
    var placeableWidth = this.size.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = ( this.size.width - placeableWidth ) / 2 + this.particleRadius;
    var numParticlesInLayer = Math.floor( placeableWidth / ( this.particleRadius * 2 ) );

    // Loop, searching for open positions in the particle stack.
    for ( var layer = 0; layer <= highestOccupiedLayer + 1; layer++ ) {

      // Add all open locations in the current layer.
      for ( var positionInLayer = 0; positionInLayer < numParticlesInLayer; positionInLayer++ ) {
        var testPosition = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                        this.getYPositionForLayer( layer ) );
        if ( this.isPositionOpen( testPosition ) ) {

          // We found a location that is unoccupied.
          if ( layer === 0 || this.countSupportingParticles( testPosition ) === 2 ) {
            // This is a valid open location.
            openLocations.push( testPosition );
          }
        }
      }

      // Adjust variables for the next layer.
      numParticlesInLayer--;
      offsetFromBucketEdge += this.particleRadius;
      if ( numParticlesInLayer === 0 ) {
        // If the stacking pyramid is full, meaning that there are
        // no locations that are open within it, this algorithm
        // classifies the locations directly above the top of the
        // pyramid as being open.  This would result in a stack
        // of particles with a pyramid base.  So far, this hasn't
        // been a problem, but this limitation may limit
        // reusability of this algorithm.
        numParticlesInLayer = 1;
        offsetFromBucketEdge -= this.particleRadius;
      }
    }

    // Find the closest open location to the provided current location.
    // Only the X-component is used for this determination, because if
    // the Y-component is used the particles often appear to fall sideways
    // when released above the bucket, which just looks weird.
    var closestOpenLocation = openLocations[0] || Vector2.ZERO;

    _.each( openLocations, function( location ) {
      if ( location.distance( position ) < closestOpenLocation.distance( position ) ) {
        // This location is closer.
        closestOpenLocation = location;
      }
    } );
    return closestOpenLocation;
  };

  SphereBucket.prototype.getYPositionForLayer = function( layer ) {
    return this.position.y + this.yOffset + layer * this.particleRadius * 2 * 0.866;
  };

  /*
   * Determine whether a particle is 'dangling', i.e. hanging above an open
   * space in the stack of particles.  Dangling particles should fall.
   */
  SphereBucket.prototype.isDangling = function( particle ) {
    var onBottomRow = particle.destination.y === this.position.y + this.yOffset;
    return !onBottomRow && this.countSupportingParticles( particle.destination ) < 2;
  };

  SphereBucket.prototype.countSupportingParticles = function( position ) {
    var count = 0;
    for ( var i = 0; i < this.particles.length; i++ ) {
      var p = this.particles[i];
      if ( p.position.y < position.y && // Must be in a lower layer
           p.position.distance( position ) < this.particleRadius * 3 ) {
        // Must be a supporting particle.
        count++;
      }
    }
    return count;
  };

  SphereBucket.prototype.relayoutBucketParticles = function() {
    var particleMoved;
    do {
      for ( var i = 0; i < this.particles.length; i++ ) {
        particleMoved = false;
        var particle = this.particles[i];
        if ( this.isDangling( particle ) ) {
          particle.destination = this.getNearestOpenLocation( particle.position );
          particleMoved = true;
          break;
        }
      }
    } while ( particleMoved );
  };

  SphereBucket.prototype.containsParticle = function( particle ) {
    return this.particles.indexOf( particle ) !== -1;
  };

  return SphereBucket;
} );
<commit_msg>Fixed an issue that was causing dangling particles to be left if a particle was removed from the bottom of a stack that was 3 or more rows tall.<commit_after>// Copyright 2002-2013, University of Colorado

/*
 * A bucket that can be used to store spherical objects.  Manages the addition
 * and removal of the spheres, and stacks them as the are added, and manages
 * the stack as spheres are removed.
 *
 * This expects the spheres to have certain properties, please inspect the
 * code to understand the "contract" between the bucket and the spheres.
 *
 * @author John Blanco
 */
define( function( require ) {
  'use strict';
  var Vector2 = require( 'DOT/Vector2' );
  var Inheritance = require( 'PHETCOMMON/util/Inheritance' );
  var Bucket = require( 'PHETCOMMON/model/Bucket' );

  function SphereBucket( options ) {
    Bucket.call( this, options );
    this.particleRadius = options.particleRadius || 10;
    this.yOffset = -this.particleRadius / 2; // Empirically determined, for positioning particles inside the bucket.
    this.particles = [];
  }

  // Inherit from base type.
  Inheritance.inheritPrototype( SphereBucket, Bucket );

  SphereBucket.prototype.addParticleFirstOpen = function( particle, animate ) {
    particle.destination = this.getFirstOpenLocation();
    if ( !animate ) {
      particle.position = particle.destination;
    }
    this.particles.push( particle );
    var thisBucket = this;
    particle.once( 'change:userControlled', function( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } );
  };

  SphereBucket.prototype.addParticleNearestOpen = function( particle, animate ) {
    particle.destination = this.getNearestOpenLocation( particle.destination );
    if ( !animate ) {
      particle.position = particle.destination;
    }
    this.particles.push( particle );
    var thisBucket = this;
    particle.once( 'change:userControlled', function( m, userControlled ) {
      if ( userControlled ) {
        thisBucket.removeParticle( particle );
      }
    } );
  };

  SphereBucket.prototype.removeParticle = function( particle ) {
    if ( this.particles.indexOf( particle ) === -1 ) {
      console.log( "Error: Attempt to remove particle not contained in bucket, ignoring." );
      return;
    }
    this.particles = _.without( this.particles, particle );
    this.relayoutBucketParticles();
  };

  SphereBucket.prototype.isPositionOpen = function( position ) {
    var positionOpen = true;
    for ( var i = 0; i < this.particles.length; i++ ) {
      var particle = this.particles[ i ];
      if ( particle.destination.equals( position ) ) {
        positionOpen = false;
        break;
      }
    }
    return positionOpen;
  };

  SphereBucket.prototype.getFirstOpenLocation = function() {
    var openLocation = Vector2.ZERO;
    var usableWidth = this.size.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = this.particleRadius * 2;
    var numParticlesInLayer = Math.floor( usableWidth / ( this.particleRadius * 2 ) );
    var row = 0;
    var positionInLayer = 0;
    var found = false;
    while ( !found ) {
      var testLocation = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                      this.getYPositionForLayer( row ) );
      if ( this.isPositionOpen( testLocation ) ) {
        // We found a location that is open.
        openLocation = testLocation;
        found = true;
        continue;
      }
      else {
        positionInLayer++;
        if ( positionInLayer >= numParticlesInLayer ) {
          // Move to the next layer.
          row++;
          positionInLayer = 0;
          numParticlesInLayer--;
          offsetFromBucketEdge += this.particleRadius;
          if ( numParticlesInLayer === 0 ) {
            // This algorithm doesn't handle the situation where
            // more particles are added than can be stacked into
            // a pyramid of the needed size, but so far it hasn't
            // needed to.  If this requirement changes, the
            // algorithm will need to change too.
            numParticlesInLayer = 1;
            offsetFromBucketEdge -= this.particleRadius;
          }
        }
      }
    }
    return openLocation;
  };

  SphereBucket.prototype.getLayerForYPosition = function( yPosition ) {
    return Math.abs( Math.round( ( yPosition - ( this.position.y + this.yOffset ) ) / ( this.particleRadius * 2 * 0.866 ) ) );
  };

  /*
   * Get the nearest open location to the provided current location.  This
   * is used for particle stacking.
   *
   * @param location
   * @return
   */
  SphereBucket.prototype.getNearestOpenLocation = function( position ) {
    // Determine the highest occupied layer.  The bottom layer is 0.
    var highestOccupiedLayer = 0;
    var self = this;
    _.each( this.particles, function( particle ) {
      var layer = self.getLayerForYPosition( particle.destination.y );
      if ( layer > highestOccupiedLayer ) {
        highestOccupiedLayer = layer;
      }
    } );

    // Make a list of all open locations in the occupied layers.
    var openLocations = [];
    var placeableWidth = this.size.width - 2 * this.particleRadius;
    var offsetFromBucketEdge = ( this.size.width - placeableWidth ) / 2 + this.particleRadius;
    var numParticlesInLayer = Math.floor( placeableWidth / ( this.particleRadius * 2 ) );

    // Loop, searching for open positions in the particle stack.
    for ( var layer = 0; layer <= highestOccupiedLayer + 1; layer++ ) {

      // Add all open locations in the current layer.
      for ( var positionInLayer = 0; positionInLayer < numParticlesInLayer; positionInLayer++ ) {
        var testPosition = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this.particleRadius,
                                        this.getYPositionForLayer( layer ) );
        if ( this.isPositionOpen( testPosition ) ) {

          // We found a location that is unoccupied.
          if ( layer === 0 || this.countSupportingParticles( testPosition ) === 2 ) {
            // This is a valid open location.
            openLocations.push( testPosition );
          }
        }
      }

      // Adjust variables for the next layer.
      numParticlesInLayer--;
      offsetFromBucketEdge += this.particleRadius;
      if ( numParticlesInLayer === 0 ) {
        // If the stacking pyramid is full, meaning that there are
        // no locations that are open within it, this algorithm
        // classifies the locations directly above the top of the
        // pyramid as being open.  This would result in a stack
        // of particles with a pyramid base.  So far, this hasn't
        // been a problem, but this limitation may limit
        // reusability of this algorithm.
        numParticlesInLayer = 1;
        offsetFromBucketEdge -= this.particleRadius;
      }
    }

    // Find the closest open location to the provided current location.
    // Only the X-component is used for this determination, because if
    // the Y-component is used the particles often appear to fall sideways
    // when released above the bucket, which just looks weird.
    var closestOpenLocation = openLocations[0] || Vector2.ZERO;

    _.each( openLocations, function( location ) {
      if ( location.distance( position ) < closestOpenLocation.distance( position ) ) {
        // This location is closer.
        closestOpenLocation = location;
      }
    } );
    return closestOpenLocation;
  };

  SphereBucket.prototype.getYPositionForLayer = function( layer ) {
    return this.position.y + this.yOffset + layer * this.particleRadius * 2 * 0.866;
  };

  /*
   * Determine whether a particle is 'dangling', i.e. hanging above an open
   * space in the stack of particles.  Dangling particles should fall.
   */
  SphereBucket.prototype.isDangling = function( particle ) {
    var onBottomRow = particle.destination.y === this.position.y + this.yOffset;
    return !onBottomRow && this.countSupportingParticles( particle.destination ) < 2;
  };

  SphereBucket.prototype.countSupportingParticles = function( position ) {
    var count = 0;
    for ( var i = 0; i < this.particles.length; i++ ) {
      var p = this.particles[i];
      if ( p.destination.y < position.y && // Must be in a lower layer
           p.destination.distance( position ) < this.particleRadius * 3 ) {
        // Must be a supporting particle.
        count++;
      }
    }
    return count;
  };

  SphereBucket.prototype.relayoutBucketParticles = function() {
    var particleMoved;
    do {
      for ( var i = 0; i < this.particles.length; i++ ) {
        particleMoved = false;
        var particle = this.particles[i];
        if ( this.isDangling( particle ) ) {
          particle.destination = this.getNearestOpenLocation( particle.destination );
          particleMoved = true;
          break;
        }
      }
    } while ( particleMoved );
  };

  SphereBucket.prototype.containsParticle = function( particle ) {
    return this.particles.indexOf( particle ) !== -1;
  };

  return SphereBucket;
} );
<|endoftext|>


<commit_before>(function(app) {
  'use strict';

  var helper = app.helper || require('../helper.js');

  var ModuleUnit = function(props) {
    this.module = props.module;
    this.port = props.port;
  };

  ModuleUnit.prototype.equal = function(other) {
    if (!other)
      return false;
    return Object.keys(this).every(function(key) {
      return helper.equal(this[key], other[key]);
    }.bind(this));
  };

  ModuleUnit.prototype.portType = function() {
    return this.port.type();
  };

  ModuleUnit.prototype.portPlugDisabled = function(value) {
    return this.port.plugDisabled(value);
  };

  ModuleUnit.prototype.portVisible = function(value) {
    return this.port.visible(value);
  };

  ModuleUnit.prototype.portSocketConnected = function(value) {
    return this.port.socketConnected(value);
  };

  ModuleUnit.prototype.portPlugHighlighted = function(value) {
    return this.port.plugHighlighted(value);
  };

  ModuleUnit.prototype.portSocketHighlighted = function(value) {
    return this.port.socketHighlighted(value);
  };

  ModuleUnit.prototype.circuitElementMember = function() {
    return this.module.circuitElementMember(this.port.name());
  };

  ModuleUnit.prototype.plugPosition = function() {
    return this.module.plugPosition(this.port);
  };

  ModuleUnit.prototype.socketPosition = function() {
    return this.module.socketPosition(this.port);
  };

  ModuleUnit.prototype.labelHighlighted = function(value) {
    this.port.labelHighlighted(value);

    // module is deletable if all port labels are NOT highlighted
    this.module.deletable(this.module.ports().every(function(port) {
      return !port.labelHighlighted();
    }));
  };

  ModuleUnit.prototype.addRelation = function(relation) {
    this.module.relations().push(relation);
    this.port.relations().push(relation);
  };

  ModuleUnit.prototype.removeRelation = function(relation) {
    helper.remove(this.module.relations(), relation);
    helper.remove(this.port.relations(), relation);
  };

  ModuleUnit.fromModuleAndPortName = function(module, portName) {
    if (!module)
      return null;

    var port = module.port(portName);
    if (!port)
      return null;

    return new ModuleUnit({ module: module, port: port });
  };

  if (typeof module !== 'undefined' && module.exports)
    module.exports = ModuleUnit;
  else
    app.ModuleUnit = ModuleUnit;
})(this.app || (this.app = {}));
<commit_msg>Add method to get/set 'socketDisabled' property of a port in module-unit<commit_after>(function(app) {
  'use strict';

  var helper = app.helper || require('../helper.js');

  var ModuleUnit = function(props) {
    this.module = props.module;
    this.port = props.port;
  };

  ModuleUnit.prototype.equal = function(other) {
    if (!other)
      return false;
    return Object.keys(this).every(function(key) {
      return helper.equal(this[key], other[key]);
    }.bind(this));
  };

  ModuleUnit.prototype.portType = function() {
    return this.port.type();
  };

  ModuleUnit.prototype.portPlugDisabled = function(value) {
    return this.port.plugDisabled(value);
  };

  ModuleUnit.prototype.portSocketDisabled = function(value) {
    return this.port.socketDisabled(value);
  };

  ModuleUnit.prototype.portVisible = function(value) {
    return this.port.visible(value);
  };

  ModuleUnit.prototype.portSocketConnected = function(value) {
    return this.port.socketConnected(value);
  };

  ModuleUnit.prototype.portPlugHighlighted = function(value) {
    return this.port.plugHighlighted(value);
  };

  ModuleUnit.prototype.portSocketHighlighted = function(value) {
    return this.port.socketHighlighted(value);
  };

  ModuleUnit.prototype.circuitElementMember = function() {
    return this.module.circuitElementMember(this.port.name());
  };

  ModuleUnit.prototype.plugPosition = function() {
    return this.module.plugPosition(this.port);
  };

  ModuleUnit.prototype.socketPosition = function() {
    return this.module.socketPosition(this.port);
  };

  ModuleUnit.prototype.labelHighlighted = function(value) {
    this.port.labelHighlighted(value);

    // module is deletable if all port labels are NOT highlighted
    this.module.deletable(this.module.ports().every(function(port) {
      return !port.labelHighlighted();
    }));
  };

  ModuleUnit.prototype.addRelation = function(relation) {
    this.module.relations().push(relation);
    this.port.relations().push(relation);
  };

  ModuleUnit.prototype.removeRelation = function(relation) {
    helper.remove(this.module.relations(), relation);
    helper.remove(this.port.relations(), relation);
  };

  ModuleUnit.fromModuleAndPortName = function(module, portName) {
    if (!module)
      return null;

    var port = module.port(portName);
    if (!port)
      return null;

    return new ModuleUnit({ module: module, port: port });
  };

  if (typeof module !== 'undefined' && module.exports)
    module.exports = ModuleUnit;
  else
    app.ModuleUnit = ModuleUnit;
})(this.app || (this.app = {}));
<|endoftext|>