<commit_before>dojo.declare( 'JBrowse.Model.TrackMetaData', null,
/**
 * @lends JBrowse.Model.TrackMetaData.prototype
 */
{
    /**
     * Data store for track metadata, supporting faceted
     * (parameterized) searching.  Keeps all of the track metadata,
     * and the indexes thereof, in memory.
     * @constructs
     * @param args.trackConfigs {Array} array of track configuration
     */
    constructor: function( args ) {
        // set up our facet name discrimination: what facets we will
        // actually provide search on
        var non_facet_attrs = this.getIdentityAttributes();
        non_facet_attrs.push('conf');
        this._filterFacet = function( facetName ) {
            var userfilter = args.filterFacets || function() {return true;};
            return userfilter(facetName)
                   && ! dojo.some( non_facet_attrs, function(a) { return a == facetName;});
        };

        // set up our onReady callbacks to fire once the data is
        // loaded
        if( ! dojo.isArray( args.onReady ) ){
            this.onReadyFuncs = args.onReady ? [ args.onReady ] : [];
        } else {
            this.onReadyFuncs = dojo.clone(args.onReady);
        }

        // interpret the track configurations as a metadata store
        this._indexItems(
            {
                store: this,
                items: dojo.map( args.trackConfigs, function(conf) {
                    var metarecord = dojo.clone( conf.metadata || {} );
                    metarecord.label = conf.label;
                    metarecord.key = conf.key;
                    metarecord.conf = conf;
                    if( conf.category )
                        metarecord.category = conf.category;
                    return metarecord;
                },this)
            }
        );

        // fetch and index all the items from each of the stores
        var stores_fetched_count = 0;
        if( ! args.metadataStores || ! args.metadataStores.length ) {
            // if we don't actually have any stores besides the track
            // confs, we're ready now.
            this._ready();
        } else  {
            // index the track metadata from each of the stores

            var storeFetchFinished = dojo.hitch( this, function() {
                if( ++stores_fetched_count == args.metadataStores.length )
                    this._ready();
            });
            dojo.forEach( args.metadataStores, function(store) {
                store.fetch({
                    scope: this,
                    onComplete: Util.debugHandler( this, function(items) {
                        // build our indexes
                        this._indexItems({ store: store, items: items, supplementalOnly: true });

                        // if this is the last store to be fetched, call
                        // our onReady callbacks
                        storeFetchFinished();
                    }),
                    onError: storeFetchFinished
                });
            },this);
        }
     },

    /**
     * Set the store's state to be ready (i.e. loaded), and calls all
     * our onReady callbacks.
     * @private
     */
    _ready: function() {
        this.ready = true;
        dojo.forEach( this.onReadyFuncs, function(f) {
                          f.call( this, this );
                      }, this );
        this.onReadyFuncs = [];
    },

    _indexItems: function( args ) {
        // get our (filtered) list of facets we will index for
        var store = args.store,
            items = args.items;

        var storeAttributes = {};

        // convert the items to a uniform format
        items = dojo.map( items, function( item ) {
                              var itemattrs = store.getAttributes(item);

                              //convert the item into a uniform data format of plain objects
                              var newitem = {};
                              dojo.forEach( itemattrs, function(attr) {
                                                storeAttributes[attr] = 1;
                                                newitem[attr] = store.getValue(item,attr);
                                            });
                              return newitem;
                          },
                          this
                        );

        // merge them with any existing records, filtering out ones
        // that should be ignored if we were passed
        // 'supplementalOnly', and update the identity index
        this.identIndex = this.identIndex || {};
        items = dojo.map( items, function(item) {
                                 // merge the new item attributes with any existing
                                 // record for this item
                                 var ident = this.getIdentity(item);
                                 var existingItem = this.identIndex[ ident ];
                                 if( args.supplementalOnly && !existingItem) {
                                     // skip this item if we are supplementalOnly and it
                                     // does not already exist
                                     return null;
                                 }
                                 return this.identIndex[ ident ] = dojo.mixin( existingItem || {}, item );
                             },
                             this
                           );
        // filter out nulls
        items = dojo.filter( items, function(i) { return i;});

        // update our facet list to include any new attrs these
        // items have
        var new_facets = this._addFacets( dojof.keys( storeAttributes ) );

        // initialize indexes for any new facets
        this.facetIndexes = this.facetIndexes || { itemCount: 0, bucketCount: 0, byName: {} };
        dojo.forEach( new_facets, function(facet) {
            if( ! this.facetIndexes.byName[facet] ) {
                this.facetIndexes.bucketCount++;
                this.facetIndexes.byName[facet] = { itemCount: 0, bucketCount: 0, byValue: {} };
            }
        }, this);

        // now update the indexes with the new facets
        if( new_facets.length ) {
            var gotDataForItem = {};
            dojo.forEach( new_facets, function(f){ gotDataForItem[f] = {};});

            dojo.forEach( items, function( item ) {
                this.facetIndexes.itemCount++;
                dojo.forEach( new_facets, function( facet ) {
                    var value = this.getValue( item, facet, undefined );
                    if( typeof value == 'undefined' )
                        return;
                    gotDataForItem[facet][this.getIdentity(item)] = 1;
                    this._indexItem( facet, value, item );
                },this);
            }, this);

            // index the items that do not have data for this facet
            var noDataValue = '(no data)';
            dojo.forEach( new_facets, function(facet) {
                var gotSomeWithNoData = false;
                dojo.forEach( dojof.values( this.identIndex ), function(item) {
                    if( ! gotDataForItem[facet][this.getIdentity(item)] ) {
                        gotSomeWithNoData = true;
                        this._indexItem( facet, noDataValue, item );
                    }
                },this);
            },this);

            // calculate the rank of the facets: make an array of
            // facet names sorted by smallest average bucket size,
            // descending
            this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
                a = this.facetIndexes.byName[a];
                b = this.facetIndexes.byName[b];
                return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
            }));
        }
    },

    /**
     * Add an item to the indexes for the given facet name and value.
     * @private
     */
    _indexItem: function( facet, value, item ) {
        var facetValues = this.facetIndexes.byName[facet];
        var bucket = facetValues.byValue[value];
        if( !bucket ) {
            bucket = facetValues.byValue[value] = { itemCount: 0, items: [] };
            facetValues.bucketCount++;
        }
        bucket.itemCount++;
        bucket.items.push(item);
    },

    /**
     * Given an array of string facet names, add records for them,
     * initializing the necessary data structures.
     * @private
     * @returns {Array[String]} facet names that did not already exist
     */
    _addFacets: function( facetNames ) {
        var old_facets = this.facets || [];
        var seen = {};
        this.facets = dojo.filter(
            old_facets.concat( facetNames ),
            function(facetName) {
                var take = this._filterFacet(facetName) && !seen[facetName];
                seen[facetName] = true;
                return take;
            },
            this
        );
        return this.facets.slice( old_facets.length );
    },

    /**
     * Get an array of the text names of the facets that are defined
     * in this track metadata.
     * @param callback {Function} called as callback( [facet,facet,...] )
     */
    getFacets: function( callback ) {
        return this.facets;
    },

    /**
     * Get an Array of the distinct values for a given facet name.
     * @param facetName {String} the facet name
     * @returns {Array} distinct values for that facet
     */
    getFacetValues: function( facetName ) {
        var index = this.facetIndexes.byName[facetName];
        if( !index )
            return [];

        return dojof.keys( index.byValue );
    },

    /**
     * Add a callback to be called when this store is ready (i.e. loaded).
     */
    onReady: function( callback ) {
        if( this.ready ) {
            callback.call( this, this );
        }
        else {
            this.onReadyFuncs.push( callback );
        }
    },

    // dojo.data.api.Read support

    getValue: function( i, attr, defaultValue ) {
        var v = i[attr];
        return typeof v == 'undefined' ? defaultValue : v;
    },
    getValues: function( i, attr ) {
        var a = [ i[attr] ];
        return typeof a[0] == 'undefined' ? [] : a;
    },

    getAttributes: function(item)  {
        return dojof.keys( item );
    },

    hasAttribute: function(item,attr) {
        return item.hasOwnProperty(attr);
    },

    containsValue: function(item, attribute, value) {
        return item[attribute] == value;
    },

    isItem: function(item) {
        return typeof item == 'object' && typeof item.label == 'string';
    },

    isItemLoaded: function() {
        return true;
    },

    loadItem: function( args ) {
    },

    // used by the dojo.data.util.simpleFetch mixin to implement fetch()
    _fetchItems: function( keywordArgs, findCallback, errorCallback ) {
        if( ! this.ready ) {
            this.onReady( dojo.hitch( this, '_fetchItems', keywordArgs, findCallback, errorCallback ) );
            return;
        }

        var query = dojo.clone( keywordArgs.query || {} );
        var textFilter = query.text;
        delete query.text;

        var results;

        // if we don't actually have any facets specified in the
        // query, the results are just all the items
        if( ! dojo.some( dojof.values(query), function(q){ return q.length > 0;}) ) {
            results = dojof.values( this.identIndex );
        }
        else {
            // start with an initial set of items that have the desired
            // value for the most specific (smallest avg index bucket
            // size) facet that was specified
            results = (function() {
                           var highestRankedFacet,
                               queryValues = [],
                               index;
                           dojo.some( this.facetIndexes.facetRank, function(facetName) {
                                          if( query[facetName] ) {
                                              highestRankedFacet = facetName;
                                              queryValues = query[highestRankedFacet];
                                              index = this.facetIndexes.byName[highestRankedFacet];
                                              return queryValues.length > 0;
                                          }
                                          return false;
                                      },this);

                           delete query[highestRankedFacet];

                           var set = [];
                           dojo.forEach( queryValues, function(val) {
                              var bucket = index.byValue[val];
                              if( bucket )
                                  set.push.apply( set, bucket.items );
                           },this);
                           return set;
                       }).call(this);

            // and filter this starting set for the other facets
            dojo.forEach( dojof.keys(query), function(facetName) {
                              var desired_values = query[facetName] || [];
                              if( ! desired_values.length )
                                  return;
                              results = dojo.filter(results, function(item) {
                                                        var value = this.getValue(item,facetName);
                                                        return dojo.some( desired_values, function(desired) {
                                                                              return desired == value;
                                                                          },this);
                                                    },this);
                          },this);
        }

        // filter with the text filter, if we have it
	if( typeof textFilter != 'undefined' ) {
            var filter = this._compileTextFilter( textFilter );
            results = dojo.filter( results, function(item) {
                return dojo.some( this.facets, function(facetName) {
                           return filter( this.getValue( item, facetName ) );
                       },this);
            },this);
        }

        // and finally, hand them to the finding callback
        findCallback(results,keywordArgs);
    },

    /**
     * Compile a text search string into a function that tests whether
     * a given piece of text matches that search string.
     * @private
     */
    _compileTextFilter: function( textString ) {
        // escape regex control chars, and convert glob-like chars to
        // their regex equivalents
        textString =
            dojo.regexp.escapeString( textString, '*?' )
            .replace(/\*/g,'.+')
            .replace(/\?/g,'.');

        // split into words, and convert each word into a regexp
        var wordREs = dojo.map( textString.split(/\s+/), function(w) { return new RegExp(w,'i'); });

        // return a function that returns true if all of the words
        // match the string, but in any order
        return function( text ) {
            return dojof.every( wordREs, function(re) { return re.test(text); } );
        };
    },

    getFeatures: function() {
        return {
	    'dojo.data.api.Read': true,
	    'dojo.data.api.Identity': true
	};
    },
    close: function() {},

    getLabel: function(i) {
        return this.getValue(i,'key',undefined);
    },
    getLabelAttributes: function(i) {
        return ['key'];
    },

    // dojo.data.api.Identity support
    getIdentityAttributes: function() {
        return ['label'];
    },
    getIdentity: function(i) {
        return this.getValue(i, 'label', undefined);
    },
    fetchItemByIdentity: function(id) {
        return this.identIndex[id];
    }
});
dojo.require('dojo.data.util.simpleFetch');
dojo.extend( JBrowse.Model.TrackMetaData, dojo.data.util.simpleFetch );<commit_msg>log errors in fetching track metadata<commit_after>dojo.declare( 'JBrowse.Model.TrackMetaData', null,
/**
 * @lends JBrowse.Model.TrackMetaData.prototype
 */
{
    /**
     * Data store for track metadata, supporting faceted
     * (parameterized) searching.  Keeps all of the track metadata,
     * and the indexes thereof, in memory.
     * @constructs
     * @param args.trackConfigs {Array} array of track configuration
     */
    constructor: function( args ) {
        // set up our facet name discrimination: what facets we will
        // actually provide search on
        var non_facet_attrs = this.getIdentityAttributes();
        non_facet_attrs.push('conf');
        this._filterFacet = function( facetName ) {
            var userfilter = args.filterFacets || function() {return true;};
            return userfilter(facetName)
                   && ! dojo.some( non_facet_attrs, function(a) { return a == facetName;});
        };

        // set up our onReady callbacks to fire once the data is
        // loaded
        if( ! dojo.isArray( args.onReady ) ){
            this.onReadyFuncs = args.onReady ? [ args.onReady ] : [];
        } else {
            this.onReadyFuncs = dojo.clone(args.onReady);
        }

        // interpret the track configurations as a metadata store
        this._indexItems(
            {
                store: this,
                items: dojo.map( args.trackConfigs, function(conf) {
                    var metarecord = dojo.clone( conf.metadata || {} );
                    metarecord.label = conf.label;
                    metarecord.key = conf.key;
                    metarecord.conf = conf;
                    if( conf.category )
                        metarecord.category = conf.category;
                    return metarecord;
                },this)
            }
        );

        // fetch and index all the items from each of the stores
        var stores_fetched_count = 0;
        if( ! args.metadataStores || ! args.metadataStores.length ) {
            // if we don't actually have any stores besides the track
            // confs, we're ready now.
            this._ready();
        } else  {
            // index the track metadata from each of the stores

            var storeFetchFinished = dojo.hitch( this, function() {
                if( ++stores_fetched_count == args.metadataStores.length )
                    this._ready();
            });
            dojo.forEach( args.metadataStores, function(store) {
                store.fetch({
                    scope: this,
                    onComplete: Util.debugHandler( this, function(items) {
                        // build our indexes
                        this._indexItems({ store: store, items: items, supplementalOnly: true });

                        // if this is the last store to be fetched, call
                        // our onReady callbacks
                        storeFetchFinished();
                    }),
                    onError: function(e) {
                        console.error(''+e);
                        storeFetchFinished();
                    },
                });
            },this);
        }
     },

    /**
     * Set the store's state to be ready (i.e. loaded), and calls all
     * our onReady callbacks.
     * @private
     */
    _ready: function() {
        this.ready = true;
        dojo.forEach( this.onReadyFuncs, function(f) {
                          f.call( this, this );
                      }, this );
        this.onReadyFuncs = [];
    },

    _indexItems: function( args ) {
        // get our (filtered) list of facets we will index for
        var store = args.store,
            items = args.items;

        var storeAttributes = {};

        // convert the items to a uniform format
        items = dojo.map( items, function( item ) {
                              var itemattrs = store.getAttributes(item);

                              //convert the item into a uniform data format of plain objects
                              var newitem = {};
                              dojo.forEach( itemattrs, function(attr) {
                                                storeAttributes[attr] = 1;
                                                newitem[attr] = store.getValue(item,attr);
                                            });
                              return newitem;
                          },
                          this
                        );

        // merge them with any existing records, filtering out ones
        // that should be ignored if we were passed
        // 'supplementalOnly', and update the identity index
        this.identIndex = this.identIndex || {};
        items = dojo.map( items, function(item) {
                                 // merge the new item attributes with any existing
                                 // record for this item
                                 var ident = this.getIdentity(item);
                                 var existingItem = this.identIndex[ ident ];
                                 if( args.supplementalOnly && !existingItem) {
                                     // skip this item if we are supplementalOnly and it
                                     // does not already exist
                                     return null;
                                 }
                                 return this.identIndex[ ident ] = dojo.mixin( existingItem || {}, item );
                             },
                             this
                           );
        // filter out nulls
        items = dojo.filter( items, function(i) { return i;});

        // update our facet list to include any new attrs these
        // items have
        var new_facets = this._addFacets( dojof.keys( storeAttributes ) );

        // initialize indexes for any new facets
        this.facetIndexes = this.facetIndexes || { itemCount: 0, bucketCount: 0, byName: {} };
        dojo.forEach( new_facets, function(facet) {
            if( ! this.facetIndexes.byName[facet] ) {
                this.facetIndexes.bucketCount++;
                this.facetIndexes.byName[facet] = { itemCount: 0, bucketCount: 0, byValue: {} };
            }
        }, this);

        // now update the indexes with the new facets
        if( new_facets.length ) {
            var gotDataForItem = {};
            dojo.forEach( new_facets, function(f){ gotDataForItem[f] = {};});

            dojo.forEach( items, function( item ) {
                this.facetIndexes.itemCount++;
                dojo.forEach( new_facets, function( facet ) {
                    var value = this.getValue( item, facet, undefined );
                    if( typeof value == 'undefined' )
                        return;
                    gotDataForItem[facet][this.getIdentity(item)] = 1;
                    this._indexItem( facet, value, item );
                },this);
            }, this);

            // index the items that do not have data for this facet
            var noDataValue = '(no data)';
            dojo.forEach( new_facets, function(facet) {
                var gotSomeWithNoData = false;
                dojo.forEach( dojof.values( this.identIndex ), function(item) {
                    if( ! gotDataForItem[facet][this.getIdentity(item)] ) {
                        gotSomeWithNoData = true;
                        this._indexItem( facet, noDataValue, item );
                    }
                },this);
            },this);

            // calculate the rank of the facets: make an array of
            // facet names sorted by smallest average bucket size,
            // descending
            this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
                a = this.facetIndexes.byName[a];
                b = this.facetIndexes.byName[b];
                return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
            }));
        }
    },

    /**
     * Add an item to the indexes for the given facet name and value.
     * @private
     */
    _indexItem: function( facet, value, item ) {
        var facetValues = this.facetIndexes.byName[facet];
        var bucket = facetValues.byValue[value];
        if( !bucket ) {
            bucket = facetValues.byValue[value] = { itemCount: 0, items: [] };
            facetValues.bucketCount++;
        }
        bucket.itemCount++;
        bucket.items.push(item);
    },

    /**
     * Given an array of string facet names, add records for them,
     * initializing the necessary data structures.
     * @private
     * @returns {Array[String]} facet names that did not already exist
     */
    _addFacets: function( facetNames ) {
        var old_facets = this.facets || [];
        var seen = {};
        this.facets = dojo.filter(
            old_facets.concat( facetNames ),
            function(facetName) {
                var take = this._filterFacet(facetName) && !seen[facetName];
                seen[facetName] = true;
                return take;
            },
            this
        );
        return this.facets.slice( old_facets.length );
    },

    /**
     * Get an array of the text names of the facets that are defined
     * in this track metadata.
     * @param callback {Function} called as callback( [facet,facet,...] )
     */
    getFacets: function( callback ) {
        return this.facets;
    },

    /**
     * Get an Array of the distinct values for a given facet name.
     * @param facetName {String} the facet name
     * @returns {Array} distinct values for that facet
     */
    getFacetValues: function( facetName ) {
        var index = this.facetIndexes.byName[facetName];
        if( !index )
            return [];

        return dojof.keys( index.byValue );
    },

    /**
     * Add a callback to be called when this store is ready (i.e. loaded).
     */
    onReady: function( callback ) {
        if( this.ready ) {
            callback.call( this, this );
        }
        else {
            this.onReadyFuncs.push( callback );
        }
    },

    // dojo.data.api.Read support

    getValue: function( i, attr, defaultValue ) {
        var v = i[attr];
        return typeof v == 'undefined' ? defaultValue : v;
    },
    getValues: function( i, attr ) {
        var a = [ i[attr] ];
        return typeof a[0] == 'undefined' ? [] : a;
    },

    getAttributes: function(item)  {
        return dojof.keys( item );
    },

    hasAttribute: function(item,attr) {
        return item.hasOwnProperty(attr);
    },

    containsValue: function(item, attribute, value) {
        return item[attribute] == value;
    },

    isItem: function(item) {
        return typeof item == 'object' && typeof item.label == 'string';
    },

    isItemLoaded: function() {
        return true;
    },

    loadItem: function( args ) {
    },

    // used by the dojo.data.util.simpleFetch mixin to implement fetch()
    _fetchItems: function( keywordArgs, findCallback, errorCallback ) {
        if( ! this.ready ) {
            this.onReady( dojo.hitch( this, '_fetchItems', keywordArgs, findCallback, errorCallback ) );
            return;
        }

        var query = dojo.clone( keywordArgs.query || {} );
        var textFilter = query.text;
        delete query.text;

        var results;

        // if we don't actually have any facets specified in the
        // query, the results are just all the items
        if( ! dojo.some( dojof.values(query), function(q){ return q.length > 0;}) ) {
            results = dojof.values( this.identIndex );
        }
        else {
            // start with an initial set of items that have the desired
            // value for the most specific (smallest avg index bucket
            // size) facet that was specified
            results = (function() {
                           var highestRankedFacet,
                               queryValues = [],
                               index;
                           dojo.some( this.facetIndexes.facetRank, function(facetName) {
                                          if( query[facetName] ) {
                                              highestRankedFacet = facetName;
                                              queryValues = query[highestRankedFacet];
                                              index = this.facetIndexes.byName[highestRankedFacet];
                                              return queryValues.length > 0;
                                          }
                                          return false;
                                      },this);

                           delete query[highestRankedFacet];

                           var set = [];
                           dojo.forEach( queryValues, function(val) {
                              var bucket = index.byValue[val];
                              if( bucket )
                                  set.push.apply( set, bucket.items );
                           },this);
                           return set;
                       }).call(this);

            // and filter this starting set for the other facets
            dojo.forEach( dojof.keys(query), function(facetName) {
                              var desired_values = query[facetName] || [];
                              if( ! desired_values.length )
                                  return;
                              results = dojo.filter(results, function(item) {
                                                        var value = this.getValue(item,facetName);
                                                        return dojo.some( desired_values, function(desired) {
                                                                              return desired == value;
                                                                          },this);
                                                    },this);
                          },this);
        }

        // filter with the text filter, if we have it
	if( typeof textFilter != 'undefined' ) {
            var filter = this._compileTextFilter( textFilter );
            results = dojo.filter( results, function(item) {
                return dojo.some( this.facets, function(facetName) {
                           return filter( this.getValue( item, facetName ) );
                       },this);
            },this);
        }

        // and finally, hand them to the finding callback
        findCallback(results,keywordArgs);
    },

    /**
     * Compile a text search string into a function that tests whether
     * a given piece of text matches that search string.
     * @private
     */
    _compileTextFilter: function( textString ) {
        // escape regex control chars, and convert glob-like chars to
        // their regex equivalents
        textString =
            dojo.regexp.escapeString( textString, '*?' )
            .replace(/\*/g,'.+')
            .replace(/\?/g,'.');

        // split into words, and convert each word into a regexp
        var wordREs = dojo.map( textString.split(/\s+/), function(w) { return new RegExp(w,'i'); });

        // return a function that returns true if all of the words
        // match the string, but in any order
        return function( text ) {
            return dojof.every( wordREs, function(re) { return re.test(text); } );
        };
    },

    getFeatures: function() {
        return {
	    'dojo.data.api.Read': true,
	    'dojo.data.api.Identity': true
	};
    },
    close: function() {},

    getLabel: function(i) {
        return this.getValue(i,'key',undefined);
    },
    getLabelAttributes: function(i) {
        return ['key'];
    },

    // dojo.data.api.Identity support
    getIdentityAttributes: function() {
        return ['label'];
    },
    getIdentity: function(i) {
        return this.getValue(i, 'label', undefined);
    },
    fetchItemByIdentity: function(id) {
        return this.identIndex[id];
    }
});
dojo.require('dojo.data.util.simpleFetch');
dojo.extend( JBrowse.Model.TrackMetaData, dojo.data.util.simpleFetch );<|endoftext|>


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

