<commit_before>/*
 *
 * Copyright (c) 2013 - 2014 Maximiliano Fierro
 * Licensed under the MIT license.
 */
'use strict';

var sequence = require('./processor/sequence'),
    cocktail,
    ANNOTATION_REG_EXP = /^@/;

cocktail = {
    /**
     * @private
     * Stack of _queues
     */
    _qStack: [],
    /**
     * @private
     * The queue of processors instances for the given mix
     */
    _queue: [],

    /**
     *@private
     * Current processor list map
     */
    _processors: {},

    /**
     * @protected
     * Returns the processor list map
     */
    getProcessors : function(){
        return this._processors;
    },

    /**
     * @protected
     * sets the processor object list. It is an Object used as a map
     */
    setProcessors: function (processor) {
        this._processors = processor;
    },

    /**
     * @protected
     * returns the list of default processors
     */
    getDefaultProcessors: function () {
        return cocktail._DEFAULT_PROCESSORS;
    },

    /**
     * @protected
     * registers a processor definition
     * @param processorsConfig {Object} a key-value pair of processors
     */
    registerProcessors: function(processorsConfig){
        var processors = this.getProcessors(),
            key;
        for(key in processorsConfig){
            if(processorsConfig.hasOwnProperty(key)){
                processors[key] =  processorsConfig[key];
            }
        }
    },

    /**
     * @public
     */
    use: function(annotation){
        var name = annotation.name || (annotation.prototype && annotation.prototype.name),
            processor = {};

        if(name && annotation.prototype){
            processor[name] = annotation;
            this.registerProcessors(processor);
        }
    },

    /**
     * @private
     * returns a processor instance for the given key or a NoOp instance if it is not found.
     */
    _getProcessorFor: function(key){
        var processors = this.getProcessors(),
            P;
        P = (processors[key] || processors['no-op']);
        return new P();
    },

    /**
     * @private
     * applies default options to the given options parameter.
     * As of today, the only default option is the configuration for the merge annotation
     */
    _applyDefaultsOptions: function(options){
        if(options && !('@merge' in options) ){
            options['@merge'] = "single";
        }
    },

    /**
     * @private
     * iterates over options to find annotations and adds processors to the queue.
     */
    _configureProcessorsWith: function(options){
        var key, value, processor;

        this._cleanQueue();

        if(options){
            for(key in options){
                if(options.hasOwnProperty(key) && ANNOTATION_REG_EXP.test(key)){
                    value = options[key];
                    //get the processor instance for this annotation
                    processor = this._getProcessorFor(key);
                    //configure the annotation parameter
                    processor.setParameter(value);
                    //check if the annotation should be removed
                    if(!processor.retain){
                        delete options[key];
                    }
                    //add the processor to the queue
                    this._addProcessorToQueue(processor);
                }
            }
        }
    },

    /**
     * @private
     * stacks current queue
     */
    _pushQueue: function() {
        this._qStack.push(this._queue);
        this._queue = [];
    },

    /**
     * @private
     * restore current queue
     */
    _popQueue: function() {
        this._queue = this._qStack.pop();
    },

    /**
     * @private
     * Cleans the processor queue
     */
    _cleanQueue: function(){
        this._queue.length = 0;
    },

    /**
     * @private
     * Adds the given processor to the queue
     */
    _addProcessorToQueue: function(processor){
        if(processor && processor.priority !== -1){
            this._queue.push(processor);
        }
    },

    /**
     * @private
     * Sorts the queue by its processor's priorities
     */
    _sortQueueByPriority: function(){
        this._queue.sort(function(a, b){
            return a.priority - b.priority;
        });
    },

    /**
     * @private
     * Runs all the processors in the queue over the given subject
     */
    _executeProcessorsOn: function(subject, options){
        var processors = this._queue,
            l = processors.length,
            i;

        this._sortQueueByPriority();

        for(i = 0; i < l; i++){
            processors[i].process(subject, options);
        }

    },

    /**
     * @private
     * returns true if the given subject is a class definition object.
     */
    _isClassDefition: function (subject) {
        var isClassDef = (subject && subject['@as'] && subject['@as'].toLowerCase() === 'class'),
            definitionProps = ['constructor', '@extends', '@traits', '@requires', '@annotation'],
            key;

        if (!isClassDef) {
            for (key in subject) {
                if(definitionProps.indexOf(key) > -1){
                    isClassDef = true;
                    break;
                }
            }
        }

        return isClassDef;
    },

    /**
     * @private
     * returns true if the given subject is a module definition object.
     */
    _isModuleDefinition: function (subject) {
        return (subject && subject['@as'] && subject['@as'].toLowerCase() === 'module');
    },

    /**
     * @private
     * If the subject has a property construtor returns it,
     * if no constructor on subject but it extends then return a function() calling super constructor,
     * or a function definition otherwise.
     */
    _getDefaultClassConstructor: function (subject) {
        var ctor, parent;

        if (this._isPropertyDefinedIn('constructor', subject)) {
            ctor = subject.constructor;
        } else if (this._isPropertyDefinedIn('@extends', subject)) {
            parent = subject['@extends'];
            ctor = function(){
                parent.prototype.constructor.apply(this, arguments);
            };
        } else {
            ctor = function(){};
        }

        return ctor;
    },

    /**
     * @private
     * checks if the given property is enumerable and defined in the obj
     */
    _isPropertyDefinedIn: function (property, obj) {
        var k;

        for (k in obj) {
            if(property === k) {
                return true;
            }
        }

        return false;
    },

    /**
     * @private
     * returns a call to mix() with the subject constructor and options
     */
    _processClassDefition: function(subject) {
        var defaultConstructor, options;

        defaultConstructor = this._getDefaultClassConstructor(subject);
        if(this._isPropertyDefinedIn('constructor', subject)) {
            delete subject.constructor;
        }
        options = subject;
        return this.mix(defaultConstructor, options);
    },

    /**
     * @private
     * @experimental 0.5.1
     * returns a call to mix() with the subject module and options
     */
    _processModuleDefinition: function (subject) {
        var options = subject;
        return this.mix(subject, options);
    },

    /**
     * @public
     */
    mix: function(subject, options){
        if(!options){
            if (this._isClassDefition(subject)) {
                return this._processClassDefition(subject);
            }
            if (this._isModuleDefinition(subject)) {
                return this._processModuleDefinition(subject);
            }
        }

        if(subject){
            this._pushQueue();
            this._applyDefaultsOptions(options);
            this._configureProcessorsWith(options);
            this._executeProcessorsOn(subject, options);
            this._popQueue();
        }

        return subject;
    }

};

module.exports = cocktail;

/**
 * @private
 * The processors class list.
 */
cocktail._DEFAULT_PROCESSORS = {
    'no-op'       : require('./processor/NoOp'),
    '@as'         : undefined, /*pseudo-processor*/
    '@merge'      : require('./processor/annotation/Merge'),
    '@extends'    : require('./processor/annotation/Extends'),
    '@properties' : require('./processor/annotation/Properties'),
    '@traits'     : require('./processor/annotation/Traits'),
    '@requires'   : require('./processor/annotation/Requires'),
    '@talents'    : require('./processor/annotation/Traits'),
    '@annotation' : require('./processor/annotation/Annotation'),
    '@exports'    : require('./processor/annotation/Exports'),
    '@static'     : require('./processor/annotation/Static'),
};

cocktail.registerProcessors(cocktail._DEFAULT_PROCESSORS);

/**
 * @public
 * SEQUENCE is used to define an enumeration of priorities for annotations
 */
 cocktail.SEQUENCE = sequence;
<commit_msg>Refactoring method to check as pseudo annotation values<commit_after>/*
 *
 * Copyright (c) 2013 - 2014 Maximiliano Fierro
 * Licensed under the MIT license.
 */
'use strict';

var sequence = require('./processor/sequence'),
    cocktail,
    ANNOTATION_REG_EXP = /^@/;

