<commit_before>"use strict";
module.exports = function (SIP) {

var RegisterContext;

RegisterContext = function (ua, options = {}) {
  this.options = {};

  this.logger = ua.getLogger('sip.registercontext');

  this.loadConfig(options);

  if (this.options.regId && !this.options.instanceId) {
    this.options.instanceId = SIP.Utils.newUUID();
  } else if (!this.options.regId && this.options.instanceId) {
    this.options.regId = 1;
  }

  this.options.params.to_uri = this.options.params.to_uri || ua.configuration.uri;
  this.options.params.to_displayName = this.options.params.to_displayName || ua.configuration.displayName;
  this.options.params.call_id = this.options.params.call_id || SIP.Utils.createRandomToken(22);
  this.options.params.cseq = this.options.params.cseq || Math.floor(Math.random() * 10000);

  /* If no 'registrarServer' is set use the 'uri' value without user portion. */
  if (!this.options.registrar) {
    let registrarServer = {};
    if (typeof ua.configuration.uri === 'object') {
      registrarServer = ua.configuration.uri.clone();
      registrarServer.user = null;
    } else {
      registrarServer = ua.configuration.uri;
    }
    this.options.registrar = registrarServer;
  }

  // Registration expires
  this.expires = this.options.expires;

  // Cseq
  this.cseq = this.options.params.cseq;

  // Contact header
  this.contact = ua.contact.toString();

  // Extends ClientContext
  SIP.Utils.augment(this, SIP.ClientContext, [ua, 'REGISTER', this.options.registrar, this.options]);

  this.registrationTimer = null;
  this.registrationExpiredTimer = null;

  // Set status
  this.registered = false;

  ua.on('transportCreated', function (transport) {
    transport.on('disconnected', this.onTransportDisconnected.bind(this));
  }.bind(this));
};

RegisterContext.prototype = Object.create({}, {
  /**
   * Configuration load.
   * @private
   * returns {Boolean}
   */
  loadConfig: {writable: true, value: function loadConfig(configuration) {
    let parameter, value, checkedValue;
    const settings = {
      expires: 600,
      extraContactHeaderParams: [],
      instanceId: null,
      params: {},
      regId: null,
      registrar: null,
    };

    const configCheck = this.getConfigurationCheck();

    // Check Mandatory parameters
    for(parameter in configCheck.mandatory) {
      if(!configuration.hasOwnProperty(parameter)) {
        throw new SIP.Exceptions.ConfigurationError(parameter);
      } else {
        value = configuration[parameter];
        checkedValue = configCheck.mandatory[parameter](value);
        if (checkedValue !== undefined) {
          settings[parameter] = checkedValue;
        } else {
          throw new SIP.Exceptions.ConfigurationError(parameter, value);
        }
      }
    }

    // Check Optional parameters
    for(parameter in configCheck.optional) {
      if(configuration.hasOwnProperty(parameter)) {
        value = configuration[parameter];

        // If the parameter value is an empty array, but shouldn't be, apply its default value.
        if (value instanceof Array && value.length === 0) { continue; }

        // If the parameter value is null, empty string, or undefined then apply its default value.
        if(value === null || value === '' || value === undefined) { continue; }
        // If it's a number with NaN value then also apply its default value.
        // NOTE: JS does not allow "value === NaN", the following does the work:
        else if(typeof(value) === 'number' && isNaN(value)) { continue; }

        checkedValue = configCheck.optional[parameter](value);
        if (checkedValue !== undefined) {
          settings[parameter] = checkedValue;
        } else {
          throw new SIP.Exceptions.ConfigurationError(parameter, value);
        }
      }
    }

    Object.assign(this.options, settings);

    this.logger.log('configuration parameters for RegisterContext after validation:');
    for(parameter in settings) {
      this.logger.log('· ' + parameter + ': ' + JSON.stringify(settings[parameter]));
    }

    return;

  }},

  getConfigurationCheck: {writable: true, value: function getConfigurationCheck () {
    return {
      mandatory: {
      },

      optional: {
        expires: function(expires) {
          if (SIP.Utils.isDecimal(expires)) {
            const value = Number(expires);
            if (value >= 0) {
              return value;
            }
          }
        },
        extraContactHeaderParams: function(extraContactHeaderParams) {
          if (extraContactHeaderParams instanceof Array) {
            return extraContactHeaderParams.filter((contactHeaderParam) => (typeof contactHeaderParam === 'string'));
          }
        },
        instanceId: function(instanceId) {
          if (typeof instanceId !== 'string') {
            return;
          }

          if ((/^uuid:/i.test(instanceId))) {
            instanceId = instanceId.substr(5);
          }

          if (SIP.Grammar.parse(instanceId, 'uuid') === -1) {
            return;
          } else {
            return instanceId;
          }
        },
        params: function(params) {
          if (typeof params === 'object') {
            return params;
          }
        },
        regId: function(regId) {
          if (SIP.Utils.isDecimal(regId)) {
            const value = Number(regId);
            if (value >= 0) {
              return value;
            }
          }
        },
        registrar: function(registrar) {
          if(typeof registrar !== 'string') {
            return;
          }

          if (!/^sip:/i.test(registrar)) {
            registrar = SIP.C.SIP + ':' + registrar;
          }
          const parsed = SIP.URI.parse(registrar);

          if(!parsed) {
            return;
          } else if(parsed.user) {
            return;
          } else {
            return parsed;
          }
        }
      }
    };
  }},

  /**
   * Helper Function to generate Contact Header
   * @private
   * returns {String}
   */
  generateContactHeader: {writable: true, value: function generateContactHeader(expires) {
    let contact = this.contact;
    if (this.options.regId && this.options.instanceId) {
      contact += ';reg-id=' + this.options.regId;
      contact += ';+sip.instance="<urn:uuid:' + this.options.instanceId + '>"';
    }

    if (this.options.extraContactHeaderParams) {
      this.options.extraContactHeaderParams.forEach((header) => {
        contact += ';' + header;
      });
    }

    if (expires) {
      contact += ';expires=' + expires;
    }

    return contact;
  }},

  register: {writable: true, value: function register (options = {}) {
    // Handle Options
    this.options = SIP.Utils.defaultOptions(this.options, options);
    const extraHeaders = (this.options.extraHeaders || []).slice();

    extraHeaders.push('Contact: ' + this.generateContactHeader(this.expires));
    extraHeaders.push('Allow: ' + SIP.UA.C.ALLOWED_METHODS.toString());

    // Save original extraHeaders to be used in .close
    this.closeHeaders = this.options.closeWithHeaders ?
      (this.options.extraHeaders || []).slice() : [];

    this.receiveResponse = function(response) {
      var contact, expires,
        contacts = response.getHeaders('contact').length,
        cause;

      // Discard responses to older REGISTER/un-REGISTER requests.
      if(response.cseq !== this.cseq) {
        return;
      }

      // Clear registration timer
      if (this.registrationTimer !== null) {
        SIP.Timers.clearTimeout(this.registrationTimer);
        this.registrationTimer = null;
      }

      switch(true) {
        case /^1[0-9]{2}$/.test(response.status_code):
          this.emit('progress', response);
          break;
        case /^2[0-9]{2}$/.test(response.status_code):
          this.emit('accepted', response);

          if(response.hasHeader('expires')) {
            expires = response.getHeader('expires');
          }

          if (this.registrationExpiredTimer !== null) {
            SIP.Timers.clearTimeout(this.registrationExpiredTimer);
            this.registrationExpiredTimer = null;
          }

          // Search the Contact pointing to us and update the expires value accordingly.
          if (!contacts) {
            this.logger.warn('no Contact header in response to REGISTER, response ignored');
            break;
          }

          while(contacts--) {
            contact = response.parseHeader('contact', contacts);
            if(contact.uri.user === this.ua.contact.uri.user) {
              expires = contact.getParam('expires');
              break;
            } else {
              contact = null;
            }
          }

          if (!contact) {
            this.logger.warn('no Contact header pointing to us, response ignored');
            break;
          }

          if(!expires) {
            expires = this.expires;
          }

          // Re-Register before the expiration interval has elapsed.
          // For that, decrease the expires value. ie: 3 seconds
          this.registrationTimer = SIP.Timers.setTimeout(() => {
            this.registrationTimer = null;
            this.register(this.options);
          }, (expires * 1000) - 3000);
          this.registrationExpiredTimer = SIP.Timers.setTimeout(() => {
            this.logger.warn('registration expired');
            if (this.registered) {
              this.unregistered(null, SIP.C.causes.EXPIRES);
            }
          }, expires * 1000);

          //Save gruu values
          if (contact.hasParam('temp-gruu')) {
            this.ua.contact.temp_gruu = SIP.URI.parse(contact.getParam('temp-gruu').replace(/"/g,''));
          }
          if (contact.hasParam('pub-gruu')) {
            this.ua.contact.pub_gruu = SIP.URI.parse(contact.getParam('pub-gruu').replace(/"/g,''));
          }

          this.registered = true;
          this.emit('registered', response || null);
          break;
        // Interval too brief RFC3261 10.2.8
        case /^423$/.test(response.status_code):
          if(response.hasHeader('min-expires')) {
            // Increase our registration interval to the suggested minimum
            this.expires = response.getHeader('min-expires');
            // Attempt the registration again immediately
            this.register(this.options);
          } else { //This response MUST contain a Min-Expires header field
            this.logger.warn('423 response received for REGISTER without Min-Expires');
            this.registrationFailure(response, SIP.C.causes.SIP_FAILURE_CODE);
          }
          break;
        default:
          cause = SIP.Utils.sipErrorCause(response.status_code);
          this.registrationFailure(response, cause);
      }
    };

    this.onRequestTimeout = function() {
      this.registrationFailure(null, SIP.C.causes.REQUEST_TIMEOUT);
    };

    this.onTransportError = function() {
      this.registrationFailure(null, SIP.C.causes.CONNECTION_ERROR);
    };

    this.cseq++;
    this.request.cseq = this.cseq;
    this.request.setHeader('cseq', this.cseq + ' REGISTER');
    this.request.extraHeaders = extraHeaders;
    this.send();
  }},

  registrationFailure: {writable: true, value: function registrationFailure  (response, cause) {
    this.emit('failed', response || null, cause || null);
  }},

  onTransportDisconnected: {writable: true, value: function onTransportDisconnected () {
    this.registered_before = this.registered;
    if (this.registrationTimer !== null) {
      SIP.Timers.clearTimeout(this.registrationTimer);
      this.registrationTimer = null;
    }

    if (this.registrationExpiredTimer !== null) {
      SIP.Timers.clearTimeout(this.registrationExpiredTimer);
      this.registrationExpiredTimer = null;
    }

    if(this.registered) {
      this.unregistered(null, SIP.C.causes.CONNECTION_ERROR);
    }
  }},

  close: {writable: true, value: function close () {
    var options = {
      all: false,
      extraHeaders: this.closeHeaders
    };

    this.registered_before = this.registered;
    if (this.registered) {
      this.unregister(options);
    }
  }},

  unregister: {writable: true, value: function unregister (options) {
    var extraHeaders;

    options = options || {};

    if(!this.registered && !options.all) {
      this.logger.warn('Already unregistered, but sending an unregister anyways.');
    }

    extraHeaders = (options.extraHeaders || []).slice();

    this.registered = false;

    // Clear the registration timer.
    if (this.registrationTimer !== null) {
      SIP.Timers.clearTimeout(this.registrationTimer);
      this.registrationTimer = null;
    }

    if(options.all) {
      extraHeaders.push('Contact: *');
      extraHeaders.push('Expires: 0');
    } else {
      extraHeaders.push('Contact: '+ this.generateContactHeader(0));
    }

    this.receiveResponse = function(response) {
      var cause;

      switch(true) {
        case /^1[0-9]{2}$/.test(response.status_code):
          this.emit('progress', response);
          break;
        case /^2[0-9]{2}$/.test(response.status_code):
          this.emit('accepted', response);
          if (this.registrationExpiredTimer !== null) {
            SIP.Timers.clearTimeout(this.registrationExpiredTimer);
            this.registrationExpiredTimer = null;
          }
          this.unregistered(response);
          break;
        default:
          cause = SIP.Utils.sipErrorCause(response.status_code);
          this.unregistered(response,cause);
      }
    };

    this.onRequestTimeout = function() {
      // Not actually unregistered...
      //this.unregistered(null, SIP.C.causes.REQUEST_TIMEOUT);
    };

    this.cseq++;
    this.request.cseq = this.cseq;
    this.request.setHeader('cseq', this.cseq + ' REGISTER');
    this.request.extraHeaders = extraHeaders;

    this.send();
  }},

  unregistered: {writable: true, value: function unregistered (response, cause) {
    this.registered = false;
    this.emit('unregistered', response || null, cause || null);
  }}

});

SIP.RegisterContext = RegisterContext;
};
<commit_msg>Allow expires on register to be set to 0<commit_after>"use strict";
module.exports = function (SIP) {

var RegisterContext;

RegisterContext = function (ua, options = {}) {
  this.options = {};

  this.logger = ua.getLogger('sip.registercontext');

  this.loadConfig(options);

  if (this.options.regId && !this.options.instanceId) {
    this.options.instanceId = SIP.Utils.newUUID();
  } else if (!this.options.regId && this.options.instanceId) {
    this.options.regId = 1;
  }

  this.options.params.to_uri = this.options.params.to_uri || ua.configuration.uri;
  this.options.params.to_displayName = this.options.params.to_displayName || ua.configuration.displayName;
  this.options.params.call_id = this.options.params.call_id || SIP.Utils.createRandomToken(22);
  this.options.params.cseq = this.options.params.cseq || Math.floor(Math.random() * 10000);

  /* If no 'registrarServer' is set use the 'uri' value without user portion. */
  if (!this.options.registrar) {
    let registrarServer = {};
    if (typeof ua.configuration.uri === 'object') {
      registrarServer = ua.configuration.uri.clone();
      registrarServer.user = null;
    } else {
      registrarServer = ua.configuration.uri;
    }
    this.options.registrar = registrarServer;
  }

  // Registration expires
  this.expires = this.options.expires;

  // Cseq
  this.cseq = this.options.params.cseq;

  // Contact header
  this.contact = ua.contact.toString();

  // Extends ClientContext
  SIP.Utils.augment(this, SIP.ClientContext, [ua, 'REGISTER', this.options.registrar, this.options]);

  this.registrationTimer = null;
  this.registrationExpiredTimer = null;

  // Set status
  this.registered = false;

  ua.on('transportCreated', function (transport) {
    transport.on('disconnected', this.onTransportDisconnected.bind(this));
  }.bind(this));
};

RegisterContext.prototype = Object.create({}, {
  /**
   * Configuration load.
   * @private
   * returns {Boolean}
   */
  loadConfig: {writable: true, value: function loadConfig(configuration) {
    let parameter, value, checkedValue;
    const settings = {
      expires: 600,
      extraContactHeaderParams: [],
      instanceId: null,
      params: {},
      regId: null,
      registrar: null,
    };

    const configCheck = this.getConfigurationCheck();

    // Check Mandatory parameters
    for(parameter in configCheck.mandatory) {
      if(!configuration.hasOwnProperty(parameter)) {
        throw new SIP.Exceptions.ConfigurationError(parameter);
      } else {
        value = configuration[parameter];
        checkedValue = configCheck.mandatory[parameter](value);
        if (checkedValue !== undefined) {
          settings[parameter] = checkedValue;
        } else {
          throw new SIP.Exceptions.ConfigurationError(parameter, value);
        }
      }
    }

    // Check Optional parameters
    for(parameter in configCheck.optional) {
      if(configuration.hasOwnProperty(parameter)) {
        value = configuration[parameter];

        // If the parameter value is an empty array, but shouldn't be, apply its default value.
        if (value instanceof Array && value.length === 0) { continue; }

        // If the parameter value is null, empty string, or undefined then apply its default value.
        if(value === null || value === '' || value === undefined) { continue; }
        // If it's a number with NaN value then also apply its default value.
        // NOTE: JS does not allow "value === NaN", the following does the work:
        else if(typeof(value) === 'number' && isNaN(value)) { continue; }

        checkedValue = configCheck.optional[parameter](value);
        if (checkedValue !== undefined) {
          settings[parameter] = checkedValue;
        } else {
          throw new SIP.Exceptions.ConfigurationError(parameter, value);
        }
      }
    }

    Object.assign(this.options, settings);

    this.logger.log('configuration parameters for RegisterContext after validation:');
    for(parameter in settings) {
      this.logger.log('· ' + parameter + ': ' + JSON.stringify(settings[parameter]));
    }

    return;

  }},

  getConfigurationCheck: {writable: true, value: function getConfigurationCheck () {
    return {
      mandatory: {
      },

      optional: {
        expires: function(expires) {
          if (SIP.Utils.isDecimal(expires)) {
            const value = Number(expires);
            if (value >= 0) {
              return value;
            }
          }
        },
        extraContactHeaderParams: function(extraContactHeaderParams) {
          if (extraContactHeaderParams instanceof Array) {
            return extraContactHeaderParams.filter((contactHeaderParam) => (typeof contactHeaderParam === 'string'));
          }
        },
        instanceId: function(instanceId) {
          if (typeof instanceId !== 'string') {
            return;
          }

          if ((/^uuid:/i.test(instanceId))) {
            instanceId = instanceId.substr(5);
          }

          if (SIP.Grammar.parse(instanceId, 'uuid') === -1) {
            return;
          } else {
            return instanceId;
          }
        },
        params: function(params) {
          if (typeof params === 'object') {
            return params;
          }
        },
        regId: function(regId) {
          if (SIP.Utils.isDecimal(regId)) {
            const value = Number(regId);
            if (value >= 0) {
              return value;
            }
          }
        },
        registrar: function(registrar) {
          if(typeof registrar !== 'string') {
            return;
          }

          if (!/^sip:/i.test(registrar)) {
            registrar = SIP.C.SIP + ':' + registrar;
          }
          const parsed = SIP.URI.parse(registrar);

          if(!parsed) {
            return;
          } else if(parsed.user) {
            return;
          } else {
            return parsed;
          }
        }
      }
    };
  }},

  /**
   * Helper Function to generate Contact Header
   * @private
   * returns {String}
   */
  generateContactHeader: {writable: true, value: function generateContactHeader(expires) {
    let contact = this.contact;
    if (this.options.regId && this.options.instanceId) {
      contact += ';reg-id=' + this.options.regId;
      contact += ';+sip.instance="<urn:uuid:' + this.options.instanceId + '>"';
    }

    if (this.options.extraContactHeaderParams) {
      this.options.extraContactHeaderParams.forEach((header) => {
        contact += ';' + header;
      });
    }

    contact += ';expires=' + (expires || '0');

    return contact;
  }},

  register: {writable: true, value: function register (options = {}) {
    // Handle Options
    this.options = SIP.Utils.defaultOptions(this.options, options);
    const extraHeaders = (this.options.extraHeaders || []).slice();

    extraHeaders.push('Contact: ' + this.generateContactHeader(this.expires));
    extraHeaders.push('Allow: ' + SIP.UA.C.ALLOWED_METHODS.toString());

    // Save original extraHeaders to be used in .close
    this.closeHeaders = this.options.closeWithHeaders ?
      (this.options.extraHeaders || []).slice() : [];

    this.receiveResponse = function(response) {
      var contact, expires,
        contacts = response.getHeaders('contact').length,
        cause;

      // Discard responses to older REGISTER/un-REGISTER requests.
      if(response.cseq !== this.cseq) {
        return;
      }

      // Clear registration timer
      if (this.registrationTimer !== null) {
        SIP.Timers.clearTimeout(this.registrationTimer);
        this.registrationTimer = null;
      }

      switch(true) {
        case /^1[0-9]{2}$/.test(response.status_code):
          this.emit('progress', response);
          break;
        case /^2[0-9]{2}$/.test(response.status_code):
          this.emit('accepted', response);

          if(response.hasHeader('expires')) {
            expires = response.getHeader('expires');
          }

          if (this.registrationExpiredTimer !== null) {
            SIP.Timers.clearTimeout(this.registrationExpiredTimer);
            this.registrationExpiredTimer = null;
          }

          // Search the Contact pointing to us and update the expires value accordingly.
          if (!contacts) {
            this.logger.warn('no Contact header in response to REGISTER, response ignored');
            break;
          }

          while(contacts--) {
            contact = response.parseHeader('contact', contacts);
            if(contact.uri.user === this.ua.contact.uri.user) {
              expires = contact.getParam('expires');
              break;
            } else {
              contact = null;
            }
          }

          if (!contact) {
            this.logger.warn('no Contact header pointing to us, response ignored');
            break;
          }

          if(!expires) {
            expires = this.expires;
          }

          // Re-Register before the expiration interval has elapsed.
          // For that, decrease the expires value. ie: 3 seconds
          this.registrationTimer = SIP.Timers.setTimeout(() => {
            this.registrationTimer = null;
            this.register(this.options);
          }, (expires * 1000) - 3000);
          this.registrationExpiredTimer = SIP.Timers.setTimeout(() => {
            this.logger.warn('registration expired');
            if (this.registered) {
              this.unregistered(null, SIP.C.causes.EXPIRES);
            }
          }, expires * 1000);

          //Save gruu values
          if (contact.hasParam('temp-gruu')) {
            this.ua.contact.temp_gruu = SIP.URI.parse(contact.getParam('temp-gruu').replace(/"/g,''));
          }
          if (contact.hasParam('pub-gruu')) {
            this.ua.contact.pub_gruu = SIP.URI.parse(contact.getParam('pub-gruu').replace(/"/g,''));
          }

          this.registered = true;
          this.emit('registered', response || null);
          break;
        // Interval too brief RFC3261 10.2.8
        case /^423$/.test(response.status_code):
          if(response.hasHeader('min-expires')) {
            // Increase our registration interval to the suggested minimum
            this.expires = response.getHeader('min-expires');
            // Attempt the registration again immediately
            this.register(this.options);
          } else { //This response MUST contain a Min-Expires header field
            this.logger.warn('423 response received for REGISTER without Min-Expires');
            this.registrationFailure(response, SIP.C.causes.SIP_FAILURE_CODE);
          }
          break;
        default:
          cause = SIP.Utils.sipErrorCause(response.status_code);
          this.registrationFailure(response, cause);
      }
    };

    this.onRequestTimeout = function() {
      this.registrationFailure(null, SIP.C.causes.REQUEST_TIMEOUT);
    };

    this.onTransportError = function() {
      this.registrationFailure(null, SIP.C.causes.CONNECTION_ERROR);
    };

    this.cseq++;
    this.request.cseq = this.cseq;
    this.request.setHeader('cseq', this.cseq + ' REGISTER');
    this.request.extraHeaders = extraHeaders;
    this.send();
  }},

  registrationFailure: {writable: true, value: function registrationFailure  (response, cause) {
    this.emit('failed', response || null, cause || null);
  }},

  onTransportDisconnected: {writable: true, value: function onTransportDisconnected () {
    this.registered_before = this.registered;
    if (this.registrationTimer !== null) {
      SIP.Timers.clearTimeout(this.registrationTimer);
      this.registrationTimer = null;
    }

    if (this.registrationExpiredTimer !== null) {
      SIP.Timers.clearTimeout(this.registrationExpiredTimer);
      this.registrationExpiredTimer = null;
    }

    if(this.registered) {
      this.unregistered(null, SIP.C.causes.CONNECTION_ERROR);
    }
  }},

  close: {writable: true, value: function close () {
    var options = {
      all: false,
      extraHeaders: this.closeHeaders
    };

    this.registered_before = this.registered;
    if (this.registered) {
      this.unregister(options);
    }
  }},

  unregister: {writable: true, value: function unregister (options) {
    var extraHeaders;

    options = options || {};

    if(!this.registered && !options.all) {
      this.logger.warn('Already unregistered, but sending an unregister anyways.');
    }

    extraHeaders = (options.extraHeaders || []).slice();

    this.registered = false;

    // Clear the registration timer.
    if (this.registrationTimer !== null) {
      SIP.Timers.clearTimeout(this.registrationTimer);
      this.registrationTimer = null;
    }

    if(options.all) {
      extraHeaders.push('Contact: *');
      extraHeaders.push('Expires: 0');
    } else {
      extraHeaders.push('Contact: '+ this.generateContactHeader(0));
    }

    this.receiveResponse = function(response) {
      var cause;

      switch(true) {
        case /^1[0-9]{2}$/.test(response.status_code):
          this.emit('progress', response);
          break;
        case /^2[0-9]{2}$/.test(response.status_code):
          this.emit('accepted', response);
          if (this.registrationExpiredTimer !== null) {
            SIP.Timers.clearTimeout(this.registrationExpiredTimer);
            this.registrationExpiredTimer = null;
          }
          this.unregistered(response);
          break;
        default:
          cause = SIP.Utils.sipErrorCause(response.status_code);
          this.unregistered(response,cause);
      }
    };

    this.onRequestTimeout = function() {
      // Not actually unregistered...
      //this.unregistered(null, SIP.C.causes.REQUEST_TIMEOUT);
    };

    this.cseq++;
    this.request.cseq = this.cseq;
    this.request.setHeader('cseq', this.cseq + ' REGISTER');
    this.request.extraHeaders = extraHeaders;

    this.send();
  }},

  unregistered: {writable: true, value: function unregistered (response, cause) {
    this.registered = false;
    this.emit('unregistered', response || null, cause || null);
  }}

});

SIP.RegisterContext = RegisterContext;
};
<|endoftext|>


<commit_before>import SourceToken from './SourceToken.js';
import SourceTokenListIndex from './SourceTokenListIndex.js';
import SourceType from './SourceType.js';
import { STRING_END, STRING_START } from './index.js';

type SourceTokenListIndexRange = [SourceTokenListIndex, SourceTokenListIndex];

/**
 * Represents a list of tokens and provides various utility functions for
 * finding tokens within it.
 */
export default class SourceTokenList {
  _tokens: Array<SourceToken>;
  _indexCache: Array<SourceTokenListIndex>;
  length: number;
  startIndex: SourceTokenListIndex;
  endIndex: SourceTokenListIndex;

  constructor(tokens: Array<SourceToken>) {
    this._tokens = tokens;
    this._indexCache = new Array(tokens.length);
    this.length = tokens.length;
    this.startIndex = this._getIndex(0);
    this.endIndex = this._getIndex(tokens.length);
  }

  /**
   * Iterate over each token.
   */
  forEach(iterator: (token: SourceToken, index: SourceTokenListIndex, list: SourceTokenList) => void) {
    this._tokens.forEach((token, i) => iterator(token, this._getIndex(i), this));
  }

  /**
   * Map each token to an element of an array.
   */
  map<T>(mapper: (token: SourceToken, index: SourceTokenListIndex, list: SourceTokenList) => T): Array<T> {
    let result = [];
    this.forEach((token, index, list) => { result.push(mapper(token, index, list)); });
    return result;
  }

  /**
   * Get a slice of this token list using the given indexes.
   */
  slice(start: SourceTokenListIndex, end: SourceTokenListIndex): SourceTokenList {
    if (start._sourceTokenList !== this || end._sourceTokenList !== this) {
      throw new Error('cannot slice a list using indexes from another list');
    }
    return new SourceTokenList(this._tokens.slice(start._index, end._index));
  }

  /**
   * Get the token at the given index, if it exists.
   *
   * NOTE: The only value for which this should return `null` is this list's
   * `endIndex`.
   */
  tokenAtIndex(index: SourceTokenListIndex): ?SourceToken {
    this._validateIndex(index);
    return this._tokens[index._index] || null;
  }

  /**
   * Get the range of tokens representing an interpolated string that contains
   * the token at `index`. This will return the innermost interpolated string in
   * the case of nesting.
   */
  rangeOfInterpolatedStringTokensContainingTokenIndex(index: SourceTokenListIndex): ?SourceTokenListIndexRange {
    return this.rangeOfMatchingTokensContainingTokenIndex(
      STRING_START,
      STRING_END,
      index
    );
  }

  /**
   * Get the range of tokens starting with a token of type `startType` and
   * ending one past a token of type `endType`, ensuring that the tokens match.
   * That is, it ensures they are balanced and properly account for nesting.
   * This range will contain `index`. If no such range can be found, `null` is
   * returned.
   */
  rangeOfMatchingTokensContainingTokenIndex(startType: SourceType, endType: SourceType, index: SourceTokenListIndex): ?SourceTokenListIndexRange {
    this._validateIndex(index);

    let token = this.tokenAtIndex(index);
    if (!token) {
      return null;
    }

    switch (token.type) {
      case startType:
      {
        let level = 0;
        let start = index;

        let endIndex = this.indexOfTokenMatchingPredicate(token => {
          if (token.type === startType) {
            level += 1;
          } else if (token.type === endType) {
            level -= 1;
            if (level === 0) {
              return true;
            }
          }
          return false;
        }, start);

        if (!endIndex) {
          return null;
        } else {
          let rangeEnd = endIndex.next();
          if (!rangeEnd) {
            return null;
          }
          return [start, rangeEnd];
        }
      }

      case endType:
      {
        let level = 0;
        let endIndex = index;

        let startIndex = this.lastIndexOfTokenMatchingPredicate(token => {
          if (token.type === startType) {
            level -= 1;
            if (level === 0) {
              return true;
            }
          } else if (token.type === endType) {
            level += 1;
          }
          return false;
        }, endIndex);

        if (!startIndex) {
          return null;
        } else {
          let rangeEnd = endIndex.next();
          if (!rangeEnd) {
            return null;
          } else {
            return [startIndex, rangeEnd];
          }
        }
      }

      default:
      {
        let level = 0;
        let startIndex = this.lastIndexOfTokenMatchingPredicate(token => {
          if (token.type === startType) {
            if (level === 0) {
              return true;
            }
            level -= 1;
          } else if (token.type === endType) {
            level += 1;
          }
          return false;
        }, index);

        if (!startIndex) {
          return null;
        } else {
          return this.rangeOfMatchingTokensContainingTokenIndex(startType, endType, startIndex);
        }
      }
    }
  }

  /**
   * Finds the index of the token whose source range includes the given index.
   */
  indexOfTokenContainingSourceIndex(index: number): ?SourceTokenListIndex {
    this._validateSourceIndex(index);
    return this.indexOfTokenMatchingPredicate(({ start, end }) => start <= index && index < end);
  }

  /**
   * Finds the index of the token whose source range starts at the given index.
   */
  indexOfTokenStartingAtSourceIndex(index: number): ?SourceTokenListIndex {
    this._validateSourceIndex(index);
    return this.indexOfTokenMatchingPredicate(({ start }) => start === index);
  }

  /**
   * Finds the index of the token whose source range ends at the given index.
   */
  indexOfTokenEndingAtSourceIndex(index: number): ?SourceTokenListIndex {
    this._validateSourceIndex(index);
    return this.indexOfTokenMatchingPredicate(({ end }) => end === index);
  }

  /**
   * Finds the index of the first token matching a predicate.
   */
  indexOfTokenMatchingPredicate(predicate: (token: SourceToken) => boolean, start: ?SourceTokenListIndex=null): ?SourceTokenListIndex {
    if (!start) {
      start = this.startIndex;
    }
    this._validateIndex(start);

    let { endIndex } = this;
    for (let i = start; i && i !== endIndex; i = i.next()) {
      let token = this.tokenAtIndex(i);
      if (!token) {
        break;
      } else if (predicate(token)) {
        return i;
      }
    }

    return null;
  }

  /**
   * Finds the index of the first token matching a predicate.
   */
  lastIndexOfTokenMatchingPredicate(predicate: (token: SourceToken) => boolean, start: ?SourceTokenListIndex=null): ?SourceTokenListIndex {
    if (!start) {
      start = this.endIndex.previous();
      if (!start) {
        return null;
      }
    }
    this._validateIndex(start);

    let { startIndex } = this;
    let i = start;
    do {
      let token = this.tokenAtIndex(i);
      if (!token) {
        break;
      } else if (predicate(token)) {
        return i;
      } else if (i) {
        i = i.previous();
      }
    } while (i && i !== startIndex);

    return null;
  }

  /**
   * Allow iterating over the tokens in this list using e.g. `for (… of …)`.
   *
   * $FlowIssue (see facebook/flow#252)
   */
  [Symbol.iterator]() {
    let index = this.startIndex;
    let { endIndex } = this;
    return () => {
      if (index === endIndex) {
        return { done: true, value: undefined };
      } else {
        let result = { done: false, value: this.tokenAtIndex(index) };
        index = index.next();
        return result;
      }
    };
  }

  /**
   * @internal
   */
  _validateIndex(index: ?SourceTokenListIndex) {
    if (!index) {
      throw new Error(
        `unexpected 'null' index, perhaps you forgot to check the result of ` +
        `'indexOfTokenContainingSourceIndex'?`
      );
    }
    if (typeof index === 'number') {
      throw new Error(
        `to get a token at index ${index}, ` +
        `use list.tokenAtIndex(list.startIndex.advance(${index}))`
      );
    }
    if (index._sourceTokenList !== this) {
      throw new Error('cannot get token in one list using an index from another');
    }
  }

  /**
   * @internal
   */
  _validateSourceIndex(index: number) {
    if (typeof index !== 'number') {
      throw new Error(`expected source index to be a number, got: ${index}`);
    }
  }

  /**
   * @internal
   */
  _getIndex(index: number): SourceTokenListIndex {
    let cached = this._indexCache[index];

    if (!cached) {
      cached = new SourceTokenListIndex(this, index);
      this._indexCache[index] = cached;
    }

    return cached;
  }

  /**
   * Get the list of tokens.
   */
  toArray(): Array<SourceToken> {
    return this._tokens.slice();
  }
}
<commit_msg>Add `SourceTokenList#filter`.<commit_after>import SourceToken from './SourceToken.js';
import SourceTokenListIndex from './SourceTokenListIndex.js';
import SourceType from './SourceType.js';
import { STRING_END, STRING_START } from './index.js';

type SourceTokenListIndexRange = [SourceTokenListIndex, SourceTokenListIndex];

/**
 * Represents a list of tokens and provides various utility functions for
 * finding tokens within it.
 */
export default class SourceTokenList {
  _tokens: Array<SourceToken>;
  _indexCache: Array<SourceTokenListIndex>;
  length: number;
  startIndex: SourceTokenListIndex;
  endIndex: SourceTokenListIndex;

  constructor(tokens: Array<SourceToken>) {
    this._tokens = tokens;
    this._indexCache = new Array(tokens.length);
    this.length = tokens.length;
    this.startIndex = this._getIndex(0);
    this.endIndex = this._getIndex(tokens.length);
  }

  /**
   * Iterate over each token.
   */
  forEach(iterator: (token: SourceToken, index: SourceTokenListIndex, list: SourceTokenList) => void) {
    this._tokens.forEach((token, i) => iterator(token, this._getIndex(i), this));
  }

  /**
   * Map each token to an element of an array.
   */
  map<T>(mapper: (token: SourceToken, index: SourceTokenListIndex, list: SourceTokenList) => T): Array<T> {
    let result = [];
    this.forEach((token, index, list) => { result.push(mapper(token, index, list)); });
    return result;
  }

  /**
   * Filter tokens by a predicate.
   */
  filter(predicate: (token: SourceToken, index: SourceTokenListIndex, list: SourceTokenList) => boolean): SourceTokenList {
    let result = [];
    this.forEach((token, index, list) => {
      if (predicate(token, index, list)) {
        result.push(token);
      }
    });
    return new SourceTokenList(result);
  }

  /**
   * Get a slice of this token list using the given indexes.
   */
  slice(start: SourceTokenListIndex, end: SourceTokenListIndex): SourceTokenList {
    if (start._sourceTokenList !== this || end._sourceTokenList !== this) {
      throw new Error('cannot slice a list using indexes from another list');
    }
    return new SourceTokenList(this._tokens.slice(start._index, end._index));
  }

  /**
   * Get the token at the given index, if it exists.
   *
   * NOTE: The only value for which this should return `null` is this list's
   * `endIndex`.
   */
  tokenAtIndex(index: SourceTokenListIndex): ?SourceToken {
    this._validateIndex(index);
    return this._tokens[index._index] || null;
  }

  /**
   * Get the range of tokens representing an interpolated string that contains
   * the token at `index`. This will return the innermost interpolated string in
   * the case of nesting.
   */
  rangeOfInterpolatedStringTokensContainingTokenIndex(index: SourceTokenListIndex): ?SourceTokenListIndexRange {
    return this.rangeOfMatchingTokensContainingTokenIndex(
      STRING_START,
      STRING_END,
      index
    );
  }

  /**
   * Get the range of tokens starting with a token of type `startType` and
   * ending one past a token of type `endType`, ensuring that the tokens match.
   * That is, it ensures they are balanced and properly account for nesting.
   * This range will contain `index`. If no such range can be found, `null` is
   * returned.
   */
  rangeOfMatchingTokensContainingTokenIndex(startType: SourceType, endType: SourceType, index: SourceTokenListIndex): ?SourceTokenListIndexRange {
    this._validateIndex(index);

    let token = this.tokenAtIndex(index);
    if (!token) {
      return null;
    }

    switch (token.type) {
      case startType:
      {
        let level = 0;
        let start = index;

        let endIndex = this.indexOfTokenMatchingPredicate(token => {
          if (token.type === startType) {
            level += 1;
          } else if (token.type === endType) {
            level -= 1;
            if (level === 0) {
              return true;
            }
          }
          return false;
        }, start);

        if (!endIndex) {
          return null;
        } else {
          let rangeEnd = endIndex.next();
          if (!rangeEnd) {
            return null;
          }
          return [start, rangeEnd];
        }
      }

      case endType:
      {
        let level = 0;
        let endIndex = index;

        let startIndex = this.lastIndexOfTokenMatchingPredicate(token => {
          if (token.type === startType) {
            level -= 1;
            if (level === 0) {
              return true;
            }
          } else if (token.type === endType) {
            level += 1;
          }
          return false;
        }, endIndex);

        if (!startIndex) {
          return null;
        } else {
          let rangeEnd = endIndex.next();
          if (!rangeEnd) {
            return null;
          } else {
            return [startIndex, rangeEnd];
          }
        }
      }

      default:
      {
        let level = 0;
        let startIndex = this.lastIndexOfTokenMatchingPredicate(token => {
          if (token.type === startType) {
            if (level === 0) {
              return true;
            }
            level -= 1;
          } else if (token.type === endType) {
            level += 1;
          }
          return false;
        }, index);

        if (!startIndex) {
          return null;
        } else {
          return this.rangeOfMatchingTokensContainingTokenIndex(startType, endType, startIndex);
        }
      }
    }
  }

  /**
   * Finds the index of the token whose source range includes the given index.
   */
  indexOfTokenContainingSourceIndex(index: number): ?SourceTokenListIndex {
    this._validateSourceIndex(index);
    return this.indexOfTokenMatchingPredicate(({ start, end }) => start <= index && index < end);
  }

  /**
   * Finds the index of the token whose source range starts at the given index.
   */
  indexOfTokenStartingAtSourceIndex(index: number): ?SourceTokenListIndex {
    this._validateSourceIndex(index);
    return this.indexOfTokenMatchingPredicate(({ start }) => start === index);
  }

  /**
   * Finds the index of the token whose source range ends at the given index.
   */
  indexOfTokenEndingAtSourceIndex(index: number): ?SourceTokenListIndex {
    this._validateSourceIndex(index);
    return this.indexOfTokenMatchingPredicate(({ end }) => end === index);
  }

  /**
   * Finds the index of the first token matching a predicate.
   */
  indexOfTokenMatchingPredicate(predicate: (token: SourceToken) => boolean, start: ?SourceTokenListIndex=null): ?SourceTokenListIndex {
    if (!start) {
      start = this.startIndex;
    }
    this._validateIndex(start);

    let { endIndex } = this;
    for (let i = start; i && i !== endIndex; i = i.next()) {
      let token = this.tokenAtIndex(i);
      if (!token) {
        break;
      } else if (predicate(token)) {
        return i;
      }
    }

    return null;
  }

  /**
   * Finds the index of the first token matching a predicate.
   */
  lastIndexOfTokenMatchingPredicate(predicate: (token: SourceToken) => boolean, start: ?SourceTokenListIndex=null): ?SourceTokenListIndex {
    if (!start) {
      start = this.endIndex.previous();
      if (!start) {
        return null;
      }
    }
    this._validateIndex(start);

    let { startIndex } = this;
    let i = start;
    do {
      let token = this.tokenAtIndex(i);
      if (!token) {
        break;
      } else if (predicate(token)) {
        return i;
      } else if (i) {
        i = i.previous();
      }
    } while (i && i !== startIndex);

    return null;
  }

  /**
   * Allow iterating over the tokens in this list using e.g. `for (… of …)`.
   *
   * $FlowIssue (see facebook/flow#252)
   */
  [Symbol.iterator]() {
    let index = this.startIndex;
    let { endIndex } = this;
    return () => {
      if (index === endIndex) {
        return { done: true, value: undefined };
      } else {
        let result = { done: false, value: this.tokenAtIndex(index) };
        index = index.next();
        return result;
      }
    };
  }

  /**
   * @internal
   */
  _validateIndex(index: ?SourceTokenListIndex) {
    if (!index) {
      throw new Error(
        `unexpected 'null' index, perhaps you forgot to check the result of ` +
        `'indexOfTokenContainingSourceIndex'?`
      );
    }
    if (typeof index === 'number') {
      throw new Error(
        `to get a token at index ${index}, ` +
        `use list.tokenAtIndex(list.startIndex.advance(${index}))`
      );
    }
    if (index._sourceTokenList !== this) {
      throw new Error('cannot get token in one list using an index from another');
    }
  }

  /**
   * @internal
   */
  _validateSourceIndex(index: number) {
    if (typeof index !== 'number') {
      throw new Error(`expected source index to be a number, got: ${index}`);
    }
  }

  /**
   * @internal
   */
  _getIndex(index: number): SourceTokenListIndex {
    let cached = this._indexCache[index];

    if (!cached) {
      cached = new SourceTokenListIndex(this, index);
      this._indexCache[index] = cached;
    }

    return cached;
  }

  /**
   * Get the list of tokens.
   */
  toArray(): Array<SourceToken> {
    return this._tokens.slice();
  }
}
<|endoftext|>


<commit_before>'use strict';

import cx from 'classnames';
import {find, isEqual, noop} from 'lodash';
import onClickOutside from 'react-onclickoutside';
import React, {PropTypes} from 'react';

import Loader from './Loader.react';
import Overlay from './Overlay.react';
import TokenizerInput from './TokenizerInput.react';
import TypeaheadInput from './TypeaheadInput.react';
import TypeaheadMenu from './TypeaheadMenu.react';

import addCustomOption from './utils/addCustomOption';
import defaultFilterBy from './utils/defaultFilterBy';
import getHintText from './utils/getHintText';
import getInputText from './utils/getInputText';
import getOptionLabel from './utils/getOptionLabel';
import getTruncatedOptions from './utils/getTruncatedOptions';
import warn from './utils/warn';

import {DOWN, ESC, RETURN, TAB, UP} from './utils/keyCode';

/**
 * Typeahead
 */
const Typeahead = React.createClass({
  displayName: 'Typeahead',

  propTypes: {
    /**
     * Allows the creation of new selections on the fly. Note that any new items
     * will be added to the list of selections, but not the list of original
     * options unless handled as such by `Typeahead`'s parent.
     */
    allowNew: PropTypes.bool,
    /**
     * Whether to render the menu inline or attach to `document.body`.
     */
    bodyContainer: PropTypes.bool,
    /**
     * Whether or not filtering should be case-sensitive.
     */
    caseSensitive: PropTypes.bool,
    /**
     * Specify any pre-selected options. Use only if you want the component to
     * be uncontrolled.
     */
    defaultSelected: PropTypes.array,
    /**
     * Specify whether the menu should appear above the input.
     */
    dropup: PropTypes.bool,
    /**
     * Either an array of fields in `option` to search, or a custom filtering
     * callback.
     */
    filterBy: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.string.isRequired),
      PropTypes.func,
    ]),
    /**
     * Indicate whether an asynchromous data fetch is happening.
     */
    isLoading: PropTypes.bool,
    /**
     * Specify the option key to use for display or a function returning the
     * display string. By default, the selector will use the `label` key.
     */
    labelKey: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.func,
    ]),
    /**
     * Maximum number of results to display by default. Mostly done for
     * performance reasons so as not to render too many DOM nodes in the case of
     * large data sets.
     */
    maxResults: PropTypes.number,
    /**
     * Number of input characters that must be entered before showing results.
     */
    minLength: PropTypes.number,
    /**
     * Whether or not multiple selections are allowed.
     */
    multiple: PropTypes.bool,
    /**
     * Callback fired when the input is blurred. Receives an event.
     */
    onBlur: PropTypes.func,
    /**
     * Callback fired whenever items are added or removed. Receives an array of
     * the selected options.
     */
    onChange: PropTypes.func,
    /**
     * Callback fired when the input is focused. Receives an event.
     */
    onFocus: PropTypes.func,
    /**
     * Callback for handling changes to the user-input text.
     */
    onInputChange: PropTypes.func,
    /**
     * Full set of options, including pre-selected options. Must either be an
     * array of objects (recommended) or strings.
     */
    options: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.object.isRequired),
      PropTypes.arrayOf(PropTypes.string.isRequired),
    ]).isRequired,
    /**
     * Give user the ability to display additional results if the number of
     * results exceeds `maxResults`.
     */
    paginate: PropTypes.bool,
    /**
     * Callback for custom menu rendering.
     */
    renderMenu: PropTypes.func,
    /**
     * The selected option(s) displayed in the input. Use this prop if you want
     * to control the component via its parent.
     */
    selected: PropTypes.array,
  },

  getDefaultProps() {
    return {
      allowNew: false,
      caseSensitive: false,
      defaultSelected: [],
      dropup: false,
      filterBy: [],
      isLoading: false,
      labelKey: 'label',
      maxResults: 100,
      onBlur: noop,
      onChange: noop,
      onFocus: noop,
      onInputChange: noop,
      minLength: 0,
      multiple: false,
      paginate: true,
      selected: [],
    };
  },

  childContextTypes: {
    activeIndex: PropTypes.number.isRequired,
    onActiveItemChange: PropTypes.func.isRequired,
    onInitialItemChange: PropTypes.func.isRequired,
    onMenuItemClick: PropTypes.func.isRequired,
  },

  getChildContext() {
    return {
      activeIndex: this.state.activeIndex,
      onActiveItemChange: this._handleActiveItemChange,
      onInitialItemChange: this._handleInitialItemChange,
      onMenuItemClick: this._handleAddOption,
    };
  },

  getInitialState() {
    const {defaultSelected, maxResults} = this.props;

    let selected = this.props.selected.slice();
    if (defaultSelected && defaultSelected.length) {
      selected = defaultSelected;
    }

    return {
      activeIndex: -1,
      activeItem: null,
      initialItem: null,
      selected,
      showMenu: false,
      shownResults: maxResults,
      text: '',
    };
  },

  componentWillMount() {
    const {allowNew, caseSensitive, filterBy, labelKey} = this.props;

    warn(
      !(typeof filterBy === 'function' && caseSensitive),
      'Because you have defined a `filterBy` callback, the `caseSensitive` ' +
      'prop will be ignored.'
    );

    warn(
      !(typeof labelKey === 'function' && allowNew),
      '`labelKey` must be a string if creating new options is allowed.'
    );
  },

  componentWillReceiveProps(nextProps) {
    const {multiple, selected} = nextProps;

    if (!isEqual(selected, this.props.selected)) {
      // If new selections are passed in via props, treat the component as a
      // controlled input.
      this.setState({selected});
    }

    if (multiple !== this.props.multiple) {
      this.setState({text: ''});
    }
  },

  render() {
    const {allowNew, className, dropup, labelKey, paginate} = this.props;
    const {shownResults, text} = this.state;

    // First filter the results by the input string.
    let results = this._getFilteredResults();

    // This must come before we truncate.
    const shouldPaginate = paginate && results.length > shownResults;

    // Truncate if necessary.
    results = getTruncatedOptions(results, shownResults);

    // Add the custom option.
    if (allowNew) {
      results = addCustomOption(results, text, labelKey);
    }

    return (
      <div
        className={cx('bootstrap-typeahead', 'open', {
          'dropup': dropup,
        }, className)}
        style={{position: 'relative'}}>
        {this._renderInput(results)}
        {this._renderLoader()}
        {this._renderMenu(results, shouldPaginate)}
      </div>
    );
  },

  _getFilteredResults() {
    const {caseSensitive, labelKey, minLength, multiple, options} = this.props;
    const {selected, text} = this.state;

    if (text.length < minLength) {
      return [];
    }

    let {filterBy} = this.props;
    if (Array.isArray(filterBy)) {
      const fields = filterBy;
      filterBy = option => defaultFilterBy(
        option,
        labelKey,
        multiple && !!find(selected, o => isEqual(o, option)),
        text,
        {caseSensitive, fields}
      );
    }

    return options.filter(filterBy);
  },

  blur() {
    this.refs.input.blur();
  },

  /**
   * Public method to allow external clearing of the input. Clears both text
   * and selection(s).
   */
  clear() {
    const {activeIndex, activeItem, showMenu} = this.getInitialState();
    const selected = [];
    const text = '';

    this.setState({
      activeIndex,
      activeItem,
      selected,
      showMenu,
      text,
    });

    this.props.onChange(selected);
    this.props.onInputChange(text);
  },

  focus() {
    this.refs.input.focus();
  },

  _renderInput(results) {
    const {
      bsSize,
      disabled,
      isLoading,
      labelKey,
      multiple,
      name,
      placeholder,
      renderToken,
    } = this.props;
    const {activeIndex, activeItem, initialItem, selected, text} = this.state;
    const Input = multiple ? TokenizerInput : TypeaheadInput;
    const inputProps = {bsSize, disabled, name, placeholder, renderToken};

    return (
      <Input
        {...inputProps}
        activeIndex={activeIndex}
        activeItem={activeItem}
        hasAux={isLoading}
        hintText={getHintText({
          activeItem,
          initialItem,
          labelKey,
          selected,
          text,
        })}
        initialItem={initialItem}
        labelKey={labelKey}
        onAdd={this._handleAddOption}
        onBlur={this._handleBlur}
        onChange={this._handleTextChange}
        onFocus={this._handleFocus}
        onKeyDown={e => this._handleKeydown(results, e)}
        onRemove={this._handleRemoveOption}
        options={results}
        ref="input"
        selected={selected.slice()}
        value={getInputText({activeItem, labelKey, multiple, selected, text})}
      />
    );
  },

  _renderMenu(results, shouldPaginate) {
    const {
      align,
      bodyContainer,
      dropup,
      emptyLabel,
      labelKey,
      maxHeight,
      minLength,
      newSelectionPrefix,
      paginationText,
      renderMenu,
      renderMenuItemChildren,
    } = this.props;

    const {showMenu, text} = this.state;

    const menuProps = {
      align,
      dropup,
      emptyLabel,
      labelKey,
      maxHeight,
      newSelectionPrefix,
      paginationText,
      onPaginate: this._handlePagination,
      paginate: shouldPaginate,
      text,
    };

    const menu = renderMenu ?
      renderMenu(results, menuProps) :
      <TypeaheadMenu
        {...menuProps}
        options={results}
        renderMenuItemChildren={renderMenuItemChildren}
      />;

    return (
      <Overlay
        container={bodyContainer ? document.body : this}
        show={showMenu && text.length >= minLength}
        target={() => this.refs.input}>
        {menu}
      </Overlay>
    );
  },

  _renderLoader() {
    if (this.props.isLoading) {
      return <Loader bsSize={this.props.bsSize} />;
    }
  },

  _handleActiveItemChange(activeItem) {
    this.setState({activeItem});
  },

  _handleBlur(e) {
    // Note: Don't hide the menu here, since that interferes with other actions
    // like making a selection by clicking on a menu item.
    this.props.onBlur(e);
  },

  _handleFocus(e) {
    this.props.onFocus(e);
    this.setState({showMenu: true});
  },

  _handleInitialItemChange(initialItem) {
    const {labelKey} = this.props;

    // Don't update the initial item if it hasn't changed. For custom items,
    // compare the `labelKey` values since a unique id is generated each time,
    // causing the comparison to always return false otherwise.
    if (
      isEqual(initialItem, this.state.initialItem) ||
      (initialItem.customOption &&
       initialItem[labelKey] === this.state.initialItem[labelKey])
    ) {
      return;
    }

    this.setState({initialItem});
  },

  _handleTextChange(text) {
    const {activeIndex, activeItem} = this.getInitialState();
    this.setState({
      activeIndex,
      activeItem,
      showMenu: true,
      text,
    });

    this.props.onInputChange(text);
  },

  _handleKeydown(options, e) {
    const {activeItem, showMenu} = this.state;

    switch (e.keyCode) {
      case UP:
      case DOWN:
        // Don't cycle through the options if the menu is hidden.
        if (!showMenu) {
          return;
        }

        let {activeIndex} = this.state;

        // Prevents input cursor from going to the beginning when pressing up.
        e.preventDefault();

        // Increment or decrement index based on user keystroke.
        activeIndex += e.keyCode === UP ? -1 : 1;

        // If we've reached the end, go back to the beginning or vice-versa.
        if (activeIndex === options.length) {
          activeIndex = -1;
        } else if (activeIndex === -2) {
          activeIndex = options.length - 1;
        }

        const newState = {activeIndex};
        if (activeIndex === -1) {
          // Reset the active item if there is no active index.
          newState.activeItem = null;
        }

        this.setState(newState);
        break;
      case ESC:
      case TAB:
        // Prevent closing dialogs.
        e.keyCode === ESC && e.preventDefault();

        this._hideDropdown();
        break;
      case RETURN:
        // Prevent submitting forms.
        e.preventDefault();

        if (showMenu) {
          activeItem && this._handleAddOption(activeItem);
        }
        break;
    }
  },

  _handleAddOption(selectedOption) {
    const {multiple, labelKey, onChange, onInputChange} = this.props;

    let selected;
    let text;

    if (multiple) {
      // If multiple selections are allowed, add the new selection to the
      // existing selections.
      selected = this.state.selected.concat(selectedOption);
      text = '';
    } else {
      // If only a single selection is allowed, replace the existing selection
      // with the new one.
      selected = [selectedOption];
      text = getOptionLabel(selectedOption, labelKey);
    }

    this.setState({
      initialItem: selectedOption,
      selected,
      text,
    });
    this._hideDropdown();

    onChange(selected);
    onInputChange(text);
  },

  _handlePagination(e) {
    let shownResults = this.state.shownResults + this.props.maxResults;

    // Keep the input focused when paginating.
    this.focus();

    this.setState({shownResults});
  },

  _handleRemoveOption(removedOption) {
    let selected = this.state.selected.slice();
    selected = selected.filter(option => !isEqual(option, removedOption));

    // Make sure the input stays focused after the item is removed.
    this.focus();

    this.setState({selected});
    this._hideDropdown();

    this.props.onChange(selected);
  },

  /**
   * From `onClickOutside` HOC.
   */
  handleClickOutside(e) {
    this.state.showMenu && this._hideDropdown();
  },

  _hideDropdown() {
    const {
      activeIndex,
      activeItem,
      showMenu,
      shownResults,
    } = this.getInitialState();

    this.setState({
      activeIndex,
      activeItem,
      showMenu,
      shownResults,
    });
  },
});