/**
 * QUnit tests for NumberProperty
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const NumberIO = require( 'TANDEM/types/NumberIO' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const Property = require( 'AXON/Property' );
  const Range = require( 'DOT/Range' );
  const Tandem = require( 'TANDEM/Tandem' );

  QUnit.module( 'NumberProperty' );

  QUnit.test( 'Test NumberProperty', assert => {

    let p = null;

    // valueType
    window.assert && assert.throws( () => {
      p = new NumberProperty( 'foo' );
    }, 'initial value has invalid valueType' );
    p = new NumberProperty( 0 );
    p.value = 1;
    window.assert && assert.throws( () => {
      p.value = 'foo';
    }, 'set value has invalid valueType' );

    // numberType
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { numberType: 0 } );
    }, 'bad numberType' );
    p = new NumberProperty( 0, { numberType: 'FloatingPoint' } );
    p.value = 1;
    p.value = 1.2;
    window.assert && assert.throws( () => {
      p = new NumberProperty( 1.2, { numberType: 'Integer' } );
    }, 'initial value has invalid numberType' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        numberType: 'Integer',
        validValues: [ 0, 1, 1.2, 2 ]
      } );
    }, 'member of validValues has invalid numberType' );
    p = new NumberProperty( 0, { numberType: 'Integer' } );
    p.value = 1;
    window.assert && assert.throws( () => {
      p.value = 1.2;
    }, 'set value has invalid numberType' );

    // range
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { range: [ 0, 10 ] } );
    }, 'bad range' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 11, { range: new Range( 0, 10 ) } );
    }, 'initial value is greater than range.max' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( -1, { range: new Range( 0, 10 ) } );
    }, 'initial value is less than range.min' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        range: new Range( 0, 10 ),
        validValues: [ 0, 1, 2, 11 ]
      } );
    }, 'member of validValues is greater than range.max' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        range: new Range( 0, 10 ),
        validValues: [ -1, 0, 1, 2 ]
      } );
    }, 'member of validValues is less than range.min' );
    p = new NumberProperty( 0, { range: new Range( 0, 10 ) } );
    p.value = 5;
    window.assert && assert.throws( () => {
      p.value = 11;
    }, 'set value is greater than range.max' );
    window.assert && assert.throws( () => {
      p.value = -1;
    }, 'set value is less than range.min' );

    // units
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { units: 'elephants' } );
    }, 'bad units' );

    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { phetioType: NumberIO } );
    }, 'EnumerationProperty sets phetioType' );

    assert.ok( true, 'one assertion for when assert is not enabled' );
  } );

  QUnit.test( 'Test NumberProperty range option as Property', assert => {

    let rangeProperty = new Property( new Range( 0, 1 ) );
    let p = null;

    // valueType
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { range: 'hi' } );
    }, 'incorrect range type' );
    p = new NumberProperty( 0, { range: rangeProperty } );
    assert.ok( p.rangeProperty === rangeProperty, 'rangeProperty should be set' );
    assert.ok( p.range === rangeProperty.value, 'rangeProperty value should be set NumberProperty.set on construction' );
    p.value = 1;
    p.value = 0;
    p.value = .5;
    window.assert && assert.throws( () => {
      p.value = 2;
    }, 'larger than range' );
    window.assert && assert.throws( () => {
      p.value = -2;
    }, 'smaller than range' );
    window.assert && assert.throws( () => {
      rangeProperty.value = new Range( 5, 10 );
    }, 'current value outside of range' );

    // reset from previous test setting to [5,10]
    p.dispose();
    rangeProperty.dispose();
    rangeProperty = new Property( new Range( 0, 1 ) );
    p = new NumberProperty( 0, { range: rangeProperty } );
    rangeProperty.value = new Range( 0, 10 );
    p.value = 2;

    p.setValueAndRange( 100, new Range( 99, 101 ) );

    const myRange = new Range( 5, 10 );
    p.setValueAndRange( 6, myRange );

    assert.ok( myRange === p.rangeProperty.value, 'reference should be kept' );

    p = new NumberProperty( 0, { range: new Range( 0, 1 ) } );
    assert.ok( p.rangeProperty instanceof Property, 'created a rangeProperty from a range' );

    // deferring ordering dependencies
    ///////////////////////////////////////////////////////
    p.setDeferred( true );
    p.rangeProperty.setDeferred( true );
    p.set( 3 );
    p.rangeProperty.set( new Range( 2, 3 ) );
    p.setDeferred( false );
    p.rangeProperty.setDeferred( false );
    /////////////////////////////////////////////////////////
    p.setDeferred( true );
    p.rangeProperty.setDeferred( true );
    p.set( 6 );
    p.rangeProperty.set( new Range( 6, 10 ) );
    p.rangeProperty.setDeferred( false );
    p.setDeferred( false );
    /////////////////////////////////////////////////////////
    p.setDeferred( true );
    p.rangeProperty.setDeferred( true );
    p.rangeProperty.set( new Range( 6, 10 ) );
    p.set( 6 );
    p.rangeProperty.setDeferred( false );
    p.setDeferred( false );
    /////////////////////////////////////////////////////////
    p.setDeferred( true );
    p.rangeProperty.setDeferred( true );
    p.rangeProperty.set( new Range( 6, 10 ) );
    p.set( 6 );
    p.setDeferred( false );
    p.rangeProperty.setDeferred( false );
    /////////////////////////////////////////////////////////

    p = new NumberProperty( 0 );
    p.value = 4;
    assert.ok( p.rangeProperty.value === null, 'rangeProperty should have been created' );
    p.rangeProperty.value = new Range( 0, 4 );
    window.assert && assert.throws( () => {
      p.value = 5;
    }, 'current value outside of range' );
  } );
  QUnit.test( 'Test NumberProperty phet-io options', assert => {

    const rootTandem = Tandem.ROOT;
    let p = new NumberProperty( 0, {
      range: new Range( 0, 20 ),
      tandem: rootTandem.createTandem( 'numberProperty' ),
      rangePropertyOptions: { tandem: rootTandem.createTandem( 'rangeProperty' ) }
    } );

    assert.ok( p.rangeProperty.isPhetioInstrumented(), 'rangeProperty instrumented' );
    assert.ok( p.rangeProperty.tandem.name === 'rangeProperty', 'rangeProperty instrumented' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        range: new Range( 0, 20 ),
        tandem: rootTandem.createTandem( 'numberProperty2' ),
        rangePropertyOptions: { tandem: rootTandem.createTandem( 'rangePropertyfdsa' ) }
      } );
    }, 'cannot instrument default rangeProperty with tandem other than "rangeProperty"' );
  } );
} );<commit_msg>add tests for "fire listeners after setDeferred false", https://github.com/phetsims/axon/issues/274<commit_after>// Copyright 2017-2020, University of Colorado Boulder

/**
 * QUnit tests for NumberProperty
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const NumberIO = require( 'TANDEM/types/NumberIO' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const Property = require( 'AXON/Property' );
  const Range = require( 'DOT/Range' );
  const Tandem = require( 'TANDEM/Tandem' );

  QUnit.module( 'NumberProperty' );

  QUnit.test( 'Test NumberProperty', assert => {

    let p = null;

    // valueType
    window.assert && assert.throws( () => {
      p = new NumberProperty( 'foo' );
    }, 'initial value has invalid valueType' );
    p = new NumberProperty( 0 );
    p.value = 1;
    window.assert && assert.throws( () => {
      p.value = 'foo';
    }, 'set value has invalid valueType' );

    // numberType
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { numberType: 0 } );
    }, 'bad numberType' );
    p = new NumberProperty( 0, { numberType: 'FloatingPoint' } );
    p.value = 1;
    p.value = 1.2;
    window.assert && assert.throws( () => {
      p = new NumberProperty( 1.2, { numberType: 'Integer' } );
    }, 'initial value has invalid numberType' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        numberType: 'Integer',
        validValues: [ 0, 1, 1.2, 2 ]
      } );
    }, 'member of validValues has invalid numberType' );
    p = new NumberProperty( 0, { numberType: 'Integer' } );
    p.value = 1;
    window.assert && assert.throws( () => {
      p.value = 1.2;
    }, 'set value has invalid numberType' );

    // range
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { range: [ 0, 10 ] } );
    }, 'bad range' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 11, { range: new Range( 0, 10 ) } );
    }, 'initial value is greater than range.max' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( -1, { range: new Range( 0, 10 ) } );
    }, 'initial value is less than range.min' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        range: new Range( 0, 10 ),
        validValues: [ 0, 1, 2, 11 ]
      } );
    }, 'member of validValues is greater than range.max' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        range: new Range( 0, 10 ),
        validValues: [ -1, 0, 1, 2 ]
      } );
    }, 'member of validValues is less than range.min' );
    p = new NumberProperty( 0, { range: new Range( 0, 10 ) } );
    p.value = 5;
    window.assert && assert.throws( () => {
      p.value = 11;
    }, 'set value is greater than range.max' );
    window.assert && assert.throws( () => {
      p.value = -1;
    }, 'set value is less than range.min' );

    // units
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { units: 'elephants' } );
    }, 'bad units' );

    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { phetioType: NumberIO } );
    }, 'EnumerationProperty sets phetioType' );

    assert.ok( true, 'one assertion for when assert is not enabled' );
  } );

  QUnit.test( 'Test NumberProperty range option as Property', assert => {

    let rangeProperty = new Property( new Range( 0, 1 ) );
    let p = null;

    // valueType
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, { range: 'hi' } );
    }, 'incorrect range type' );
    p = new NumberProperty( 0, { range: rangeProperty } );
    assert.ok( p.rangeProperty === rangeProperty, 'rangeProperty should be set' );
    assert.ok( p.range === rangeProperty.value, 'rangeProperty value should be set NumberProperty.set on construction' );
    p.value = 1;
    p.value = 0;
    p.value = .5;
    window.assert && assert.throws( () => {
      p.value = 2;
    }, 'larger than range' );
    window.assert && assert.throws( () => {
      p.value = -2;
    }, 'smaller than range' );
    window.assert && assert.throws( () => {
      rangeProperty.value = new Range( 5, 10 );
    }, 'current value outside of range' );

    // reset from previous test setting to [5,10]
    p.dispose();
    rangeProperty.dispose();
    rangeProperty = new Property( new Range( 0, 1 ) );
    p = new NumberProperty( 0, { range: rangeProperty } );
    rangeProperty.value = new Range( 0, 10 );
    p.value = 2;

    p.setValueAndRange( 100, new Range( 99, 101 ) );

    const myRange = new Range( 5, 10 );
    p.setValueAndRange( 6, myRange );

    assert.ok( myRange === p.rangeProperty.value, 'reference should be kept' );

    p = new NumberProperty( 0, { range: new Range( 0, 1 ) } );
    assert.ok( p.rangeProperty instanceof Property, 'created a rangeProperty from a range' );

    // deferring ordering dependencies
    ///////////////////////////////////////////////////////
    let pCalled = 0;
    let pRangeCalled = 0;
    p.lazyLink( () => pCalled++ );
    p.rangeProperty.lazyLink( () => pRangeCalled++ );
    p.setDeferred( true );
    p.rangeProperty.setDeferred( true );
    p.set( 3 );
    assert.ok( pCalled === 0, 'p is still deferred, should not call listeners' );
    p.rangeProperty.set( new Range( 2, 3 ) );
    assert.ok( pRangeCalled === 0, 'p.rangeProperty is still deferred, should not call listeners' );
    const notifyPListeners = p.setDeferred( false );
    notifyPListeners();
    assert.ok( pCalled === 1, 'p listeners should have been called' );
    const notifyRangeListeners = p.rangeProperty.setDeferred( false );
    notifyRangeListeners();
    assert.ok( pRangeCalled === 1, 'p.rangeProperty is still deferred, should not call listeners' );

    p.setValueAndRange( -100, new Range( -101, -99 ) );
    assert.ok( pCalled === 2, 'p listeners should have been called again' );
    assert.ok( pRangeCalled === 2, 'p.rangeProperty is still deferred, should not call listeners again' );

    p = new NumberProperty( 0 );
    p.value = 4;
    assert.ok( p.rangeProperty.value === null, 'rangeProperty should have been created' );
    p.rangeProperty.value = new Range( 0, 4 );
    window.assert && assert.throws( () => {
      p.value = 5;
    }, 'current value outside of range' );
  } );
  QUnit.test( 'Test NumberProperty phet-io options', assert => {

    const rootTandem = Tandem.ROOT;
    let p = new NumberProperty( 0, {
      range: new Range( 0, 20 ),
      tandem: rootTandem.createTandem( 'numberProperty' ),
      rangePropertyOptions: { tandem: rootTandem.createTandem( 'rangeProperty' ) }
    } );

    assert.ok( p.rangeProperty.isPhetioInstrumented(), 'rangeProperty instrumented' );
    assert.ok( p.rangeProperty.tandem.name === 'rangeProperty', 'rangeProperty instrumented' );
    window.assert && assert.throws( () => {
      p = new NumberProperty( 0, {
        range: new Range( 0, 20 ),
        tandem: rootTandem.createTandem( 'numberProperty2' ),
        rangePropertyOptions: { tandem: rootTandem.createTandem( 'rangePropertyfdsa' ) }
      } );
    }, 'cannot instrument default rangeProperty with tandem other than "rangeProperty"' );
  } );
} );<|endoftext|>


<commit_before>// Initialize all modules by requiring them. Also makes sure they get bundled (see gulpfile.js).
var $ = require('jquery-detached').getJQuery();

var getItems = function() {
  var d = $.Deferred();
  $.get('itemCategories?depth=3&iconStyle=icon-xlg').done(
    function(data){
      d.resolve(data);
    }
  );
  return d.promise();
};

var jRoot = $('head').attr('data-rooturl');

$.when(getItems()).done(function(data) {
  $(function() {

    //////////////////////////
    // helper functions...

    function parseResponseFromCheckJobName(data) {
      var html = $.parseHTML(data);
      var element = html[0];
      if (element !== undefined) {
        return $(element).text();
      }
      return undefined;
    }

    function cleanClassName(className){
      return className.replace(/\./g,'_');
    }

    function checkForLink(desc) {
      if (desc.indexOf('<a href="') === -1) {
        return desc;
      }
      var newDesc = desc.replace(/\</g,'<').replace(/\>/g,'>');
      return newDesc;
    }

    function getCopyFromValue() {
      return $('input[type="text"][name="from"]', '#createItem').val();
    }

    function isItemNameEmpty() {
      var itemName = $('input[name="name"]', '#createItem').val();
      return (itemName === '') ? true : false;
    }

    function getFieldValidationStatus(fieldId) {
      return $('#' + fieldId).data('valid');
    }

    function setFieldValidationStatus(fieldId, status) {
      $('#' + fieldId).data('valid', status);
    }

    function activateValidationMessage(messageId, context, message) {
      if (message !== undefined && message !== '') {
        $(messageId, context).html('» ' + message);
      }
      cleanValidationMessages(context);
      hideInputHelp(context);
      $(messageId).removeClass('input-message-disabled');
    }

    function cleanValidationMessages(context) {
      $(context).find('.input-validation-message').addClass('input-message-disabled');
    }

    function hideInputHelp(context) {
      $('.input-help', context).addClass('input-message-disabled');
    }

    function showInputHelp(context) {
      $('.input-help', context).removeClass('input-message-disabled');
    }

    // About Scroll-linked effect: https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Scroll-linked_effects
    function doSticky() {
      var decorator = $('form .footer .btn-decorator');
      var pos = decorator.offset();
      var vpH = $(window).height();
      if (pos.top >= vpH) {
        decorator.css({position: 'fixed'});
      }

      $(window).scroll(function() {
        var footer = $('form .footer');
        var ref1 = decorator.offset().top + decorator.outerHeight();
        var ref2 = footer.offset().top + footer.outerHeight();
        var vpH = $(window).height();
        if (ref2 > vpH + $(window).scrollTop()) {
          decorator.css({position: 'fixed'});
        } else if (ref2 - 1 <= ref1) {
          decorator.css({position: 'absolute'});
        }
      });
    }

    function enableSubmit(status) {
      var btn = $('form .footer .btn-decorator button[type=submit]');
      if (status === true) {
        if (btn.hasClass('disabled')) {
          btn.removeClass('disabled');
          btn.prop('disabled', false);
        }
      } else {
        if (!btn.hasClass('disabled')) {
          btn.addClass('disabled');
          btn.prop('disabled', true);
        }
      }
    }

    function getFormValidationStatus() {
      if (getFieldValidationStatus('name') && (getFieldValidationStatus('items') || getFieldValidationStatus('from'))) {
          return true;
      }
      return false;
    }

    function cleanItemSelection() {
      $('.categories').find('li[role="radio"]').attr("aria-checked", "false");
      $('#createItem').find('input[type="radio"][name="mode"]').removeAttr('checked');
      $('.categories').find('.active').removeClass('active');
      setFieldValidationStatus('items', false);
    }

    function cleanCopyFromOption() {
      $('#createItem').find('input[type="radio"][value="copy"]').removeAttr('checked');
      $('input[type="text"][name="from"]', '#createItem').val('');
      setFieldValidationStatus('from', false);
    }

    //////////////////////////////////
    // Draw functions

    function drawCategory(category) {
      var $category = $('
').addClass('category').attr('id', 'j-add-item-type-' + cleanClassName(category.id));
      var $items = $('
').addClass('j-item-options');
      var $catHeader = $('
');
      var title = '
' + category.name + '
';
      var description = '
' + category.description + '
';

      // Add items
      $.each(category.items, function(i, elem) {
        $items.append(drawItem(elem));
      });

      $catHeader.append(title);
      $catHeader.append(description);
      $category.append($catHeader);
      $category.append($items);

      return $category;
    }

    function drawItem(elem) {
      var desc = checkForLink(elem.description);
      var $item = $(['
<input type="radio" name="mode" value="',
      elem.class ,'"/> ', elem.displayName, '
'].join('')).append(['
', desc, '
'].join('')).append(drawIcon(elem));

      function select(e) {
        e.preventDefault();
        cleanCopyFromOption();
        cleanItemSelection();

        $(this).attr("aria-checked", "true");
        $(this).find('input[type="radio"][name="mode"]').prop('checked', true);
        $(this).addClass('active');

        setFieldValidationStatus('items', true);
        if (!getFieldValidationStatus('name')) {
          $('input[name="name"][type="text"]', '#createItem').focus();
        } else {
          if (getFormValidationStatus()) {
            enableSubmit(true);
          }
        }
      }

      $item.click(select);

      $item.keypress(function(e) {
        switch (e.which) {
          case 13:
          case 32:
            $(this).trigger('click');
            e.stopPropagation();
            break;
        }
      });

      return $item;
    }

    function drawIcon(elem) {
      var $icn;
      if (elem.iconClassName && elem.iconQualifiedUrl) {
        $icn = $('
');
        $([''].join('')).appendTo($icn);
      } else if (elem.iconFilePathPattern) {
        $icn = $('
');
        var iconFilePath = jRoot + '/' + elem.iconFilePathPattern.replace(":size", "48x48");
        $([''].join('')).appendTo($icn);
      } else {
        $icn = $('
');
        var colors = ['c-49728B','c-335061','c-D33833','c-6D6B6D', 'c-6699CC'];
        var desc = elem.description || '';
        var name = elem.displayName;
        var colorClass= colors[(desc.length) % 4];
        var aName = name.split(' ');
        var a = name.substring(0,1);
        var b = ((aName.length === 1) ? name.substring(1,2) : aName[1].substring(0,1));
        $(['',a,'',b,''].join('')).appendTo($icn);
        $icn.addClass(colorClass);
      }
      return $icn;
    }

    // The main panel content is hidden by default via an inline style. We're ready to remove that now.
    $('#add-item-panel').removeAttr('style');

    // Render all categories
    var $categories = $('div.categories');
    $.each(data.categories, function(i, elem) {
      drawCategory(elem).appendTo($categories);
    });

    // Focus
    $("#add-item-panel").find("#name").focus();

    // Init NameField
    $('input[name="name"]', '#createItem').on("blur input", function() {
      if (!isItemNameEmpty()) {
        var itemName = $('input[name="name"]', '#createItem').val();
        $.get("checkJobName", { value: itemName }).done(function(data) {
          var message = parseResponseFromCheckJobName(data);
          if (message !== '') {
            activateValidationMessage('#itemname-invalid', '.add-item-name', message);
          } else {
            cleanValidationMessages('.add-item-name');
            showInputHelp('.add-item-name');
            setFieldValidationStatus('name', true);
            if (getFormValidationStatus()) {
              enableSubmit(true);
            }
          }
        });
      } else {
        enableSubmit(false);
        setFieldValidationStatus('name', false);
        cleanValidationMessages('.add-item-name');
        activateValidationMessage('#itemname-required', '.add-item-name');
      }
    });

    // Init CopyFromField
    $('input[name="from"]', '#createItem').blur(function() {
      if (getCopyFromValue() === '') {
        $('#createItem').find('input[type="radio"][value="copy"]').removeAttr('checked');
      } else {
        cleanItemSelection();
        $('#createItem').find('input[type="radio"][value="copy"]').prop('checked', true);
        setFieldValidationStatus('from', true);
        if (!getFieldValidationStatus('name')) {
          activateValidationMessage('#itemname-required', '.add-item-name');
          setTimeout(function() {
            $('input[name="name"][type="text"]', '#createItem').focus();
          }, 400);
        } else {
          if (getFormValidationStatus()) {
            enableSubmit(true);
          }
        }
      }
    });

    // Client-side validation
    $("#createItem").submit(function(event) {
      if (!getFormValidationStatus()) {
        event.preventDefault();
        if (!getFieldValidationStatus('name')) {
          activateValidationMessage('#itemname-required', '.add-item-name');
          $('input[name="name"][type="text"]', '#createItem').focus();
        } else {
          if (!getFieldValidationStatus('items') && !getFieldValidationStatus('from'))  {
            activateValidationMessage('#itemtype-required', '.add-item-name');
            $('input[name="name"][type="text"]', '#createItem').focus();
          }
        }
      }
    });

    // Disable the submit button
    enableSubmit(false);

    // Do sticky the form buttons
    doSticky();
  });
});
<commit_msg>change Init CopyFromField function and let the ok button in newJob page be changed when user input words to CopyFromField<commit_after>// Initialize all modules by requiring them. Also makes sure they get bundled (see gulpfile.js).
var $ = require('jquery-detached').getJQuery();

var getItems = function() {
  var d = $.Deferred();
  $.get('itemCategories?depth=3&iconStyle=icon-xlg').done(
    function(data){
      d.resolve(data);
    }
  );
  return d.promise();
};

var jRoot = $('head').attr('data-rooturl');

$.when(getItems()).done(function(data) {
  $(function() {

    //////////////////////////
    // helper functions...

    function parseResponseFromCheckJobName(data) {
      var html = $.parseHTML(data);
      var element = html[0];
      if (element !== undefined) {
        return $(element).text();
      }
      return undefined;
    }

    function cleanClassName(className){
      return className.replace(/\./g,'_');
    }

    function checkForLink(desc) {
      if (desc.indexOf('<a href="') === -1) {
        return desc;
      }
      var newDesc = desc.replace(/\</g,'<').replace(/\>/g,'>');
      return newDesc;
    }

    function getCopyFromValue() {
      return $('input[type="text"][name="from"]', '#createItem').val();
    }

    function isItemNameEmpty() {
      var itemName = $('input[name="name"]', '#createItem').val();
      return (itemName === '') ? true : false;
    }

    function getFieldValidationStatus(fieldId) {
      return $('#' + fieldId).data('valid');
    }

    function setFieldValidationStatus(fieldId, status) {
      $('#' + fieldId).data('valid', status);
    }

    function activateValidationMessage(messageId, context, message) {
      if (message !== undefined && message !== '') {
        $(messageId, context).html('» ' + message);
      }
      cleanValidationMessages(context);
      hideInputHelp(context);
      $(messageId).removeClass('input-message-disabled');
    }

    function cleanValidationMessages(context) {
      $(context).find('.input-validation-message').addClass('input-message-disabled');
    }

    function hideInputHelp(context) {
      $('.input-help', context).addClass('input-message-disabled');
    }

    function showInputHelp(context) {
      $('.input-help', context).removeClass('input-message-disabled');
    }

    // About Scroll-linked effect: https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Scroll-linked_effects
    function doSticky() {
      var decorator = $('form .footer .btn-decorator');
      var pos = decorator.offset();
      var vpH = $(window).height();
      if (pos.top >= vpH) {
        decorator.css({position: 'fixed'});
      }

      $(window).scroll(function() {
        var footer = $('form .footer');
        var ref1 = decorator.offset().top + decorator.outerHeight();
        var ref2 = footer.offset().top + footer.outerHeight();
        var vpH = $(window).height();
        if (ref2 > vpH + $(window).scrollTop()) {
          decorator.css({position: 'fixed'});
        } else if (ref2 - 1 <= ref1) {
          decorator.css({position: 'absolute'});
        }
      });
    }

    function enableSubmit(status) {
      var btn = $('form .footer .btn-decorator button[type=submit]');
      if (status === true) {
        if (btn.hasClass('disabled')) {
          btn.removeClass('disabled');
          btn.prop('disabled', false);
        }
      } else {
        if (!btn.hasClass('disabled')) {
          btn.addClass('disabled');
          btn.prop('disabled', true);
        }
      }
    }

    function getFormValidationStatus() {
      if (getFieldValidationStatus('name') && (getFieldValidationStatus('items') || getFieldValidationStatus('from'))) {
          return true;
      }
      return false;
    }

    function cleanItemSelection() {
      $('.categories').find('li[role="radio"]').attr("aria-checked", "false");
      $('#createItem').find('input[type="radio"][name="mode"]').removeAttr('checked');
      $('.categories').find('.active').removeClass('active');
      setFieldValidationStatus('items', false);
    }

    function cleanCopyFromOption() {
      $('#createItem').find('input[type="radio"][value="copy"]').removeAttr('checked');
      $('input[type="text"][name="from"]', '#createItem').val('');
      setFieldValidationStatus('from', false);
    }

    //////////////////////////////////
    // Draw functions

    function drawCategory(category) {
      var $category = $('
').addClass('category').attr('id', 'j-add-item-type-' + cleanClassName(category.id));
      var $items = $('
').addClass('j-item-options');
      var $catHeader = $('
');
      var title = '
' + category.name + '
';
      var description = '
' + category.description + '
';

      // Add items
      $.each(category.items, function(i, elem) {
        $items.append(drawItem(elem));
      });

      $catHeader.append(title);
      $catHeader.append(description);
      $category.append($catHeader);
      $category.append($items);

      return $category;
    }

    function drawItem(elem) {
      var desc = checkForLink(elem.description);
      var $item = $(['
<input type="radio" name="mode" value="',
      elem.class ,'"/> ', elem.displayName, '
'].join('')).append(['
', desc, '
'].join('')).append(drawIcon(elem));

      function select(e) {
        e.preventDefault();
        cleanCopyFromOption();
        cleanItemSelection();

        $(this).attr("aria-checked", "true");
        $(this).find('input[type="radio"][name="mode"]').prop('checked', true);
        $(this).addClass('active');

        setFieldValidationStatus('items', true);
        if (!getFieldValidationStatus('name')) {
          $('input[name="name"][type="text"]', '#createItem').focus();
        } else {
          if (getFormValidationStatus()) {
            enableSubmit(true);
          }
        }
      }

      $item.click(select);

      $item.keypress(function(e) {
        switch (e.which) {
          case 13:
          case 32:
            $(this).trigger('click');
            e.stopPropagation();
            break;
        }
      });

      return $item;
    }

    function drawIcon(elem) {
      var $icn;
      if (elem.iconClassName && elem.iconQualifiedUrl) {
        $icn = $('
');
        $([''].join('')).appendTo($icn);
      } else if (elem.iconFilePathPattern) {
        $icn = $('
');
        var iconFilePath = jRoot + '/' + elem.iconFilePathPattern.replace(":size", "48x48");
        $([''].join('')).appendTo($icn);
      } else {
        $icn = $('
');
        var colors = ['c-49728B','c-335061','c-D33833','c-6D6B6D', 'c-6699CC'];
        var desc = elem.description || '';
        var name = elem.displayName;
        var colorClass= colors[(desc.length) % 4];
        var aName = name.split(' ');
        var a = name.substring(0,1);
        var b = ((aName.length === 1) ? name.substring(1,2) : aName[1].substring(0,1));
        $(['',a,'',b,''].join('')).appendTo($icn);
        $icn.addClass(colorClass);
      }
      return $icn;
    }

    // The main panel content is hidden by default via an inline style. We're ready to remove that now.
    $('#add-item-panel').removeAttr('style');

    // Render all categories
    var $categories = $('div.categories');
    $.each(data.categories, function(i, elem) {
      drawCategory(elem).appendTo($categories);
    });

    // Focus
    $("#add-item-panel").find("#name").focus();

    // Init NameField
    $('input[name="name"]', '#createItem').on("blur input", function() {
      if (!isItemNameEmpty()) {
        var itemName = $('input[name="name"]', '#createItem').val();
        $.get("checkJobName", { value: itemName }).done(function(data) {
          var message = parseResponseFromCheckJobName(data);
          if (message !== '') {
            activateValidationMessage('#itemname-invalid', '.add-item-name', message);
          } else {
            cleanValidationMessages('.add-item-name');
            showInputHelp('.add-item-name');
            setFieldValidationStatus('name', true);
            if (getFormValidationStatus()) {
              enableSubmit(true);
            }
          }
        });
      } else {
        enableSubmit(false);
        setFieldValidationStatus('name', false);
        cleanValidationMessages('.add-item-name');
        activateValidationMessage('#itemname-required', '.add-item-name');
      }
    });

    // Init CopyFromField
    $('input[name="from"]', '#createItem').on("blur input", function() {
      if (getCopyFromValue() === '') {
        $('#createItem').find('input[type="radio"][value="copy"]').removeAttr('checked');
      } else {
        cleanItemSelection();
        $('#createItem').find('input[type="radio"][value="copy"]').prop('checked', true);
        setFieldValidationStatus('from', true);
        if (!getFieldValidationStatus('name')) {
          activateValidationMessage('#itemname-required', '.add-item-name');
          setTimeout(function() {
            $('input[name="name"][type="text"]', '#createItem').focus();
          }, 400);
        } else {
          if (getFormValidationStatus()) {
            enableSubmit(true);
          }
        }
      }
    });

    // Client-side validation
    $("#createItem").submit(function(event) {
      if (!getFormValidationStatus()) {
        event.preventDefault();
        if (!getFieldValidationStatus('name')) {
          activateValidationMessage('#itemname-required', '.add-item-name');
          $('input[name="name"][type="text"]', '#createItem').focus();
        } else {
          if (!getFieldValidationStatus('items') && !getFieldValidationStatus('from'))  {
            activateValidationMessage('#itemtype-required', '.add-item-name');
            $('input[name="name"][type="text"]', '#createItem').focus();
          }
        }
      }
    });

    // Disable the submit button
    enableSubmit(false);

    // Do sticky the form buttons
    doSticky();
  });
});
<|endoftext|>


<commit_before>// Initialize all modules by requiring them. Also makes sure they get bundled (see gulpfile.js).
var $ = require('jquery-detached').getJQuery();

var getItems = function(){
  var d = $.Deferred();
  $.get('categories?depth=3').done(
      function(data){
        d.resolve(data);
      }
  );
  return d.promise();
};

var jRoot = $('head').attr('data-rooturl');

$.when(getItems()).done(function(data){
  $(function() {

    // The main panel content is hidden by default via an
    // inline style. We're ready to remove that now.
    $('#create-item-panel').removeAttr('style');

    //////////////////////////////
    // helpful reference DOM

    var defaultMinToShow = 2;
    var defaultLooseItems = 'jenkins.category.uncategorized';
    var $root = $('#main-panel');
    var $form = $root.find('form[name="createItem"]').addClass('jenkins-config new-view');
    var $newView = $('
')
      .attr('name','createItem')
      .attr('action','createItem')
      .prependTo($form);
    var $navBox = $('');
    var $tabs = $('
').appendTo($newView);
    var $categories = $('
').appendTo($newView);
    var $subBtn = $('#bottom-sticker .yui-submit-button');
    var sectionsToShow = [];

    $tabs.prepend($navBox);

    ////////////////////////////////
    // submit button click

    function makeButtonWrapper(){
      var $p = window.$; // jshint ignore:line
      var btn = $p('ok');
      var okButton = window.makeButton(btn, null); // jshint ignore:line

      $subBtn = $('#bottom-sticker .yui-submit-button');

      checkFormReady();

      $subBtn.on('click',function() {
        $(this).addClass('yui-button-disabled yui-submit-button-disabled')
          .find('button')
            .attr('disabled','disabled')
            .text('. . .');
      });

    }

    ////////////////////////////////
    // scroll action......

    var isManualScrolling = false;
    var ignoreNextScrollEvent = false;
    var $window = $(window);
    var $breadcrumbBar = $('#breadcrumbBar');
    var $createItemPanel = $('#create-item-panel');
    var createPanelOffset = $createItemPanel.offset().top;

    function autoActivateTabs(){
      if (isManualScrolling === true) {
        // We ignore scroll events when a manual scroll is in
        // operation e.g. when the user clicks on a category tab.
        return;
      }
      if (ignoreNextScrollEvent === true) {
        // Things like repositioning of the tabbar can trigger scroll
        // events that we want to ignore.
        ignoreNextScrollEvent = false;
        return;
      }

      var winScoll = $window.scrollTop();

      $tabs.find('.active').removeClass('active');
      $.each(data.categories,function(i,cat){
        var domId = '#j-add-item-type-'+cat.id;
        var $cat = $(domId);
        var catHeight = ($cat.length > 0)?
        $cat.offset().top + $cat.outerHeight() - createPanelOffset: 0;

        if(winScoll < catHeight){
          var $thisTab = $tabs.find(['[href="',cleanHref(domId),'"]'].join(''));
          resetActiveTab($thisTab);
          return false;
        }
      });
    }

    function stickTabbar() {
      var winScoll = $window.scrollTop();
      if(winScoll > createPanelOffset - $breadcrumbBar.height()){
        $tabs.width($tabs.width()).css({
          'position':'fixed',
          'top':($breadcrumbBar.height() - 5 )+'px'});
        $categories.css({'margin-top':$tabs.outerHeight()+'px'});
        return true;
      } else{
        $tabs.add($categories).removeAttr('style');
        return false;
      }
    }

    //////////////////////////
    // helper functions...

    function checkFormReady(){
      //make sure everyone has changed and gotten attached...
      setTimeout(function(){
        var $name = $form.removeClass('no-select').find('input[name="name"]').removeClass('no-val');

        function checkItems(){
          var selected = $form.find('input[type="radio"]:checked').length > 0;
          var named = $.trim($name.val()).length > 0;
          return {selected:selected,named:named};
        }
        if(checkItems().selected && checkItems().named){
          $subBtn.removeClass('yui-button-disabled').find('button').removeAttr('disabled');
        }
        else{
          $subBtn.addClass('yui-button-disabled').find('button').attr('disabled','disabled');
        }
      },10);
    }

    function addCopyOption(data){
      var $copy = $('#copy').closest('tr');
      if($copy.length === 0) {return data;} // exit if copy should not be added to page. Jelly page holds that logic.
      var copyTitle = $copy.find('label').text();
      var copyDom = $copy.next().find('.setting-main').html();
      var copy = {
          name:'Copy',
          id:'copy',
          minToShow:0,
          items:[
            {
              class:"copy",
              description:copyDom,
              displayName:copyTitle,
              iconFilePathPattern:'images/48x48/copy.png'
            }
          ]
      };
      var newData = [];

      $.each(data,function(i,elem){
        if(elem.id !== "category-id-copy")
          { newData.push(elem); }
      });

      newData.push(copy);

      return newData;
    }
    function checkForLink(desc){
      if(desc.indexOf('<a href="') === -1) {
        return false;
      }
      var newDesc = desc.replace(/\</g,'<').replace(/\>/g,'>');
      return newDesc;
    }
    function sortItemsByOrder(itemTypes) {
      function sortByOrder(a, b) {
        var result = (a.weight - b.weight);
        result = Math.max(result, -1); // If lt than -1, make it -1
        result = Math.min(result, +1); // If gt than +1, make it +1
        return result;
      }
      return itemTypes.sort(sortByOrder);
    }

    function checkCatCount(elem){
      var minToShow = (typeof elem.minToShow === 'number')? elem.minToShow : defaultMinToShow;
      var showIt = ($.isArray(elem.items) && elem.items.length >= minToShow);
      return showIt;
    }

    function cleanClassName(className){
      return className.replace(/\./g,'_');
    }

    function cleanHref(id,reverse){
      if(reverse){
        var gotHash = (id.indexOf('#') === 0)?
           '#j-add-item-type-'+ id.substring(1).replace(/\./g,'_'):
             'j-add-item-type-'+ id.replace(/\./g,'_');
        return gotHash;
      }
      else{
        return id.replace('j-add-item-type-','');
      }
    }

    function cleanLayout(){
      // Do a little shimmy-hack to force legacy code to resize correctly and set tab state.
      $('html,body').animate({scrollTop: 1}, 1);
      $('html,body').animate({scrollTop: 0}, 10);

      setTimeout(fireBottomStickerAdjustEvent,410);
    }

    function resetActiveTab($this){
      var $nav = $this.closest('.nav');
      $nav.find('.active').removeClass('active');
      $this.addClass('active');
    }

    //////////////////////////////////
    // Draw functions

    function drawName() {
      var $name = $('
');

      var $input = $('<input type="text" name="name" class="name" id="name" placeholder="New item name..." />')
        .keyup(function(){
          $form.find('input[name="name"]').val($(this).val());
          checkFormReady();
        })
        .appendTo($name);

      $tabs.prepend($name);
      setTimeout(function(){$input.focus();},100);
    }

    function drawTabs(data){
      $('#main-panel').addClass('container');
      var $nav = $('
');

      $.each(data,function(i,elem){

        // little bit hacky here... need to keep track if I have tabs to show, so if there is just 1, I can hide it later....
        if (elem.minToShow !== 0 && checkCatCount(elem)) {sectionsToShow.push(elem.id);}

        var $tab = drawTab(i,elem);
        var $items = drawCategory(elem);
        var $cat = $items.parent();

        $.each(elem.items,function(i,elem){
          var $item = drawItem(elem);
          $items.append($item);
        });

        if(checkCatCount(elem)) {$nav.append($tab);}
        $categories.append($cat);

      });
      $(window).on('scroll', autoActivateTabs);
      $(window).on('scroll', stickTabbar);

      if(sectionsToShow.length > 3){
        $navBox.append($nav);
      }else{
        $categories.find('.category-header').hide();
      }
      drawName();
      cleanLayout();
    }

    function drawTab(i,elem){
      if(!elem) {elem = i;}
      var $tab = $(['
',elem.name,'
'].join(''))
        .click(function(){
          var $this = $(this).children('a');

          var tab = $this.attr('href');
          var scrollTop = $(cleanHref(tab,true)).offset().top - ($newView.children('.jenkins-config-widgets').height() + 15);

          setTimeout(function(){resetActiveTab($this);},510);

          isManualScrolling = true;
          $('html,body').animate({
            scrollTop: scrollTop
          }, 500, function() {
            isManualScrolling = false;
            ignoreNextScrollEvent = stickTabbar();
          });
        });
      return $tab;
    }

    function drawCategory(i,elem){
      // categories are smart, so this is a little tricky....
      if (!elem) {
        elem = i;
      }
      var $category = $('
').addClass('category jenkins-config hide-cat').attr('id', 'j-add-item-type-'+cleanClassName(elem.id));
      var $items = $('
').addClass('j-item-options').appendTo($category);

      // if there are enough items for a category, attach the category and its header...
      if(checkCatCount(elem)){
        var $catHeader = $('
').prependTo($category);
        var catheader = ['
', elem.name, '
'].join('');
        var catDesc = ['
', elem.description, '
'].join('');

        if((elem.minToShow > 0)){
          $(catheader).appendTo($catHeader);
          $(catDesc).appendTo($catHeader);
        }
        $category.removeClass('hide-cat');
      }
      else{
        var targ = elem.remainders || defaultLooseItems;
        $items = $('#'+cleanHref(targ,true)).find('.j-item-options');
      }

      return $items;
    }

    function drawItem(elem){
      var desc = (checkForLink(elem.description))? checkForLink(elem.description):elem.description;
      var $item = $([
          '
<input name="mode" value="',elem.class,'" type="radio" /> ', elem.displayName, '
'
      ].join('')).append([
          '
', desc, '
'
      ].join('')).append(drawIcon(elem));

      function setSelectState(e){
        e.preventDefault();
        var href = $(e.target).attr('href');
        if(href) {
          window.open(href);
        }

        var $this = $(this).closest('li');
        $this.closest('.categories').find('input[type="radio"][name="mode"]').removeAttr('checked');
        //if this is a hyperlink, don't move the selection.
        if($this.find('a:focus').length === 1) {return false;}
        $this.closest('.categories').find('.active').removeClass('active');
        $this.addClass('active');
        $this.find('input[type="radio"]').prop('checked', true);
        checkFormReady();

        $('html, body').animate({
          scrollTop:$this.offset().top - 200
        },50);

      }

      $item.click(setSelectState);

      return $item;
    }

    function drawIcon(elem){
      var $icn = $('
');
      if (!elem.iconFilePathPattern) {
        var colors = ['c-49728B','c-335061','c-D33833','c-6D6B6D','c-DCD9D8','other'];
        var desc = elem.description || '';
        var name = elem.displayName;
        var colorClass= colors[(desc.length) % 6];
        var aName = name.split(' ');
        var a = name.substring(0,1);
        var b = ((aName.length === 1)?
            name.substring(1,2):
              aName[1].substring(0,1));
        $([
          '',a,'',b,''
         ].join(''))
          .appendTo($icn);
        return $icn.addClass('df').addClass(colorClass);
      }

      var iconFilePath = jRoot + '/' + elem.iconFilePathPattern.replace(":size", "48x48");
      $([''].join(''))
        .appendTo($icn);

      return $icn;
    }

    // initialize
    var sortedDCategories = sortItemsByOrder(data.categories);
    var sortedDCategoriesWithCopy = addCopyOption(sortedDCategories);
    makeButtonWrapper();
    drawTabs(sortedDCategoriesWithCopy);

  });
});

function fireBottomStickerAdjustEvent() {
  Event.fire(window, 'jenkins:bottom-sticker-adjust'); // jshint ignore:line
}<commit_msg>Fix scroll on click bug<commit_after>// Initialize all modules by requiring them. Also makes sure they get bundled (see gulpfile.js).
var $ = require('jquery-detached').getJQuery();

var getItems = function(){
  var d = $.Deferred();
  $.get('categories?depth=3').done(
      function(data){
        d.resolve(data);
      }
  );
  return d.promise();
};

var jRoot = $('head').attr('data-rooturl');

$.when(getItems()).done(function(data){
  $(function() {

    // The main panel content is hidden by default via an
    // inline style. We're ready to remove that now.
    $('#create-item-panel').removeAttr('style');

    //////////////////////////////
    // helpful reference DOM

    var defaultMinToShow = 2;
    var defaultLooseItems = 'jenkins.category.uncategorized';
    var $root = $('#main-panel');
    var $form = $root.find('form[name="createItem"]').addClass('jenkins-config new-view');
    var $newView = $('
')
      .attr('name','createItem')
      .attr('action','createItem')
      .prependTo($form);
    var $navBox = $('');
    var $tabs = $('
').appendTo($newView);
    var $categories = $('
').appendTo($newView);
    var $subBtn = $('#bottom-sticker .yui-submit-button');
    var sectionsToShow = [];

    $tabs.prepend($navBox);

    ////////////////////////////////
    // submit button click

    function makeButtonWrapper(){
      var $p = window.$; // jshint ignore:line
      var btn = $p('ok');
      var okButton = window.makeButton(btn, null); // jshint ignore:line

      $subBtn = $('#bottom-sticker .yui-submit-button');

      checkFormReady();

      $subBtn.on('click',function() {
        $(this).addClass('yui-button-disabled yui-submit-button-disabled')
          .find('button')
            .attr('disabled','disabled')
            .text('. . .');
      });

    }

    ////////////////////////////////
    // scroll action......

    var isManualScrolling = false;
    var ignoreNextScrollEvent = false;
    var $window = $(window);
    var $breadcrumbBar = $('#breadcrumbBar');
    var $createItemPanel = $('#create-item-panel');
    var createPanelOffset = $createItemPanel.offset().top;

    function autoActivateTabs(){
      if (isManualScrolling === true) {
        // We ignore scroll events when a manual scroll is in
        // operation e.g. when the user clicks on a category tab.
        return;
      }
      if (ignoreNextScrollEvent === true) {
        // Things like repositioning of the tabbar can trigger scroll
        // events that we want to ignore.
        ignoreNextScrollEvent = false;
        return;
      }

      var winScoll = $window.scrollTop();

      $tabs.find('.active').removeClass('active');
      $.each(data.categories,function(i,cat){
        var domId = '#j-add-item-type-'+cat.id;
        var $cat = $(domId);
        var catHeight = ($cat.length > 0)?
        $cat.offset().top + $cat.outerHeight() - createPanelOffset: 0;

        if(winScoll < catHeight){
          var $thisTab = $tabs.find(['[href="',cleanHref(domId),'"]'].join(''));
          resetActiveTab($thisTab);
          return false;
        }
      });
    }

    function stickTabbar() {
      var winScoll = $window.scrollTop();
      if(winScoll > createPanelOffset - $breadcrumbBar.height()){
        $tabs.width($tabs.width()).css({
          'position':'fixed',
          'top':($breadcrumbBar.height() - 5 )+'px'});
        $categories.css({'margin-top':$tabs.outerHeight()+'px'});
        return true;
      } else{
        $tabs.add($categories).removeAttr('style');
        return false;
      }
    }

    //////////////////////////
    // helper functions...

    function checkFormReady(){
      //make sure everyone has changed and gotten attached...
      setTimeout(function(){
        var $name = $form.removeClass('no-select').find('input[name="name"]').removeClass('no-val');

        function checkItems(){
          var selected = $form.find('input[type="radio"]:checked').length > 0;
          var named = $.trim($name.val()).length > 0;
          return {selected:selected,named:named};
        }
        if(checkItems().selected && checkItems().named){
          $subBtn.removeClass('yui-button-disabled').find('button').removeAttr('disabled');
        }
        else{
          $subBtn.addClass('yui-button-disabled').find('button').attr('disabled','disabled');
        }
      },10);
    }

    function addCopyOption(data){
      var $copy = $('#copy').closest('tr');
      if($copy.length === 0) {return data;} // exit if copy should not be added to page. Jelly page holds that logic.
      var copyTitle = $copy.find('label').text();
      var copyDom = $copy.next().find('.setting-main').html();
      var copy = {
          name:'Copy',
          id:'copy',
          minToShow:0,
          items:[
            {
              class:"copy",
              description:copyDom,
              displayName:copyTitle,
              iconFilePathPattern:'images/48x48/copy.png'
            }
          ]
      };
      var newData = [];

      $.each(data,function(i,elem){
        if(elem.id !== "category-id-copy")
          { newData.push(elem); }
      });

      newData.push(copy);

      return newData;
    }
    function checkForLink(desc){
      if(desc.indexOf('<a href="') === -1) {
        return false;
      }
      var newDesc = desc.replace(/\</g,'<').replace(/\>/g,'>');
      return newDesc;
    }
    function sortItemsByOrder(itemTypes) {
      function sortByOrder(a, b) {
        var result = (a.weight - b.weight);
        result = Math.max(result, -1); // If lt than -1, make it -1
        result = Math.min(result, +1); // If gt than +1, make it +1
        return result;
      }
      return itemTypes.sort(sortByOrder);
    }

    function checkCatCount(elem){
      var minToShow = (typeof elem.minToShow === 'number')? elem.minToShow : defaultMinToShow;
      var showIt = ($.isArray(elem.items) && elem.items.length >= minToShow);
      return showIt;
    }

    function cleanClassName(className){
      return className.replace(/\./g,'_');
    }

    function cleanHref(id,reverse){
      if(reverse){
        var gotHash = (id.indexOf('#') === 0)?
           '#j-add-item-type-'+ id.substring(1).replace(/\./g,'_'):
             'j-add-item-type-'+ id.replace(/\./g,'_');
        return gotHash;
      }
      else{
        return id.replace('j-add-item-type-','');
      }
    }

    function cleanLayout(){
      // Do a little shimmy-hack to force legacy code to resize correctly and set tab state.
      $('html,body').animate({scrollTop: 1}, 1);
      $('html,body').animate({scrollTop: 0}, 10);

      setTimeout(fireBottomStickerAdjustEvent,410);
    }

    function resetActiveTab($this){
      var $nav = $this.closest('.nav');
      $nav.find('.active').removeClass('active');
      $this.addClass('active');
    }

    //////////////////////////////////
    // Draw functions

    function drawName() {
      var $name = $('
');

      var $input = $('<input type="text" name="name" class="name" id="name" placeholder="New item name..." />')
        .keyup(function(){
          $form.find('input[name="name"]').val($(this).val());
          checkFormReady();
        })
        .appendTo($name);

      $tabs.prepend($name);
      setTimeout(function(){$input.focus();},100);
    }

    function drawTabs(data){
      $('#main-panel').addClass('container');
      var $nav = $('
');

      $.each(data,function(i,elem){

        // little bit hacky here... need to keep track if I have tabs to show, so if there is just 1, I can hide it later....
        if (elem.minToShow !== 0 && checkCatCount(elem)) {sectionsToShow.push(elem.id);}

        var $tab = drawTab(i,elem);
        var $items = drawCategory(elem);
        var $cat = $items.parent();

        $.each(elem.items,function(i,elem){
          var $item = drawItem(elem);
          $items.append($item);
        });

        if(checkCatCount(elem)) {$nav.append($tab);}
        $categories.append($cat);

      });
      $(window).on('scroll', autoActivateTabs);
      $(window).on('scroll', stickTabbar);

      if(sectionsToShow.length > 3){
        $navBox.append($nav);
      }else{
        $categories.find('.category-header').hide();
      }
      drawName();
      cleanLayout();
    }

    function drawTab(i,elem){
      if(!elem) {elem = i;}
      var $tab = $(['
',elem.name,'
'].join(''))
        .click(function(){
          var $this = $(this).children('a');

          var tab = $this.attr('href');
          var scrollTop = $(cleanHref(tab,true)).offset().top - ($newView.children('.jenkins-config-widgets').height() + 15);

          setTimeout(function(){resetActiveTab($this);},510);

          isManualScrolling = true;
          $('html,body').animate({
            scrollTop: scrollTop
          }, 500, function() {
            isManualScrolling = false;
            ignoreNextScrollEvent = stickTabbar();
          });
        });
      return $tab;
    }

    function drawCategory(i,elem){
      // categories are smart, so this is a little tricky....
      if (!elem) {
        elem = i;
      }
      var $category = $('
').addClass('category jenkins-config hide-cat').attr('id', 'j-add-item-type-'+cleanClassName(elem.id));
      var $items = $('
').addClass('j-item-options').appendTo($category);

      // if there are enough items for a category, attach the category and its header...
      if(checkCatCount(elem)){
        var $catHeader = $('
').prependTo($category);
        var catheader = ['
', elem.name, '
'].join('');
        var catDesc = ['
', elem.description, '
'].join('');

        if((elem.minToShow > 0)){
          $(catheader).appendTo($catHeader);
          $(catDesc).appendTo($catHeader);
        }
        $category.removeClass('hide-cat');
      }
      else{
        var targ = elem.remainders || defaultLooseItems;
        $items = $('#'+cleanHref(targ,true)).find('.j-item-options');
      }

      return $items;
    }

    function drawItem(elem){
      var desc = (checkForLink(elem.description))? checkForLink(elem.description):elem.description;
      var $item = $([
          '
<input name="mode" value="',elem.class,'" type="radio" /> ', elem.displayName, '
'
      ].join('')).append([
          '
', desc, '
'
      ].join('')).append(drawIcon(elem));

      function setSelectState(e){
        e.preventDefault();
        var href = $(e.target).attr('href');
        if(href) {
          window.open(href);
        }

        var $this = $(this).closest('li');
        $this.closest('.categories').find('input[type="radio"][name="mode"]').removeAttr('checked');
        //if this is a hyperlink, don't move the selection.
        if($this.find('a:focus').length === 1) {return false;}
        $this.closest('.categories').find('.active').removeClass('active');
        $this.addClass('active');
        $this.find('input[type="radio"]').prop('checked', true);
        checkFormReady();
      }

      $item.click(setSelectState);

      return $item;
    }

    function drawIcon(elem){
      var $icn = $('
');
      if (!elem.iconFilePathPattern) {
        var colors = ['c-49728B','c-335061','c-D33833','c-6D6B6D','c-DCD9D8','other'];
        var desc = elem.description || '';
        var name = elem.displayName;
        var colorClass= colors[(desc.length) % 6];
        var aName = name.split(' ');
        var a = name.substring(0,1);
        var b = ((aName.length === 1)?
            name.substring(1,2):
              aName[1].substring(0,1));
        $([
          '',a,'',b,''
         ].join(''))
          .appendTo($icn);
        return $icn.addClass('df').addClass(colorClass);
      }

      var iconFilePath = jRoot + '/' + elem.iconFilePathPattern.replace(":size", "48x48");
      $([''].join(''))
        .appendTo($icn);

      return $icn;
    }

    // initialize
    var sortedDCategories = sortItemsByOrder(data.categories);
    var sortedDCategoriesWithCopy = addCopyOption(sortedDCategories);
    makeButtonWrapper();
    drawTabs(sortedDCategoriesWithCopy);

  });
});

function fireBottomStickerAdjustEvent() {
  Event.fire(window, 'jenkins:bottom-sticker-adjust'); // jshint ignore:line
}<|endoftext|>


<commit_before>function hotkey()
{
var a=window.event.keyCode;
if(event.ctrlKey)
{
display('need')
}
}
function display(y)
{
	$(y).style.display=($(y).style.display=="none")?"":"none";
}
function $(s)
{
	return document.getElementById(s);
}
document.getElementsByTagName("html")[0].setAttribute("id","need");
document.onkeydown = hotkey;<commit_msg>fix name of variable<commit_after>function hotkey()
{
var a=window.event.keyCode;
if(event.ctrlKey)
{
display('needToHide')
}
}
function display(y)
{
	$(y).style.display=($(y).style.display=="none")?"":"none";
}
function $(s)
{
	return document.getElementById(s);
}
document.getElementsByTagName("html")[0].setAttribute("id","needToHide");
document.onkeydown = hotkey;<|endoftext|>