cocktail = {
    /**
     * @private
     * Stack of _queues
     */
    _qStack: [],
    /**
     * @private
     * The queue of processors instances for the given mix
     */
    _queue: [],

    /**
     *@private
     * Current processor list map
     */
    _processors: {},

    /**
     * @protected
     * Returns the processor list map
     */
    getProcessors : function(){
        return this._processors;
    },

    /**
     * @protected
     * sets the processor object list. It is an Object used as a map
     */
    setProcessors: function (processor) {
        this._processors = processor;
    },

    /**
     * @protected
     * returns the list of default processors
     */
    getDefaultProcessors: function () {
        return cocktail._DEFAULT_PROCESSORS;
    },

    /**
     * @protected
     * registers a processor definition
     * @param processorsConfig {Object} a key-value pair of processors
     */
    registerProcessors: function(processorsConfig){
        var processors = this.getProcessors(),
            key;
        for(key in processorsConfig){
            if(processorsConfig.hasOwnProperty(key)){
                processors[key] =  processorsConfig[key];
            }
        }
    },

    /**
     * @public
     */
    use: function(annotation){
        var name = annotation.name || (annotation.prototype && annotation.prototype.name),
            processor = {};

        if(name && annotation.prototype){
            processor[name] = annotation;
            this.registerProcessors(processor);
        }
    },

    /**
     * @private
     * returns a processor instance for the given key or a NoOp instance if it is not found.
     */
    _getProcessorFor: function(key){
        var processors = this.getProcessors(),
            P;
        P = (processors[key] || processors['no-op']);
        return new P();
    },

    /**
     * @private
     * applies default options to the given options parameter.
     * As of today, the only default option is the configuration for the merge annotation
     */
    _applyDefaultsOptions: function(options){
        if(options && !('@merge' in options) ){
            options['@merge'] = "single";
        }
    },

    /**
     * @private
     * iterates over options to find annotations and adds processors to the queue.
     */
    _configureProcessorsWith: function(options){
        var key, value, processor;

        this._cleanQueue();

        if(options){
            for(key in options){
                if(options.hasOwnProperty(key) && ANNOTATION_REG_EXP.test(key)){
                    value = options[key];
                    //get the processor instance for this annotation
                    processor = this._getProcessorFor(key);
                    //configure the annotation parameter
                    processor.setParameter(value);
                    //check if the annotation should be removed
                    if(!processor.retain){
                        delete options[key];
                    }
                    //add the processor to the queue
                    this._addProcessorToQueue(processor);
                }
            }
        }
    },

    /**
     * @private
     * stacks current queue
     */
    _pushQueue: function() {
        this._qStack.push(this._queue);
        this._queue = [];
    },

    /**
     * @private
     * restore current queue
     */
    _popQueue: function() {
        this._queue = this._qStack.pop();
    },

    /**
     * @private
     * Cleans the processor queue
     */
    _cleanQueue: function(){
        this._queue.length = 0;
    },

    /**
     * @private
     * Adds the given processor to the queue
     */
    _addProcessorToQueue: function(processor){
        if(processor && processor.priority !== -1){
            this._queue.push(processor);
        }
    },

    /**
     * @private
     * Sorts the queue by its processor's priorities
     */
    _sortQueueByPriority: function(){
        this._queue.sort(function(a, b){
            return a.priority - b.priority;
        });
    },

    /**
     * @private
     * Runs all the processors in the queue over the given subject
     */
    _executeProcessorsOn: function(subject, options){
        var processors = this._queue,
            l = processors.length,
            i;

        this._sortQueueByPriority();

        for(i = 0; i < l; i++){
            processors[i].process(subject, options);
        }

    },

    /**
     * @private
     * returns true if the given subject has a pseudo annotation `@as` with the given value.
     */
    _isSubjectDefinedAs: function (subject, asType) {
        return (subject && subject['@as'] && subject['@as'].toLowerCase() === asType);
    },

    /**
     * @private
     * returns true if the given subject is a class definition object.
     */
    _isClassDefition: function (subject) {
        var isClassDef = this._isSubjectDefinedAs(subject, 'class'),
            definitionProps = ['constructor', '@extends', '@traits', '@requires', '@annotation'],
            key;

        if (!isClassDef) {
            for (key in subject) {
                if(definitionProps.indexOf(key) > -1){
                    isClassDef = true;
                    break;
                }
            }
        }

        return isClassDef;
    },

    /**
     * @private
     * returns true if the given subject is a module definition object.
     */
    _isModuleDefinition: function (subject) {
        return this._isSubjectDefinedAs(subject, 'module');
    },

    /**
     * @private
     * If the subject has a property construtor returns it,
     * if no constructor on subject but it extends then return a function() calling super constructor,
     * or a function definition otherwise.
     */
    _getDefaultClassConstructor: function (subject) {
        var ctor, parent;

        if (this._isPropertyDefinedIn('constructor', subject)) {
            ctor = subject.constructor;
        } else if (this._isPropertyDefinedIn('@extends', subject)) {
            parent = subject['@extends'];
            ctor = function(){
                parent.prototype.constructor.apply(this, arguments);
            };
        } else {
            ctor = function(){};
        }

        return ctor;
    },

    /**
     * @private
     * checks if the given property is enumerable and defined in the obj
     */
    _isPropertyDefinedIn: function (property, obj) {
        var k;

        for (k in obj) {
            if(property === k) {
                return true;
            }
        }

        return false;
    },

    /**
     * @private
     * returns a call to mix() with the subject constructor and options
     */
    _processClassDefition: function(subject) {
        var defaultConstructor, options;

        defaultConstructor = this._getDefaultClassConstructor(subject);
        if(this._isPropertyDefinedIn('constructor', subject)) {
            delete subject.constructor;
        }
        options = subject;
        return this.mix(defaultConstructor, options);
    },

    /**
     * @private
     * @experimental 0.5.1
     * returns a call to mix() with the subject module and options
     */
    _processModuleDefinition: function (subject) {
        var options = subject;
        return this.mix(subject, options);
    },

    /**
     * @public
     */
    mix: function(subject, options){
        if(!options){
            if (this._isClassDefition(subject)) {
                return this._processClassDefition(subject);
            }
            if (this._isModuleDefinition(subject)) {
                return this._processModuleDefinition(subject);
            }
        }

        if(subject){
            this._pushQueue();
            this._applyDefaultsOptions(options);
            this._configureProcessorsWith(options);
            this._executeProcessorsOn(subject, options);
            this._popQueue();
        }

        return subject;
    }

};

//export module
module.exports = cocktail;

/**
 * @private
 * The processors class list.
 */
cocktail._DEFAULT_PROCESSORS = {
    'no-op'       : require('./processor/NoOp'),
    '@as'         : undefined, /*pseudo-processor*/
    '@merge'      : require('./processor/annotation/Merge'),
    '@extends'    : require('./processor/annotation/Extends'),
    '@properties' : require('./processor/annotation/Properties'),
    '@traits'     : require('./processor/annotation/Traits'),
    '@requires'   : require('./processor/annotation/Requires'),
    '@talents'    : require('./processor/annotation/Traits'),
    '@annotation' : require('./processor/annotation/Annotation'),
    '@exports'    : require('./processor/annotation/Exports'),
    '@static'     : require('./processor/annotation/Static'),
};

//register processors
cocktail.registerProcessors(cocktail._DEFAULT_PROCESSORS);

/**
 * @public
 * SEQUENCE is used to define an enumeration of priorities for annotations
 */
 cocktail.SEQUENCE = sequence;
<|endoftext|>


<commit_before>var Q = require('q');
var chalk = require('chalk');

var Gitter = require('./core/connection');
var User = require('./models/user');
var Configuration = require('./core/configuration');

var Commands = {

  /**
   * Authorization required wrapper.
   *
   * @private
   * @param {bool} requireAuthentication
   * @return {void}
   */
  _requireAuthentication: function (requireAuthentication) {
    var defer = Q.defer();

    if (typeof requireAuthentication === 'undefined' || requireAuthentication) {
      Gitter.then(function (gitterInstance) {
        defer.resolve();
      }).catch(function () {
        defer.reject();
      });
    } else {
      defer.resolve();
    }

    return defer.promise;
  },

  /**
   * Authorize a token or display a message to informe
   * the user how to get it.
   *
   * @param {string} token
   * @return {void}
   */
  authorize: function (token) {
    if (typeof token === 'string') {
      // Set the token based on the user input.
      Configuration.access.setToken(token).catch(function () {
        // Or if the token is not correct, display an error.
        console.log('\n%s\n', chalk.red('The token is not correct.'));
      });

      // Stop the execution of the method.
      return undefined;
    }

    // Display the "Go to url" message with the instructions on the screen.
    console.log('\nLog in at %s to get your access token.',
      chalk.blue('https://developer.gitter.im/apps')
    );
    console.log('Then, copy your %s and run:\n', chalk.bold('Personal Access Token'));
    console.log('    %s %s',
      chalk.bold('gitter-cli authorize'),
      chalk.bold(chalk.green('paste-your-token-here'))
    );
    console.log(chalk.dim(
      '\n\nVisit the repository to know what\'s happening behind the scenes: %s ',
      chalk.blue('https://github.com/RodrigoEspinosa/gitter-cli')
    ));
  },

  /**
   * Display current user information or "You are not logged in."
   * message if the user doesn't have a valid token.
   *
   * @return {void}
   */
  whoAmI: function () {
    Commands._requireAuthentication(true).then(function () {
      User.getCurrentUser().then(function (user) {
        console.log('\n     You are: %s (%s)\n',
          chalk.bold(user.displayName),
          user.username
        );
      });

    }).catch(function () {
      console.log('You are not logged in.');
    });
  },

  /**
   * Join a room an display the interface for the chat.
   *
   * @param {string} roomName
   * @return {void}
   */
  joinRoom: function (roomName) {
    // Ensure that the room name is a string.
    if (typeof roomName !== 'string') {
      // If the room name is not a string, display a error message.
      console.log(chalk.red('You must provide a valid room name.'));
      // Finish the script excecution.
      process.exit(1);
    }

    Commands._requireAuthentication(true).then(function () {
      // Require the `Room` model.
      var Room = require('./models/room');

      // Try to join a room.
      var room = new Room();
      room.join(roomName);

      // Load the user interface.
      var ui = require('./ui/main')({ room: room });
    });
  },

  /**
   * Displays a list of rooms that the users is part of.
   *
   * @return {void}
   */
  rooms: function () {
    /**
     * Display every room on the rooms lists with a color
     * based on the unreadItems.
     *
     * @param {list}
     * @return {void}
     */
    var displayRooms = function (rooms) {
      // Get the maximum room length name of the list.
      var maxRoomLength = rooms.map(function (room) {
        return room.name.length;
      }).reduce(function (prev, val) {
        return (prev > val) ? prev : val;
      }, 0);

      // Increase the max room length to consider the space.
      maxRoomLength += 1;

      // Display every room.
      rooms.forEach(function (room, index) {
        // Set the format for the room name.
        var roomName = room.name;
        if (roomName.length > maxRoomLength) {
          roomName = roomName.substring(0, maxRoomLength - 3) + '...';
        } else {
          roomName += Array(maxRoomLength - roomName.length).join(' ');
        }

        // Get the badge for the current item based on the number of unread messages.
        var badge = (room.unreadItems > 0) ? chalk.green : chalk.bgBlack;

        // Display the item in the console.
        console.log(badge(
          ++index + '.',                        // Display the index number.
          Array((index > 9) ? 4 : 5).join(' '), // Display a empty amount of spaces.
          roomName,                             // Display the room name.
          room.unreadItems                      // Display the unread messages count.
        ));
      });
    };

    // Display each of the user rooms if the user is authenticated.
    Commands._requireAuthentication(true)
      .then(User.getCurrentUser)
      .then(function (user) { return user.rooms(); })
      .then(displayRooms)
      .catch(function () {
        console.log('You are not logged in.');
      });
  },

  /**
   * Set the access token to be the specified token.
   *
   * @param {string} token
   * @return {void}
   */
  setToken: function (token) {
    require('./core/configuration').access.setToken(token);
  }
};

