<commit_before>const EventEmitter = require('events').EventEmitter;
const Readable = require('stream').Readable;

const AWS = require('aws-sdk');
const Promise = require('bluebird');
const _ = require('lodash');
const uuid = require('node-uuid');
const type = require('type-of');
const CustomError = require('customerror');

class BloodySimpleSQS extends EventEmitter {

  /**
   * Constructs and returns a new bloody simple SQS client.
   * @param {Object} options SQS client options.
   * @param {String} options.queueName the name of the queue as provided by AWS.
   * @param {String} options.accessKeyId your AWS access key ID.
   * @param {String} options.secretAccessKey your AWS secret access key.
   * @param {String} [options.region=us-east-1] the region of the queue as provided by AWS.
   * @constructor
   */
  constructor(options) {
    // init event emitter
    super();
    this.setMaxListeners(999);

    // validate arguments
    if (!_.isPlainObject(options)) {
      throw new CustomError(`Invalid options param; expected object, received ${type(options)}`, 'InvalidArgument');
    }

    // set default options
    options = _.defaults(options, {
      region: 'us-east-1'
    });

    // validate options
    if (!_.isString(options.queueName)) {
      throw new CustomError(`Invalid queueName option; expected string, received ${type(options.queueName)}`, 'InvalidArgument');
    }

    if (!_.isString(options.accessKeyId)) {
      throw new CustomError(`Invalid accessKeyId option; expected string, received ${type(options.accessKeyId)}`, 'InvalidArgument');
    }

    if (!_.isString(options.secretAccessKey)) {
      throw new CustomError(`Invalid secretAccessKey option; expected string, received ${type(options.secretAccessKey)}`, 'InvalidArgument');
    }

    if (!_.isString(options.region)) {
      throw new CustomError(`Invalid region option; expected string, received ${type(options.region)}`, 'InvalidArgument');
    }

    // init internal aws-sdk client
    this.queueName = options.queueName;

    this.sqs = new AWS.SQS({
      queueName: options.queueName,
      accessKeyId: options.accessKeyId,
      secretAccessKey: options.secretAccessKey,
      region: options.region,
      apiVersion: '2012-11-05'
    });

    // retrieve queue URL and emit ready
    this.getUrl().then((url) => {
      this.queueUrl = url;
      this.isReady = true;
      this.emit('ready');
    });
  }

  /**
   * Constructs and returns a new Promise based on the designated resolver.
   * Delays the execution of the resolver until the queue emits ready.
   * @param {Function} resolver
   * @return {Promise}
   * @private
   */
  _contructPromise(resolver) {
    return new Promise((resolve, reject) => {
      if (this.isReady) {
        resolver(resolve, reject);
      } else {
        this.once('ready', () => resolver(resolve, reject));
      }
    });
  }

  /**
   * Retrieves the URL of the queue from AWS.
   * @param {Function} [callback] an optional callback function with (err, url) arguments.
   * @return {Promise}
   */
  getUrl(callback) {
    // check if queue URL is already set
    if (this.isReady) {
      return Promise.resolve(this.queueUrl).nodeify(callback);
    }

    let resolver = (resolve, reject) => {
      this.sqs.getQueueUrl({
        QueueName: this.queueName
      }, (err, response) => {
        if (err) return reject(err);

        resolve(response.QueueUrl);
      });
    };

    return new Promise(resolver).nodeify(callback);
  }

  /**
   * Returns the number of messages in the queue.
   * @param {Function} [callback] an optional callback function with (err, size) arguments.
   * @return {Promise}
   */
  size(callback) {
    let resolver = (resolve, reject) => {
      this.sqs.getQueueAttributes({
        QueueUrl: this.queueUrl,
        AttributeNames: ['ApproximateNumberOfMessages', 'ApproximateNumberOfMessagesNotVisible']
      }, (err, response) => {
        if (err) return reject(err);

        let messagesCount = parseInt(response.Attributes.ApproximateNumberOfMessages, 10) +
          parseInt(response.Attributes.ApproximateNumberOfMessagesNotVisible, 10);

        resolve(messagesCount);
      });
    };

    return this._contructPromise(resolver).nodeify(callback);
  }

  /**
   * Indicates whether the queue is empty.
   * @param {Function} [callback] an optional callback function (err, isEmpty) arguments.
   * @return {Promise}
   */
  isEmpty(callback) {
    return this.size()

      .then((messagesCount) => {
        return messagesCount === 0;
      })

      .nodeify(callback);
  }