export default onClickOutside(Typeahead);
<commit_msg>Add `bodyContainer` default prop<commit_after>'use strict';

import cx from 'classnames';
import {find, isEqual, noop} from 'lodash';
import onClickOutside from 'react-onclickoutside';
import React, {PropTypes} from 'react';

import Loader from './Loader.react';
import Overlay from './Overlay.react';
import TokenizerInput from './TokenizerInput.react';
import TypeaheadInput from './TypeaheadInput.react';
import TypeaheadMenu from './TypeaheadMenu.react';

import addCustomOption from './utils/addCustomOption';
import defaultFilterBy from './utils/defaultFilterBy';
import getHintText from './utils/getHintText';
import getInputText from './utils/getInputText';
import getOptionLabel from './utils/getOptionLabel';
import getTruncatedOptions from './utils/getTruncatedOptions';
import warn from './utils/warn';

import {DOWN, ESC, RETURN, TAB, UP} from './utils/keyCode';

/**
 * Typeahead
 */
const Typeahead = React.createClass({
  displayName: 'Typeahead',

  propTypes: {
    /**
     * Allows the creation of new selections on the fly. Note that any new items
     * will be added to the list of selections, but not the list of original
     * options unless handled as such by `Typeahead`'s parent.
     */
    allowNew: PropTypes.bool,
    /**
     * Whether to render the menu inline or attach to `document.body`.
     */
    bodyContainer: PropTypes.bool,
    /**
     * Whether or not filtering should be case-sensitive.
     */
    caseSensitive: PropTypes.bool,
    /**
     * Specify any pre-selected options. Use only if you want the component to
     * be uncontrolled.
     */
    defaultSelected: PropTypes.array,
    /**
     * Specify whether the menu should appear above the input.
     */
    dropup: PropTypes.bool,
    /**
     * Either an array of fields in `option` to search, or a custom filtering
     * callback.
     */
    filterBy: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.string.isRequired),
      PropTypes.func,
    ]),
    /**
     * Indicate whether an asynchromous data fetch is happening.
     */
    isLoading: PropTypes.bool,
    /**
     * Specify the option key to use for display or a function returning the
     * display string. By default, the selector will use the `label` key.
     */
    labelKey: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.func,
    ]),
    /**
     * Maximum number of results to display by default. Mostly done for
     * performance reasons so as not to render too many DOM nodes in the case of
     * large data sets.
     */
    maxResults: PropTypes.number,
    /**
     * Number of input characters that must be entered before showing results.
     */
    minLength: PropTypes.number,
    /**
     * Whether or not multiple selections are allowed.
     */
    multiple: PropTypes.bool,
    /**
     * Callback fired when the input is blurred. Receives an event.
     */
    onBlur: PropTypes.func,
    /**
     * Callback fired whenever items are added or removed. Receives an array of
     * the selected options.
     */
    onChange: PropTypes.func,
    /**
     * Callback fired when the input is focused. Receives an event.
     */
    onFocus: PropTypes.func,
    /**
     * Callback for handling changes to the user-input text.
     */
    onInputChange: PropTypes.func,
    /**
     * Full set of options, including pre-selected options. Must either be an
     * array of objects (recommended) or strings.
     */
    options: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.object.isRequired),
      PropTypes.arrayOf(PropTypes.string.isRequired),
    ]).isRequired,
    /**
     * Give user the ability to display additional results if the number of
     * results exceeds `maxResults`.
     */
    paginate: PropTypes.bool,
    /**
     * Callback for custom menu rendering.
     */
    renderMenu: PropTypes.func,
    /**
     * The selected option(s) displayed in the input. Use this prop if you want
     * to control the component via its parent.
     */
    selected: PropTypes.array,
  },

  getDefaultProps() {
    return {
      allowNew: false,
      bodyContainer: false,
      caseSensitive: false,
      defaultSelected: [],
      dropup: false,
      filterBy: [],
      isLoading: false,
      labelKey: 'label',
      maxResults: 100,
      onBlur: noop,
      onChange: noop,
      onFocus: noop,
      onInputChange: noop,
      minLength: 0,
      multiple: false,
      paginate: true,
      selected: [],
    };
  },

  childContextTypes: {
    activeIndex: PropTypes.number.isRequired,
    onActiveItemChange: PropTypes.func.isRequired,
    onInitialItemChange: PropTypes.func.isRequired,
    onMenuItemClick: PropTypes.func.isRequired,
  },

  getChildContext() {
    return {
      activeIndex: this.state.activeIndex,
      onActiveItemChange: this._handleActiveItemChange,
      onInitialItemChange: this._handleInitialItemChange,
      onMenuItemClick: this._handleAddOption,
    };
  },

  getInitialState() {
    const {defaultSelected, maxResults} = this.props;

    let selected = this.props.selected.slice();
    if (defaultSelected && defaultSelected.length) {
      selected = defaultSelected;
    }

    return {
      activeIndex: -1,
      activeItem: null,
      initialItem: null,
      selected,
      showMenu: false,
      shownResults: maxResults,
      text: '',
    };
  },

  componentWillMount() {
    const {allowNew, caseSensitive, filterBy, labelKey} = this.props;

    warn(
      !(typeof filterBy === 'function' && caseSensitive),
      'Because you have defined a `filterBy` callback, the `caseSensitive` ' +
      'prop will be ignored.'
    );

    warn(
      !(typeof labelKey === 'function' && allowNew),
      '`labelKey` must be a string if creating new options is allowed.'
    );
  },

  componentWillReceiveProps(nextProps) {
    const {multiple, selected} = nextProps;

    if (!isEqual(selected, this.props.selected)) {
      // If new selections are passed in via props, treat the component as a
      // controlled input.
      this.setState({selected});
    }

    if (multiple !== this.props.multiple) {
      this.setState({text: ''});
    }
  },

  render() {
    const {allowNew, className, dropup, labelKey, paginate} = this.props;
    const {shownResults, text} = this.state;

    // First filter the results by the input string.
    let results = this._getFilteredResults();

    // This must come before we truncate.
    const shouldPaginate = paginate && results.length > shownResults;

    // Truncate if necessary.
    results = getTruncatedOptions(results, shownResults);

    // Add the custom option.
    if (allowNew) {
      results = addCustomOption(results, text, labelKey);
    }

    return (
      <div
        className={cx('bootstrap-typeahead', 'open', {
          'dropup': dropup,
        }, className)}
        style={{position: 'relative'}}>
        {this._renderInput(results)}
        {this._renderLoader()}
        {this._renderMenu(results, shouldPaginate)}
      </div>
    );
  },

  _getFilteredResults() {
    const {caseSensitive, labelKey, minLength, multiple, options} = this.props;
    const {selected, text} = this.state;

    if (text.length < minLength) {
      return [];
    }

    let {filterBy} = this.props;
    if (Array.isArray(filterBy)) {
      const fields = filterBy;
      filterBy = option => defaultFilterBy(
        option,
        labelKey,
        multiple && !!find(selected, o => isEqual(o, option)),
        text,
        {caseSensitive, fields}
      );
    }

    return options.filter(filterBy);
  },

  blur() {
    this.refs.input.blur();
  },

  /**
   * Public method to allow external clearing of the input. Clears both text
   * and selection(s).
   */
  clear() {
    const {activeIndex, activeItem, showMenu} = this.getInitialState();
    const selected = [];
    const text = '';

    this.setState({
      activeIndex,
      activeItem,
      selected,
      showMenu,
      text,
    });

    this.props.onChange(selected);
    this.props.onInputChange(text);
  },

  focus() {
    this.refs.input.focus();
  },

  _renderInput(results) {
    const {
      bsSize,
      disabled,
      isLoading,
      labelKey,
      multiple,
      name,
      placeholder,
      renderToken,
    } = this.props;
    const {activeIndex, activeItem, initialItem, selected, text} = this.state;
    const Input = multiple ? TokenizerInput : TypeaheadInput;
    const inputProps = {bsSize, disabled, name, placeholder, renderToken};

    return (
      <Input
        {...inputProps}
        activeIndex={activeIndex}
        activeItem={activeItem}
        hasAux={isLoading}
        hintText={getHintText({
          activeItem,
          initialItem,
          labelKey,
          selected,
          text,
        })}
        initialItem={initialItem}
        labelKey={labelKey}
        onAdd={this._handleAddOption}
        onBlur={this._handleBlur}
        onChange={this._handleTextChange}
        onFocus={this._handleFocus}
        onKeyDown={e => this._handleKeydown(results, e)}
        onRemove={this._handleRemoveOption}
        options={results}
        ref="input"
        selected={selected.slice()}
        value={getInputText({activeItem, labelKey, multiple, selected, text})}
      />
    );
  },

  _renderMenu(results, shouldPaginate) {
    const {
      align,
      bodyContainer,
      dropup,
      emptyLabel,
      labelKey,
      maxHeight,
      minLength,
      newSelectionPrefix,
      paginationText,
      renderMenu,
      renderMenuItemChildren,
    } = this.props;

    const {showMenu, text} = this.state;

    const menuProps = {
      align,
      dropup,
      emptyLabel,
      labelKey,
      maxHeight,
      newSelectionPrefix,
      paginationText,
      onPaginate: this._handlePagination,
      paginate: shouldPaginate,
      text,
    };

    const menu = renderMenu ?
      renderMenu(results, menuProps) :
      <TypeaheadMenu
        {...menuProps}
        options={results}
        renderMenuItemChildren={renderMenuItemChildren}
      />;

    return (
      <Overlay
        container={bodyContainer ? document.body : this}
        show={showMenu && text.length >= minLength}
        target={() => this.refs.input}>
        {menu}
      </Overlay>
    );
  },

  _renderLoader() {
    if (this.props.isLoading) {
      return <Loader bsSize={this.props.bsSize} />;
    }
  },

  _handleActiveItemChange(activeItem) {
    this.setState({activeItem});
  },

  _handleBlur(e) {
    // Note: Don't hide the menu here, since that interferes with other actions
    // like making a selection by clicking on a menu item.
    this.props.onBlur(e);
  },

  _handleFocus(e) {
    this.props.onFocus(e);
    this.setState({showMenu: true});
  },

  _handleInitialItemChange(initialItem) {
    const {labelKey} = this.props;

    // Don't update the initial item if it hasn't changed. For custom items,
    // compare the `labelKey` values since a unique id is generated each time,
    // causing the comparison to always return false otherwise.
    if (
      isEqual(initialItem, this.state.initialItem) ||
      (initialItem.customOption &&
       initialItem[labelKey] === this.state.initialItem[labelKey])
    ) {
      return;
    }

    this.setState({initialItem});
  },

  _handleTextChange(text) {
    const {activeIndex, activeItem} = this.getInitialState();
    this.setState({
      activeIndex,
      activeItem,
      showMenu: true,
      text,
    });

    this.props.onInputChange(text);
  },

  _handleKeydown(options, e) {
    const {activeItem, showMenu} = this.state;

    switch (e.keyCode) {
      case UP:
      case DOWN:
        // Don't cycle through the options if the menu is hidden.
        if (!showMenu) {
          return;
        }

        let {activeIndex} = this.state;

        // Prevents input cursor from going to the beginning when pressing up.
        e.preventDefault();

        // Increment or decrement index based on user keystroke.
        activeIndex += e.keyCode === UP ? -1 : 1;

        // If we've reached the end, go back to the beginning or vice-versa.
        if (activeIndex === options.length) {
          activeIndex = -1;
        } else if (activeIndex === -2) {
          activeIndex = options.length - 1;
        }

        const newState = {activeIndex};
        if (activeIndex === -1) {
          // Reset the active item if there is no active index.
          newState.activeItem = null;
        }

        this.setState(newState);
        break;
      case ESC:
      case TAB:
        // Prevent closing dialogs.
        e.keyCode === ESC && e.preventDefault();

        this._hideDropdown();
        break;
      case RETURN:
        // Prevent submitting forms.
        e.preventDefault();

        if (showMenu) {
          activeItem && this._handleAddOption(activeItem);
        }
        break;
    }
  },

  _handleAddOption(selectedOption) {
    const {multiple, labelKey, onChange, onInputChange} = this.props;

    let selected;
    let text;

    if (multiple) {
      // If multiple selections are allowed, add the new selection to the
      // existing selections.
      selected = this.state.selected.concat(selectedOption);
      text = '';
    } else {
      // If only a single selection is allowed, replace the existing selection
      // with the new one.
      selected = [selectedOption];
      text = getOptionLabel(selectedOption, labelKey);
    }

    this.setState({
      initialItem: selectedOption,
      selected,
      text,
    });
    this._hideDropdown();

    onChange(selected);
    onInputChange(text);
  },

  _handlePagination(e) {
    let shownResults = this.state.shownResults + this.props.maxResults;

    // Keep the input focused when paginating.
    this.focus();

    this.setState({shownResults});
  },

  _handleRemoveOption(removedOption) {
    let selected = this.state.selected.slice();
    selected = selected.filter(option => !isEqual(option, removedOption));

    // Make sure the input stays focused after the item is removed.
    this.focus();

    this.setState({selected});
    this._hideDropdown();

    this.props.onChange(selected);
  },

  /**
   * From `onClickOutside` HOC.
   */
  handleClickOutside(e) {
    this.state.showMenu && this._hideDropdown();
  },

  _hideDropdown() {
    const {
      activeIndex,
      activeItem,
      showMenu,
      shownResults,
    } = this.getInitialState();

    this.setState({
      activeIndex,
      activeItem,
      showMenu,
      shownResults,
    });
  },
});

export default onClickOutside(Typeahead);
<|endoftext|>