exports = module.exports = Commands;
<commit_msg>Remove unintentional `%s` on authorize command<commit_after>var Q = require('q');
var chalk = require('chalk');

var Gitter = require('./core/connection');
var User = require('./models/user');
var Configuration = require('./core/configuration');

var Commands = {

  /**
   * Authorization required wrapper.
   *
   * @private
   * @param {bool} requireAuthentication
   * @return {void}
   */
  _requireAuthentication: function (requireAuthentication) {
    var defer = Q.defer();

    if (typeof requireAuthentication === 'undefined' || requireAuthentication) {
      Gitter.then(function (gitterInstance) {
        defer.resolve();
      }).catch(function () {
        defer.reject();
      });
    } else {
      defer.resolve();
    }

    return defer.promise;
  },

  /**
   * Authorize a token or display a message to informe
   * the user how to get it.
   *
   * @param {string} token
   * @return {void}
   */
  authorize: function (token) {
    if (typeof token === 'string') {
      // Set the token based on the user input.
      Configuration.access.setToken(token).catch(function () {
        // Or if the token is not correct, display an error.
        console.log('\n%s\n', chalk.red('The token is not correct.'));
      });

      // Stop the execution of the method.
      return undefined;
    }

    // Display the "Go to url" message with the instructions on the screen.
    console.log('\nLog in at %s to get your access token.',
      chalk.blue('https://developer.gitter.im/apps')
    );
    console.log('Then, copy your %s and run:\n', chalk.bold('Personal Access Token'));
    console.log('    %s %s',
      chalk.bold('gitter-cli authorize'),
      chalk.bold(chalk.green('paste-your-token-here'))
    );
    console.log(chalk.dim(
      '\n\nVisit the repository to know what\'s happening behind the scenes:',
      chalk.blue('https://github.com/RodrigoEspinosa/gitter-cli')
    ));
  },

  /**
   * Display current user information or "You are not logged in."
   * message if the user doesn't have a valid token.
   *
   * @return {void}
   */
  whoAmI: function () {
    Commands._requireAuthentication(true).then(function () {
      User.getCurrentUser().then(function (user) {
        console.log('\n     You are: %s (%s)\n',
          chalk.bold(user.displayName),
          user.username
        );
      });

    }).catch(function () {
      console.log('You are not logged in.');
    });
  },

  /**
   * Join a room an display the interface for the chat.
   *
   * @param {string} roomName
   * @return {void}
   */
  joinRoom: function (roomName) {
    // Ensure that the room name is a string.
    if (typeof roomName !== 'string') {
      // If the room name is not a string, display a error message.
      console.log(chalk.red('You must provide a valid room name.'));
      // Finish the script excecution.
      process.exit(1);
    }

    Commands._requireAuthentication(true).then(function () {
      // Require the `Room` model.
      var Room = require('./models/room');

      // Try to join a room.
      var room = new Room();
      room.join(roomName);

      // Load the user interface.
      var ui = require('./ui/main')({ room: room });
    });
  },

  /**
   * Displays a list of rooms that the users is part of.
   *
   * @return {void}
   */
  rooms: function () {
    /**
     * Display every room on the rooms lists with a color
     * based on the unreadItems.
     *
     * @param {list}
     * @return {void}
     */
    var displayRooms = function (rooms) {
      // Get the maximum room length name of the list.
      var maxRoomLength = rooms.map(function (room) {
        return room.name.length;
      }).reduce(function (prev, val) {
        return (prev > val) ? prev : val;
      }, 0);

      // Increase the max room length to consider the space.
      maxRoomLength += 1;

      // Display every room.
      rooms.forEach(function (room, index) {
        // Set the format for the room name.
        var roomName = room.name;
        if (roomName.length > maxRoomLength) {
          roomName = roomName.substring(0, maxRoomLength - 3) + '...';
        } else {
          roomName += Array(maxRoomLength - roomName.length).join(' ');
        }

        // Get the badge for the current item based on the number of unread messages.
        var badge = (room.unreadItems > 0) ? chalk.green : chalk.bgBlack;

        // Display the item in the console.
        console.log(badge(
          ++index + '.',                        // Display the index number.
          Array((index > 9) ? 4 : 5).join(' '), // Display a empty amount of spaces.
          roomName,                             // Display the room name.
          room.unreadItems                      // Display the unread messages count.
        ));
      });
    };

    // Display each of the user rooms if the user is authenticated.
    Commands._requireAuthentication(true)
      .then(User.getCurrentUser)
      .then(function (user) { return user.rooms(); })
      .then(displayRooms)
      .catch(function () {
        console.log('You are not logged in.');
      });
  },

  /**
   * Set the access token to be the specified token.
   *
   * @param {string} token
   * @return {void}
   */
  setToken: function (token) {
    require('./core/configuration').access.setToken(token);
  }
};

exports = module.exports = Commands;
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import debounce from 'lodash/fp/debounce'
import noop from 'lodash/fp/noop'

import { remoteFunction } from 'src/util/webextensionRPC'
import { Tags, NewTagRow, TagRow, NoResult } from '../components'

class TagsContainer extends Component {
    static propTypes = {
        // The URL to use for dis/associating new tags with
        url: PropTypes.string.isRequired,

        // Opt. cb to run when new tag added to DB
        onTagAdd: PropTypes.func,

        // Opt. cb to run when tag deleted from DB
        onTagDel: PropTypes.func,
    }

    static defaultProps = {
        onTagAdd: noop,
        onTagDel: noop,
    }

    constructor(props) {
        super(props)

        this.suggestTags = remoteFunction('suggestTags')
        this.fetchTags = remoteFunction('fetchTags')
        this.addTags = remoteFunction('addTags')
        this.delTags = remoteFunction('delTags')

        this.fetchTagSuggestions = debounce(300)(this.fetchTagSuggestions)
    }

    state = {
        searchVal: '',
        isLoading: false,
        displayTags: [], // Display state objects; will change all the time
        tags: [], // Actual tags associated with the page; will only change when DB updates
    }

    componentDidMount() {
        this.fetchInitTags()
    }

    isPageTag = value => this.state.tags.includes(value)

    setInputRef = el => (this.inputEl = el)

    async fetchInitTags() {
        this.setState(state => ({ ...state, isLoading: true }))

        let tags = []
        try {
            tags = await this.fetchTags({ url: this.props.url })
        } catch (err) {
        } finally {
            this.setState(state => ({
                ...state,
                isLoading: false,
                tags,
                displayTags: tags,
            }))
        }
    }

    getDisplayTags = () =>
        this.state.displayTags.map(value => ({
            value,
            active: this.isPageTag(value),
        }))

    getSearchVal = () => this.state.searchVal.trim().replace(/\s\s+/g, ' ')

    canCreateTag() {
        const searchVal = this.getSearchVal()

        return (
            !!searchVal.length &&
            !this.state.tags.reduce(
                (acc, tag) => acc || tag === searchVal,
                false,
            )
        )
    }

    /**
     * Used for 'Enter' presses or 'Add new tag' clicks.
     */
    addTag = async () => {
        const newTag = this.getSearchVal()
        let newTags = this.state.tags

        try {
            await this.addTags({ url: this.props.url }, [newTag])
            newTags = [newTag, ...this.state.tags]
        } catch (err) {
        } finally {
            this.inputEl.focus()
            this.setState(state => ({
                ...state,
                searchVal: '',
                tags: newTags,
                displayTags: newTags,
            }))
            this.props.onTagAdd(newTag)
        }
    }

    /**
     * Used for clicks on displayed tags. Will either add or remove tags to the page
     * depending on their current status as assoc. tags or not.
     */
    handleTagSelection = index => async event => {
        const tag = this.state.displayTags[index]
        const tagIndex = this.state.tags.findIndex(val => val === tag)

        let tagsReducer = tags => tags
        // Either add or remove it to the main `state.tags` array
        try {
            if (tagIndex === -1) {
                await this.addTags({ url: this.props.url }, [tag])
                this.props.onTagAdd(tag)
                tagsReducer = tags => [tag, ...tags]
            } else {
                await this.delTags({ url: this.props.url }, [tag])
                this.props.onTagDel(tag)
                tagsReducer = tags => [
                    ...tags.slice(0, tagIndex),
                    ...tags.slice(tagIndex + 1),
                ]
            }
        } catch (err) {
        } finally {
            this.setState(state => ({
                ...state,
                tags: tagsReducer(state.tags),
            }))
        }
    }

    handleSearchEnterPress(event) {
        event.preventDefault()

        if (this.canCreateTag()) {
            this.addTag()
        }
    }

    handleSearchKeyDown = event => {
        switch (event.key) {
            case 'Enter':
                return this.handleSearchEnterPress(event)
        }
    }

    handleSearchChange = event => {
        const searchVal = event.target.value

        // If user backspaces to clear input, show the current assoc tags again
        const displayTags = !searchVal.length
            ? this.state.tags
            : this.state.displayTags

        this.setState(
            state => ({ ...state, searchVal, displayTags }),
            this.fetchTagSuggestions, // Debounced suggestion fetfh
        )
    }

    fetchTagSuggestions = async () => {
        if (!this.state.searchVal.length) {
            return
        }

        let suggestions = this.state.tags

        try {
            suggestions = await this.suggestTags(this.state.searchVal)
        } catch (err) {
        } finally {
            this.setState(state => ({
                ...state,
                displayTags: suggestions,
            }))
        }
    }

    renderTags() {
        const tags = this.getDisplayTags()

        const tagOptions =
            !tags.length && !this.state.searchVal.length
                ? [<NoResult key="-" />]
                : tags.map((tag, i) => (
                      <TagRow
                          {...tag}
                          key={i}
                          onClick={this.handleTagSelection(i)}
                      />
                  ))

        if (this.canCreateTag()) {
            tagOptions.push(
                <NewTagRow
                    key="+"
                    value={this.state.searchVal}
                    onClick={this.addTag}
                />,
            )
        }

        return tagOptions
    }