  /**
   * Appends a new message, with the given payload, at the end of the queue.
   * @param {(Boolean|String|Number|Object|null)} payload the message payload
   * @param {Object} [options] request options
   * @param {Integer} [options.delaySeconds] the number of seconds (0 to 900 - 15 minutes) to delay the delivery of the message
   * @param {Function} [callback] an optional callback function with (err, response) arguments
   * @return {Promise}
   */
  add(payload, options, callback) {
    // validate arguments
    if (
      !_.isNumber(payload) &&
      !_.isString(payload) &&
      !_.isPlainObject(payload) &&
      !_.isBoolean(payload) &&
      !_.isNull(payload)
    ) {
      return Promise.reject(new CustomError(`Invalid payload argument; expected number, string, boolean, object or null, received ${type(payload)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // define promise resolver
    let resolver = (resolve, reject) => {
      let params = {
        QueueUrl: this.queueUrl,
        MessageBody: JSON.stringify(payload)
      };

      if (!_.isUndefined(options.delaySeconds)) {
        params.DelaySeconds = options.delaySeconds;
      }

      this.sqs.sendMessage(params, (err, response) => {
        if (err) return reject(err);

        resolve({
          id: response.MessageId,
          body: payload,
          md5: response.MD5OfMessageBody
        });
      });
    };

    return this._contructPromise(resolver).nodeify(callback);
  }

  /**
   * Appends the elements of the specified array as messages to the queue.
   * @param {Array} arr an array of elements to append.
   * @param {Object} [options] optional request options
   * @param {Integer} [options.delaySeconds] the number of seconds (0 to 900 - 15 minutes) to delay the delivery of the message
   * @param {Function} [callback] an optional callback function with (err, response) arguments.
   * @return {Promise}
   */
  addAll(arr, options, callback) {
    // validate arguments
    if (!_.isArray(arr)) {
      return Promise.reject(new CustomError(`Invalid arr argument; expected array, received ${type(arr)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // check if array is empty
    if (arr.length === 0) {
      return Promise.resolve([]).nodeify(callback); // exit
    }

    // check if array contains more than 10 elements
    if (arr.length > 10) {
      // split in chunks
      return Promise.map(_.chunk(arr, 10), (chunk) => {
        return this.addAll(chunk);
      })
        .then(_.flatten)
        .nodeify(callback);
    }

    // define promise resolver
    let resolver = (resolve, reject) => {
      let params = {
        QueueUrl: this.queueUrl,
        Entries: arr.map((e) => {
          return {
            Id: uuid.v4(),
            MessageBody: JSON.stringify(e)
          };
        })
      };

      if (!_.isUndefined(options.delaySeconds)) {
        params.Entries.map((e) => {
          e.DelaySeconds = options.delaySeconds;
          return e;
        });
      }

      this.sqs.sendMessageBatch(params, (err) => {
        if (err) return reject(err);
        resolve();
      });
    };

    return this._contructPromise(resolver).nodeify(callback);
  }

  /**
   * Retrieves, but does not remove, the specified number of messages from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Integer} [options.limit=1] maximum number of messages to return.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  peek(options, callback) {
    // validate arguments
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // set default options
    options = _.defaults(options, {
      timeout: 0,
      limit: 1
    });

    // validate options
    if (!_.isNumber(options.timeout)) {
      return Promise.reject(new CustomError(`Invalid timeout option; expected number, received ${type(options.timeout)}`))
        .nodeify(callback);
    }

    if (!_.isNumber(options.limit)) {
      return Promise.reject(new CustomError(`Invalid limit option; expected number, received ${type(options.limit)}`))
        .nodeify(callback);
    }

    // check if array contains more than 10 elements
    if (options.limit > 10) {
      // split in chunks
      return Promise.map(_.chunk(new Array(options.limit), 10), (chunk) => {
        return this.peek({timeout: options.timeout, limit: chunk.length});
      })
        .then(_.flatten)
        .nodeify(callback);
    }

    // define promise resolver
    let resolver = (resolve, reject) => {
      this.sqs.receiveMessage({
        QueueUrl: this.queueUrl,
        MaxNumberOfMessages: options.limit,
        WaitTimeSeconds: options.timeout
      }, (err, response) => {
        if (err) return reject(err);

        let messages = (response.Messages || [])

          .map((obj) => {
            return {
              id: obj.MessageId,
              body: JSON.parse(obj.Body),
              md5: obj.MD5OfBody,
              receiptHandle: obj.ReceiptHandle
            };
          });

        resolve(messages);
      });
    };

    return this._contructPromise(resolver).nodeify(callback);
  }

  /**
   * Retrieves, but does not remove, the first message from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  peekOne(options, callback) {
    // validate arguments
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // set default options
    options = _.defaults(options, {
      timeout: 0
    });

    // set hard limit to 1
    options.limit = 1;

    // peek and unpack returned messages
    return this.peek(options)
      .then((messages) => {
        if (messages.length === 0) return null;
        return messages[0];
      })
      .nodeify(callback);
  }

  /**
   * Removes the designated message from queue.
   * @param {String} receiptHandle the message's receipt handle, as given on peek().
   * @param {Function} [callback] an optional callback function with (err) arguments.
   * @return {Promise}
   */
  remove(receiptHandle, callback) {
    // validate arguments
    if (!_.isString(receiptHandle)) {
      return Promise.reject(new CustomError(`Invalid receiptHandle argument; expected string, received ${type(receiptHandle)}`))
        .nodeify(callback);
    }

    // define promise resolver
    let resolver = (resolve, reject) => {
      this.sqs.deleteMessage({
        QueueUrl: this.queueUrl,
        ReceiptHandle: receiptHandle
      }, (err) => {
        if (err) return reject(err);
        resolve();
      });
    };

    return this._contructPromise(resolver).nodeify(callback);
  }

  /**
   * Retrieves and removes the specified number of messages from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Integer} [options.limit=1] maximum number of messages to return.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  poll(options, callback) {
    return this.peek(options)

      .map((message) => {
        return this.remove(message.receiptHandle)
          .return(message);
      })

      .nodeify(callback);
  }

  /**
   * Retrieves and removes the first message from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  pollOne(options, callback) {
    return this.peekOne(options)

      .then((message) => {
        if (!message) return null;

        return this.remove(message.receiptHandle)
          .return(message);
      })

      .nodeify(callback);
  }

  /**
   * Removes all messages from queue.
   * @param {Function} [callback] an optional callback function with (err) arguments.
   * @return {Promise}
   */
  clear(callback) {
    let resolver = (resolve, reject) => {
      this.sqs.purgeQueue({QueueUrl: this.queueUrl}, (err) => {
        if (err) return reject(err);
        resolve();
      });
    };

    return this._contructPromise(resolver).nodeify(callback);
  }

  /**
   * Returns a new ReadStream object consuming the queue's messages.
   * @return {stream.Readable}
   */
  createReadStream() {
    let rs = new Readable({
      highWaterMark: 1, // as little as possible
      objectMode: true,
      encoding: 'utf8'
    });

    rs._read = () => {
      this.pollOne()

        .then((message) => {
          if (!message) {
            return rs.push(null); // exit
          }

          rs.push(message.body, 'utf8');
        })

        .catch((err) => {
          rs.emit('error', err);
        });
    };

    return rs;
  }
}

module.exports = BloodySimpleSQS;
<commit_msg>Minor refactor<commit_after>const EventEmitter = require('events').EventEmitter;
const Readable = require('stream').Readable;

const AWS = require('aws-sdk');
const Promise = require('bluebird');
const _ = require('lodash');
const uuid = require('node-uuid');
const type = require('type-of');
const CustomError = require('customerror');

class BloodySimpleSQS extends EventEmitter {

  /**
   * Constructs and returns a new bloody simple SQS client.
   * @param {Object} options SQS client options.
   * @param {String} options.queueName the name of the queue as provided by AWS.
   * @param {String} options.accessKeyId your AWS access key ID.
   * @param {String} options.secretAccessKey your AWS secret access key.
   * @param {String} [options.region=us-east-1] the region of the queue as provided by AWS.
   * @constructor
   */
  constructor(options) {
    // init event emitter
    super();
    this.setMaxListeners(999);

    // validate arguments
    if (!_.isPlainObject(options)) {
      throw new CustomError(`Invalid options param; expected object, received ${type(options)}`, 'InvalidArgument');
    }

    // set default options
    options = _.defaults(options, {
      region: 'us-east-1'
    });

    // validate options
    if (!_.isString(options.queueName)) {
      throw new CustomError(`Invalid queueName option; expected string, received ${type(options.queueName)}`, 'InvalidArgument');
    }

    if (!_.isString(options.accessKeyId)) {
      throw new CustomError(`Invalid accessKeyId option; expected string, received ${type(options.accessKeyId)}`, 'InvalidArgument');
    }

    if (!_.isString(options.secretAccessKey)) {
      throw new CustomError(`Invalid secretAccessKey option; expected string, received ${type(options.secretAccessKey)}`, 'InvalidArgument');
    }

    if (!_.isString(options.region)) {
      throw new CustomError(`Invalid region option; expected string, received ${type(options.region)}`, 'InvalidArgument');
    }

    // init internal aws-sdk client
    this.queueName = options.queueName;

    this.sqs = new AWS.SQS({
      queueName: options.queueName,
      accessKeyId: options.accessKeyId,
      secretAccessKey: options.secretAccessKey,
      region: options.region,
      apiVersion: '2012-11-05'
    });

    // retrieve queue URL and emit ready
    this.getUrl().then((url) => {
      this.queueUrl = url;
      this.isReady = true;
      this.emit('ready');
    });
  }

  /**
   * Constructs and returns a new Promise based on the designated resolver.
   * Delays the execution of the resolver until the queue emits ready.
   * @param {Function} resolver
   * @return {Promise}
   * @private
   */
  _contructPromise(resolver) {
    return new Promise((resolve, reject) => {
      if (this.isReady) {
        resolver(resolve, reject);
      } else {
        this.once('ready', () => resolver(resolve, reject));
      }
    });
  }

  /**
   * Returns a promise that will be resolved when the queue emits ready.
   * @return {Promise}
   * @private
   */
  _ready() {
    return new Promise((resolve) => {
      if (this.isReady) {
        resolve();
      } else {
        this.once('ready', () => resolve());
      }
    });
  }

  /**
   * Retrieves the URL of the queue from AWS.
   * @param {Function} [callback] an optional callback function with (err, url) arguments.
   * @return {Promise}
   */
  getUrl(callback) {
    // check if queue URL is already set
    if (this.isReady) {
      return Promise.resolve(this.queueUrl).nodeify(callback);
    }

    const resolver = (resolve, reject) => {
      this.sqs.getQueueUrl({
        QueueName: this.queueName
      }, (err, response) => {
        if (err) return reject(err);

        resolve(response.QueueUrl);
      });
    };

    return new Promise(resolver).nodeify(callback);
  }

  /**
   * Returns the number of messages in the queue.
   * @param {Function} [callback] an optional callback function with (err, size) arguments.
   * @return {Promise}
   */
  size(callback) {
    const resolver = (resolve, reject) => {
      this.sqs.getQueueAttributes({
        QueueUrl: this.queueUrl,
        AttributeNames: ['ApproximateNumberOfMessages', 'ApproximateNumberOfMessagesNotVisible']
      }, (err, response) => {
        if (err) return reject(err);

        const n = parseInt(response.Attributes.ApproximateNumberOfMessages, 10) +
          parseInt(response.Attributes.ApproximateNumberOfMessagesNotVisible, 10);

        resolve(n);
      });
    };

    return this._ready()
      .then(() => new Promise(resolver))
      .nodeify(callback);
  }

  /**
   * Indicates whether the queue is empty.
   * @param {Function} [callback] an optional callback function (err, isEmpty) arguments.
   * @return {Promise}
   */
  isEmpty(callback) {
    return this.size()
      .then((n) => n === 0)
      .nodeify(callback);
  }

  /**
   * Appends a new message, with the given payload, at the end of the queue.
   * @param {(Boolean|String|Number|Object|null)} payload the message payload
   * @param {Object} [options] request options
   * @param {Integer} [options.delaySeconds] the number of seconds (0 to 900 - 15 minutes) to delay the delivery of the message
   * @param {Function} [callback] an optional callback function with (err, response) arguments
   * @return {Promise}
   */
  add(payload, options, callback) {
    // validate arguments
    if (
      !_.isNumber(payload) &&
      !_.isString(payload) &&
      !_.isPlainObject(payload) &&
      !_.isBoolean(payload) &&
      !_.isNull(payload)
    ) {
      return Promise.reject(new CustomError(`Invalid payload argument; expected number, string, boolean, object or null, received ${type(payload)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // define promise resolver
    const resolver = (resolve, reject) => {
      const params = {
        QueueUrl: this.queueUrl,
        MessageBody: JSON.stringify(payload)
      };

      if (!_.isUndefined(options.delaySeconds)) {
        params.DelaySeconds = options.delaySeconds;
      }

      this.sqs.sendMessage(params, (err, response) => {
        if (err) return reject(err);

        resolve({
          id: response.MessageId,
          body: payload,
          md5: response.MD5OfMessageBody
        });
      });
    };

    return this._ready()
      .then(() => new Promise(resolver))
      .nodeify(callback);
  }

  /**
   * Appends the elements of the specified array as messages to the queue.
   * @param {Array} arr an array of elements to append.
   * @param {Object} [options] optional request options
   * @param {Integer} [options.delaySeconds] the number of seconds (0 to 900 - 15 minutes) to delay the delivery of the message
   * @param {Function} [callback] an optional callback function with (err, response) arguments.
   * @return {Promise}
   */
  addAll(arr, options, callback) {
    // validate arguments
    if (!_.isArray(arr)) {
      return Promise.reject(new CustomError(`Invalid arr argument; expected array, received ${type(arr)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // check if array is empty
    if (arr.length === 0) {
      return Promise.resolve([]).nodeify(callback); // exit
    }

    // check if array contains more than 10 elements
    if (arr.length > 10) {
      // split in chunks
      return Promise.map(_.chunk(arr, 10), (chunk) => {
        return this.addAll(chunk);
      })
        .then(_.flatten)
        .nodeify(callback);
    }

    // define promise resolver
    const resolver = (resolve, reject) => {
      const params = {
        QueueUrl: this.queueUrl,
        Entries: arr.map((e) => {
          return {
            Id: uuid.v4(),
            MessageBody: JSON.stringify(e)
          };
        })
      };

      if (!_.isUndefined(options.delaySeconds)) {
        params.Entries.map((e) => {
          e.DelaySeconds = options.delaySeconds;
          return e;
        });
      }

      this.sqs.sendMessageBatch(params, (err) => {
        if (err) return reject(err);
        resolve();
      });
    };

    return this._ready()
      .then(() => new Promise(resolver))
      .nodeify(callback);
  }

  /**
   * Retrieves, but does not remove, the specified number of messages from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Integer} [options.limit=1] maximum number of messages to return.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  peek(options, callback) {
    // validate arguments
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // set default options
    options = _.defaults(options, {
      timeout: 0,
      limit: 1
    });

    // validate options
    if (!_.isNumber(options.timeout)) {
      return Promise.reject(new CustomError(`Invalid timeout option; expected number, received ${type(options.timeout)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    if (!_.isNumber(options.limit)) {
      return Promise.reject(new CustomError(`Invalid limit option; expected number, received ${type(options.limit)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // check if array contains more than 10 elements
    if (options.limit > 10) {
      // split in chunks
      return Promise.map(_.chunk(new Array(options.limit), 10), (chunk) => {
        return this.peek({timeout: options.timeout, limit: chunk.length});
      })
        .then(_.flatten)
        .nodeify(callback);
    }

    // define promise resolver
    const resolver = (resolve, reject) => {
      this.sqs.receiveMessage({
        QueueUrl: this.queueUrl,
        MaxNumberOfMessages: options.limit,
        WaitTimeSeconds: options.timeout
      }, (err, response) => {
        if (err) return reject(err);

        const messages = (response.Messages || [])
          .map((obj) => {
            return {
              id: obj.MessageId,
              body: JSON.parse(obj.Body),
              md5: obj.MD5OfBody,
              receiptHandle: obj.ReceiptHandle
            };
          });

        resolve(messages);
      });
    };

    return this._ready()
      .then(() => new Promise(resolver))
      .nodeify(callback);
  }

  /**
   * Retrieves, but does not remove, the first message from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  peekOne(options, callback) {
    // validate arguments
    if (_.isFunction(options)) {
      callback = options;
      options = {};
    } else if (_.isUndefined(options)) {
      options = {};
    } else if (!_.isPlainObject(options)) {
      return Promise.reject(new CustomError(`Invalid options argument; expected object, received ${type(options)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // set default options
    options = _.defaults(options, {
      timeout: 0
    });

    // set hard limit to 1
    options.limit = 1;

    // peek and unpack returned messages
    return this.peek(options)

      .then((messages) => {
        if (messages.length === 0) {
          return null;
        }

        return messages[0];
      })

      .nodeify(callback);
  }

  /**
   * Removes the designated message from queue.
   * @param {String} receiptHandle the message's receipt handle, as given on peek().
   * @param {Function} [callback] an optional callback function with (err) arguments.
   * @return {Promise}
   */
  remove(receiptHandle, callback) {
    // validate arguments
    if (!_.isString(receiptHandle)) {
      return Promise.reject(new CustomError(`Invalid receiptHandle argument; expected string, received ${type(receiptHandle)}`, 'InvalidArgument'))
        .nodeify(callback);
    }

    // define promise resolver
    const resolver = (resolve, reject) => {
      this.sqs.deleteMessage({
        QueueUrl: this.queueUrl,
        ReceiptHandle: receiptHandle
      }, (err) => {
        if (err) return reject(err);
        resolve();
      });
    };

    return this._ready
      .then(() => new Promise(resolver))
      .nodeify(callback);
  }

  /**
   * Retrieves and removes the specified number of messages from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Integer} [options.limit=1] maximum number of messages to return.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  poll(options, callback) {
    return this.peek(options)

      .map((message) => {
        return this.remove(message.receiptHandle).return(message);
      })

      .nodeify(callback);
  }

  /**
   * Retrieves and removes the first message from the head of the queue.
   * @param {Object} [options] optional request options.
   * @param {Integer} [options.timeout=0] number of seconds to wait until a message becomes available for retrieval; must be between 0 and 20.
   * @param {Function} [callback] an optional callback function with (err, messages) arguments.
   * @return {Promise}
   */
  pollOne(options, callback) {
    return this.peekOne(options)

      .then((message) => {
        if (!message) {
          return null;
        }

        return this.remove(message.receiptHandle).return(message);
      })

      .nodeify(callback);
  }

  /**
   * Removes all messages from queue.
   * @param {Function} [callback] an optional callback function with (err) arguments.
   * @return {Promise}
   */
  clear(callback) {
    const resolver = (resolve, reject) => {
      this.sqs.purgeQueue({QueueUrl: this.queueUrl}, (err) => {
        if (err) return reject(err);
        resolve();
      });
    };

    return this._ready()
      .then(() => new Promise(resolver))
      .nodeify(callback);
  }

  /**
   * Returns a new ReadStream object consuming the queue's messages.
   * @return {stream.Readable}
   */
  createReadStream() {
    const rs = new Readable({
      highWaterMark: 1, // as little as possible
      objectMode: true,
      encoding: 'utf8'
    });

    rs._read = () => {
      this.pollOne()

        .then((message) => {
          if (!message) {
            return rs.push(null); // exit
          }

          rs.push(message.body, 'utf8');
        })

        .catch((err) => {
          rs.emit('error', err);
        });
    };

    return rs;
  }
}

module.exports = BloodySimpleSQS;
<|endoftext|>


<commit_before>/**Breadth-First Search
 * 图的广度优先遍历
*/

//javascript无向图

function Graph() {

    var vertices = [];
    var adjList = new Dictionary();//邻接表

    var initializeColor = function() {
        var color = [];
        for (var i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white';
        }

        return color;
    };

    /**添加顶点
     * @param v
     * @return null
    */
    this.addVertex = function(v) {
        vertices.push(v);
        adjList.set(v, []);
    };

    /**添加边
     * @param v,w
     * @return null
    */
    this.addEdge = function(v, w) {
        adjList.get(v).push(w);
        adjList.get(w).push(v);
    };

    //控制台输出图
    this.toString = function() {
        var s = '';
        for(var i = 0; i < vertices.length; i++) {
            s += vertices[i] + '->';
            var neighbors = adjList.get(vertices[i]);

            for(j = 0; j < neighbors.length; j++) {
                s += neighbors[j] + ' ';
            }

            s += '\n';
        }

        console.log(s);
    };

    this.bfs = function(v, callback) {
        var color = initializeColor(),
            queue = new Queue();

        queue.enqueue(v);

        while(!queue.isEmpty()) {
            var u = queue.dequeue();
            var neighbors = adjList.get(u);

            color[u] = 'grey';

            for(var i = 0; i < neighbors.length; i++) {
                var w = neighbors[i];

                if(color[w] === 'white') {
                    color[w] = 'grey';
                    queue.enqueue(w);
                }
            }

            color[u] = 'black';

            if(callback) {
                callback(u);
            }
        }
    };
}

function Dictionary() {

    var items = {};

    /**判断给定键是否存在
     * @param key
     * @return boolean
    */
    this.has = function(key) {
        return key in items;
    };

    /*更新或添加键值
    *@param key
    *@param value
    */
    this.set = function(key, value) {
        items[key] = value;
    };

    /**删除键值
     * @param key
     * @return boolean
    */
    this.remove = function(key) {
        if(this.has(key)) {
            delete items[key];
            return true;
        }

        return false;
    };

    /**获取给定键的值
     * @param key
     * @return value
     */
    this.get = function(key) {
        return this.has(key) ? items[key] : undefined;
    };

    //返回所有键组成的数组
    this.keys = function() {
        return Object.keys(items);
    };

    //返回所有键值组成的数组
    this.values = function() {
        var values = [];

        for(var key in items) {
            if(this.has(key)) {
                values.push(items[key]);
            }
        }

        return values;
    };

    //清空字典
    this.clear = function() {
        items = {};
    };

    //字典大小
    this.size = function() {
        return Object.keys(items).length;
    };
}

//javascript队列

function Queue() {

    //用数组保存队列元素
    var items = [];

    /**入列
     * @param element
    */
    this.enqueue = function(element) {
        items.push(element);
    };

    //出列
    this.dequeue = function() {
        return items.shift();
    };

    //获取队首元素
    this.front = function() {
        return items[0];
    };

    //判断队列是否为空
    this.isEmpty = function() {
        return items.length == 0;
    };

    //清空队列
    this.clear = function() {
        items = [];
    };

    //获取队列大小
    this.size = function() {
        return items.length;
    };

    //控制台打印输出队列
    this.print = function() {
        console.log(items.toString());
    };
}

//BFS测试

function test() {
    var graph = new Graph();
    var myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];

    for (var i = 0; i < myVertices.length; i++) {
        graph.addVertex(myVertices[i]);
    }

    graph.addEdge('A', 'B');
    graph.addEdge('A', 'C');
    graph.addEdge('A', 'D');
    graph.addEdge('C', 'D');
    graph.addEdge('C', 'G');
    graph.addEdge('D', 'G');
    graph.addEdge('D', 'H');
    graph.addEdge('B', 'E');
    graph.addEdge('B', 'F');
    graph.addEdge('E', 'I');

    function printNode(value) {
        console.log('Visited vertex:' + value);
    }

    graph.bfs(myVertices[0], printNode);
}

test()
<commit_msg>project updated<commit_after>/**
 * Breadth-First Search
 * 图的广度优先遍历
*/

//javascript无向图

function Graph() {

    var vertices = [];
    var adjList = new Dictionary();//邻接表

    var initializeColor = function() {
        var color = [];
        for (var i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white';
        }

        return color;
    };

    /**添加顶点
     * @param v
     * @return null
    */
    this.addVertex = function(v) {
        vertices.push(v);
        adjList.set(v, []);
    };

    /**添加边
     * @param v,w
     * @return null
    */
    this.addEdge = function(v, w) {
        adjList.get(v).push(w);
        adjList.get(w).push(v);
    };

    //控制台输出图
    this.toString = function() {
        var s = '';
        for(var i = 0; i < vertices.length; i++) {
            s += vertices[i] + '->';
            var neighbors = adjList.get(vertices[i]);

            for(j = 0; j < neighbors.length; j++) {
                s += neighbors[j] + ' ';
            }

            s += '\n';
        }

        console.log(s);
    };

    this.bfs = function(v, callback) {
        var color = initializeColor(),
            queue = new Queue();

        queue.enqueue(v);

        while(!queue.isEmpty()) {
            var u = queue.dequeue();
            var neighbors = adjList.get(u);

            color[u] = 'grey';

            for(var i = 0; i < neighbors.length; i++) {
                var w = neighbors[i];

                if(color[w] === 'white') {
                    color[w] = 'grey';
                    queue.enqueue(w);
                }
            }

            color[u] = 'black';

            if(callback) {
                callback(u);
            }
        }
    };
}

function Dictionary() {

    var items = {};

    /**判断给定键是否存在
     * @param key
     * @return boolean
    */
    this.has = function(key) {
        return key in items;
    };

    /*更新或添加键值
    *@param key
    *@param value
    */
    this.set = function(key, value) {
        items[key] = value;
    };

    /**删除键值
     * @param key
     * @return boolean
    */
    this.remove = function(key) {
        if(this.has(key)) {
            delete items[key];
            return true;
        }

        return false;
    };

    /**获取给定键的值
     * @param key
     * @return value
     */
    this.get = function(key) {
        return this.has(key) ? items[key] : undefined;
    };

    //返回所有键组成的数组
    this.keys = function() {
        return Object.keys(items);
    };

    //返回所有键值组成的数组
    this.values = function() {
        var values = [];

        for(var key in items) {
            if(this.has(key)) {
                values.push(items[key]);
            }
        }

        return values;
    };

    //清空字典
    this.clear = function() {
        items = {};
    };

    //字典大小
    this.size = function() {
        return Object.keys(items).length;
    };
}

//javascript队列

function Queue() {

    //用数组保存队列元素
    var items = [];

    /**入列
     * @param element
    */
    this.enqueue = function(element) {
        items.push(element);
    };

    //出列
    this.dequeue = function() {
        return items.shift();
    };

    //获取队首元素
    this.front = function() {
        return items[0];
    };

    //判断队列是否为空
    this.isEmpty = function() {
        return items.length == 0;
    };

    //清空队列
    this.clear = function() {
        items = [];
    };

    //获取队列大小
    this.size = function() {
        return items.length;
    };

    //控制台打印输出队列
    this.print = function() {
        console.log(items.toString());
    };
}

//BFS测试

function test() {
    var graph = new Graph();
    var myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];

    for (var i = 0; i < myVertices.length; i++) {
        graph.addVertex(myVertices[i]);
    }

    graph.addEdge('A', 'B');
    graph.addEdge('A', 'C');
    graph.addEdge('A', 'D');
    graph.addEdge('C', 'D');
    graph.addEdge('C', 'G');
    graph.addEdge('D', 'G');
    graph.addEdge('D', 'H');
    graph.addEdge('B', 'E');
    graph.addEdge('B', 'F');
    graph.addEdge('E', 'I');

    function printNode(value) {
        console.log('Visited vertex:' + value);
    }

    graph.bfs(myVertices[0], printNode);
}

test()
<|endoftext|>


<commit_before>(function () {
    // good coding practices
    "use strict";

    angular.module("ngImageService", []).service("ngImageService", ["$q", function ($q) {

        Element.prototype.remove = function () {
            this.parentElement.removeChild(this);
        }
        NodeList.prototype.remove = HTMLCollection.prototype.remove = function () {
            for (var i = this.length - 1; i >= 0; i--) {
                if (this[i] && this[i].parentElement) {
                    this[i].parentElement.removeChild(this[i]);
                }
            }
        }

        var dataUrlToBlob = function (dataUrl) {
            var base64Marker = ";base64,";
            var parts;
            var contentType;
            var raw;
            if (dataUrl.indexOf(base64Marker) === -1) {
                parts = dataUrl.split(",");
                contentType = parts[0].split(":")[1];
                raw = decodeURIComponent(parts[1]);
                return new Blob([raw], { type: contentType });
            } else {
                parts = dataUrl.split(base64Marker);
                contentType = parts[0].split(":")[1];
                raw = window.atob(parts[1]);
                var rawLength = raw.length;

                var uInt8Array = new Uint8Array(rawLength);

                for (var i = 0; i < rawLength; ++i) {
                    uInt8Array[i] = raw.charCodeAt(i);
                }

                return new Blob([uInt8Array], { type: contentType });
            }
        }

        this.getDataUrlFromFile = function (file) {
            var defer = $q.defer();
            var fileLoader = new FileReader();
            fileLoader.readAsDataURL(file);

            // setup the file loader onload function
            // once the file loader has the data it passes it to the
            // image object which, once the image has loaded,
            // triggers the images onload function
            fileLoader.onload = function () {
                defer.resolve(this.result);
            };

            fileLoader.onabort = function () {
                defer.reject("Error (ImageService): The upload was aborted.");
            };

            fileLoader.onerror = function () {
                defer.reject("Error (ImageService): An error occured while reading the file.");
            };

            return defer.promise;
        }

        var resize = function (imageObj, height, width, imageEncoding) {

            // Check for empty images
            if (imageObj.width === 0 || imageObj.height === 0) {
                console.log("Error (ImageService): empty image");
                return false;
            }
            // create a hidden canvas object we can use to create the new resized image data
            var canvas = document.createElement("canvas");
            canvas.id = "hiddenCanvas";
            canvas.width = width;
            canvas.height = height;
            canvas.style.visibility = "hidden";
            document.body.appendChild(canvas);

            // draw image on canvas
            var ctx = canvas.getContext("2d");
            ctx.drawImage(imageObj, 0, 0, width, height);

            var blob = dataUrlToBlob(canvas.toDataURL(imageEncoding));

            canvas.remove();

            return blob;
        }

        var resizeCalculate = function (dataUrl, maxWidth, maxHeight, imageEncoding, smallerThanIsAllowed) {
            var defer = $q.defer();
            var imageObj = new Image();

            imageObj.src = dataUrl;

            // set up the images onload function which creates a hidden canvas context,
            // draws the new image then gets the blob data from it
            imageObj.onload = function () {

                if (smallerThanIsAllowed && this.width < maxWidth && this.height < maxHeight) {
                    var blob = dataUrlToBlob(dataUrl);
                    // tell the blob it's size
                    blob.height = this.height;
                    blob.width = this.width;
                    defer.resolve(blob);
                }
                var height = imageObj.height;
                var width = imageObj.width;

                // calculate the width and height, constraining the proportions
                if (width > height && width > maxWidth) {
                    height = Math.round(height * (maxWidth / width));
                    width = maxWidth;
                }
                else if (height > width && height > maxHeight) {
                    width = Math.round(width * (maxHeight / height));
                    height = maxHeight;
                }
                var resizedBlob = resize(imageObj, height, width, imageEncoding);
                if (!resizedBlob) {
                    defer.reject("Error (ImageService): Image is empty");
                };
                // tell the blob it's size
                resizedBlob.height = height;
                resizedBlob.width = width;
                defer.resolve(resizedBlob);
            };

            imageObj.onabort = function () {
                defer.reject("Error (ImageService): Image load was aborted.");
            };

            imageObj.onerror = function () {
                defer.reject("Error (ImageService): An error occured while loading image.");
            };

            return defer.promise;
        }

        this.resizeFileTo = function (file, maxHeight, maxWidth, imageEncoding, smallerThanIsAllowed) {
            // check for an image
            if (!file.type.match("image.*")) {
                console.log("Error (ImageService): File is not an image");
                return false;
            };
            return this.getDataUrlFromFile(file).then(
                function (dataUrl) {
                    return resizeCalculate(dataUrl, maxWidth, maxHeight, imageEncoding, smallerThanIsAllowed).then(
                        function (image) {
                            // reset the meta data of the file to the blob
                            if (file.name) {
                                image.name = file.name;
                            }
                            if (file.lastModified) {
                                image.lastModified = file.lastModified;
                            }
                            if (file.lastModifiedDate) {
                                image.lastModifiedDate = file.lastModifiedDate;
                            }
                            return image;
                        });
                });
        }

        this.resizeDatabaseFileTo = function (databaseFile, maxWidth, maxHeight, imageEncoding, smallerThanIsAllowed) {

            return resizeCalculate(databaseFile.Content, maxWidth, maxHeight, imageEncoding, smallerThanIsAllowed).then(
                        function (image) {
                            // reset the meta data of the file to the blob
                            if (databaseFile.name) {
                                image.name = databaseFile.name;
                            }
                            if (databaseFile.lastModified) {
                                image.lastModified = databaseFile.lastModified;
                            }
                            if (databaseFile.lastModifiedDate) {
                                image.lastModifiedDate = databaseFile.lastModifiedDate;
                            }
                            return image;
                        });
        }

    }]);

}
	());
<commit_msg>implemented normal options object and option to ignore aspectRatio<commit_after>(function () {
    // good coding practices
    "use strict";

    angular.module("imageService", []).service("imageService", ["$q", function ($q) {

        Element.prototype.remove = function () {
            this.parentElement.removeChild(this);
        }
        NodeList.prototype.remove = HTMLCollection.prototype.remove = function () {
            for (var i = this.length - 1; i >= 0; i--) {
                if (this[i] && this[i].parentElement) {
                    this[i].parentElement.removeChild(this[i]);
                }
            }
        }

        var dataUrlToBlob = function (dataUrl) {
            var base64Marker = ";base64,";
            var parts;
            var contentType;
            var raw;
            if (dataUrl.indexOf(base64Marker) === -1) {
                parts = dataUrl.split(",");
                contentType = parts[0].split(":")[1];
                raw = decodeURIComponent(parts[1]);
                return new Blob([raw], { type: contentType });
            } else {
                parts = dataUrl.split(base64Marker);
                contentType = parts[0].split(":")[1];
                raw = window.atob(parts[1]);
                var rawLength = raw.length;

                var uInt8Array = new Uint8Array(rawLength);

                for (var i = 0; i < rawLength; ++i) {
                    uInt8Array[i] = raw.charCodeAt(i);
                }

                return new Blob([uInt8Array], { type: contentType });
            }
        }

        this.getDataUrlFromFile = function (file) {
            var defer = $q.defer();
            var fileLoader = new FileReader();
            fileLoader.readAsDataURL(file);

            // setup the file loader onload function
            // once the file loader has the data it passes it to the
            // image object which, once the image has loaded,
            // triggers the images onload function
            fileLoader.onload = function () {
                defer.resolve(this.result);
            };

            fileLoader.onabort = function () {
                defer.reject("Error (ImageService): The upload was aborted.");
            };

            fileLoader.onerror = function () {
                defer.reject("Error (ImageService): An error occured while reading the file.");
            };

            return defer.promise;
        }

        var resize = function (imageObj, height, width, imageEncoding) {

            // Check for empty images
            if (imageObj.width === 0 || imageObj.height === 0) {
                console.log("Error (ImageService): empty image");
                return false;
            }
            // create a hidden canvas object we can use to create the new resized image data
            var canvas = document.createElement("canvas");
            canvas.id = "hiddenCanvas";
            canvas.width = width;
            canvas.height = height;
            canvas.style.visibility = "hidden";
            document.body.appendChild(canvas);

            // draw image on canvas
            var ctx = canvas.getContext("2d");
            ctx.drawImage(imageObj, 0, 0, width, height);

            var blob = dataUrlToBlob(canvas.toDataURL(imageEncoding));

            canvas.remove();

            return blob;
        }

        var resizeCalculate = function (dataUrl, maxWidth, maxHeight, imageEncoding, smallerThanIsAllowed, keepAspectRatio) {
            var defer = $q.defer();
            var imageObj = new Image();

            imageObj.src = dataUrl;

            // set up the images onload function which creates a hidden canvas context,
            // draws the new image then gets the blob data from it
            imageObj.onload = function () {

                if (smallerThanIsAllowed && this.width < maxWidth && this.height < maxHeight) {
                    var blob = dataUrlToBlob(dataUrl);
                    // tell the blob it's size
                    blob.height = this.height;
                    blob.width = this.width;
                    defer.resolve(blob);
                }
                var height = imageObj.height;
                var width = imageObj.width;

                if (keepAspectRatio) {
                    // calculate the width and height, constraining the proportions
                    if (width > height && width > maxWidth) {
                        height = Math.round(height * (maxWidth / width));
                        width = maxWidth;
                    } else if (height > width && height > maxHeight) {
                        width = Math.round(width * (maxHeight / height));
                        height = maxHeight;
                    }
                } else {
                    // do not calculate anything, just set the values
                    if (width > maxWidth) {
                        width = maxWidth;
                    }
                    if (height > maxHeight) {
                        height = maxHeight;
                    }
                }

                var resizedBlob = resize(imageObj, height, width, imageEncoding);
                if (!resizedBlob) {
                    defer.reject("Error (ImageService): Image is empty");
                };
                // tell the blob it's size
                resizedBlob.height = height;
                resizedBlob.width = width;
                defer.resolve(resizedBlob);
            };

            imageObj.onabort = function () {
                defer.reject("Error (ImageService): Image load was aborted.");
            };

            imageObj.onerror = function () {
                defer.reject("Error (ImageService): An error occured while loading image.");
            };

            return defer.promise;
        }

        this.resizeFileTo = function (file, options) {

            var smallerThanIsAllowed = options.smallerThanIsAllowed || true;
            var maxWidth = options.maxWidth || 1920;
            var maxHeight = options.maxHeight || 1080;
            var imageEncoding = options.imageEncoding || null; // todo write detection for file type if it is not defined
            var keepAspectRatio = options.keepAspectRatio || true;

            // check for an image
            if (!file.type.match("image.*")) {
                console.log("Error (ImageService): File is not an image");
                return false;
            };
            return this.getDataUrlFromFile(file).then(
                function (dataUrl) {
                    return resizeCalculate(dataUrl, maxWidth, maxHeight, imageEncoding, smallerThanIsAllowed, keepAspectRatio).then(
                        function (image) {
                            // reset the meta data of the file to the blob
                            if (file.name) {
                                image.name = file.name;
                            }
                            if (file.lastModified) {
                                image.lastModified = file.lastModified;
                            }
                            if (file.lastModifiedDate) {
                                image.lastModifiedDate = file.lastModifiedDate;
                            }
                            return image;
                        });
                });
        }

    }]);

}
	());
<|endoftext|>


<commit_before>/**
 * Created by kgrube on 12/15/2015.
 * @author kgrube
 */

/**
 * @typedef {object} CWOptions
 * @property companyId
 * @property publicKey
 * @property privateKey
 * @property companyUrl
 */

/**
 * @typedef {object} ConnectWiseRest
 * @property {ConnectWise} API
 * @property {FinanceAPI} FinanceAPI
 * @property {ServiceDeskAPI} ServiceDeskAPI
 * @property {TimeAPI} TimeAPI
 * @property {ProjectAPI} ProjectAPI
 * @property {CompanyAPI} CompanyAPI
 * @property {ScheduleAPI} ScheduleAPI
 * @property {SystemAPI} SystemAPI
 */

/**
 *
 * @param options
 * @returns {ConnectWiseRest}
 * @constructor
 */
function ConnectWiseRest(options) {
  var _ConnectWise = require('./ConnectWise'),
    _CompanyAPI = require('./CompanyAPI'),
    _FinanceAPI = require('./FinanceAPI'),
    _TimeAPI = require('./TimeAPI'),
    _ServiceDeskAPI = require('./ServiceDeskAPI'),
    _ProjectAPI = require('./ProjectAPI'),
    _ScheduleAPI = require('./ScheduleAPI'),
    _SystemAPI = require('./SystemAPI');

  return {
    API: new _ConnectWise(options),
    CompanyAPI: new _CompanyAPI(options),
    FinanceAPI: new _FinanceAPI(options),
    ServiceDeskAPI: new _ServiceDeskAPI(options),
    TimeAPI: new _TimeAPI(options),
    ProjectAPI: new _ProjectAPI(options),
    ScheduleAPI: new _ScheduleAPI(options),
    SystemAPI: new _SystemAPI(options)
  }
}

/**
 *
 * @type {ConnectWiseRest}
 */
module.exports = ConnectWiseRest;

/**
 * @typedef {object} InfoHref
 * @property lastUpdated
 * @property updatedBy
 */

/**
 * @typedef {object} AgreementHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.agreement_href
 */

/**
 * @typedef {object} AgreementTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.type_href
 */

/**
 * @typedef {object} BoardHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.board_href
 */

/**
 * @typedef {object} CompanyHref
 * @property {number} id
 * @property {string} identifier The company ID in ConnectWise
 * @property {object} _info
 * @property {string} _info.company_href
 */

/**
 * @typedef {object} ContactHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.contact_href
 */

/**
 * @typedef {object} CountryHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */

/**
 * @typedef {object} DocumentHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.filename
 */

/**
 * @typedef {object} ItemHref Service Item
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.item_href
 */

/**
 * @typedef {object} PriorityHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.image_href
 * @property {string} _info.priority_href
 */

/**
 * @typedef {object} ProductHref
 * @property {number} id
 * @property {string} identifier
 * @property {object} _info
 * @property {string} _info.identifier
 */

/**
 * @typedef {object} ServiceLocationHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.location_href
 */

/**
 * @typedef {object} ServiceTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */

/**
 * @typedef {object} ScheduleEntryHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.description
 * @property {string} _info.schedule_href
 */

/**
 * @typedef {object} SiteHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.name
 * @property {string} _info.site_href
 */

/**
 * @typedef {object} SourceHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.source_href
 */

/**
 * @typedef {object} StatusHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.status_href
 */

/**
 * @typedef {object} SubTypeHref Service Subtype
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.subType_href
 */

/**
 * @typedef {object} TeamHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.team_href
 */

/**
 * @typedef {object} TypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.type_href
 */

/**
 * @typedef {object} OpportunityHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} opportunity_href
 */

/**
 * @typedef {object} Params
 * @property {string} [conditions]
 * @property {string} [orderBy]
 * @property {string|number} [page]
 * @property {string|number} [pageSize]
 */

/**
 * @typedef {object} ParamsPage
 * @property {string|number} [page]
 * @property {string|number} [pageSize]
 */

/**
 * @typedef {object} ParamsConditions
 * @property {string} [conditions]
 * @property {string} [orderBy]
 */

/**
 * @typedef {object} ParamsImage
 * @property {boolean} [useDefaultFlag]
 * @property {string} [lastmodifier] note the lower case M (not a typo)
 */

/**
 * @typedef {object} ParamsConfig
 * @property {string} [conditions]
 * @property {string} [managedIdentifier]
 */

/**
 * @typedef {object} ParamsConfigUpdate
 * @property {string} [managementSolutionName]
 * @property {string} [managedIdentifier]
 * @property {string} [type]
 * @property {string} [level]
 * @property {string} [childConfigurationsMatchingOn]
 * @property {string} [inactivateConfigurationsMatchingOn]
 * @property {number} [inactiveConfigurationStatusId]
 */

/**
 * @typedef {object} ProductHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.description
 */

/**
 * @typedef {object} WorkRoleHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.role_href
 */

/**
 * @typedef {object} WorkTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.type_href
 */

/**
 * @typedef {object[]} Operations
 * @property {string} op ['replace','remove']
 * @property {string} path the relative path to the variable to be updated, e.g. status
 * @property {string} value the value of the object property to replace, e.g. { id: 123 }
 */

/**
 * @typedef {object} Count
 * @property {number} count the number of matched records.
 */

/**
 * @typedef {object} ConfigurationHref
 * @property {number} id
 * @property {string} deviceIdentifier
 * @property {object} _info
 * @property {string} _info.name
 * @property {string} _info.configuration_href
 *
 */

/**
 * @typedef {object} MemberHref
 * @property {number} id
 * @property {string} identifier the member's ID in CW
 * @property {object} _info
 * @property {string} _info.lastUpdated
 * @property {string} _info.updatedBy
 */

/**
 * @typedef {object} DeleteResponse
 * @property {string} error null if no error
 */

/**
 * @typedef {object} PostResponse
 * @property {string} error null if no error
 */

/**
 * @typedef {object} ErrorResponse
 * @property {string} code
 * @property {string} message
 * @property {object[]} errors
 */

/**
 * @typedef {object} CustomField
 * @property {string} caption
 * @property {string} entryMethod
 * @property {number} id
 * @property {number} numberOfDecimals
 * @property {string} type
 * @property {string} value
 *
 */

/**
 * @typedef {object} ProjectStatusHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 *
 */

/**
 * @typedef {object} ProjectTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 *
 */

/**
 * @typedef {object} OwnerHref
 * @property {number} id
 * @property {string} identifier
 * @property {object} _info
 * @property {string} image_href
 * @property {string} member_href
 */

/**
 * @typedef {object} CountryHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */

/**
 * @typedef {object} CustomField
 * @property {string} caption
 * @property {string} entryMethod
 * @property {number} id
 * @property {number} numberOfDecimals
 * @property {string} type
 * @property {string} value
 *
 */

/**
 * @typedef {object} CommunicationItemHref
 * @property {string} communicationType
 * @property {boolean} defaultFlag
 * @property extension
 * @property {number} id
 * @property {TypeHref} type
 * @property {string} value
 *
 */

/**
 * @typedef {object} DepartmentHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.department_href
 *
 */

/**
 * @typedef {object} RelationshipHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} relationship_href
 *
 */

/**
 * @typedef {object} ScheduleTypeHref
 * @property id
 * @property identifier
 * @property {object} _info
 * @property _info.type_href
 */

/**
 * @typedef {object} ScheduleStatusHref
 * @property id
 * @property name
 * @property {object} _info
 * @property _info.status_href
 */

/**
 * @typedef {object} ScheduleSpanHref
 * @property id
 * @property name
 * @property _info
 */

/**
 * @typedef {object} ReminderHref
 * @property id
 * @property name
 * @property _info
 *
 */

/**
 * @typedef {object} ServiceLocationHref
 * @property id
 * @property name
 * @property _info
 * @property _info.location_href
 */

/**
 * @typedef {object} ChargeCodeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */<commit_msg>Update params jsdoc for new 2016/2017 properties<commit_after>/**
 * Created by kgrube on 12/15/2015.
 * @author kgrube
 */

/**
 * @typedef {object} CWOptions
 * @property companyId
 * @property publicKey
 * @property privateKey
 * @property companyUrl
 */

/**
 * @typedef {object} ConnectWiseRest
 * @property {ConnectWise} API
 * @property {FinanceAPI} FinanceAPI
 * @property {ServiceDeskAPI} ServiceDeskAPI
 * @property {TimeAPI} TimeAPI
 * @property {ProjectAPI} ProjectAPI
 * @property {CompanyAPI} CompanyAPI
 * @property {ScheduleAPI} ScheduleAPI
 * @property {SystemAPI} SystemAPI
 */

/**
 *
 * @param options
 * @returns {ConnectWiseRest}
 * @constructor
 */
function ConnectWiseRest(options) {
  var _ConnectWise = require('./ConnectWise'),
    _CompanyAPI = require('./CompanyAPI'),
    _FinanceAPI = require('./FinanceAPI'),
    _TimeAPI = require('./TimeAPI'),
    _ServiceDeskAPI = require('./ServiceDeskAPI'),
    _ProjectAPI = require('./ProjectAPI'),
    _ScheduleAPI = require('./ScheduleAPI'),
    _SystemAPI = require('./SystemAPI');

  return {
    API: new _ConnectWise(options),
    CompanyAPI: new _CompanyAPI(options),
    FinanceAPI: new _FinanceAPI(options),
    ServiceDeskAPI: new _ServiceDeskAPI(options),
    TimeAPI: new _TimeAPI(options),
    ProjectAPI: new _ProjectAPI(options),
    ScheduleAPI: new _ScheduleAPI(options),
    SystemAPI: new _SystemAPI(options)
  }
}

/**
 *
 * @type {ConnectWiseRest}
 */
module.exports = ConnectWiseRest;

/**
 * @typedef {object} InfoHref
 * @property lastUpdated
 * @property updatedBy
 */

/**
 * @typedef {object} AgreementHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.agreement_href
 */

/**
 * @typedef {object} AgreementTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.type_href
 */

/**
 * @typedef {object} BoardHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.board_href
 */

/**
 * @typedef {object} CompanyHref
 * @property {number} id
 * @property {string} identifier The company ID in ConnectWise
 * @property {object} _info
 * @property {string} _info.company_href
 */

/**
 * @typedef {object} ContactHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.contact_href
 */

/**
 * @typedef {object} CountryHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */

/**
 * @typedef {object} DocumentHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.filename
 */

/**
 * @typedef {object} ItemHref Service Item
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.item_href
 */

/**
 * @typedef {object} PriorityHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.image_href
 * @property {string} _info.priority_href
 */

/**
 * @typedef {object} ProductHref
 * @property {number} id
 * @property {string} identifier
 * @property {object} _info
 * @property {string} _info.identifier
 */

/**
 * @typedef {object} ServiceLocationHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.location_href
 */

/**
 * @typedef {object} ServiceTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */

/**
 * @typedef {object} ScheduleEntryHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.description
 * @property {string} _info.schedule_href
 */

/**
 * @typedef {object} SiteHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.name
 * @property {string} _info.site_href
 */

/**
 * @typedef {object} SourceHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.source_href
 */

/**
 * @typedef {object} StatusHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.status_href
 */

/**
 * @typedef {object} SubTypeHref Service Subtype
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.subType_href
 */

/**
 * @typedef {object} TeamHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.team_href
 */

/**
 * @typedef {object} TypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.type_href
 */

/**
 * @typedef {object} OpportunityHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} opportunity_href
 */

/**
 * @typedef {object} Params
 * @property {string} [conditions] Search results based on the fields returned in a GET, board/name="Integration" and summary="xyz" or board/id in (3, 2, 4) and lastUpdated > [2016-08-20T18:04:26Z]
 * @property {string} [orderBy] Choose which field to sort the results by, 'contact/name asc'
 * @property {string|number} [page] Used in pagination to cycle through results
 * @property {string|number} [pageSize] Number of results returned per page (Defaults to 25) (Max page size was increased to 1,000 in 2016.2.)
 * @property {string} [childConditions] Allows searching arrays on endpoints that list childConditions under parameters
 * @property {string} [customFieldConditions] Allows searching custom fields when customFieldConditions is listed in the parameters
 * @property {string} [fields] Limits which information is returned in the response - Not available on the reporting endpoints
 * @property {string} [columns] Limits which information is returned in the response - Only used for the Reporting Endpoints
 */

/**
 * @typedef {object} ParamsPage
 * @property {string|number} [page]
 * @property {string|number} [pageSize]
 */

/**
 * @typedef {object} ParamsConditions
 * @property {string} [conditions]
 * @property {string} [orderBy]
 */

/**
 * @typedef {object} ParamsImage
 * @property {boolean} [useDefaultFlag]
 * @property {string} [lastmodifier] note the lower case M (not a typo)
 */

/**
 * @typedef {object} ParamsConfig
 * @property {string} [conditions]
 * @property {string} [managedIdentifier]
 */

/**
 * @typedef {object} ParamsConfigUpdate
 * @property {string} [managementSolutionName]
 * @property {string} [managedIdentifier]
 * @property {string} [type]
 * @property {string} [level]
 * @property {string} [childConfigurationsMatchingOn]
 * @property {string} [inactivateConfigurationsMatchingOn]
 * @property {number} [inactiveConfigurationStatusId]
 */

/**
 * @typedef {object} ProductHref
 * @property {number} id
 * @property {object} _info
 * @property {string} _info.description
 */

/**
 * @typedef {object} WorkRoleHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.role_href
 */

/**
 * @typedef {object} WorkTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.type_href
 */

/**
 * @typedef {object[]} Operations
 * @property {string} op ['replace','remove']
 * @property {string} path the relative path to the variable to be updated, e.g. status
 * @property {string} value the value of the object property to replace, e.g. { id: 123 }
 */

/**
 * @typedef {object} Count
 * @property {number} count the number of matched records.
 */

/**
 * @typedef {object} ConfigurationHref
 * @property {number} id
 * @property {string} deviceIdentifier
 * @property {object} _info
 * @property {string} _info.name
 * @property {string} _info.configuration_href
 *
 */

/**
 * @typedef {object} MemberHref
 * @property {number} id
 * @property {string} identifier the member's ID in CW
 * @property {object} _info
 * @property {string} _info.lastUpdated
 * @property {string} _info.updatedBy
 */

/**
 * @typedef {object} DeleteResponse
 * @property {string} error null if no error
 */

/**
 * @typedef {object} PostResponse
 * @property {string} error null if no error
 */

/**
 * @typedef {object} ErrorResponse
 * @property {string} code
 * @property {string} message
 * @property {object[]} errors
 */

/**
 * @typedef {object} CustomField
 * @property {string} caption
 * @property {string} entryMethod
 * @property {number} id
 * @property {number} numberOfDecimals
 * @property {string} type
 * @property {string} value
 *
 */

/**
 * @typedef {object} ProjectStatusHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 *
 */

/**
 * @typedef {object} ProjectTypeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 *
 */

/**
 * @typedef {object} OwnerHref
 * @property {number} id
 * @property {string} identifier
 * @property {object} _info
 * @property {string} image_href
 * @property {string} member_href
 */

/**
 * @typedef {object} CountryHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */

/**
 * @typedef {object} CustomField
 * @property {string} caption
 * @property {string} entryMethod
 * @property {number} id
 * @property {number} numberOfDecimals
 * @property {string} type
 * @property {string} value
 *
 */

/**
 * @typedef {object} CommunicationItemHref
 * @property {string} communicationType
 * @property {boolean} defaultFlag
 * @property extension
 * @property {number} id
 * @property {TypeHref} type
 * @property {string} value
 *
 */

/**
 * @typedef {object} DepartmentHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} _info.department_href
 *
 */

/**
 * @typedef {object} RelationshipHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 * @property {string} relationship_href
 *
 */

/**
 * @typedef {object} ScheduleTypeHref
 * @property id
 * @property identifier
 * @property {object} _info
 * @property _info.type_href
 */

/**
 * @typedef {object} ScheduleStatusHref
 * @property id
 * @property name
 * @property {object} _info
 * @property _info.status_href
 */

/**
 * @typedef {object} ScheduleSpanHref
 * @property id
 * @property name
 * @property _info
 */

/**
 * @typedef {object} ReminderHref
 * @property id
 * @property name
 * @property _info
 *
 */

/**
 * @typedef {object} ServiceLocationHref
 * @property id
 * @property name
 * @property _info
 * @property _info.location_href
 */

/**
 * @typedef {object} ChargeCodeHref
 * @property {number} id
 * @property {string} name
 * @property {object} _info
 */
<|endoftext|>


<commit_before>/*jshint smarttabs:true */

$(document).ready(function() {
	// Global

	// Functions
	var defaults = function(dest,source) {
				for(var k in source) {
					if(dest[k] === void 0) {
						dest[k] = source[k];
					}
				}

				return dest;
			},

		  markupEscapeCodes = {
		 		'&': '&',
		 		'<': '<',
		 		'>': '>',
		 		'"': '"',
		 		"'": ''',
		 		'/': '/'
		 	};

  function escapeMarkup(subject) {
    return subject.replace(/[&<>"'\/]/g, function (char) {
      return markupEscapeCodes[char];
    });
  }

	function debounce(func, wait, immediate) {
		var timeout;
		return function() {
			var context = this, args = arguments;
			clearTimeout(timeout);
			timeout = setTimeout(function() {
				timeout = null;
				if (!immediate) func.apply(context, args);
			}, wait);
				if (immediate && !timeout) func.apply(context, args);
		};
	}

	// arrayRemove([1,2],1);
	// See http://stackoverflow.com/a/3955096
	var arrayRemove = function(arr) {
		var what, a = Array.prototype.slice.call(arguments, 1), L = a.length, ax;
		while (L && arr.length) {
			what = a[--L];
			while ((ax = arr.indexOf(what)) !== -1) {
				arr.splice(ax, 1);
			}
		}
		return arr;
	};

	// Config
	var defaultConfig = {
		selector: '.input.input-select:not([data-fancy-dropdowns="off"]) select',
		beforeActive: '

',
		enabled: true
	};

	window.FormHelpers = window.FormHelpers || {};
	window.FormHelpers.FancyDropdowns = window.FormHelpers.FancyDropdowns || {};
	config = defaults(window.FormHelpers.FancyDropdowns,defaultConfig);

	// If plugin is not enabled, don't continue
	if(window.FormHelpers.FancyDropdowns.enabled === false) {
		return;
	}

	var selector = config.selector;
	var ids = [];
	var selectified = [];

	// Example:
	// $('body').after('
<select><option value="volvo">Volvo</option></select>
');

	// Plugin
	$.fn.selectify = function() {
		// Loop through all elements in jQ obj.
		this.each(function () {

			// Wrap select in
..

			// if it needs it
			if(!$(this).parents('.input').length) {
				var selectClasses = $(this).attr('class') ? ' ' + $(this).attr('class') : '';
				$(this).wrap('
');
			}

			// Only run once
			if($.inArray($(this)[0],selectified) !== -1) {
				return false;
			}
			selectified.push($(this)[0]);

			// If not running on a select, pass through
			if(!$(this).is('select')) {
				return false;
			}

			// References
			var $select = $(this);
			var options = $select[0].options;
			var $parent = $(this).parents('.input').first();

			// Context object
			var ctx = {
				$select : $select,
				options: options,
				$parent: $parent,
				status: 'closed',
				config: config,
				labels: [],
				events: {}
			};

			// Add a unique ID
			var uniqid = ids.length + 1;
			ids.push('');

			// Create new list that will be used as a replacement for the <select>
			var wrapperSelector = 'select-wrapper-' + uniqid;
			ctx.wrapperSelector = '.' + wrapperSelector;
			var html = '
';

			// Top bar
			var $active = $select.find('option:selected');
			var value = $active.attr('value');
			ctx.selected = value;
			var label = $active.html();

			html += '
';
			html += (config.beforeActive || '');
			html += label + '
';

			// Options
			html += '
';

			$.each(options, function(i,option) {
				var $option = $(option);
				var value = $option.attr('value');

				var label = $option.html();
				var labelSlug = label.trim().toLowerCase();
				ctx.labels.push(labelSlug);
				html += '
' + label + '
';
			});

			html += '
';
			html += '
'; // /select-wrapper

			// Append the UL
			$parent.append(html);

			// Add more data
			ctx.$wrapper = $('.select-wrapper', $parent);
			ctx.$active = $('.select-active', $parent);
			ctx.$options = $('.select-options', $parent);

			// Create events
			var ns = '.ns_select_' + uniqid;
			ctx.ns = ns;
			$('body').on('click'+ns, ctx, bodyClicked);
			$('.select-active', $parent).on('click'+ns, ctx, selectToggle);
			$('.select-wrapper', $parent).on('destroyed'+ns, ctx, divRemoved);
			$('.select-options li', $parent).on('click'+ns, ctx, clickedItem);
			$('.select-options', $parent).on('mouseover'+ns, 'li', ctx, hoverOverItem);
			$select.on('change'+ns, ctx, selectionChanged);
		});
	};

	// Updates the UI whenever the select element changes
	function selectionChanged(evt) {
		var ctx = evt.data;
		var $allSelected = $(this).find(':selected');
		var value = $allSelected.attr('value');
		var $selectedOptions = ctx.$options.find('[data-value="' + value + '"]');
		selectUpdateValue(ctx, $selectedOptions, undefined, false);
	}

	// Click anywhere on the body
	function bodyClicked(e) {
		// http://stackoverflow.com/a/1423722
		if(!$(e.target).closest(e.data.wrapperSelector).length) {
			// Close menu when clicking off
			selectClose(e.data);
		}
	}

	// Callback when the select-wrapper is removed
	function divRemoved(e) {
		events = e.data.events;
		ns = e.data.ns;
		$select = e.data.$select;

		// Clean up events
		$('body').off(ns);
		$(document).off(ns);
		$(window).off(ns);

		// Remove from selectified list
		arrayRemove(selectified, $select[0]);
	}

	// Clicked on any of the select options
	function clickedItem(e) {
		var $li = $(e.target);
		var label = $li.attr('data-label');
		var value = $li.attr('data-value');

		// Forward to update function
		selectUpdateValue(e.data,$li);

		selectClose(e.data);
	}

	function hoverOverItem(e) {
		// Update currently hovered over item
		selectUpdateHover(e.data,$(e.target));
	}

	// Update the active value for the select based on the attributes from $li
	function selectUpdateValue(ctx,$li,flash,retrigger) {
		var label = $li.attr('data-label');
		var value = $li.attr('data-value');

		// Update original select
		ctx.$select.val(value);

		// Fire a change event, but don't allow **our** onChange event to fire.. that
		// is just asking for trouble..
		var ns = ctx.ns;
		ctx.$select.off('change'+ns, selectionChanged);
		if (retrigger !== false) { ctx.$select.trigger('change'); }
		ctx.$select.on('change'+ns, ctx, selectionChanged);

		// Update context with new value
		ctx.selected = value;

		// Update active class
		ctx.$options.find('> li.active')
			 .not('li[data-value="' + $li.attr('data-value') + '"]')
			 .removeClass('active');

		if(typeof(flash) !== "undefined") {
			$li.removeClass('active hover');
			window.setTimeout(function() {
				$li.addClass('active');
			}, flash);
		} else {
			$li.addClass('active');
		}

		// Update label
		ctx.$active.attr('data-label',label);
		ctx.$active.attr('data-value',value);
		ctx.$active.html((ctx.config.beforeActive || '') + label);
	}

	// Update the LI that is currently being hovered on.. Accessible by function
	// for search and what not..
	function selectUpdateHover(ctx,$li) {
		var action = function() {
			if(ctx.hasOwnProperty('$hover')) {
				ctx.$hover.removeClass('hover');
			}

			ctx.$hover = $li;
			$li.addClass('hover');
		};

		if(window.requestAnimationFrame) {
			window.requestAnimationFrame(action);
		} else {
			action();
		}
	}

	// Toggle menu
	function selectToggle(event) {
		// If it's closed, open
		var status = event.data.status;

		if(status === 'closed') {
			selectOpen(event.data);
		} else {
			selectClose(event.data);
		}
	}

	// Open menu
	function selectOpen(ctx) {
		var search_delay = 475;

		ctx.$options.css({
			'visibility': 'visible',
			'max-height': 'none',
			'overflow': 'visible'
		});

		$(ctx.$wrapper).addClass('select-wrapper-open');
		$(ctx.$active).addClass('select-wrapper-open');

		// Add active class to whichever item was active
		ctx.$hover = ctx.$options.find('> li[data-value="' + ctx.selected + '"]');
		ctx.$hover.addClass('active');

		// Bindings for search
		$(document).on('keyup' + ctx.ns + 'active', function(e) {
			// Don't close the dropdown now, because you are probably searching for
			// something
			window.clearTimeout(ctx.updateAndCloseTimeout);

			ctx.search_term = ctx.search_term || '';
			ctx.search_term += String.fromCharCode(e.keyCode).toLowerCase();

			// Do a new search
			debounce(function() {
				selectSearch.call(this,ctx);
			}, 15)();
		});

		$(document).on('keyup' + ctx.ns + 'active', debounce(function(e) {
			ctx.search_term = '';
		}, search_delay));

		$(document).on('keydown' + ctx.ns + 'active', function(e) {
			// 40: down arrow
			// 38: up arrow
			// 27: esc
			if(e.shiftKey) {
				return;
			}

			// Can use arrows to move between items
			if(e.keyCode == 40 || e.keyCode == 38) {
				e.preventDefault();

				if(ctx.hasOwnProperty('$hover') && ctx.$hover.length) {
					var dir;
					if(e.keyCode == 40) {
						dir = 'next';
					} else {
						dir = 'prev';
					}

					if(ctx.$hover[dir]('li').length) {
						var $next = ctx.$hover[dir]('li');
						selectUpdateHover(ctx,$next);

						if (Element.prototype.scrollIntoViewIfNeeded) {
							$next.get(0).scrollIntoViewIfNeeded(true);
						}
					}
				}
			}

			// Esc key, just close
			if (!e.shiftKey && e.keyCode == 27) {
				selectClose(ctx);
			}

			// Change value to hovered-over LI when hitting enter or space
			if(!e.shiftKey && (e.keyCode == 32 || e.keyCode == 13)) {
				// Don't scroll if you hit space when select is open
				if(e.keyCode == 32) {
					e.preventDefault();
				}

				var updateAndClose = function() {
					if(ctx.hasOwnProperty('$hover') && ctx.$hover.length) {
						selectUpdateValue(ctx,ctx.$hover,80);
					}

					window.setTimeout(function() { selectClose(ctx); }, 120);
				};

				if(e.keyCode == 32) {
					// If you hit space bar, wait a little before triggering, because
					// you could be in the middle of a search
					ctx.updateAndCloseTimeout = window.setTimeout(updateAndClose, 60);
				} else {
					updateAndClose();
				}
			}
		});

		// Update status
		ctx.status = 'opened';
	}

	// If there is a matching term, focus on it
	function selectSearch(ctx) {
		var searchTerm = ctx.search_term.trim();
		var match = ctx.$options
									 .find('> li[data-label-slug^="' + escapeMarkup(searchTerm) + '"]')
									 .first();

		if(match.length) {
			// Update the hover state to the new LI
			selectUpdateHover(ctx,match);

			// Scroll to this LI
			if (Element.prototype.scrollIntoViewIfNeeded) {
				match[0].scrollIntoViewIfNeeded(true);
			}
		}
	}

	// Close menu
	function selectClose(data) {
		data.$options.css({
			'visibility': 'hidden',
			'max-height': '0',
			'overflow': 'hidden'
		});

		// Remove active listeners.. listeners that were only active when menu was
		// open
		$(document).off('keydown' + data.ns + 'active');
		$(document).off('keyup' + data.ns + 'active');

		$(data.$wrapper).removeClass('select-wrapper-open');
		$(data.$active).removeClass('select-wrapper-open');

		// Update status
		data.status = 'closed';

		// Clear the hover state
		$(data.$options).find(' > li.hover').removeClass('hover');
	}

	// Trigger plugin on existing <selects> and new ones that get added
	$(document).on('everyinsert', selector, function() { $(this).selectify(); });
});
<commit_msg>Use tabs instead of spaces.<commit_after>/*jshint smarttabs:true */

$(document).ready(function() {
	// Global

	// Functions
	var defaults = function(dest,source) {
			for(var k in source) {
				if(dest[k] === void 0) {
					dest[k] = source[k];
				}
			}

			return dest;
		},

		markupEscapeCodes = {
			'&': '&',
			'<': '<',
			'>': '>',
			'"': '"',
			"'": ''',
			'/': '/'
		};

	function escapeMarkup(subject) {
		return subject.replace(/[&<>"'\/]/g, function (char) {
			return markupEscapeCodes[char];
		});
	}

	function debounce(func, wait, immediate) {
		var timeout;
		return function() {
			var context = this, args = arguments;
			clearTimeout(timeout);
			timeout = setTimeout(function() {
				timeout = null;
				if (!immediate) func.apply(context, args);
			}, wait);
				if (immediate && !timeout) func.apply(context, args);
		};
	}

	// arrayRemove([1,2],1);
	// See http://stackoverflow.com/a/3955096
	var arrayRemove = function(arr) {
		var what, a = Array.prototype.slice.call(arguments, 1), L = a.length, ax;
		while (L && arr.length) {
			what = a[--L];
			while ((ax = arr.indexOf(what)) !== -1) {
				arr.splice(ax, 1);
			}
		}
		return arr;
	};

	// Config
	var defaultConfig = {
		selector: '.input.input-select:not([data-fancy-dropdowns="off"]) select',
		beforeActive: '

',
		enabled: true
	};

	window.FormHelpers = window.FormHelpers || {};
	window.FormHelpers.FancyDropdowns = window.FormHelpers.FancyDropdowns || {};
	config = defaults(window.FormHelpers.FancyDropdowns,defaultConfig);

	// If plugin is not enabled, don't continue
	if(window.FormHelpers.FancyDropdowns.enabled === false) {
		return;
	}

	var selector = config.selector;
	var ids = [];
	var selectified = [];

	// Example:
	// $('body').after('
<select><option value="volvo">Volvo</option></select>
');

	// Plugin
	$.fn.selectify = function() {
		// Loop through all elements in jQ obj.
		this.each(function () {

			// Wrap select in
..

			// if it needs it
			if(!$(this).parents('.input').length) {
				var selectClasses = $(this).attr('class') ? ' ' + $(this).attr('class') : '';
				$(this).wrap('
');
			}

			// Only run once
			if($.inArray($(this)[0],selectified) !== -1) {
				return false;
			}
			selectified.push($(this)[0]);

			// If not running on a select, pass through
			if(!$(this).is('select')) {
				return false;
			}

			// References
			var $select = $(this);
			var options = $select[0].options;
			var $parent = $(this).parents('.input').first();

			// Context object
			var ctx = {
				$select : $select,
				options: options,
				$parent: $parent,
				status: 'closed',
				config: config,
				labels: [],
				events: {}
			};

			// Add a unique ID
			var uniqid = ids.length + 1;
			ids.push('');

			// Create new list that will be used as a replacement for the <select>
			var wrapperSelector = 'select-wrapper-' + uniqid;
			ctx.wrapperSelector = '.' + wrapperSelector;
			var html = '
';

			// Top bar
			var $active = $select.find('option:selected');
			var value = $active.attr('value');
			ctx.selected = value;
			var label = $active.html();

			html += '
';
			html += (config.beforeActive || '');
			html += label + '
';

			// Options
			html += '
';

			$.each(options, function(i,option) {
				var $option = $(option);
				var value = $option.attr('value');

				var label = $option.html();
				var labelSlug = label.trim().toLowerCase();
				ctx.labels.push(labelSlug);
				html += '
' + label + '
';
			});

			html += '
';
			html += '
'; // /select-wrapper

			// Append the UL
			$parent.append(html);

			// Add more data
			ctx.$wrapper = $('.select-wrapper', $parent);
			ctx.$active = $('.select-active', $parent);
			ctx.$options = $('.select-options', $parent);

			// Create events
			var ns = '.ns_select_' + uniqid;
			ctx.ns = ns;
			$('body').on('click'+ns, ctx, bodyClicked);
			$('.select-active', $parent).on('click'+ns, ctx, selectToggle);
			$('.select-wrapper', $parent).on('destroyed'+ns, ctx, divRemoved);
			$('.select-options li', $parent).on('click'+ns, ctx, clickedItem);
			$('.select-options', $parent).on('mouseover'+ns, 'li', ctx, hoverOverItem);
			$select.on('change'+ns, ctx, selectionChanged);
		});
	};

	// Updates the UI whenever the select element changes
	function selectionChanged(evt) {
		var ctx = evt.data;
		var $allSelected = $(this).find(':selected');
		var value = $allSelected.attr('value');
		var $selectedOptions = ctx.$options.find('[data-value="' + value + '"]');
		selectUpdateValue(ctx, $selectedOptions, undefined, false);
	}

	// Click anywhere on the body
	function bodyClicked(e) {
		// http://stackoverflow.com/a/1423722
		if(!$(e.target).closest(e.data.wrapperSelector).length) {
			// Close menu when clicking off
			selectClose(e.data);
		}
	}

	// Callback when the select-wrapper is removed
	function divRemoved(e) {
		events = e.data.events;
		ns = e.data.ns;
		$select = e.data.$select;

		// Clean up events
		$('body').off(ns);
		$(document).off(ns);
		$(window).off(ns);

		// Remove from selectified list
		arrayRemove(selectified, $select[0]);
	}

	// Clicked on any of the select options
	function clickedItem(e) {
		var $li = $(e.target);
		var label = $li.attr('data-label');
		var value = $li.attr('data-value');

		// Forward to update function
		selectUpdateValue(e.data,$li);

		selectClose(e.data);
	}

	function hoverOverItem(e) {
		// Update currently hovered over item
		selectUpdateHover(e.data,$(e.target));
	}

	// Update the active value for the select based on the attributes from $li
	function selectUpdateValue(ctx,$li,flash,retrigger) {
		var label = $li.attr('data-label');
		var value = $li.attr('data-value');

		// Update original select
		ctx.$select.val(value);

		// Fire a change event, but don't allow **our** onChange event to fire.. that
		// is just asking for trouble..
		var ns = ctx.ns;
		ctx.$select.off('change'+ns, selectionChanged);
		if (retrigger !== false) { ctx.$select.trigger('change'); }
		ctx.$select.on('change'+ns, ctx, selectionChanged);

		// Update context with new value
		ctx.selected = value;

		// Update active class
		ctx.$options.find('> li.active')
			 .not('li[data-value="' + $li.attr('data-value') + '"]')
			 .removeClass('active');

		if(typeof(flash) !== "undefined") {
			$li.removeClass('active hover');
			window.setTimeout(function() {
				$li.addClass('active');
			}, flash);
		} else {
			$li.addClass('active');
		}

		// Update label
		ctx.$active.attr('data-label',label);
		ctx.$active.attr('data-value',value);
		ctx.$active.html((ctx.config.beforeActive || '') + label);
	}

	// Update the LI that is currently being hovered on.. Accessible by function
	// for search and what not..
	function selectUpdateHover(ctx,$li) {
		var action = function() {
			if(ctx.hasOwnProperty('$hover')) {
				ctx.$hover.removeClass('hover');
			}

			ctx.$hover = $li;
			$li.addClass('hover');
		};

		if(window.requestAnimationFrame) {
			window.requestAnimationFrame(action);
		} else {
			action();
		}
	}

	// Toggle menu
	function selectToggle(event) {
		// If it's closed, open
		var status = event.data.status;

		if(status === 'closed') {
			selectOpen(event.data);
		} else {
			selectClose(event.data);
		}
	}

	// Open menu
	function selectOpen(ctx) {
		var search_delay = 475;

		ctx.$options.css({
			'visibility': 'visible',
			'max-height': 'none',
			'overflow': 'visible'
		});

		$(ctx.$wrapper).addClass('select-wrapper-open');
		$(ctx.$active).addClass('select-wrapper-open');

		// Add active class to whichever item was active
		ctx.$hover = ctx.$options.find('> li[data-value="' + ctx.selected + '"]');
		ctx.$hover.addClass('active');

		// Bindings for search
		$(document).on('keyup' + ctx.ns + 'active', function(e) {
			// Don't close the dropdown now, because you are probably searching for
			// something
			window.clearTimeout(ctx.updateAndCloseTimeout);

			ctx.search_term = ctx.search_term || '';
			ctx.search_term += String.fromCharCode(e.keyCode).toLowerCase();

			// Do a new search
			debounce(function() {
				selectSearch.call(this,ctx);
			}, 15)();
		});

		$(document).on('keyup' + ctx.ns + 'active', debounce(function(e) {
			ctx.search_term = '';
		}, search_delay));

		$(document).on('keydown' + ctx.ns + 'active', function(e) {
			// 40: down arrow
			// 38: up arrow
			// 27: esc
			if(e.shiftKey) {
				return;
			}

			// Can use arrows to move between items
			if(e.keyCode == 40 || e.keyCode == 38) {
				e.preventDefault();

				if(ctx.hasOwnProperty('$hover') && ctx.$hover.length) {
					var dir;
					if(e.keyCode == 40) {
						dir = 'next';
					} else {
						dir = 'prev';
					}

					if(ctx.$hover[dir]('li').length) {
						var $next = ctx.$hover[dir]('li');
						selectUpdateHover(ctx,$next);

						if (Element.prototype.scrollIntoViewIfNeeded) {
							$next.get(0).scrollIntoViewIfNeeded(true);
						}
					}
				}
			}

			// Esc key, just close
			if (!e.shiftKey && e.keyCode == 27) {
				selectClose(ctx);
			}

			// Change value to hovered-over LI when hitting enter or space
			if(!e.shiftKey && (e.keyCode == 32 || e.keyCode == 13)) {
				// Don't scroll if you hit space when select is open
				if(e.keyCode == 32) {
					e.preventDefault();
				}

				var updateAndClose = function() {
					if(ctx.hasOwnProperty('$hover') && ctx.$hover.length) {
						selectUpdateValue(ctx,ctx.$hover,80);
					}

					window.setTimeout(function() { selectClose(ctx); }, 120);
				};

				if(e.keyCode == 32) {
					// If you hit space bar, wait a little before triggering, because
					// you could be in the middle of a search
					ctx.updateAndCloseTimeout = window.setTimeout(updateAndClose, 60);
				} else {
					updateAndClose();
				}
			}
		});

		// Update status
		ctx.status = 'opened';
	}

	// If there is a matching term, focus on it
	function selectSearch(ctx) {
		var searchTerm = ctx.search_term.trim();
		var match = ctx.$options
									 .find('> li[data-label-slug^="' + escapeMarkup(searchTerm) + '"]')
									 .first();

		if(match.length) {
			// Update the hover state to the new LI
			selectUpdateHover(ctx,match);

			// Scroll to this LI
			if (Element.prototype.scrollIntoViewIfNeeded) {
				match[0].scrollIntoViewIfNeeded(true);
			}
		}
	}

	// Close menu
	function selectClose(data) {
		data.$options.css({
			'visibility': 'hidden',
			'max-height': '0',
			'overflow': 'hidden'
		});

		// Remove active listeners.. listeners that were only active when menu was
		// open
		$(document).off('keydown' + data.ns + 'active');
		$(document).off('keyup' + data.ns + 'active');

		$(data.$wrapper).removeClass('select-wrapper-open');
		$(data.$active).removeClass('select-wrapper-open');

		// Update status
		data.status = 'closed';

		// Clear the hover state
		$(data.$options).find(' > li.hover').removeClass('hover');
	}

	// Trigger plugin on existing <selects> and new ones that get added
	$(document).on('everyinsert', selector, function() { $(this).selectify(); });
});
<|endoftext|>