    render() {
        return (
            <Tags
                onTagSearchChange={this.handleSearchChange}
                onTagSearchKeyDown={this.handleSearchKeyDown}
                setInputRef={this.setInputRef}
                numberOfTags={this.state.tags.length}
                tagSearchValue={this.state.searchVal}
                {...this.props}
            >
                {this.renderTags()}
            </Tags>
        )
    }
}

export default TagsContainer
<commit_msg>Enforce case insensitivity for tags<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import debounce from 'lodash/fp/debounce'
import noop from 'lodash/fp/noop'

import { remoteFunction } from 'src/util/webextensionRPC'
import { Tags, NewTagRow, TagRow, NoResult } from '../components'

class TagsContainer extends Component {
    static propTypes = {
        // The URL to use for dis/associating new tags with
        url: PropTypes.string.isRequired,

        // Opt. cb to run when new tag added to DB
        onTagAdd: PropTypes.func,

        // Opt. cb to run when tag deleted from DB
        onTagDel: PropTypes.func,
    }

    static defaultProps = {
        onTagAdd: noop,
        onTagDel: noop,
    }

    constructor(props) {
        super(props)

        this.suggestTags = remoteFunction('suggestTags')
        this.fetchTags = remoteFunction('fetchTags')
        this.addTags = remoteFunction('addTags')
        this.delTags = remoteFunction('delTags')

        this.fetchTagSuggestions = debounce(300)(this.fetchTagSuggestions)
    }

    state = {
        searchVal: '',
        isLoading: false,
        displayTags: [], // Display state objects; will change all the time
        tags: [], // Actual tags associated with the page; will only change when DB updates
    }

    componentDidMount() {
        this.fetchInitTags()
    }

    isPageTag = value => this.state.tags.includes(value)

    setInputRef = el => (this.inputEl = el)

    async fetchInitTags() {
        this.setState(state => ({ ...state, isLoading: true }))

        let tags = []
        try {
            tags = await this.fetchTags({ url: this.props.url })
        } catch (err) {
        } finally {
            this.setState(state => ({
                ...state,
                isLoading: false,
                tags,
                displayTags: tags,
            }))
        }
    }

    /**
     * Selector for derived display tags state
     */
    getDisplayTags = () =>
        this.state.displayTags.map(value => ({
            value,
            active: this.isPageTag(value),
        }))

    /**
     * Selector for derived search value/new tag input state
     */
    getSearchVal = () =>
        this.state.searchVal
            .trim()
            .replace(/\s\s+/g, ' ')
            .toLowerCase()

    canCreateTag() {
        const searchVal = this.getSearchVal()

        return (
            !!searchVal.length &&
            !this.state.tags.reduce(
                (acc, tag) => acc || tag === searchVal,
                false,
            )
        )
    }

    /**
     * Used for 'Enter' presses or 'Add new tag' clicks.
     */
    addTag = async () => {
        const newTag = this.getSearchVal()
        let newTags = this.state.tags

        try {
            await this.addTags({ url: this.props.url }, [newTag])
            newTags = [newTag, ...this.state.tags]
        } catch (err) {
        } finally {
            this.inputEl.focus()
            this.setState(state => ({
                ...state,
                searchVal: '',
                tags: newTags,
                displayTags: newTags,
            }))
            this.props.onTagAdd(newTag)
        }
    }

    /**
     * Used for clicks on displayed tags. Will either add or remove tags to the page
     * depending on their current status as assoc. tags or not.
     */
    handleTagSelection = index => async event => {
        const tag = this.getDisplayTags()[index].value
        const tagIndex = this.state.tags.findIndex(val => val === tag)

        let tagsReducer = tags => tags
        // Either add or remove it to the main `state.tags` array
        try {
            if (tagIndex === -1) {
                await this.addTags({ url: this.props.url }, [tag])
                this.props.onTagAdd(tag)
                tagsReducer = tags => [tag, ...tags]
            } else {
                await this.delTags({ url: this.props.url }, [tag])
                this.props.onTagDel(tag)
                tagsReducer = tags => [
                    ...tags.slice(0, tagIndex),
                    ...tags.slice(tagIndex + 1),
                ]
            }
        } catch (err) {
        } finally {
            this.setState(state => ({
                ...state,
                tags: tagsReducer(state.tags),
            }))
        }
    }

    handleSearchEnterPress(event) {
        event.preventDefault()

        if (this.canCreateTag()) {
            this.addTag()
        }
    }

    handleSearchKeyDown = event => {
        switch (event.key) {
            case 'Enter':
                return this.handleSearchEnterPress(event)
        }
    }

    handleSearchChange = event => {
        const searchVal = event.target.value

        // If user backspaces to clear input, show the current assoc tags again
        const displayTags = !searchVal.length
            ? this.state.tags
            : this.state.displayTags

        this.setState(
            state => ({ ...state, searchVal, displayTags }),
            this.fetchTagSuggestions, // Debounced suggestion fetfh
        )
    }

    fetchTagSuggestions = async () => {
        const searchVal = this.getSearchVal()
        if (!searchVal.length) {
            return
        }

        let suggestions = this.state.tags

        try {
            suggestions = await this.suggestTags(searchVal)
        } catch (err) {
        } finally {
            this.setState(state => ({
                ...state,
                displayTags: suggestions,
            }))
        }
    }

    renderTags() {
        const tags = this.getDisplayTags()

        const tagOptions =
            !tags.length && !this.state.searchVal.length
                ? [<NoResult key="-" />]
                : tags.map((tag, i) => (
                      <TagRow
                          {...tag}
                          key={i}
                          onClick={this.handleTagSelection(i)}
                      />
                  ))

        if (this.canCreateTag()) {
            tagOptions.push(
                <NewTagRow
                    key="+"
                    value={this.state.searchVal}
                    onClick={this.addTag}
                />,
            )
        }

        return tagOptions
    }

    render() {
        return (
            <Tags
                onTagSearchChange={this.handleSearchChange}
                onTagSearchKeyDown={this.handleSearchKeyDown}
                setInputRef={this.setInputRef}
                numberOfTags={this.state.tags.length}
                tagSearchValue={this.state.searchVal}
                {...this.props}
            >
                {this.renderTags()}
            </Tags>
        )
    }
}

export default TagsContainer
<|endoftext|>


<commit_before>define(function(require) {

    var VisualMapModel = require('./VisualMapModel');
    var zrUtil = require('zrender/core/util');
    var VisualMapping = require('../../visual/VisualMapping');

    var PiecewiseModel = VisualMapModel.extend({

        type: 'visualMap.piecewise',

        /**
         * Order Rule:
         *
         * option.categories / option.pieces / option.text / option.selected:
         *     If !option.inverse,
         *     Order when vertical: ['top', ..., 'bottom'].
         *     Order when horizontal: ['left', ..., 'right'].
         *     If option.inverse, the meaning of
         *     the order should be reversed.
         *
         * this._pieceList:
         *     The order is always [low, ..., high].
         *
         * Mapping from location to low-high:
         *     If !option.inverse
         *     When vertical, top is high.
         *     When horizontal, right is high.
         *     If option.inverse, reverse.
         */

        /**
         * @protected
         */
        defaultOption: {
            selected: null,             // Object. If not specified, means selected.
                                        // When pieces and splitNumber: {'0': true, '5': true}
                                        // When categories: {'cate1': false, 'cate3': true}
                                        // When selected === false, means all unselected.
            align: 'auto',              // 'auto', 'left', 'right'
            itemWidth: 20,              // When put the controller vertically, it is the length of
                                        // horizontal side of each item. Otherwise, vertical side.
            itemHeight: 14,             // When put the controller vertically, it is the length of
                                        // vertical side of each item. Otherwise, horizontal side.
            itemSymbol: 'roundRect',
            pieceList: null,            // Each item is Object, with some of those attrs:
                                        // {min, max, lt, gt, lte, gte, value,
                                        // color, colorSaturation, colorAlpha, opacity,
                                        // symbol, symbolSize}, which customize the range or visual
                                        // coding of the certain piece. Besides, see "Order Rule".
            categories: null,           // category names, like: ['some1', 'some2', 'some3'].
                                        // Attr min/max are ignored when categories set. See "Order Rule"
            splitNumber: 5,             // If set to 5, auto split five pieces equally.
                                        // If set to 0 and component type not set, component type will be
                                        // determined as "continuous". (It is less reasonable but for ec2
                                        // compatibility, see echarts/component/visualMap/typeDefaulter)
            selectedMode: 'multiple',   // Can be 'multiple' or 'single'.
            itemGap: 10,                // The gap between two items, in px.
            hoverLink: true             // Enable hover highlight.
        },

        /**
         * @override
         */
        optionUpdated: function (newOption, isInit) {
            PiecewiseModel.superApply(this, 'optionUpdated', arguments);

            /**
             * The order is always [low, ..., high].
             * [{text: string, interval: Array.<number>}, ...]
             * @private
             * @type {Array.<Object>}
             */
            this._pieceList = [];

            this.resetTargetSeries();
            this.resetExtent();

            /**
             * 'pieces', 'categories', 'splitNumber'
             * @type {string}
             */
            var mode = this._mode = this._determineMode();

            resetMethods[this._mode].call(this);

            this._resetSelected(newOption, isInit);

            var categories = this.option.categories;

            this.resetVisual(function (mappingOption, state) {
                if (mode === 'categories') {
                    mappingOption.mappingMethod = 'category';
                    mappingOption.categories = zrUtil.clone(categories);
                }
                else {
                    mappingOption.dataExtent = this.getExtent();
                    mappingOption.mappingMethod = 'piecewise';
                    mappingOption.pieceList = zrUtil.map(this._pieceList, function (piece) {
                        var piece = zrUtil.clone(piece);
                        if (state !== 'inRange') {
                            piece.visual = null;
                        }
                        return piece;
                    });
                }
            });
        },

        _resetSelected: function (newOption, isInit) {
            var thisOption = this.option;
            var pieceList = this._pieceList;

            // Selected do not merge but all override.
            var selected = (isInit ? thisOption : newOption).selected || {};
            thisOption.selected = selected;

            // Consider 'not specified' means true.
            zrUtil.each(pieceList, function (piece, index) {
                var key = this.getSelectedMapKey(piece);
                if (!(key in selected)) {
                    selected[key] = true;
                }
            }, this);

            if (thisOption.selectedMode === 'single') {
                // Ensure there is only one selected.
                var hasSel = false;

                zrUtil.each(pieceList, function (piece, index) {
                    var key = this.getSelectedMapKey(piece);
                    if (selected[key]) {
                        hasSel
                            ? (selected[key] = false)
                            : (hasSel = true);
                    }
                }, this);
            }
            // thisOption.selectedMode === 'multiple', default: all selected.
        },

        /**
         * @public
         */
        getSelectedMapKey: function (piece) {
            return this._mode === 'categories'
                ? piece.value + '' : piece.index + '';
        },

        /**
         * @public
         */
        getPieceList: function () {
            return this._pieceList;
        },

        /**
         * @private
         * @return {string}
         */
        _determineMode: function () {
            var option = this.option;

            return option.pieces && option.pieces.length > 0
                ? 'pieces'
                : this.option.categories
                ? 'categories'
                : 'splitNumber';
        },

        /**
         * @public
         * @override
         */
        setSelected: function (selected) {
            this.option.selected = zrUtil.clone(selected);
        },

        /**
         * @public
         * @override
         */
        getValueState: function (value) {
            var index = VisualMapping.findPieceIndex(value, this._pieceList);

            return index != null
                ? (this.option.selected[this.getSelectedMapKey(this._pieceList[index])]
                    ? 'inRange' : 'outOfRange'
                )
                : 'outOfRange';
        },

        /**
         * @public
         * @params {number} pieceIndex piece index in visualMapModel.getPieceList()
         * @return {Array.<Object>} [{seriesId, dataIndices: <Array.<number>>}, ...]
         */
        findTargetDataIndices: function (pieceIndex) {
            var result = [];

            this.eachTargetSeries(function (seriesModel) {
                var dataIndices = [];
                var data = seriesModel.getData();

                data.each(this.getDataDimension(data), function (value, dataIndex) {
                    // Should always base on model pieceList, because it is order sensitive.
                    var pIdx = VisualMapping.findPieceIndex(value, this._pieceList);
                    pIdx === pieceIndex && dataIndices.push(dataIndex);
                }, true, this);

                result.push({seriesId: seriesModel.id, dataIndex: dataIndices});
            }, this);

            return result;
        },

        /**
         * @private
         */
        getRepresentValue: function (piece) {
            var representValue;
            if (this.isCategory()) {
                representValue = piece.value;
            }
            else {
                if (piece.value != null) {
                    representValue = piece.value;
                }
                else {
                    var pieceInterval = piece.interval || [];
                    representValue = (pieceInterval[0] + pieceInterval[1]) / 2;
                }
            }
            return representValue;
        },

        getStops: function (seriesModel, getColorVisual) {
            var result = [];
            var model = this;

            if (this.isTargetSeries(seriesModel)) {
                var curr = -Infinity;
                zrUtil.each(this._pieceList, function (piece) {
                    // Do not support category yet.
                    var interval = piece.interval;
                    if (interval) {
                        interval[0] > curr && setPiece({
                            interval: [curr, interval[0]],
                            valueState: 'outOfRange'
                        });
                        setPiece({
                            interval: interval.slice(),
                            valueState: this.getValueState((interval[0] + interval[1]) / 2)
                        });
                    }
                    curr = interval[1];
                }, this);
            }
            return result;

            function setPiece(piece) {
                result.push(piece);
                piece.color = getColorVisual(
                    model, model.getRepresentValue(piece), piece.valueState
                );
            }
        }

    });

    /**
     * Key is this._mode
     * @type {Object}
     * @this {module:echarts/component/viusalMap/PiecewiseMode}
     */
    var resetMethods = {

        splitNumber: function () {
            var thisOption = this.option;
            var pieceList = this._pieceList;
            var precision = thisOption.precision;
            var dataExtent = this.getExtent();
            var splitNumber = thisOption.splitNumber;
            splitNumber = Math.max(parseInt(splitNumber, 10), 1);
            thisOption.splitNumber = splitNumber;

            var splitStep = (dataExtent[1] - dataExtent[0]) / splitNumber;
            // Precision auto-adaption
            while (+splitStep.toFixed(precision) !== splitStep && precision < 5) {
                precision++;
            }
            thisOption.precision = precision;
            splitStep = +splitStep.toFixed(precision);

            for (var i = 0, curr = dataExtent[0]; i < splitNumber; i++, curr += splitStep) {
                var max = i === splitNumber - 1 ? dataExtent[1] : (curr + splitStep);

                pieceList.push({
                    index: i,
                    interval: [curr, max],
                    close: [1, 1]
                });
            }

            normalizePieces(pieceList);

            zrUtil.each(pieceList, function (piece) {
                piece.text = this.formatValueText(piece.interval);
            }, this);
        },

        categories: function () {
            var thisOption = this.option;
            zrUtil.each(thisOption.categories, function (cate) {
                // FIXME category模式也使用pieceList，但在visualMapping中不是使用pieceList。
                // 是否改一致。
                this._pieceList.push({
                    text: this.formatValueText(cate, true),
                    value: cate
                });
            }, this);

            // See "Order Rule".
            normalizeReverse(thisOption, this._pieceList);
        },

        pieces: function () {
            var thisOption = this.option;
            var pieceList = this._pieceList;

            zrUtil.each(thisOption.pieces, function (pieceListItem, index) {

                if (!zrUtil.isObject(pieceListItem)) {
                    pieceListItem = {value: pieceListItem};
                }

                var item = {text: '', index: index};

                if (pieceListItem.label != null) {
                    item.text = pieceListItem.label;
                }

                if (pieceListItem.hasOwnProperty('value')) {
                    var value = item.value = pieceListItem.value;
                    item.interval = [value, value];
                    item.close = [1, 1];
                }
                else {
                    // `min` `max` is legacy option.
                    // `lt` `gt` `lte` `gte` is recommanded.
                    var interval = item.interval = [];
                    var close = item.close = [0, 0];

                    var closeList = [1, 0, 1];
                    var infinityList = [-Infinity, Infinity];

                    var useMinMax = [];
                    for (var lg = 0; lg < 2; lg++) {
                        var names = [['gte', 'gt', 'min'], ['lte', 'lt', 'max']][lg];
                        for (var i = 0; i < 3 && interval[lg] == null; i++) {
                            interval[lg] = pieceListItem[names[i]];
                            close[lg] = closeList[i];
                            useMinMax[lg] = i === 2;
                        }
                        interval[lg] == null && (interval[lg] = infinityList[lg]);
                    }
                    useMinMax[0] && interval[1] === Infinity && (close[0] = 0);
                    useMinMax[1] && interval[0] === -Infinity && (close[1] = 0);

                    if (__DEV__) {
                        if (interval[0] > interval[1]) {
                            console.warn(
                                'Piece ' + index + 'is illegal: ' + interval
                                + ' lower bound should not greater then uppper bound.'
                            );
                        }
                    }

                    if (interval[0] === interval[1] && close[0] && close[1]) {
                        // Consider: [{min: 5, max: 5, visual: {...}}, {min: 0, max: 5}],
                        // we use value to lift the priority when min === max
                        item.value = interval[0];
                    }
                }

                item.visual = VisualMapping.retrieveVisuals(pieceListItem);

                pieceList.push(item);

            }, this);

            // See "Order Rule".
            normalizeReverse(thisOption, pieceList);
            // Only pieces
            normalizePieces(pieceList);

            zrUtil.each(pieceList, function (piece) {
                var close = piece.close;
                var edgeSymbols = [['<', '≤'][close[1]], ['>', '≥'][close[0]]];
                piece.text = piece.text || this.formatValueText(
                    piece.value != null ? piece.value : piece.interval,
                    false,
                    edgeSymbols
                );
            }, this);
        }
    };

    function normalizeReverse(thisOption, pieceList) {
        var inverse = thisOption.inverse;
        if (thisOption.orient === 'vertical' ? !inverse : inverse) {
             pieceList.reverse();
        }
    }

    // Reorder, remove duplicate, which are needed when using gradient.
    // Not applicable for categories.
    function normalizePieces(pieceList) {
        pieceList.sort(function (a, b) {
            return littleThan(a, b) ? -1 : 1;
        });

        var curr = -Infinity;
        for (var i = 0; i < pieceList.length; i++) {
            var interval = pieceList[i].interval;
            var close = pieceList[i].close;
            for (var lg = 0; lg < 2; lg++) {
                if (interval[lg] < curr) {
                    interval[lg] = curr;
                    close[lg] = 1 - lg;
                }
                curr = interval[lg];
            }
        }
        // console.log(JSON.stringify(pieceList.map(a => a.interval)));

        function littleThan(piece, standard, lg) {
            lg = lg || 0;
            return piece.interval[lg] < standard.interval[lg]
                || (
                    piece.interval[lg] === standard.interval[lg]
                    && (
                        +piece.close[lg] > standard.close[lg]
                        || littleThan(piece, standard, 1)
                    )
                );
        }
    }

    return PiecewiseModel;
});<commit_msg>fix piece visual bug.<commit_after>define(function(require) {

    var VisualMapModel = require('./VisualMapModel');
    var zrUtil = require('zrender/core/util');
    var VisualMapping = require('../../visual/VisualMapping');

    var PiecewiseModel = VisualMapModel.extend({

        type: 'visualMap.piecewise',

        /**
         * Order Rule:
         *
         * option.categories / option.pieces / option.text / option.selected:
         *     If !option.inverse,
         *     Order when vertical: ['top', ..., 'bottom'].
         *     Order when horizontal: ['left', ..., 'right'].
         *     If option.inverse, the meaning of
         *     the order should be reversed.
         *
         * this._pieceList:
         *     The order is always [low, ..., high].
         *
         * Mapping from location to low-high:
         *     If !option.inverse
         *     When vertical, top is high.
         *     When horizontal, right is high.
         *     If option.inverse, reverse.
         */

        /**
         * @protected
         */
        defaultOption: {
            selected: null,             // Object. If not specified, means selected.
                                        // When pieces and splitNumber: {'0': true, '5': true}
                                        // When categories: {'cate1': false, 'cate3': true}
                                        // When selected === false, means all unselected.
            align: 'auto',              // 'auto', 'left', 'right'
            itemWidth: 20,              // When put the controller vertically, it is the length of
                                        // horizontal side of each item. Otherwise, vertical side.
            itemHeight: 14,             // When put the controller vertically, it is the length of
                                        // vertical side of each item. Otherwise, horizontal side.
            itemSymbol: 'roundRect',
            pieceList: null,            // Each item is Object, with some of those attrs:
                                        // {min, max, lt, gt, lte, gte, value,
                                        // color, colorSaturation, colorAlpha, opacity,
                                        // symbol, symbolSize}, which customize the range or visual
                                        // coding of the certain piece. Besides, see "Order Rule".
            categories: null,           // category names, like: ['some1', 'some2', 'some3'].
                                        // Attr min/max are ignored when categories set. See "Order Rule"
            splitNumber: 5,             // If set to 5, auto split five pieces equally.
                                        // If set to 0 and component type not set, component type will be
                                        // determined as "continuous". (It is less reasonable but for ec2
                                        // compatibility, see echarts/component/visualMap/typeDefaulter)
            selectedMode: 'multiple',   // Can be 'multiple' or 'single'.
            itemGap: 10,                // The gap between two items, in px.
            hoverLink: true             // Enable hover highlight.
        },

        /**
         * @override
         */
        optionUpdated: function (newOption, isInit) {
            PiecewiseModel.superApply(this, 'optionUpdated', arguments);

            /**
             * The order is always [low, ..., high].
             * [{text: string, interval: Array.<number>}, ...]
             * @private
             * @type {Array.<Object>}
             */
            this._pieceList = [];

            this.resetTargetSeries();
            this.resetExtent();

            /**
             * 'pieces', 'categories', 'splitNumber'
             * @type {string}
             */
            var mode = this._mode = this._determineMode();

            resetMethods[this._mode].call(this);

            this._resetSelected(newOption, isInit);

            var categories = this.option.categories;

            this.resetVisual(function (mappingOption, state) {
                if (mode === 'categories') {
                    mappingOption.mappingMethod = 'category';
                    mappingOption.categories = zrUtil.clone(categories);
                }
                else {
                    mappingOption.dataExtent = this.getExtent();
                    mappingOption.mappingMethod = 'piecewise';
                    mappingOption.pieceList = zrUtil.map(this._pieceList, function (piece) {
                        var piece = zrUtil.clone(piece);
                        if (state !== 'inRange') {
                            piece.visual = null;
                        }
                        return piece;
                    });
                }
            });
        },

        _resetSelected: function (newOption, isInit) {
            var thisOption = this.option;
            var pieceList = this._pieceList;

            // Selected do not merge but all override.
            var selected = (isInit ? thisOption : newOption).selected || {};
            thisOption.selected = selected;

            // Consider 'not specified' means true.
            zrUtil.each(pieceList, function (piece, index) {
                var key = this.getSelectedMapKey(piece);
                if (!(key in selected)) {
                    selected[key] = true;
                }
            }, this);

            if (thisOption.selectedMode === 'single') {
                // Ensure there is only one selected.
                var hasSel = false;

                zrUtil.each(pieceList, function (piece, index) {
                    var key = this.getSelectedMapKey(piece);
                    if (selected[key]) {
                        hasSel
                            ? (selected[key] = false)
                            : (hasSel = true);
                    }
                }, this);
            }
            // thisOption.selectedMode === 'multiple', default: all selected.
        },

        /**
         * @public
         */
        getSelectedMapKey: function (piece) {
            return this._mode === 'categories'
                ? piece.value + '' : piece.index + '';
        },

        /**
         * @public
         */
        getPieceList: function () {
            return this._pieceList;
        },

        /**
         * @private
         * @return {string}
         */
        _determineMode: function () {
            var option = this.option;

            return option.pieces && option.pieces.length > 0
                ? 'pieces'
                : this.option.categories
                ? 'categories'
                : 'splitNumber';
        },

        /**
         * @public
         * @override
         */
        setSelected: function (selected) {
            this.option.selected = zrUtil.clone(selected);
        },

        /**
         * @public
         * @override
         */
        getValueState: function (value) {
            var index = VisualMapping.findPieceIndex(value, this._pieceList);

            return index != null
                ? (this.option.selected[this.getSelectedMapKey(this._pieceList[index])]
                    ? 'inRange' : 'outOfRange'
                )
                : 'outOfRange';
        },

        /**
         * @public
         * @params {number} pieceIndex piece index in visualMapModel.getPieceList()
         * @return {Array.<Object>} [{seriesId, dataIndices: <Array.<number>>}, ...]
         */
        findTargetDataIndices: function (pieceIndex) {
            var result = [];

            this.eachTargetSeries(function (seriesModel) {
                var dataIndices = [];
                var data = seriesModel.getData();

                data.each(this.getDataDimension(data), function (value, dataIndex) {
                    // Should always base on model pieceList, because it is order sensitive.
                    var pIdx = VisualMapping.findPieceIndex(value, this._pieceList);
                    pIdx === pieceIndex && dataIndices.push(dataIndex);
                }, true, this);

                result.push({seriesId: seriesModel.id, dataIndex: dataIndices});
            }, this);

            return result;
        },

        /**
         * @private
         */
        getRepresentValue: function (piece) {
            var representValue;
            if (this.isCategory()) {
                representValue = piece.value;
            }
            else {
                if (piece.value != null) {
                    representValue = piece.value;
                }
                else {
                    var pieceInterval = piece.interval || [];
                    representValue = (pieceInterval[0] + pieceInterval[1]) / 2;
                }
            }
            return representValue;
        },

        getStops: function (seriesModel, getColorVisual) {
            var result = [];
            var model = this;

            if (this.isTargetSeries(seriesModel)) {
                var curr = -Infinity;
                zrUtil.each(this._pieceList, function (piece) {
                    // Do not support category yet.
                    var interval = piece.interval;
                    if (interval) {
                        interval[0] > curr && setPiece({
                            interval: [curr, interval[0]],
                            valueState: 'outOfRange'
                        });
                        setPiece({
                            interval: interval.slice(),
                            valueState: this.getValueState((interval[0] + interval[1]) / 2)
                        });
                        curr = interval[1];
                    }
                }, this);
            }
            return result;

            function setPiece(piece) {
                result.push(piece);
                piece.color = getColorVisual(
                    model, model.getRepresentValue(piece), piece.valueState
                );
            }
        }

    });

    /**
     * Key is this._mode
     * @type {Object}
     * @this {module:echarts/component/viusalMap/PiecewiseMode}
     */
    var resetMethods = {

        splitNumber: function () {
            var thisOption = this.option;
            var pieceList = this._pieceList;
            var precision = thisOption.precision;
            var dataExtent = this.getExtent();
            var splitNumber = thisOption.splitNumber;
            splitNumber = Math.max(parseInt(splitNumber, 10), 1);
            thisOption.splitNumber = splitNumber;

            var splitStep = (dataExtent[1] - dataExtent[0]) / splitNumber;
            // Precision auto-adaption
            while (+splitStep.toFixed(precision) !== splitStep && precision < 5) {
                precision++;
            }
            thisOption.precision = precision;
            splitStep = +splitStep.toFixed(precision);

            for (var i = 0, curr = dataExtent[0]; i < splitNumber; i++, curr += splitStep) {
                var max = i === splitNumber - 1 ? dataExtent[1] : (curr + splitStep);

                pieceList.push({
                    index: i,
                    interval: [curr, max],
                    close: [1, 1]
                });
            }

            normalizePieces(pieceList);

            zrUtil.each(pieceList, function (piece) {
                piece.text = this.formatValueText(piece.interval);
            }, this);
        },

        categories: function () {
            var thisOption = this.option;
            zrUtil.each(thisOption.categories, function (cate) {
                // FIXME category模式也使用pieceList，但在visualMapping中不是使用pieceList。
                // 是否改一致。
                this._pieceList.push({
                    text: this.formatValueText(cate, true),
                    value: cate
                });
            }, this);

            // See "Order Rule".
            normalizeReverse(thisOption, this._pieceList);
        },

        pieces: function () {
            var thisOption = this.option;
            var pieceList = this._pieceList;

            zrUtil.each(thisOption.pieces, function (pieceListItem, index) {

                if (!zrUtil.isObject(pieceListItem)) {
                    pieceListItem = {value: pieceListItem};
                }

                var item = {text: '', index: index};

                if (pieceListItem.label != null) {
                    item.text = pieceListItem.label;
                }

                if (pieceListItem.hasOwnProperty('value')) {
                    var value = item.value = pieceListItem.value;
                    item.interval = [value, value];
                    item.close = [1, 1];
                }
                else {
                    // `min` `max` is legacy option.
                    // `lt` `gt` `lte` `gte` is recommanded.
                    var interval = item.interval = [];
                    var close = item.close = [0, 0];

                    var closeList = [1, 0, 1];
                    var infinityList = [-Infinity, Infinity];

                    var useMinMax = [];
                    for (var lg = 0; lg < 2; lg++) {
                        var names = [['gte', 'gt', 'min'], ['lte', 'lt', 'max']][lg];
                        for (var i = 0; i < 3 && interval[lg] == null; i++) {
                            interval[lg] = pieceListItem[names[i]];
                            close[lg] = closeList[i];
                            useMinMax[lg] = i === 2;
                        }
                        interval[lg] == null && (interval[lg] = infinityList[lg]);
                    }
                    useMinMax[0] && interval[1] === Infinity && (close[0] = 0);
                    useMinMax[1] && interval[0] === -Infinity && (close[1] = 0);

                    if (__DEV__) {
                        if (interval[0] > interval[1]) {
                            console.warn(
                                'Piece ' + index + 'is illegal: ' + interval
                                + ' lower bound should not greater then uppper bound.'
                            );
                        }
                    }

                    if (interval[0] === interval[1] && close[0] && close[1]) {
                        // Consider: [{min: 5, max: 5, visual: {...}}, {min: 0, max: 5}],
                        // we use value to lift the priority when min === max
                        item.value = interval[0];
                    }
                }

                item.visual = VisualMapping.retrieveVisuals(pieceListItem);

                pieceList.push(item);

            }, this);

            // See "Order Rule".
            normalizeReverse(thisOption, pieceList);
            // Only pieces
            normalizePieces(pieceList);

            zrUtil.each(pieceList, function (piece) {
                var close = piece.close;
                var edgeSymbols = [['<', '≤'][close[1]], ['>', '≥'][close[0]]];
                piece.text = piece.text || this.formatValueText(
                    piece.value != null ? piece.value : piece.interval,
                    false,
                    edgeSymbols
                );
            }, this);
        }
    };

    function normalizeReverse(thisOption, pieceList) {
        var inverse = thisOption.inverse;
        if (thisOption.orient === 'vertical' ? !inverse : inverse) {
             pieceList.reverse();
        }
    }

    // Reorder, remove duplicate, which are needed when using gradient.
    // Not applicable for categories.
    function normalizePieces(pieceList) {
        pieceList.sort(function (a, b) {
            return littleThan(a, b) ? -1 : 1;
        });

        var curr = -Infinity;
        for (var i = 0; i < pieceList.length; i++) {
            var interval = pieceList[i].interval;
            var close = pieceList[i].close;
            for (var lg = 0; lg < 2; lg++) {
                if (interval[lg] < curr) {
                    interval[lg] = curr;
                    close[lg] = 1 - lg;
                }
                curr = interval[lg];
            }
        }
        // console.log(JSON.stringify(pieceList.map(a => a.interval)));

        function littleThan(piece, standard, lg) {
            lg = lg || 0;
            return piece.interval[lg] < standard.interval[lg]
                || (
                    piece.interval[lg] === standard.interval[lg]
                    && (
                        +piece.close[lg] > standard.close[lg]
                        || littleThan(piece, standard, 1)
                    )
                );
        }
    }

    return PiecewiseModel;
});<|endoftext|>


<commit_before>/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */
 /* global $:false */

import React, { Component, PropTypes } from 'react';
import s from './AudioPlayer.scss';
import withStyles from '../../decorators/withStyles';
import Audio from '../Audio';

@withStyles(s)
class AudioPlayer extends Component {

  static propTypes = {
    audioSrc: PropTypes.string.isRequired,
    nbRepeat: PropTypes.number,
  };

  // Always call this function before doing anything with audio.
  // This is because the audio could be changed by another element.
  updateAudio() {
    self.audio = document.getElementById('audio-current');
    console.log(self.audio);
    self.audio.load();
    console.log(self.audio);
  }

  componentDidMount() {
    self = this;
    console.log('mounted');
    self.updateAudio();

    // self.audio.src = this.props.audioSrc;
    self.counter = this.props.nbRepeat;
    self.audio.addEventListener('ended', self.endOfAudio);

    self.counter -= 1;
    self.pause();
  }

  endOfAudio() {
    console.log(self.counter);
    if (self.counter !== 0) {
      self.counter = self.counter - 1;
      self.audio.play();
    } else {
      self.pause();
    }
  }

  play() {
    self.audio.play();
    self.playing = true;
    document.getElementById('btn').textContent = 'Pause';
  }

  pause() {
    self.audio.pause();
    self.playing = false;
    document.getElementById('btn').textContent = 'Play';
  }

  playPauseClick() {
    self.updateAudio();

    console.log('onClick, playing: ' + self.playing);
    if (self.playing) {
      self.pause();
    } else {
      self.play();
    }
  }

  render() {
    return (

        <button id="btn" onClick={this.playPauseClick}></button>
        <Audio src={this.props.audioSrc}/>

    );
  }

}

export default AudioPlayer;
<commit_msg>Error of self remaining<commit_after>/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */
 /* global $:false */

import React, { Component, PropTypes } from 'react';
import s from './AudioPlayer.scss';
import withStyles from '../../decorators/withStyles';
import Audio from '../Audio';

@withStyles(s)
class AudioPlayer extends Component {

  static propTypes = {
    audioSrc: PropTypes.string.isRequired,
    nbRepeat: PropTypes.number,
  };

  componentDidMount() {
    self = this;
    console.log('mounted');
    self.updateAudio();

    // self.audio.src = this.props.audioSrc;
    self.counter = this.props.nbRepeat;
    self.audio.addEventListener('ended', self.endOfAudio);

    self.counter -= 1;
    self.pause();
  }

  // Always call this function before doing anything with audio.
  // This is because the audio could be changed by another element.
  updateAudio() {
    self.audio = document.getElementById('audio-current');
    console.log(self.audio);
    self.audio.load();
    console.log(self.audio);
  }

  endOfAudio() {
    console.log(self.counter);
    if (self.counter !== 0) {
      self.counter = self.counter - 1;
      self.audio.play();
    } else {
      self.pause();
    }
  }

  play() {
    self.audio.play();
    self.playing = true;
    document.getElementById('btn').textContent = 'Pause';
  }

  pause() {
    self.audio.pause();
    self.playing = false;
    document.getElementById('btn').textContent = 'Play';
  }

  playPauseClick() {
    self.updateAudio();

    console.log('onClick, playing: ' + self.playing);
    if (self.playing) {
      self.pause();
    } else {
      self.play();
    }
  }

  render() {
    return (

        <button id="btn" onClick={this.playPauseClick}></button>
        <Audio src={this.props.audioSrc}/>

    );
  }

}

export default AudioPlayer;
<|endoftext|>


<commit_before>import React, { useEffect } from 'react'
import { isDefined, isObject } from '../../utilities/is'
import { ITEM_TYPES } from './DropList.constants'
import { SelectTag } from './DropList.togglers'
import { ListItemUI, EmptyListUI } from './DropList.css'

// No need to test this helper
/* istanbul ignore next */
export function displayWarnings({
  toggler,
  withMultipleSelection,
  menuCSS,
  tippyOptions,
}) {
  if (
    process.env.NODE_ENV !== 'production' &&
    process.env.NODE_ENV !== 'test'
  ) {
    if (!React.isValidElement(toggler)) {
      console.info(
        'Pass one of the provided togglers or a custom one to the `toggler` prop'
      )
    }
    if (isTogglerOfType(toggler, SelectTag) && withMultipleSelection) {
      console.info(
        'The Select toggler option should not have withMultipleSelection enabled, it has been disabled for you'
      )
    }
    if (menuCSS != null && tippyOptions.appendTo === undefined) {
      console.error(
        'menuCSS is only needed when using tippyOptions.appendTo to portal the DropList, please use regular styled components if you need custom styles'
      )
    }
  }
}

export function useWarnings(props) {
  useEffect(() => {
    displayWarnings(props)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])
}

export function isTogglerOfType(toggler, type) {
  return React.isValidElement(toggler) && toggler.type === type
}

export function itemToString(item) {
  if (item == null || checkIfGroupOrDividerItem(item)) return ''
  if (isObject(item)) return item[getItemContentKeyName(item)]
  return item
}

export function parseSelectionFromProps({ withMultipleSelection, selection }) {
  if (withMultipleSelection) {
    return selection != null ? [].concat(selection) : []
  }
  if (selection != null) {
    return selection != null ? selection : null
  }
}

export function isItemSelected({ item, selectedItem, selectedItems }) {
  if (selectedItem == null && selectedItems.length === 0) return false

  if (isObject(item)) {
    const itemContentKey = getItemContentKeyName(item)
    const itemContent = item[itemContentKey]

    if (selectedItem != null && selectedItems.length === 0) {
      const selectedItemContentKey = getItemContentKeyName(selectedItem)
      const selectedItemContent = selectedItem[selectedItemContentKey]

      return selectedItemContent === itemContent
    }

    return Boolean(
      selectedItems.find(item => item[itemContentKey] === itemContent)
    )
  }

  return selectedItem === item || selectedItems.includes(item)
}

export function getItemContentKeyName(item) {
  if (objectHasKey(item, 'label')) return 'label'
  if (objectHasKey(item, 'value')) return 'value'

  return undefined
}

export function objectHasKey(obj, key) {
  return isObject(obj) && isDefined(obj[key])
}

export function findItemInArray({ item, arr, key = 'label' }) {
  if (item == null) return undefined

  return arr.find(i => {
    if (isObject(i)) {
      return i[key] === item[key]
    }
    if (isObject(item)) {
      return i === item[key]
    }
    return i === item
  })
}

export function removeItemFromArray({ item, arr, key = 'label' }) {
  return arr.filter(i => {
    if (isObject(i)) {
      return i[key] !== item[key]
    }
    if (isObject(item)) {
      return i !== item[key]
    }
    return i !== item
  })
}

export function isItemADivider(item) {
  return objectHasKey(item, 'type') && item.type === ITEM_TYPES.DIVIDER
}

export function isItemAGroup(item) {
  return objectHasKey(item, 'type') && item.type === ITEM_TYPES.GROUP
}

export function isItemAGroupLabel(item) {
  return objectHasKey(item, 'type') && item.type === ITEM_TYPES.GROUP_LABEL
}

export function flattenListItems(listItems) {
  return listItems.reduce((accumulator, listItem) => {
    const contentKey = getItemContentKeyName(listItem)

    if (isItemAGroup(listItem)) {
      const itemsInGroup = listItem.items.map(item => ({
        ...item,
        group: listItem[contentKey],
      }))

      return itemsInGroup.length > 0
        ? accumulator
            .concat({
              type: ITEM_TYPES.GROUP_LABEL,
              [contentKey]: listItem[contentKey],
            })
            .concat(itemsInGroup)
        : accumulator
    }

    return accumulator.concat(listItem)
  }, [])
}

export function renderListContents({
  customEmptyList,
  emptyList,
  inputValue,
  items,
  renderListItem,
}) {
  if (emptyList && !customEmptyList) return <EmptyListUI>No items</EmptyListUI>

  if (emptyList && customEmptyList) {
    return React.isValidElement(customEmptyList) ? (
      React.cloneElement(customEmptyList)
    ) : (
      <EmptyListUI>No items</EmptyListUI>
    )
  }

  if (items.length > 0) {
    return items.map(renderListItem)
  }

  return <ListItemUI>No results for {inputValue}</ListItemUI>
}

// No need to test this helper
/* istanbul ignore next */
export function requiredItemPropsCheck(props, propName, componentName) {
  if (!props.label && !props.value) {
    return new Error(
      `One of 'label' or 'value' is required by '${componentName}' component.`
    )
  }
}

function checkIfGroupOrDividerItem(item) {
  return isItemADivider(item) || isItemAGroup(item) || isItemAGroupLabel(item)
}

export function getEnabledItemIndex({
  currentHighlightedIndex,
  nextHighlightedIndex,
  items,
  arrowKey,
}) {
  // When nextHighlightedIndex === -1 it means there are no items to be highlighted
  // like in the case of a combobox being filtered to "no results"
  if (nextHighlightedIndex === -1) return -1

  const isNextIndexItemHighlightable =
    !checkIfGroupOrDividerItem(items[nextHighlightedIndex]) &&
    !items[nextHighlightedIndex].isDisabled

  if (
    isNextIndexItemHighlightable &&
    currentHighlightedIndex !== nextHighlightedIndex
  ) {
    return nextHighlightedIndex
  }

  let newNextHighlightedIndex
  const firstIndex = 0
  const lastIndex = items.length - 1

  if (arrowKey === 'UP') {
    const isNextIndexAfterFirst = nextHighlightedIndex - 1 >= firstIndex

    newNextHighlightedIndex = isNextIndexAfterFirst
      ? nextHighlightedIndex - 1
      : lastIndex
  } else {
    const isNextIndexBeforeLast = nextHighlightedIndex + 1 <= lastIndex

    newNextHighlightedIndex = isNextIndexBeforeLast
      ? nextHighlightedIndex + 1
      : firstIndex
  }

  return getEnabledItemIndex({
    currentHighlightedIndex,
    nextHighlightedIndex: newNextHighlightedIndex,
    items,
    arrowKey,
  })
}
<commit_msg>Improve guard<commit_after>import React, { useEffect } from 'react'
import { isDefined, isObject, isString } from '../../utilities/is'
import { ITEM_TYPES } from './DropList.constants'
import { SelectTag } from './DropList.togglers'
import { ListItemUI, EmptyListUI } from './DropList.css'

// No need to test this helper
/* istanbul ignore next */
export function displayWarnings({
  toggler,
  withMultipleSelection,
  menuCSS,
  tippyOptions,
}) {
  if (
    process.env.NODE_ENV !== 'production' &&
    process.env.NODE_ENV !== 'test'
  ) {
    if (!React.isValidElement(toggler)) {
      console.info(
        'Pass one of the provided togglers or a custom one to the `toggler` prop'
      )
    }
    if (isTogglerOfType(toggler, SelectTag) && withMultipleSelection) {
      console.info(
        'The Select toggler option should not have withMultipleSelection enabled, it has been disabled for you'
      )
    }
    if (menuCSS != null && tippyOptions.appendTo === undefined) {
      console.error(
        'menuCSS is only needed when using tippyOptions.appendTo to portal the DropList, please use regular styled components if you need custom styles'
      )
    }
  }
}

export function useWarnings(props) {
  useEffect(() => {
    displayWarnings(props)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])
}

export function isTogglerOfType(toggler, type) {
  return React.isValidElement(toggler) && toggler.type === type
}

export function itemToString(item) {
  if (item == null || checkIfGroupOrDividerItem(item)) return ''
  if (isString(item)) return item
  if (isObject(item)) {
    const itemContentKeyName = getItemContentKeyName(item)

    return itemContentKeyName ? item[itemContentKeyName] : ''
  }

  return ''
}

export function parseSelectionFromProps({ withMultipleSelection, selection }) {
  if (withMultipleSelection) {
    return selection != null ? [].concat(selection) : []
  }
  if (selection != null) {
    return selection != null ? selection : null
  }
}

export function isItemSelected({ item, selectedItem, selectedItems }) {
  if (selectedItem == null && selectedItems.length === 0) return false

  if (isObject(item)) {
    const itemContentKey = getItemContentKeyName(item)
    const itemContent = item[itemContentKey]

    if (selectedItem != null && selectedItems.length === 0) {
      const selectedItemContentKey = getItemContentKeyName(selectedItem)
      const selectedItemContent = selectedItem[selectedItemContentKey]

      return selectedItemContent === itemContent
    }

    return Boolean(
      selectedItems.find(item => item[itemContentKey] === itemContent)
    )
  }

  return selectedItem === item || selectedItems.includes(item)
}

export function getItemContentKeyName(item) {
  if (objectHasKey(item, 'label')) return 'label'
  if (objectHasKey(item, 'value')) return 'value'

  return undefined
}

export function objectHasKey(obj, key) {
  return isObject(obj) && isDefined(obj[key])
}

export function findItemInArray({ item, arr, key = 'label' }) {
  if (item == null) return undefined

  return arr.find(i => {
    if (isObject(i)) {
      return i[key] === item[key]
    }
    if (isObject(item)) {
      return i === item[key]
    }
    return i === item
  })
}

export function removeItemFromArray({ item, arr, key = 'label' }) {
  return arr.filter(i => {
    if (isObject(i)) {
      return i[key] !== item[key]
    }
    if (isObject(item)) {
      return i !== item[key]
    }
    return i !== item
  })
}

export function isItemADivider(item) {
  return objectHasKey(item, 'type') && item.type === ITEM_TYPES.DIVIDER
}

export function isItemAGroup(item) {
  return objectHasKey(item, 'type') && item.type === ITEM_TYPES.GROUP
}

export function isItemAGroupLabel(item) {
  return objectHasKey(item, 'type') && item.type === ITEM_TYPES.GROUP_LABEL
}

export function flattenListItems(listItems) {
  return listItems.reduce((accumulator, listItem) => {
    const contentKey = getItemContentKeyName(listItem)

    if (isItemAGroup(listItem)) {
      const itemsInGroup = listItem.items.map(item => ({
        ...item,
        group: listItem[contentKey],
      }))

      return itemsInGroup.length > 0
        ? accumulator
            .concat({
              type: ITEM_TYPES.GROUP_LABEL,
              [contentKey]: listItem[contentKey],
            })
            .concat(itemsInGroup)
        : accumulator
    }

    return accumulator.concat(listItem)
  }, [])
}

export function renderListContents({
  customEmptyList,
  emptyList,
  inputValue,
  items,
  renderListItem,
}) {
  if (emptyList && !customEmptyList) return <EmptyListUI>No items</EmptyListUI>

  if (emptyList && customEmptyList) {
    return React.isValidElement(customEmptyList) ? (
      React.cloneElement(customEmptyList)
    ) : (
      <EmptyListUI>No items</EmptyListUI>
    )
  }

  if (items.length > 0) {
    return items.map(renderListItem)
  }

  return <ListItemUI>No results for {inputValue}</ListItemUI>
}

// No need to test this helper
/* istanbul ignore next */
export function requiredItemPropsCheck(props, propName, componentName) {
  if (!props.label && !props.value) {
    return new Error(
      `One of 'label' or 'value' is required by '${componentName}' component.`
    )
  }
}

function checkIfGroupOrDividerItem(item) {
  return isItemADivider(item) || isItemAGroup(item) || isItemAGroupLabel(item)
}

export function getEnabledItemIndex({
  currentHighlightedIndex,
  nextHighlightedIndex,
  items,
  arrowKey,
}) {
  // When nextHighlightedIndex === -1 it means there are no items to be highlighted
  // like in the case of a combobox being filtered to "no results"
  if (nextHighlightedIndex === -1) return -1

  const isNextIndexItemHighlightable =
    !checkIfGroupOrDividerItem(items[nextHighlightedIndex]) &&
    !items[nextHighlightedIndex].isDisabled

  if (
    isNextIndexItemHighlightable &&
    currentHighlightedIndex !== nextHighlightedIndex
  ) {
    return nextHighlightedIndex
  }

  let newNextHighlightedIndex
  const firstIndex = 0
  const lastIndex = items.length - 1

  if (arrowKey === 'UP') {
    const isNextIndexAfterFirst = nextHighlightedIndex - 1 >= firstIndex

    newNextHighlightedIndex = isNextIndexAfterFirst
      ? nextHighlightedIndex - 1
      : lastIndex
  } else {
    const isNextIndexBeforeLast = nextHighlightedIndex + 1 <= lastIndex

    newNextHighlightedIndex = isNextIndexBeforeLast
      ? nextHighlightedIndex + 1
      : firstIndex
  }

  return getEnabledItemIndex({
    currentHighlightedIndex,
    nextHighlightedIndex: newNextHighlightedIndex,
    items,
    arrowKey,
  })
}
<|endoftext|>