<commit_before>const Plugged = require('plugged')
const path = require('path')
const { inherits } = require('util')
const fs = require('fs')
const debug = require('debug')('sekshi:sekshi')
const logChat = require('debug')('sekshi:chat')
const mongoose = require('mongoose')
const Promise = require('promise')
const { User } = require('./models')
const find = require('array-find')
const unescape = require('ent/decode')

module.exports = Sekshi

function Sekshi(args) {
    Sekshi.super_.call(this)

    this.invokeLogger((msg, verbosity, color) => {
        debug(msg)
    })

    this.options = args

    this.db = mongoose.connect(args.mongo);

    this.modules = {}
    this._availableModules = []
    this.delimiter = args.delimiter || '!'
    this.modulePath = args.modulePath || path.join(__dirname, 'modules')

    this._room = args.room

    this.onMessage = this.onMessage.bind(this)

    this.addUnescapeListeners()
}

inherits(Sekshi, Plugged)

// ugly hack to deal with plug.dj's mildly insane html escaping behaviour
Sekshi.prototype.addUnescapeListeners = function () {
    const unescapeUser = user => {
        user.username = unescape(user.username)
        user.blurb && (user.blurb = unescape(user.blurb))
    }

    // these handlers are added before any other handlers,
    // hopefully EventEmitter will always fire them in
    // that same order!
    this.on(this.USER_JOIN, unescapeUser)
    this.on(this.ADVANCE, ({}, { media }) => {
        media.author = unescape(media.author)
        media.title = unescape(media.title)
    })
    this.on(this.CHAT, chat => {
        chat.username = unescape(chat.username)
        chat.message = unescape(chat.message)
    })
    this.on(this.JOINED_ROOM, () => {
        this.state.room.users.forEach(unescapeUser)
    })
}

// Override Plugged#getUserByName with a case insensitive version
Sekshi.prototype.getUserByName = function (username, checkCache = false) {
    username = username.toLowerCase()

    if (username === this.state.self.username.toLowerCase())
        return this.state.self

    let user = find(this.state.room.users, user => user.username.toLowerCase() === username)

    if (checkCache && !user) {
        user = find(this.state.usercache, user => user.username.toLowerCase() === username)
    }

    return user || null
}

Sekshi.prototype.start = function (credentials) {
    try {
        this.login(credentials);
    } catch (err) {
        console.error("error at login: " + err.message);
        return;
    }

    this.on(this.CHAT, this.onMessage);

    this.once(this.LOGIN_SUCCESS, function _onLoginSuccess() {
        this.cacheChat(true);
        this.connect(this._room);
        this.loadModules()
    });

    this.once(this.LOGIN_ERROR, function _onLoginError(err) {
        console.error("something strange happened while trying to log in");
        console.error("Error: " + err);
    })

    this.once(this.JOINED_ROOM, err => {
        if (err) {
            console.error(err.message);
            process.exit(1);
        }

        Promise.all(this.getUsers().map(user => User.fromPlugUser(user)))
            .then(users => { debug('updated users', users.length) })
    });
}

Sekshi.prototype.stop = function (cb) {
    this.unloadModules()

    this.logout()
    // should *probably* also wait for this before callback-ing
    mongoose.disconnect()

    this.once(this.LOGOUT_SUCCESS, () => {
        this.removeAllListeners()
        cb()
    })
    this.once(this.LOGOUT_ERROR, e => {
        // might have to close some other stuff here too
        this.removeAllListeners()
        cb(e)
    })
}

Sekshi.prototype.setDelimiter = function (delimiter) {
    this.delimiter = delimiter
}

Sekshi.prototype.setRoom = function(room) {
    this._room = room
    this.connect(this._room)
}

// Parses space-separated chat command arguments.
// single words become single arguments.
//    word → [ 'word ']
// strings surrounded by double quotes become single arguments.
//    "quoted words" word → [ 'quoted words', 'word' ]
// strings prefixed with "@" are matched to the online user list.
// if any online user's name matches the string, it will be passed instead.
// this is so that you don't need quotes around usernames with spaces.
//    @Online User parameter @Offline User → [ 'Online User', 'parameter', '@Offline', 'User' ]
//
// feature-bugs:
// if you forget to close a quoted string it will go until the end of the line (might be unexpected)
// if you forget to add a space after a quoted string, the rest will be read as a separate parameter
Sekshi.prototype.parseArguments = function (str = '') {
    let args = []
    let i = 0
    let chunk

    let usernames = str.indexOf('@') !== -1 // might contain a username
        ? [ this.getSelf(), ...this.getUsers() ].map(u => u.username)
        : []

    while (chunk = str.slice(i)) {
        // separator
        if (chunk.charAt(0) === ' ') {
            i++
            continue
        }
        // quoted string
        else if (chunk.charAt(0) === '"') {
            let end = chunk.indexOf('"', 1)
            // end of param string
            if (end === -1) {
                args.push(chunk.slice(1))
                break
            }
            args.push(chunk.slice(1, end))
            i += end + 1
            continue
        }
        // possible username
        else if (chunk.charAt(0) === '@') {
            let username = find(usernames,
                                name => chunk.slice(1, name.length + 1).toLowerCase() === name.toLowerCase())
            if (username) {
                args.push(username)
                i += `@${username}`.length
                continue
            }
        }
        // single parameter word
        let end = chunk.indexOf(' ')
        // end of param string
        if (end === -1) {
            args.push(chunk)
            break
        }
        args.push(chunk.slice(0, end))
        i += end + 1
        continue
    }

    return args
}

Sekshi.prototype.reloadModules = function () {
    this.unloadModules()
    this.loadModules()
}

Sekshi.prototype.lockskipDJ = function (id, position, cb) {
    this.skipDJ(id, e => {
        if (e) cb && cb(e)
        else this.moveDJ(id, position, cb)
    })
}

Sekshi.prototype.onMessage = function(msg) {
    if (!this.getCurrentRoomStats()) {
        return
    }

    if (msg.message.charAt(0) === this.delimiter) {
        let func = null
        let user = msg.id === 'sekshi' ? { role: this.USERROLE.HOST }
                                       : this.getUserByID(msg.id, true)

        // nonexistent user
        if (!user) return

        user.role = user.role || this.USERROLE.NONE

        let args = this.parseArguments(msg.message)

        func = args.shift().replace(this.delimiter, '').toLowerCase()
        args.unshift(user)

        for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
            let mod = this.modules[name]
            if (mod.enabled() && typeof mod[func] === 'function' && mod.permissions.hasOwnProperty(func)) {
                if (mod.ninjaVanish.indexOf(func) !== -1) {
                    this.deleteMessage(msg.cid)
                }
                if (user.role >= mod.permissions[func]) {
                    mod[func](...args)
                }
                else {
                    this.sendChat(`@${msg.username}: You don't have sufficient permissions to use this command.`, 5 * 1000)
                }
            }
        }
    } else {
        logChat(msg.username, msg.message)
    }
}

Sekshi.prototype.getModule = function (name) {
    return this.modules[name.toLowerCase()]
}

//get array of module files
Sekshi.prototype.getModuleFiles = function(modulePath) {
    if (!fs.existsSync(modulePath)) {
        console.log(modulePath + " does not exist!");
        return [];
    }

    let modules = [];
    var stat = fs.statSync(modulePath);

    if (typeof stat === "undefined") {
        console.log("An error occured while fetching the path");
    } else {
        if (stat.isDirectory()) {
            var files = fs.readdirSync(modulePath);

            if (typeof files !== "undefined") {
                for (var i = 0, l = files.length; i < l; i++)
                    modules.push(...this.getModuleFiles(path.join(modulePath, files[i])));
            }

        } else if (stat.isFile()) {
            if (modulePath.slice(modulePath.indexOf('.') + 1) === "module.js")
                modules.push(modulePath);
        }
    }

    return modules;
};

Sekshi.prototype.updateAvailableModules = function () {
    debug('updateAvailableModules')
    return this._availableModules = this.getModuleFiles(this.modulePath)
        .map(file => file.match(/\/([^\/]+?)\.module\.js$/)[1])
}

Sekshi.prototype.getAvailableModules = function () {
    return this._availableModules
}

Sekshi.prototype.getModulePath = function (name) {
    name = find(this.getAvailableModules(), avail => avail.toLowerCase() === name.toLowerCase())
    return name ? path.join(this.modulePath, `${name}.module.js`) : null
}

Sekshi.prototype.moduleExists = function (name) {
    return this.getModulePath(name) !== null
}

// disable a module
Sekshi.prototype.disable = function (name) {
    debug('disable', name)
    const mod = this.getModule(name)
    if (mod) {
        mod.disable()
    }
}

Sekshi.prototype.enable = function (name) {
    debug('enable', name)
    const mod = this.getModule(name)
    if (mod) {
        mod.enable()
    }
}

Sekshi.prototype.loadModules = function () {
    debug('load all')
    this.updateAvailableModules()
    this.getAvailableModules().forEach(this._loadModule, this)
}

Sekshi.prototype.unloadModules = function() {
    debug('unload all')
    for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
        this._unloadModule(name)
    }
}

Sekshi.prototype.reloadModule = function (name) {
    let mod = this.getModule(name)
    let enabled = false
    if (mod) {
        enabled = mod.enabled()
        mod = null
    }

    this._unloadModule(name)
    this._loadModule(name)

    if (enabled) {
        this.getModule(name).enable()
    }
}

Sekshi.prototype._loadModule = function (name) {
    debug('load', name)
    const lName = name.toLowerCase()
    let mod = this.modules[lName]
    if (!mod) {
        const Module = require(this.getModulePath(name))
        mod = new Module(this, {})
        this.modules[lName] = mod
    }

    // TODO make configurable
    mod.enable()

    // the event is fired on nextTick so modules can simply listen for "moduleloaded"
    // and get events for *all* the modules when loadModules() is called, even for those
    // that register earlier
    process.nextTick(() => { this.emit('moduleloaded', mod, lName) })

    return mod
}

Sekshi.prototype._unloadModule = function (name, fireEvent = true) {
    debug('unload', name)
    const mod = this.getModule(name)
    if (mod) {
        mod.destroy()
    }
    const file = this.getModulePath(name)
    delete require.cache[path.resolve(file)]
    delete this.modules[name.toLowerCase()]

    process.nextTick(() => { this.emit('moduleunloaded', mod, name.toLowerCase()) })
}
<commit_msg>add chat throttling<commit_after>const Plugged = require('plugged')
const path = require('path')
const { inherits } = require('util')
const fs = require('fs')
const debug = require('debug')('sekshi:sekshi')
const logChat = require('debug')('sekshi:chat')
const mongoose = require('mongoose')
const Promise = require('promise')
const { User } = require('./models')
const find = require('array-find')
const unescape = require('ent/decode')

module.exports = Sekshi

// these are fine-tuned to fit pretty much exactly within plug.dj's rate limiting scheme,
// while also being really simple
const CHAT_INTERVAL = 150
const CHAT_BACKOFF_INC = 60
const CHAT_BACKOFF_MAX = 700

function Sekshi(args) {
    Sekshi.super_.call(this)

    this.invokeLogger((msg, verbosity, color) => {
        debug(msg)
    })

    this.options = args

    this.db = mongoose.connect(args.mongo);

    this.modules = {}
    this._availableModules = []
    this.delimiter = args.delimiter || '!'
    this.modulePath = args.modulePath || path.join(__dirname, 'modules')

    this._room = args.room

    this.onMessage = this.onMessage.bind(this)

    this.addUnescapeListeners()

    this._chatQueue = []
    this._lastMessage = 0
    this._chatTimer = false
    this._chatBackoff = CHAT_INTERVAL
}

inherits(Sekshi, Plugged)

// ugly hack to deal with plug.dj's mildly insane html escaping behaviour
Sekshi.prototype.addUnescapeListeners = function () {
    const unescapeUser = user => {
        user.username = unescape(user.username)
        user.blurb && (user.blurb = unescape(user.blurb))
    }

    // these handlers are added before any other handlers,
    // hopefully EventEmitter will always fire them in
    // that same order!
    this.on(this.USER_JOIN, unescapeUser)
    this.on(this.ADVANCE, ({}, { media }) => {
        media.author = unescape(media.author)
        media.title = unescape(media.title)
    })
    this.on(this.CHAT, chat => {
        chat.username = unescape(chat.username)
        chat.message = unescape(chat.message)
    })
    this.on(this.JOINED_ROOM, () => {
        this.state.room.users.forEach(unescapeUser)
    })
}

// Override Plugged#getUserByName with a case insensitive version
Sekshi.prototype.getUserByName = function (username, checkCache = false) {
    username = username.toLowerCase()

    if (username === this.state.self.username.toLowerCase())
        return this.state.self

    let user = find(this.state.room.users, user => user.username.toLowerCase() === username)

    if (checkCache && !user) {
        user = find(this.state.usercache, user => user.username.toLowerCase() === username)
    }

    return user || null
}

Sekshi.prototype.sendChat = function (message, autodelete) {
    const MAX_LENGTH = 256
    while (message.length >= MAX_LENGTH) {
        this._chatQueue.push([ message.slice(0, MAX_LENGTH), autodelete ])
        message = message.slice(MAX_LENGTH)
    }
    this._chatQueue.push([ message, autodelete ])
    if (!this._chatTimer) {
        this._processChatQueue()
    }
}

Sekshi.prototype._processChatQueue = function () {
    if (this._chatQueue.length > 0) {
        if (this._lastMessage + this._chatBackoff <= Date.now()) {
            Plugged.prototype.sendChat.call(this, ...this._chatQueue.shift())
            this._lastMessage = Date.now()
            if (this._chatBackoff < CHAT_BACKOFF_MAX) {
                this._chatBackoff += CHAT_BACKOFF_INC
            }
        }
        setTimeout(() => this._processChatQueue(), this._chatBackoff + 1)
        this._chatTimer = true
    }

    if (this._chatQueue.length === 0) {
        this._chatTimer = false
        this._chatBackoff = CHAT_INTERVAL
    }
}

Sekshi.prototype.start = function (credentials) {
    try {
        this.login(credentials);
    } catch (err) {
        console.error("error at login: " + err.message);
        return;
    }

    this.on(this.CHAT, this.onMessage);

    this.once(this.LOGIN_SUCCESS, function _onLoginSuccess() {
        this.cacheChat(true);
        this.connect(this._room);
        this.loadModules()
    });

    this.once(this.LOGIN_ERROR, function _onLoginError(err) {
        console.error("something strange happened while trying to log in");
        console.error("Error: " + err);
    })

    this.once(this.JOINED_ROOM, err => {
        if (err) {
            console.error(err.message);
            process.exit(1);
        }

        Promise.all(this.getUsers().map(user => User.fromPlugUser(user)))
            .then(users => { debug('updated users', users.length) })
    });
}

Sekshi.prototype.stop = function (cb) {
    this.unloadModules()

    this.logout()
    // should *probably* also wait for this before callback-ing
    mongoose.disconnect()

    this.once(this.LOGOUT_SUCCESS, () => {
        this.removeAllListeners()
        cb()
    })
    this.once(this.LOGOUT_ERROR, e => {
        // might have to close some other stuff here too
        this.removeAllListeners()
        cb(e)
    })
}

Sekshi.prototype.setDelimiter = function (delimiter) {
    this.delimiter = delimiter
}

Sekshi.prototype.setRoom = function(room) {
    this._room = room
    this.connect(this._room)
}

// Parses space-separated chat command arguments.
// single words become single arguments.
//    word → [ 'word ']
// strings surrounded by double quotes become single arguments.
//    "quoted words" word → [ 'quoted words', 'word' ]
// strings prefixed with "@" are matched to the online user list.
// if any online user's name matches the string, it will be passed instead.
// this is so that you don't need quotes around usernames with spaces.
//    @Online User parameter @Offline User → [ 'Online User', 'parameter', '@Offline', 'User' ]
//
// feature-bugs:
// if you forget to close a quoted string it will go until the end of the line (might be unexpected)
// if you forget to add a space after a quoted string, the rest will be read as a separate parameter
Sekshi.prototype.parseArguments = function (str = '') {
    let args = []
    let i = 0
    let chunk

    let usernames = str.indexOf('@') !== -1 // might contain a username
        ? [ this.getSelf(), ...this.getUsers() ].map(u => u.username)
        : []

    while (chunk = str.slice(i)) {
        // separator
        if (chunk.charAt(0) === ' ') {
            i++
            continue
        }
        // quoted string
        else if (chunk.charAt(0) === '"') {
            let end = chunk.indexOf('"', 1)
            // end of param string
            if (end === -1) {
                args.push(chunk.slice(1))
                break
            }
            args.push(chunk.slice(1, end))
            i += end + 1
            continue
        }
        // possible username
        else if (chunk.charAt(0) === '@') {
            let username = find(usernames,
                                name => chunk.slice(1, name.length + 1).toLowerCase() === name.toLowerCase())
            if (username) {
                args.push(username)
                i += `@${username}`.length
                continue
            }
        }
        // single parameter word
        let end = chunk.indexOf(' ')
        // end of param string
        if (end === -1) {
            args.push(chunk)
            break
        }
        args.push(chunk.slice(0, end))
        i += end + 1
        continue
    }

    return args
}

Sekshi.prototype.reloadModules = function () {
    this.unloadModules()
    this.loadModules()
}

Sekshi.prototype.lockskipDJ = function (id, position, cb) {
    this.skipDJ(id, e => {
        if (e) cb && cb(e)
        else this.moveDJ(id, position, cb)
    })
}

Sekshi.prototype.onMessage = function(msg) {
    if (!this.getCurrentRoomStats()) {
        return
    }

    if (msg.message.charAt(0) === this.delimiter) {
        let func = null
        let user = msg.id === 'sekshi' ? { role: this.USERROLE.HOST }
                                       : this.getUserByID(msg.id, true)

        // nonexistent user
        if (!user) return

        user.role = user.role || this.USERROLE.NONE

        let args = this.parseArguments(msg.message)

        func = args.shift().replace(this.delimiter, '').toLowerCase()
        args.unshift(user)

        for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
            let mod = this.modules[name]
            if (mod.enabled() && typeof mod[func] === 'function' && mod.permissions.hasOwnProperty(func)) {
                if (mod.ninjaVanish.indexOf(func) !== -1) {
                    this.deleteMessage(msg.cid)
                }
                if (user.role >= mod.permissions[func]) {
                    mod[func](...args)
                }
                else {
                    this.sendChat(`@${msg.username}: You don't have sufficient permissions to use this command.`, 5 * 1000)
                }
            }
        }
    } else {
        logChat(msg.username, msg.message)
    }
}

Sekshi.prototype.getModule = function (name) {
    return this.modules[name.toLowerCase()]
}

//get array of module files
Sekshi.prototype.getModuleFiles = function(modulePath) {
    if (!fs.existsSync(modulePath)) {
        console.log(modulePath + " does not exist!");
        return [];
    }

    let modules = [];
    var stat = fs.statSync(modulePath);

    if (typeof stat === "undefined") {
        console.log("An error occured while fetching the path");
    } else {
        if (stat.isDirectory()) {
            var files = fs.readdirSync(modulePath);

            if (typeof files !== "undefined") {
                for (var i = 0, l = files.length; i < l; i++)
                    modules.push(...this.getModuleFiles(path.join(modulePath, files[i])));
            }

        } else if (stat.isFile()) {
            if (modulePath.slice(modulePath.indexOf('.') + 1) === "module.js")
                modules.push(modulePath);
        }
    }

    return modules;
};

Sekshi.prototype.updateAvailableModules = function () {
    debug('updateAvailableModules')
    return this._availableModules = this.getModuleFiles(this.modulePath)
        .map(file => file.match(/\/([^\/]+?)\.module\.js$/)[1])
}

Sekshi.prototype.getAvailableModules = function () {
    return this._availableModules
}

Sekshi.prototype.getModulePath = function (name) {
    name = find(this.getAvailableModules(), avail => avail.toLowerCase() === name.toLowerCase())
    return name ? path.join(this.modulePath, `${name}.module.js`) : null
}

Sekshi.prototype.moduleExists = function (name) {
    return this.getModulePath(name) !== null
}

// disable a module
Sekshi.prototype.disable = function (name) {
    debug('disable', name)
    const mod = this.getModule(name)
    if (mod) {
        mod.disable()
    }
}

Sekshi.prototype.enable = function (name) {
    debug('enable', name)
    const mod = this.getModule(name)
    if (mod) {
        mod.enable()
    }
}

Sekshi.prototype.loadModules = function () {
    debug('load all')
    this.updateAvailableModules()
    this.getAvailableModules().forEach(this._loadModule, this)
}

Sekshi.prototype.unloadModules = function() {
    debug('unload all')
    for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
        this._unloadModule(name)
    }
}

Sekshi.prototype.reloadModule = function (name) {
    let mod = this.getModule(name)
    let enabled = false
    if (mod) {
        enabled = mod.enabled()
        mod = null
    }

    this._unloadModule(name)
    this._loadModule(name)

    if (enabled) {
        this.getModule(name).enable()
    }
}

Sekshi.prototype._loadModule = function (name) {
    debug('load', name)
    const lName = name.toLowerCase()
    let mod = this.modules[lName]
    if (!mod) {
        const Module = require(this.getModulePath(name))
        mod = new Module(this, {})
        this.modules[lName] = mod
    }

    // TODO make configurable
    mod.enable()

    // the event is fired on nextTick so modules can simply listen for "moduleloaded"
    // and get events for *all* the modules when loadModules() is called, even for those
    // that register earlier
    process.nextTick(() => { this.emit('moduleloaded', mod, lName) })

    return mod
}

Sekshi.prototype._unloadModule = function (name, fireEvent = true) {
    debug('unload', name)
    const mod = this.getModule(name)
    if (mod) {
        mod.destroy()
    }
    const file = this.getModulePath(name)
    delete require.cache[path.resolve(file)]
    delete this.modules[name.toLowerCase()]

    process.nextTick(() => { this.emit('moduleunloaded', mod, name.toLowerCase()) })
}
<|endoftext|>


<commit_before>'use strict';
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var globby = require('globby');
var debug = require('debug')('yeoman:environment');

var win32 = process.platform === 'win32';

/**
 * @mixin
 * @alias env/resolver
 */
var resolver = module.exports;

/**
 * Search for generators and their sub generators.
 *
 * A generator is a `:lookup/:name/index.js` file placed inside an npm package.
 *
 * Defaults lookups are:
 *   - ./
 *   - generators/
 *   - lib/generators/
 *
 * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
 * registered as `dummy:yo` generator.
 *
 * @param {function} cb - Callback called once the lookup is done. Take err as first
 *                        parameter.
 */

resolver.lookup = function (cb) {
  var generatorsModules = this.findGeneratorsIn(this.getNpmPaths());
  var patterns = [];

  this.lookups.forEach(function (lookup) {
    generatorsModules.forEach(function (modulePath) {
      patterns.push(path.join(modulePath, lookup));
    });
  });

  patterns.forEach(function (pattern) {
    globby.sync('*/index.js', { cwd: pattern }).forEach(function (filename) {
      this._tryRegistering(path.join(pattern, filename));
    }, this);
  }, this);

  if (_.isFunction(cb)) {
    return cb(null);
  }
};

/**
 * Search npm for every available generators.
 * Generators are npm packages who's name start with `generator-` and who're placed in the
 * top level `node_module` path. They can be installed globally or locally.
 *
 * @param {Array}  List of search paths
 * @return {Array} List of the generator modules path
 */

resolver.findGeneratorsIn = function (searchPaths) {
  var modules = [];

  searchPaths.forEach(function (root) {
    if (!root) {
      return;
    }

    modules = globby.sync([
      'generator-*',
      '@*/generator-*'
    ], { cwd: root }).map(function (match) {
      return path.join(root, match);
    }).concat(modules);
  });

  return modules;
};

/**
 * Try registering a Generator to this environment.
 * @private
 * @param  {String} generatorReference A generator reference, usually a file path.
 */

resolver._tryRegistering = function (generatorReference) {
  var namespace;
  var realPath = fs.realpathSync(generatorReference);

  try {
    debug('found %s, trying to register', generatorReference);

    if (realPath !== generatorReference) {
      namespace = this.namespace(generatorReference);
    }

    this.register(realPath, namespace);
  } catch (e) {
    console.error('Unable to register %s (Error: %s)', generatorReference, e.message);
  }
};

/**
 * Get the npm lookup directories (`node_modules/`)
 * @return {Array} lookup paths
 */
resolver.getNpmPaths = function () {
  var paths = [];

  // Add NVM prefix directory
  if (process.env.NVM_PATH) {
    paths.push(path.join(path.dirname(process.env.NVM_PATH), 'node_modules'));
  }

  // Adding global npm directories
  // We tried using npm to get the global modules path, but it haven't work out
  // because of bugs in the parseable implementation of `ls` command and mostly
  // performance issues. So, we go with our best bet for now.
  if (process.env.NODE_PATH) {
    paths = _.compact(process.env.NODE_PATH.split(path.delimiter)).concat(paths);
  }

  // global node_modules should be 4 or 2 directory up this one (most of the time)
  paths.push(path.join(__dirname, '../../../..'));
  paths.push(path.join(__dirname, '../..'));

  // adds support for generator resolving when yeoman-generator has been linked
  if (process.argv[1]) {
    paths.push(path.join(path.dirname(process.argv[1]), '../..'));
  }

  // Default paths for each system
  if (win32) {
    paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
  } else {
    paths.push('/usr/lib/node_modules');
  }

  // Walk up the CWD and add `node_modules/` folder lookup on each level
  process.cwd().split(path.sep).forEach(function (part, i, parts) {
    var lookup = path.join.apply(path, parts.slice(0, i + 1).concat(['node_modules']));

    if (!win32) {
      lookup = '/' + lookup;
    }

    paths.push(lookup);
  });

  return paths.reverse();
};

/**
 * Get or create an alias.
 *
 * Alias allows the `get()` and `lookup()` methods to search in alternate
 * filepath for a given namespaces. It's used for example to map `generator-*`
 * npm package to their namespace equivalent (without the generator- prefix),
 * or to default a single namespace like `angular` to `angular:app` or
 * `angular:all`.
 *
 * Given a single argument, this method acts as a getter. When both name and
 * value are provided, acts as a setter and registers that new alias.
 *
 * If multiple alias are defined, then the replacement is recursive, replacing
 * each alias in reverse order.
 *
 * An alias can be a single String or a Regular Expression. The finding is done
 * based on .match().
 *
 * @param {String|RegExp} match
 * @param {String} value
 *
 * @example
 *
 *     env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1');
 *     env.alias(/^([^:]+)$/, '$1:app');
 *     env.alias(/^([^:]+)$/, '$1:all');
 *     env.alias('foo');
 *     // => generator-foo:all
 */

resolver.alias = function alias(match, value) {
  if (match && value) {
    this.aliases.push({
      match: match instanceof RegExp ? match : new RegExp('^' + match + '$'),
      value: value
    });
    return this;
  }

  var aliases = this.aliases.slice(0).reverse();

  return aliases.reduce(function (res, alias) {
    if (!alias.match.test(res)) {
      return res;
    }

    return res.replace(alias.match, alias.value);
  }, match);
};
<commit_msg>getNpmPaths default path for Mac OS (#72)<commit_after>'use strict';
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var globby = require('globby');
var debug = require('debug')('yeoman:environment');

var win32 = process.platform === 'win32';

/**
 * @mixin
 * @alias env/resolver
 */
var resolver = module.exports;

/**
 * Search for generators and their sub generators.
 *
 * A generator is a `:lookup/:name/index.js` file placed inside an npm package.
 *
 * Defaults lookups are:
 *   - ./
 *   - generators/
 *   - lib/generators/
 *
 * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
 * registered as `dummy:yo` generator.
 *
 * @param {function} cb - Callback called once the lookup is done. Take err as first
 *                        parameter.
 */

resolver.lookup = function (cb) {
  var generatorsModules = this.findGeneratorsIn(this.getNpmPaths());
  var patterns = [];

  this.lookups.forEach(function (lookup) {
    generatorsModules.forEach(function (modulePath) {
      patterns.push(path.join(modulePath, lookup));
    });
  });

  patterns.forEach(function (pattern) {
    globby.sync('*/index.js', { cwd: pattern }).forEach(function (filename) {
      this._tryRegistering(path.join(pattern, filename));
    }, this);
  }, this);

  if (_.isFunction(cb)) {
    return cb(null);
  }
};

/**
 * Search npm for every available generators.
 * Generators are npm packages who's name start with `generator-` and who're placed in the
 * top level `node_module` path. They can be installed globally or locally.
 *
 * @param {Array}  List of search paths
 * @return {Array} List of the generator modules path
 */

resolver.findGeneratorsIn = function (searchPaths) {
  var modules = [];

  searchPaths.forEach(function (root) {
    if (!root) {
      return;
    }

    modules = globby.sync([
      'generator-*',
      '@*/generator-*'
    ], { cwd: root }).map(function (match) {
      return path.join(root, match);
    }).concat(modules);
  });

  return modules;
};

/**
 * Try registering a Generator to this environment.
 * @private
 * @param  {String} generatorReference A generator reference, usually a file path.
 */

resolver._tryRegistering = function (generatorReference) {
  var namespace;
  var realPath = fs.realpathSync(generatorReference);

  try {
    debug('found %s, trying to register', generatorReference);

    if (realPath !== generatorReference) {
      namespace = this.namespace(generatorReference);
    }

    this.register(realPath, namespace);
  } catch (e) {
    console.error('Unable to register %s (Error: %s)', generatorReference, e.message);
  }
};

/**
 * Get the npm lookup directories (`node_modules/`)
 * @return {Array} lookup paths
 */
resolver.getNpmPaths = function () {
  var paths = [];

  // Add NVM prefix directory
  if (process.env.NVM_PATH) {
    paths.push(path.join(path.dirname(process.env.NVM_PATH), 'node_modules'));
  }

  // Adding global npm directories
  // We tried using npm to get the global modules path, but it haven't work out
  // because of bugs in the parseable implementation of `ls` command and mostly
  // performance issues. So, we go with our best bet for now.
  if (process.env.NODE_PATH) {
    paths = _.compact(process.env.NODE_PATH.split(path.delimiter)).concat(paths);
  }

  // global node_modules should be 4 or 2 directory up this one (most of the time)
  paths.push(path.join(__dirname, '../../../..'));
  paths.push(path.join(__dirname, '../..'));

  // adds support for generator resolving when yeoman-generator has been linked
  if (process.argv[1]) {
    paths.push(path.join(path.dirname(process.argv[1]), '../..'));
  }

  // Default paths for each system
  if (win32) {
    paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
  } else {
    paths.push('/usr/lib/node_modules');
    paths.push('/usr/local/lib/node_modules');
  }

  // Walk up the CWD and add `node_modules/` folder lookup on each level
  process.cwd().split(path.sep).forEach(function (part, i, parts) {
    var lookup = path.join.apply(path, parts.slice(0, i + 1).concat(['node_modules']));

    if (!win32) {
      lookup = '/' + lookup;
    }

    paths.push(lookup);
  });

  return paths.reverse();
};

/**
 * Get or create an alias.
 *
 * Alias allows the `get()` and `lookup()` methods to search in alternate
 * filepath for a given namespaces. It's used for example to map `generator-*`
 * npm package to their namespace equivalent (without the generator- prefix),
 * or to default a single namespace like `angular` to `angular:app` or
 * `angular:all`.
 *
 * Given a single argument, this method acts as a getter. When both name and
 * value are provided, acts as a setter and registers that new alias.
 *
 * If multiple alias are defined, then the replacement is recursive, replacing
 * each alias in reverse order.
 *
 * An alias can be a single String or a Regular Expression. The finding is done
 * based on .match().
 *
 * @param {String|RegExp} match
 * @param {String} value
 *
 * @example
 *
 *     env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1');
 *     env.alias(/^([^:]+)$/, '$1:app');
 *     env.alias(/^([^:]+)$/, '$1:all');
 *     env.alias('foo');
 *     // => generator-foo:all
 */

resolver.alias = function alias(match, value) {
  if (match && value) {
    this.aliases.push({
      match: match instanceof RegExp ? match : new RegExp('^' + match + '$'),
      value: value
    });
    return this;
  }

  var aliases = this.aliases.slice(0).reverse();

  return aliases.reduce(function (res, alias) {
    if (!alias.match.test(res)) {
      return res;
    }

    return res.replace(alias.match, alias.value);
  }, match);
};
<|endoftext|>


<commit_before>define(function(require, exports, module) {

  'use strict';

  /**
   * Select
   *
   * @module Select
   * @class Select
   * @constructor
   * @extends Widget
   * @examples
   * // 将原生 dom 转换为 select
   * var select = new Select({
   *   filed: '#select'
   * });
   *
   * // 通过 model 定义 select 数据
   * var select = new Select({
   *   field: '#select1',
   *   model: [
   *     {text: 'text1', value:'1'},
   *     {text: 'text2}, value: '2'}
   *   ]
   * });
   */
  var $ = require('$'),
    Widget = require('widget');

  /**
   * 模拟 select 组件
   *
   * @class Select
   * @constructor
   * @extends Widget
   * @type {*|Object|Function|void}
   */

  var Select = Widget.extend({

    /**
     * 默认参数
     *
     * @property {object} defaults
     * @type {object}
     */
    defaults: {
      classPrefix: 'ue-select',

      // 可多选，为 checkbox 多选
      multiple: false,

      // 分隔符，多选时才有用
      delimiter: ',',

      placeholder: '',

      // input 或 select 元素，必填
      // field: null,
      // label: null,

      css: {
        // width: '200',
        position: 'relative'
      },

      model: [],

      // name: null,
      // value: null,

      // minWidth: null,
      // maxWidth: null,

      selectedIndex: 0,

      template: require('./select.handlebars'),
      templateOptions: {
        partials: {
          singleItem: require('./select-single-item.handlebars'),
          multiItem: require('./select-multi-item.handlebars')
        }
      },

      insert: function() {
        this.element.insertAfter(this.option('field')).show();
      },

      delegates: {
        'click': function(e) {
          e.stopPropagation();
        },
        'click [data-role=select]': function(e) {
          this.showDropdown();
        },
        'click [data-role=item]': function(e) {
          if (e.currentTarget.tagName === 'LABEL') {
            e.stopPropagation();
            this.check($(e.currentTarget), e.target.tagName === 'INPUT');
          } else {
            this.select($(e.currentTarget));
          }
        },
        'mouseenter [data-role=item]': function(e) {
          $(e.currentTarget).addClass('active');
        },
        'mouseleave [data-role=item]': function(e) {
          $(e.currentTarget).removeClass('active');
        }
      }
    },

    setup: function() {
      var self = this,
        field = self.option('field'),
        optionLoad;

      if (!field) {
        throw new Error('请设置 field');
      }

      field = self.field = $(field).hide();

      // 存储 field 的 tagName
      self.tagName = field[0].tagName.toLowerCase();

      self.data('hasSelected', !!self.option('value') || !!field.val());

      self.data('label',
        self.option('label') ||
        field.data('label') ||
        self.option('placeholder') ||
        field.attr('placeholder'));

      self.once('render', function() {
        self.element.addClass(
          self.option('classPrefix') + '-' + (self.option('multiple') ? 'multiple' : 'single')
        );
        self.initDelegates({
          'mousedown': function(e) {
            if (!(self.is(e.target) ||
              self.$(e.target).length)) {
              self.hideDropdown();
            }
          }
        }, self.document);
      });

      optionLoad = self.option('load');

      // 异步请求
      if (optionLoad) {
        optionLoad.call(self, function(data) {
          self.setDataSelect(data);
        });
      } else {
        self.initAttrs();
        self.setDataSelect(self.option('model'));
      }
    },

    /**
     * 初始化属性值
     *
     * @method initAttrs
     * @private
     */
    initAttrs: function() {
      var self = this,
        selectName,
        selectElem,
        field = self.field,
        tagName = self.tagName,
        model = self.option('model'),
        value = self.option('value') || field.val();

      if (tagName === 'select') {
        // option 设置 model 优先级高
        if (model && model.length) {
          self.data('select', completeModel(model, value));
        } else {
          self.data('select', convertSelect(field[0], value));
        }
      } else {
        if (!model || !model.length) {
          throw new Error('option model invalid');
        }

        // trigger 如果为其他 DOM，则由用户提供 model
        self.data('select', completeModel(model, value));

        // 如果 name 存在则创建隐藏域
        selectName = self.option('name');

        if (selectName) {
          selectElem = $('[name="' + selectName + '"]', self.viewport);
          if (selectElem.length === 0) {
            selectElem = $(
              '<input type="hidden" name="' + selectName + '" />', self.viewport
            ).css({
              position: 'absolute',
              zIndex: -1
            }).insertAfter(field);
          }
          self.field = selectElem;
        }
      }
    },

    /**
     * 设置下拉选项数据
     *
     * @method setDataSelect
     * @param {object} data 下拉选项
     * @private
     */
    setDataSelect: function(data) {
      var self = this;

      self.data({
        select: data,
        multiple: self.option('multiple')
      });

      self.render();
    },

    /**
     * 渲染
     *
     * @method render
     */
    render: function() {
      this.initValue();
      Select.superclass.render.apply(this);
      this.setWidth();
    },

    /**
     * 回填 value
     *
     * @method initValue
     * @private
     */
    initValue: function() {
      var self = this,
        datas = self.data('select'),
        i, l, values = [],
        newValue,
        hasSelected = false;

      for (i = 0, l = datas.length; i < l; i++) {
        if (datas[i].selected) {
          self.text = datas[i].text;
          values.push(datas[i].value);
          hasSelected = true;
        }
      }

      newValue = values.join(self.option('delimiter'));

      self.data('hasSelected', hasSelected);

      if (typeof self.value === 'undefined') {
        self.value = self.field.val();
      }

      self.field.val(newValue);

      if (self.value !== newValue) {
        self.value = newValue;
        self.fire('change');
      }
    },

    /**
     * 设置组件宽度
     *
     * @method setWidth
     * @private
     */
    setWidth: function() {
      var self = this,
        minWidth = self.option('minWidth'),
        maxWidth = self.option('maxWidth'),
        calWidth;

      if (!minWidth) {
        minWidth = (self.option('multiple') ? 20 : 0) +
          getStringWidth(self.role('selected'), self.data('select'));
      }

      // 设置宽度同时保存到data备用

      if (maxWidth) {
        if (minWidth >= maxWidth) {
          self.role('selected')
            .css('width', maxWidth);
          self.data('width', maxWidth + 'px');
        } else {
          self.role('selected')
            .css('min-width', minWidth)
            .css('max-width', maxWidth);
          self.data('minWidth', minWidth + 'px');
          self.data('maxWidth', maxWidth + 'px');
        }
      } else {
        self.role('selected')
          .css('min-width', minWidth);
        self.data('minWidth', minWidth + 'px');
      }
    },

    /**
     * 显示 select
     *
     * @method show
     * @private
     */
    showDropdown: function() {
      var self = this,
        roleSelect = self.role('select');

      roleSelect.addClass('focus input-active dropdown-active');

      if (!self.option('multiple')) {
        self.role('selected')
          .addClass('is-label')
          .text(self.data('label') || undefined);
      }

      self.role('dropdown')
        .css({
          width: roleSelect.outerWidth(),
          left: 0,
          top: roleSelect.outerHeight(),
          visibility: 'visible'
        })
        .show();
    },

    /**
     * 隐藏 select
     *
     * @method hide
     * @private
     */
    hideDropdown: function() {
      var self = this;

      if (self.role('dropdown').is(':hidden')) {
        return;
      }

      self.role('select')
        .removeClass('focus input-active dropdown-active');

      if (!self.option('multiple')) {
        self.role('selected')
          .removeClass('is-label')
          .text(self.text || undefined);
      }

      self.role('dropdown')
        .hide();
    },

    /**
     * 单选
     *
     * @method select
     * @param item
     * @private
     */
    select: function(item) {
      var self = this,
        datas = self.data('select'),
        index = +item.data('index'),
        i, l;

      if (datas[index].selected) {
        self.hideDropdown();
        return;
      }

      if (datas[index].disabled) {
        return;
      }

      self.text = datas[index].text;

      for (i = 0, l = datas.length; i < l; i++) {
        datas[i].selected = (i === index);
      }

      self.render();
    },

    /**
     * 多选
     *
     * @method check
     * @private
     */
    check: function(item, isInput) {
      var self = this,
        datas = self.data('select'),
        index = +item.data('index'),
        checked;

      if (datas[index].disabled) {
        return;
      }

      checked = item.find('input').is(':checked');

      datas[index].selected = isInput ? checked : !checked;

      self.render();

      self.showDropdown();
    }

  });

  module.exports = Select;

  function convertSelect(select, value) {
    var i, j, o, option,
      fields, field,
      model = [],
      options = select.options,
      l = options.length,
      selected,
      selectedFound = false;

    for (i = 0; i < l; i++) {
      option = options[i];

      if (!selectedFound) {
        if (value !== null) {
          selected = (value === option.value);
        } else {
          selected = option.defaultSelected && option.selected;
        }
      }

      o = {
        text: option.text,
        value: option.value,
        selected: !selectedFound && selected,
        disabled: option.disabled
      };

      model.push(o);

      if (selected) {
        selectedFound = true;
      }
    }

    // 当所有都没有设置 selected，默认设置第一个
    if (!selectedFound && model.length) {
      model[0].selected = true;
    }

    return model;
  }

  // 补全 model 对象
  function completeModel(model, value) {
    var i, l,
      selected,
      selectedFound = false;

    for (i = 0, l = model.length; i < l; i++) {

      if (value !== null) {
        model[i].selected = (value === model[i].value);
      }

      if (model[i].selected) {
        selectedFound = true;
      }
    }

    if (!selectedFound && model.length) {
      model[0].selected = true;
    }

    return model;
  }

  function getStringWidth(sibling, data) {
    var i, l, m = 0,
      n, text, value, dummy, width;

    for (i = 0, l = data.length; i < l; i++) {
      text = data[i].text;
      n = text.replace(/[^\x00-\xff]/g, 'xx').length;
      if (n > m) {
        m = n;
        value = text;
      }
    }

    dummy = $('<div/>')
      .css({
        position: 'absolute',
        left: -9999,
        top: -9999,
        width: 'auto',
        whiteSpace: 'nowrap'
      })
      .html(value.replace(/ /g, ' '))
      .insertAfter(sibling),

    width = dummy.width();

    dummy.remove();

    return width;
  }

});
<commit_msg>修复  纯 select 选中问题<commit_after>define(function(require, exports, module) {

  'use strict';

  /**
   * Select
   *
   * @module Select
   * @class Select
   * @constructor
   * @extends Widget
   * @examples
   * // 将原生 dom 转换为 select
   * var select = new Select({
   *   filed: '#select'
   * });
   *
   * // 通过 model 定义 select 数据
   * var select = new Select({
   *   field: '#select1',
   *   model: [
   *     {text: 'text1', value:'1'},
   *     {text: 'text2}, value: '2'}
   *   ]
   * });
   */
  var $ = require('$'),
    Widget = require('widget');

  /**
   * 模拟 select 组件
   *
   * @class Select
   * @constructor
   * @extends Widget
   * @type {*|Object|Function|void}
   */

  var Select = Widget.extend({

    /**
     * 默认参数
     *
     * @property {object} defaults
     * @type {object}
     */
    defaults: {
      classPrefix: 'ue-select',

      // 可多选，为 checkbox 多选
      multiple: false,

      // 分隔符，多选时才有用
      delimiter: ',',

      placeholder: '',

      // input 或 select 元素，必填
      // field: null,
      // label: null,

      css: {
        // width: '200',
        position: 'relative'
      },

      model: [],

      // name: null,
      // value: null,

      // minWidth: null,
      // maxWidth: null,

      selectedIndex: 0,

      template: require('./select.handlebars'),
      templateOptions: {
        partials: {
          singleItem: require('./select-single-item.handlebars'),
          multiItem: require('./select-multi-item.handlebars')
        }
      },

      insert: function() {
        this.element.insertAfter(this.option('field')).show();
      },

      delegates: {
        'click': function(e) {
          e.stopPropagation();
        },
        'click [data-role=select]': function(e) {
          this.showDropdown();
        },
        'click [data-role=item]': function(e) {
          if (e.currentTarget.tagName === 'LABEL') {
            e.stopPropagation();
            this.check($(e.currentTarget), e.target.tagName === 'INPUT');
          } else {
            this.select($(e.currentTarget));
          }
        },
        'mouseenter [data-role=item]': function(e) {
          $(e.currentTarget).addClass('active');
        },
        'mouseleave [data-role=item]': function(e) {
          $(e.currentTarget).removeClass('active');
        }
      }
    },

    setup: function() {
      var self = this,
        field = self.option('field'),
        optionLoad;

      if (!field) {
        throw new Error('请设置 field');
      }

      field = self.field = $(field).hide();

      // 存储 field 的 tagName
      self.tagName = field[0].tagName.toLowerCase();

      self.data('hasSelected', !!self.option('value') || !!field.val());

      self.data('label',
        self.option('label') ||
        field.data('label') ||
        self.option('placeholder') ||
        field.attr('placeholder'));

      self.once('render', function() {
        self.element.addClass(
          self.option('classPrefix') + '-' + (self.option('multiple') ? 'multiple' : 'single')
        );
        self.initDelegates({
          'mousedown': function(e) {
            if (!(self.is(e.target) ||
              self.$(e.target).length)) {
              self.hideDropdown();
            }
          }
        }, self.document);
      });

      optionLoad = self.option('load');

      // 异步请求
      if (optionLoad) {
        optionLoad.call(self, function(data) {
          self.setDataSelect(data);
        });
      } else {
        self.initAttrs();
        self.setDataSelect(self.option('model'));
      }
    },

    /**
     * 初始化属性值
     *
     * @method initAttrs
     * @private
     */
    initAttrs: function() {
      var self = this,
        selectName,
        selectElem,
        field = self.field,
        tagName = self.tagName,
        model = self.option('model'),
        value = self.option('value') || field.attr('value');

      if (tagName === 'select') {
        // option 设置 model 优先级高
        if (model && model.length) {
          self.data('select', completeModel(model, value));
        } else {
          self.data('select', convertSelect(field[0], value));
        }
      } else {
        if (!model || !model.length) {
          throw new Error('option model invalid');
        }

        // trigger 如果为其他 DOM，则由用户提供 model
        self.data('select', completeModel(model, value));

        // 如果 name 存在则创建隐藏域
        selectName = self.option('name');

        if (selectName) {
          selectElem = $('[name="' + selectName + '"]', self.viewport);
          if (selectElem.length === 0) {
            selectElem = $(
              '<input type="hidden" name="' + selectName + '" />', self.viewport
            ).css({
              position: 'absolute',
              zIndex: -1
            }).insertAfter(field);
          }
          self.field = selectElem;
        }
      }
    },

    /**
     * 设置下拉选项数据
     *
     * @method setDataSelect
     * @param {object} data 下拉选项
     * @private
     */
    setDataSelect: function(data) {
      var self = this;

      self.data({
        select: data,
        multiple: self.option('multiple')
      });

      self.render();
    },

    /**
     * 渲染
     *
     * @method render
     */
    render: function() {
      this.initValue();
      Select.superclass.render.apply(this);
      this.setWidth();
    },

    /**
     * 回填 value
     *
     * @method initValue
     * @private
     */
    initValue: function() {
      var self = this,
        datas = self.data('select'),
        i, l, values = [],
        newValue,
        hasSelected = false;

      for (i = 0, l = datas.length; i < l; i++) {
        if (datas[i].selected) {
          self.text = datas[i].text;
          values.push(datas[i].value);
          hasSelected = true;
        }
      }

      newValue = values.join(self.option('delimiter'));

      self.data('hasSelected', hasSelected);

      if (typeof self.value === 'undefined') {
        self.value = self.field.val();
      }

      self.field.val(newValue);

      if (self.value !== newValue) {
        self.value = newValue;
        self.fire('change');
      }
    },

    /**
     * 设置组件宽度
     *
     * @method setWidth
     * @private
     */
    setWidth: function() {
      var self = this,
        minWidth = self.option('minWidth'),
        maxWidth = self.option('maxWidth'),
        calWidth;

      if (!minWidth) {
        minWidth = (self.option('multiple') ? 20 : 0) +
          getStringWidth(self.role('selected'), self.data('select'));
      }

      // 设置宽度同时保存到data备用

      if (maxWidth) {
        if (minWidth >= maxWidth) {
          self.role('selected')
            .css('width', maxWidth);
          self.data('width', maxWidth + 'px');
        } else {
          self.role('selected')
            .css('min-width', minWidth)
            .css('max-width', maxWidth);
          self.data('minWidth', minWidth + 'px');
          self.data('maxWidth', maxWidth + 'px');
        }
      } else {
        self.role('selected')
          .css('min-width', minWidth);
        self.data('minWidth', minWidth + 'px');
      }
    },

    /**
     * 显示 select
     *
     * @method show
     * @private
     */
    showDropdown: function() {
      var self = this,
        roleSelect = self.role('select');

      roleSelect.addClass('focus input-active dropdown-active');

      if (!self.option('multiple')) {
        self.role('selected')
          .addClass('is-label')
          .text(self.data('label') || undefined);
      }

      self.role('dropdown')
        .css({
          width: roleSelect.outerWidth(),
          left: 0,
          top: roleSelect.outerHeight(),
          visibility: 'visible'
        })
        .show();
    },

    /**
     * 隐藏 select
     *
     * @method hide
     * @private
     */
    hideDropdown: function() {
      var self = this;

      if (self.role('dropdown').is(':hidden')) {
        return;
      }

      self.role('select')
        .removeClass('focus input-active dropdown-active');

      if (!self.option('multiple')) {
        self.role('selected')
          .removeClass('is-label')
          .text(self.text || undefined);
      }

      self.role('dropdown')
        .hide();
    },

    /**
     * 单选
     *
     * @method select
     * @param item
     * @private
     */
    select: function(item) {
      var self = this,
        datas = self.data('select'),
        index = +item.data('index'),
        i, l;

      if (datas[index].selected) {
        self.hideDropdown();
        return;
      }

      if (datas[index].disabled) {
        return;
      }

      self.text = datas[index].text;

      for (i = 0, l = datas.length; i < l; i++) {
        datas[i].selected = (i === index);
      }

      self.render();
    },

    /**
     * 多选
     *
     * @method check
     * @private
     */
    check: function(item, isInput) {
      var self = this,
        datas = self.data('select'),
        index = +item.data('index'),
        checked;

      if (datas[index].disabled) {
        return;
      }

      checked = item.find('input').is(':checked');

      datas[index].selected = isInput ? checked : !checked;

      self.render();

      self.showDropdown();
    }

  });

  module.exports = Select;

  function convertSelect(select, value) {
    var i, j, o, option,
      fields, field,
      model = [],
      options = select.options,
      l = options.length,
      selected,
      selectedFound = false;

    for (i = 0; i < l; i++) {
      option = options[i];

      if (!selectedFound) {
        if (value !== null) {
          selected = (value === option.value);
        } else {
          selected = option.defaultSelected && option.selected;
        }
      }

      o = {
        text: option.text,
        value: option.value,
        selected: !selectedFound && selected,
        disabled: option.disabled
      };

      model.push(o);

      if (selected) {
        selectedFound = true;
      }
    }

    // 当所有都没有设置 selected，默认设置第一个
    if (!selectedFound && model.length) {
      model[0].selected = true;
    }

    return model;
  }

  // 补全 model 对象
  function completeModel(model, value) {
    var i, l,
      selected,
      selectedFound = false;

    for (i = 0, l = model.length; i < l; i++) {

      if (value !== null) {
        model[i].selected = (value === model[i].value);
      }

      if (model[i].selected) {
        selectedFound = true;
      }
    }

    if (!selectedFound && model.length) {
      model[0].selected = true;
    }

    return model;
  }

  function getStringWidth(sibling, data) {
    var i, l, m = 0,
      n, text, value, dummy, width;

    for (i = 0, l = data.length; i < l; i++) {
      text = data[i].text;
      n = text.replace(/[^\x00-\xff]/g, 'xx').length;
      if (n > m) {
        m = n;
        value = text;
      }
    }

    dummy = $('<div/>')
      .css({
        position: 'absolute',
        left: -9999,
        top: -9999,
        width: 'auto',
        whiteSpace: 'nowrap'
      })
      .html(value.replace(/ /g, ' '))
      .insertAfter(sibling),

    width = dummy.width();

    dummy.remove();

    return width;
  }

});
<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');
var graphite = require('./graphite');
var EventEmitter2 = require('eventemitter2').EventEmitter2;

var Response = function( config ) {

    /*process.on('uncaughtException', function (err) {
        console.log('Caught exception: ' + err);
        //if we have an uncaught exception, who knows what the state looks like, bail!
        process.exit(1);
    });*/

    var that = this;

    //create response dispatcher, plugins can communicate through this
    this.dispatch_emitter = new EventEmitter2({
          wildcard: true,
          maxListeners: 0, //infinite listeners
        });

    var listen_host = config.listen_host;
    var listen_port = config.listen_port;

    var graphite_host = config.graphite_host;
    var graphite_port = config.graphite_port;

    //this pretends to be a graphite server, emitting events for each message
    //it inherits from EventEmitter2 and namespaces use periods just like
    //graphite
    this.graphite_emitter = new graphite.GraphiteEventEmitter( listen_host, listen_port );

    //this takes a GraphiteEventEmitter, subscribes to all events and sends sends them off to Graphite
    //it can also buffer messages should graphite go down
    this.proxy = new graphite.GraphiteEventProxy( this.graphite_emitter, graphite_host, graphite_port );

    this.plugin_path = config.plugin_path;

    //plugins could except, but we still want to full our proxy duties
    try {
        this.load_plugins();
    } catch( e ) {
        console.log( "plugin load failed: " + e );
    };

};
module.exports.Response = Response;

Response.prototype.load_plugins = function () {

    var that = this;

    var full_plugins_path = path.join( this.plugin_path, 'node_modules' );

    fs.readdir( full_plugins_path, function( err, entries ) {

        var dirs = [];
        entries = entries || [];
        debugger;
	    entries.forEach( function( entry ) {

		    var plugin = full_plugins_path + '/' + entry;
		    fs.stat( plugin, function( err, stats ) {
			    if ( stats.isDirectory() ) {

				require( plugin )( that.graphite_emitter, that.dispatch_emitter );
			    }
			} );
	    } );

	}

	);
};

Response.prototype.destroy = function() {

    this.graphite_emitter.destroy();
    this.proxy.destroy();

    //remove all the listeners, no stranded references please
    this.dispatch_emitter.removeAllListeners();

}<commit_msg>corrected the plugin loading mechanism<commit_after>var fs = require('fs');
var path = require('path');
var graphite = require('./graphite');
var EventEmitter2 = require('eventemitter2').EventEmitter2;

var Response = function( config ) {

    /*process.on('uncaughtException', function (err) {
        console.log('Caught exception: ' + err);
        //if we have an uncaught exception, who knows what the state looks like, bail!
        process.exit(1);
    });*/

    var that = this;

    //create response dispatcher, plugins can communicate through this
    this.dispatch_emitter = new EventEmitter2({
          wildcard: true,
          maxListeners: 0, //infinite listeners
        });

    var listen_host = config.listen_host;
    var listen_port = config.listen_port;

    var graphite_host = config.graphite_host;
    var graphite_port = config.graphite_port;

    //this pretends to be a graphite server, emitting events for each message
    //it inherits from EventEmitter2 and namespaces use periods just like
    //graphite
    this.graphite_emitter = new graphite.GraphiteEventEmitter( listen_host, listen_port );

    //this takes a GraphiteEventEmitter, subscribes to all events and sends sends them off to Graphite
    //it can also buffer messages should graphite go down
    this.proxy = new graphite.GraphiteEventProxy( this.graphite_emitter, graphite_host, graphite_port );

    this.plugin_path = config.plugin_path;

    //plugins could except, but we still want to full our proxy duties
    try {
        this.load_plugins();
    } catch( e ) {
        console.log( "plugin load failed: " + e );
    };

};
module.exports.Response = Response;

Response.prototype.load_plugins = function () {

    var that = this;

    var full_plugins_path = path.dirname( require.resolve( '../plugins/.keep' ) );

    fs.readdir( full_plugins_path, function( err, entries ) {

        var dirs = [];
        entries = entries || [];
	entries.forEach( function( entry ) {
	    var plugin = full_plugins_path + '/' + entry;
	    fs.stat( plugin, function( err, stats ) {
		if ( stats.isDirectory() ) {
		    require( plugin )( that.graphite_emitter, that.dispatch_emitter );
		}
	    } );
	} );

	}

	);
};

Response.prototype.destroy = function() {

    this.graphite_emitter.destroy();
    this.proxy.destroy();

    //remove all the listeners, no stranded references please
    this.dispatch_emitter.removeAllListeners();

}<|endoftext|>


<commit_before>'use strict';

/**
 * Add querySelectorAll() to jqLite.
 *
 * jqLite find() is limited to lookups by tag name.
 * TODO This will change with future versions of AngularJS, to be removed when this happens
 *
 * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
 * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
 */
if (angular.element.prototype.querySelectorAll === undefined) {
  angular.element.prototype.querySelectorAll = function(selector) {
    return angular.element(this[0].querySelectorAll(selector));
  };
}

angular.module('ui.select', [])

.constant('uiSelectConfig', {
  theme: 'bootstrap',
  placeholder: '' // Empty by default, like HTML tag <select>
})

// See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
.service('uiSelectMinErr', function() {
  var minErr = angular.$$minErr('ui.select');
  return function() {
    var error = minErr.apply(this, arguments);
    var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
    return new Error(message);
  }
})

/**
 * Parses "repeat" attribute.
 *
 * Taken from AngularJS ngRepeat source code
 * See https://github.com/angular/angular.js/blob/55848a9139/src/ng/directive/ngRepeat.js#L211
 *
 * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
 * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
 */
.service('RepeatParser', ['uiSelectMinErr', function(uiSelectMinErr) {
  var self = this;

  /**
   * Example:
   * expression = "address in addresses | filter: {street: $select.search} track by $index"
   * lhs = "address",
   * rhs = "addresses | filter: {street: $select.search}",
   * trackByExp = "$index",
   * valueIdentifier = "address",
   * keyIdentifier = undefined
   */
  self.parse = function(expression) {
    var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);

    if (!match) {
      throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
                           expression);
    }

    var lhs = match[1]; // Left-hand side
    var rhs = match[2]; // Right-hand side
    var trackByExp = match[3];

    match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
    if (!match) {
      throw uiSelectMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
                           lhs);
    }

    var valueIdentifier = match[3] || match[1];
    var keyIdentifier = match[2];

    return {
      lhs: lhs,
      rhs: rhs,
      trackByExp: trackByExp
    };
  };

  self.getNgRepeatExpression = function(lhs, rhs, trackByExp) {
    var expression = lhs + ' in ' + rhs;
    if (trackByExp) {
      expression += ' track by ' + trackByExp;
    }
    return expression;
  };
}])

/**
 * Contains ui-select "intelligence".
 *
 * The goal is to limit dependency on the DOM whenever possible and
 * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
 */
.controller('uiSelectCtrl',
  ['$scope', '$element', '$timeout', 'RepeatParser', '$parse', 'uiSelectMinErr',
  function($scope, $element, $timeout, RepeatParser, $parse, uiSelectMinErr) {

  var ctrl = this;

  var EMPTY_SEARCH = '';

  ctrl.placeholder = undefined;
  ctrl.search = EMPTY_SEARCH;
  ctrl.activeIndex = 0;
  ctrl.items = [];
  ctrl.selected = undefined;
  ctrl.open = false;
  ctrl.disabled = false;

  ctrl.searchInput = $element.querySelectorAll('input.ui-select-search');
  if (ctrl.searchInput.length !== 1) {
    throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", ctrl.searchInput.length);
  }

  // When the user clicks on ui-select, displays the dropdown list
  ctrl.activate = function() {
    if (ctrl.disabled === false) {
      ctrl.open = true;
      // Give it time to appear before focus
      $timeout(function() {
        ctrl.searchInput[0].focus();
      });
    }
  };

  ctrl.parseRepeatAttr = function(repeatAttr) {
    var repeat = RepeatParser.parse(repeatAttr);
    var repeatRhsFn = $parse(repeat.rhs);

    var collection = repeatRhsFn($scope);
    if (!angular.isArray(collection)) {
      throw uiSelectMinErr('repeat', "Expected a collection but got '{0}'.", repeat.rhs);
    }

    // See https://github.com/angular/angular.js/blob/55848a9139/src/ng/directive/ngRepeat.js#L259
    $scope.$watchCollection(repeat.rhs, function(items) {
      ctrl.items = items;
    });
  };

  ctrl.refresh = function(refreshAttr) {
    if (refreshAttr !== undefined) {
      $timeout(function() {
        $scope.$apply(refreshAttr);
      });
    }
  };

  // When the user clicks on an item inside the dropdown list
  ctrl.select = function(item) {
    ctrl.selected = item;
    ctrl.close();
    // Using a watch instead of $scope.ngModel.$setViewValue(item)
  };

  ctrl.close = function() {
    if (ctrl.open) {
      ctrl.open = false;
      ctrl.search = EMPTY_SEARCH;
    }
  };

  var Key = {
    Enter: 13,
    Tab: 9,
    Up: 38,
    Down: 40,
    Escape: 27
  };

  ctrl.onKeydown = function(key) {
    var processed = true;
    switch (key) {
      case Key.Down:
        if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
        break;
      case Key.Up:
        if (ctrl.activeIndex > 0) { ctrl.activeIndex--; }
        break;
      case Key.Tab:
      case Key.Enter:
        ctrl.select(ctrl.items[ctrl.activeIndex]);
        break;
      case Key.Escape:
        ctrl.close();
        break;
      default:
        processed = false;
    }
    return processed;
  };
}])

.directive('uiSelect',
  ['$document', 'uiSelectConfig', 'uiSelectMinErr',
  function($document, uiSelectConfig, uiSelectMinErr) {

  return {
    restrict: 'EA',
    templateUrl: function(tElement, tAttrs) {
      var theme = tAttrs.theme || uiSelectConfig.theme;
      return theme + '/select.tpl.html';
    },
    replace: true,
    transclude: true,
    require: ['uiSelect', 'ngModel'],
    scope: true,

    controller: 'uiSelectCtrl',
    controllerAs: '$select',

    link: function(scope, element, attrs, ctrls, transcludeFn) {
      var $select = ctrls[0];
      var ngModel = ctrls[1];

      attrs.$observe('disabled', function() {
        $select.disabled = attrs.disabled ? true : false;
      });

      scope.$watch('$select.selected', function(newValue, oldValue) {
        if (ngModel.$viewValue !== newValue) {
          ngModel.$setViewValue(newValue);
        }
      });

      ngModel.$render = function() {
        $select.selected = ngModel.$viewValue;
      };

      function ensureHighlightVisible() {
        var container = element.querySelectorAll('.ui-select-choices-content');
        var rows = container.querySelectorAll('.ui-select-choices-row');
        if (rows.length < 1) {
          throw uiSelectMinErr('rows', "Expected multiple .ui-select-choices-row but got '{0}'.", rows.length);
        }

        var highlighted = rows[$select.activeIndex];
        var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop;
        var height = container[0].offsetHeight;

        if (posY > height) {
          container[0].scrollTop += posY - height;
        } else if (posY < highlighted.clientHeight) {
          container[0].scrollTop -= highlighted.clientHeight - posY;
        }
      }

      // Bind to keyboard shortcuts
      $select.searchInput.on('keydown', function(e) {
        scope.$apply(function() {
          var processed = $select.onKeydown(e.which);
          if (processed) {
            e.preventDefault();
            e.stopPropagation();

            ensureHighlightVisible();
          }
        });
      });

      // See Click everywhere but here event http://stackoverflow.com/questions/12931369
      $document.on('mousedown', function(e) {
        var contains = false;

        if (window.jQuery) {
          // Firefox 3.6 does not support element.contains()
          // See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
          contains = window.jQuery.contains(element[0], e.target);
        } else {
          contains = element[0].contains(e.target);
        }

        if (!contains) {
          $select.close();
          scope.$digest();
        }
      });

      scope.$on('$destroy', function() {
        $select.searchInput.off('keydown');
        $document.off('mousedown');
      });

      // Move transcluded elements to their correct position in main template
      transcludeFn(scope, function(clone) {
        // See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html

        // One day jqLite will be replaced by jQuery and we will be able to write:
        // var transcludedElement = clone.filter('.my-class')
        // instead of creating a hackish DOM element:
        var transcluded = angular.element('
').append(clone);

        var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
        if (transcludedMatch.length !== 1) {
          throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
        }
        element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);

        var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
        if (transcludedChoices.length !== 1) {
          throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
        }
        element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
      });
    }
  };
}])

.directive('choices',
  ['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr',
  function(uiSelectConfig, RepeatParser, uiSelectMinErr) {

  return {
    restrict: 'EA',
    require: '^uiSelect',
    replace: true,
    transclude: true,
    templateUrl: function(tElement) {
      // Gets theme attribute from parent (ui-select)
      var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
      return theme + '/choices.tpl.html';
    },

    compile: function(tElement, tAttrs) {
      var repeat = RepeatParser.parse(tAttrs.repeat);

      var rows = tElement.querySelectorAll('.ui-select-choices-row');
      if (rows.length !== 1) {
        throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", rows.length);
      }

      rows.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp))
          .attr('ng-mouseenter', '$select.activeIndex = $index')
          .attr('ng-click', '$select.select(' + repeat.lhs + ')');

      return function link(scope, element, attrs, $select) {
        $select.parseRepeatAttr(attrs.repeat);

        scope.$watch('$select.search', function() {
          $select.activeIndex = 0;
          $select.refresh(attrs.refresh);
        });
      };
    }
  };
}])

.directive('match', ['uiSelectConfig', function(uiSelectConfig) {
  return {
    restrict: 'EA',
    require: '^uiSelect',
    replace: true,
    transclude: true,
    templateUrl: function(tElement) {
      // Gets theme attribute from parent (ui-select)
      var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
      return theme + '/match.tpl.html';
    },
    link: function(scope, element, attrs, $select) {
      attrs.$observe('placeholder', function(placeholder) {
        $select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
      });
    }
  };
}])

/**
 * Highlights text that matches $select.search.
 *
 * Taken from AngularUI Bootstrap Typeahead
 * See https://github.com/angular-ui/bootstrap/blob/d0024931de/src/typeahead/typeahead.js#L352
 */
.filter('highlight', function() {
  function escapeRegexp(queryToEscape) {
    return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
  }

  return function(matchItem, query) {
    return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem;
  };
});
<commit_msg>Add resetSearchInput attribute<commit_after>'use strict';

/**
 * Add querySelectorAll() to jqLite.
 *
 * jqLite find() is limited to lookups by tag name.
 * TODO This will change with future versions of AngularJS, to be removed when this happens
 *
 * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
 * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
 */
if (angular.element.prototype.querySelectorAll === undefined) {
  angular.element.prototype.querySelectorAll = function(selector) {
    return angular.element(this[0].querySelectorAll(selector));
  };
}

angular.module('ui.select', [])

.constant('uiSelectConfig', {
  theme: 'bootstrap',
  placeholder: '' // Empty by default, like HTML tag <select>
})

// See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
.service('uiSelectMinErr', function() {
  var minErr = angular.$$minErr('ui.select');
  return function() {
    var error = minErr.apply(this, arguments);
    var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
    return new Error(message);
  }
})

/**
 * Parses "repeat" attribute.
 *
 * Taken from AngularJS ngRepeat source code
 * See https://github.com/angular/angular.js/blob/55848a9139/src/ng/directive/ngRepeat.js#L211
 *
 * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
 * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
 */
.service('RepeatParser', ['uiSelectMinErr', function(uiSelectMinErr) {
  var self = this;

  /**
   * Example:
   * expression = "address in addresses | filter: {street: $select.search} track by $index"
   * lhs = "address",
   * rhs = "addresses | filter: {street: $select.search}",
   * trackByExp = "$index",
   * valueIdentifier = "address",
   * keyIdentifier = undefined
   */
  self.parse = function(expression) {
    var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);

    if (!match) {
      throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
                           expression);
    }

    var lhs = match[1]; // Left-hand side
    var rhs = match[2]; // Right-hand side
    var trackByExp = match[3];

    match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
    if (!match) {
      throw uiSelectMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
                           lhs);
    }

    var valueIdentifier = match[3] || match[1];
    var keyIdentifier = match[2];

    return {
      lhs: lhs,
      rhs: rhs,
      trackByExp: trackByExp
    };
  };

  self.getNgRepeatExpression = function(lhs, rhs, trackByExp) {
    var expression = lhs + ' in ' + rhs;
    if (trackByExp) {
      expression += ' track by ' + trackByExp;
    }
    return expression;
  };
}])

/**
 * Contains ui-select "intelligence".
 *
 * The goal is to limit dependency on the DOM whenever possible and
 * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
 */
.controller('uiSelectCtrl',
  ['$scope', '$element', '$timeout', 'RepeatParser', '$parse', 'uiSelectMinErr',
  function($scope, $element, $timeout, RepeatParser, $parse, uiSelectMinErr) {

  var ctrl = this;

  var EMPTY_SEARCH = '';

  ctrl.placeholder = undefined;
  ctrl.search = EMPTY_SEARCH;
  ctrl.activeIndex = 0;
  ctrl.items = [];
  ctrl.selected = undefined;
  ctrl.open = false;
  ctrl.disabled = undefined; // Initialized inside uiSelect directive link function
  ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function

  ctrl.searchInput = $element.querySelectorAll('input.ui-select-search');
  if (ctrl.searchInput.length !== 1) {
    throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", ctrl.searchInput.length);
  }

  // Most of the time the user does not want to empty the search input when in typeahead mode
  function _resetSearchInput() {
    if (ctrl.resetSearchInput) {
      ctrl.search = EMPTY_SEARCH;
    }
  }

  // When the user clicks on ui-select, displays the dropdown list
  ctrl.activate = function() {
    if (!ctrl.disabled) {
      _resetSearchInput();
      ctrl.open = true;

      // Give it time to appear before focus
      $timeout(function() {
        ctrl.searchInput[0].focus();
      });
    }
  };

  ctrl.parseRepeatAttr = function(repeatAttr) {
    var repeat = RepeatParser.parse(repeatAttr);
    var repeatRhsFn = $parse(repeat.rhs);

    var collection = repeatRhsFn($scope);
    if (!angular.isArray(collection)) {
      throw uiSelectMinErr('repeat', "Expected a collection but got '{0}'.", repeat.rhs);
    }

    // See https://github.com/angular/angular.js/blob/55848a9139/src/ng/directive/ngRepeat.js#L259
    $scope.$watchCollection(repeat.rhs, function(items) {
      ctrl.items = items;
    });
  };

  /**
   * Typeahead mode: lets the user refresh the collection using his own function.
   *
   * See Expose $select.search for external / remote filtering https://github.com/angular-ui/ui-select/pull/31
   */
  ctrl.refresh = function(refreshAttr) {
    if (refreshAttr !== undefined) {
      $timeout(function() {
        $scope.$apply(refreshAttr);
      });
    }
  };

  // When the user clicks on an item inside the dropdown list
  ctrl.select = function(item) {
    ctrl.selected = item;
    ctrl.close();
    // Using a watch instead of $scope.ngModel.$setViewValue(item)
  };

  ctrl.close = function() {
    if (ctrl.open) {
      _resetSearchInput();
      ctrl.open = false;
    }
  };

  var Key = {
    Enter: 13,
    Tab: 9,
    Up: 38,
    Down: 40,
    Escape: 27
  };

  ctrl.onKeydown = function(key) {
    var processed = true;
    switch (key) {
      case Key.Down:
        if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
        break;
      case Key.Up:
        if (ctrl.activeIndex > 0) { ctrl.activeIndex--; }
        break;
      case Key.Tab:
      case Key.Enter:
        ctrl.select(ctrl.items[ctrl.activeIndex]);
        break;
      case Key.Escape:
        ctrl.close();
        break;
      default:
        processed = false;
    }
    return processed;
  };
}])

.directive('uiSelect',
  ['$document', 'uiSelectConfig', 'uiSelectMinErr',
  function($document, uiSelectConfig, uiSelectMinErr) {

  return {
    restrict: 'EA',
    templateUrl: function(tElement, tAttrs) {
      var theme = tAttrs.theme || uiSelectConfig.theme;
      return theme + '/select.tpl.html';
    },
    replace: true,
    transclude: true,
    require: ['uiSelect', 'ngModel'],
    scope: true,

    controller: 'uiSelectCtrl',
    controllerAs: '$select',

    link: function(scope, element, attrs, ctrls, transcludeFn) {
      var $select = ctrls[0];
      var ngModel = ctrls[1];

      attrs.$observe('disabled', function() {
        // No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
        $select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
      });

      attrs.$observe('resetSearchInput', function() {
        // $eval() is needed otherwise we get a string instead of a boolean
        var resetSearchInput = scope.$eval(attrs.resetSearchInput);
        $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
      });

      scope.$watch('$select.selected', function(newValue, oldValue) {
        if (ngModel.$viewValue !== newValue) {
          ngModel.$setViewValue(newValue);
        }
      });

      ngModel.$render = function() {
        $select.selected = ngModel.$viewValue;
      };

      function ensureHighlightVisible() {
        var container = element.querySelectorAll('.ui-select-choices-content');
        var rows = container.querySelectorAll('.ui-select-choices-row');
        if (rows.length < 1) {
          throw uiSelectMinErr('rows', "Expected multiple .ui-select-choices-row but got '{0}'.", rows.length);
        }

        var highlighted = rows[$select.activeIndex];
        var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop;
        var height = container[0].offsetHeight;

        if (posY > height) {
          container[0].scrollTop += posY - height;
        } else if (posY < highlighted.clientHeight) {
          container[0].scrollTop -= highlighted.clientHeight - posY;
        }
      }

      // Bind to keyboard shortcuts
      $select.searchInput.on('keydown', function(e) {
        scope.$apply(function() {
          var processed = $select.onKeydown(e.which);
          if (processed) {
            e.preventDefault();
            e.stopPropagation();

            ensureHighlightVisible();
          }
        });
      });

      // See Click everywhere but here event http://stackoverflow.com/questions/12931369
      $document.on('mousedown', function(e) {
        var contains = false;

        if (window.jQuery) {
          // Firefox 3.6 does not support element.contains()
          // See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
          contains = window.jQuery.contains(element[0], e.target);
        } else {
          contains = element[0].contains(e.target);
        }

        if (!contains) {
          $select.close();
          scope.$digest();
        }
      });

      scope.$on('$destroy', function() {
        $select.searchInput.off('keydown');
        $document.off('mousedown');
      });

      // Move transcluded elements to their correct position in main template
      transcludeFn(scope, function(clone) {
        // See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html

        // One day jqLite will be replaced by jQuery and we will be able to write:
        // var transcludedElement = clone.filter('.my-class')
        // instead of creating a hackish DOM element:
        var transcluded = angular.element('
').append(clone);

        var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
        if (transcludedMatch.length !== 1) {
          throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
        }
        element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);

        var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
        if (transcludedChoices.length !== 1) {
          throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
        }
        element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
      });
    }
  };
}])

.directive('choices',
  ['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr',
  function(uiSelectConfig, RepeatParser, uiSelectMinErr) {

  return {
    restrict: 'EA',
    require: '^uiSelect',
    replace: true,
    transclude: true,
    templateUrl: function(tElement) {
      // Gets theme attribute from parent (ui-select)
      var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
      return theme + '/choices.tpl.html';
    },

    compile: function(tElement, tAttrs) {
      var repeat = RepeatParser.parse(tAttrs.repeat);

      var rows = tElement.querySelectorAll('.ui-select-choices-row');
      if (rows.length !== 1) {
        throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", rows.length);
      }

      rows.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp))
          .attr('ng-mouseenter', '$select.activeIndex = $index')
          .attr('ng-click', '$select.select(' + repeat.lhs + ')');

      return function link(scope, element, attrs, $select) {
        $select.parseRepeatAttr(attrs.repeat);

        scope.$watch('$select.search', function() {
          $select.activeIndex = 0;
          $select.refresh(attrs.refresh);
        });
      };
    }
  };
}])

.directive('match', ['uiSelectConfig', function(uiSelectConfig) {
  return {
    restrict: 'EA',
    require: '^uiSelect',
    replace: true,
    transclude: true,
    templateUrl: function(tElement) {
      // Gets theme attribute from parent (ui-select)
      var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
      return theme + '/match.tpl.html';
    },
    link: function(scope, element, attrs, $select) {
      attrs.$observe('placeholder', function(placeholder) {
        $select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
      });
    }
  };
}])

/**
 * Highlights text that matches $select.search.
 *
 * Taken from AngularUI Bootstrap Typeahead
 * See https://github.com/angular-ui/bootstrap/blob/d0024931de/src/typeahead/typeahead.js#L352
 */
.filter('highlight', function() {
  function escapeRegexp(queryToEscape) {
    return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
  }

  return function(matchItem, query) {
    return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem;
  };
});
<|endoftext|>


<commit_before>
var qs = require('qs')

var tokens = (provider, response) => {
  var data = {}

  if (provider.concur) {
    data.access_token = response.replace(
      /[\s\S]+<Token>([^<]+)<\/Token>[\s\S]+/, '$1')
    data.refresh_token = response.replace(
      /[\s\S]+<Refresh_Token>([^<]+)<\/Refresh_Token>[\s\S]+/, '$1')
  }
  else if (provider.getpocket) {
    data.access_token = response.access_token
  }
  else if (provider.yammer) {
    data.access_token = response.access_token.token
  }

  else if (provider.oauth === 1) {
    if (response.oauth_token) {
      data.access_token = response.oauth_token
    }
    if (response.oauth_token_secret) {
      data.access_secret = response.oauth_token_secret
    }
  }
  else if (provider.oauth === 2) {
    if (response.id_token) {
      data.id_token = response.id_token
    }
    if (response.access_token) {
      data.access_token = response.access_token
    }
    if (response.refresh_token) {
      data.refresh_token = response.refresh_token
    }
  }

  return data
}

var oidc = (provider, session, response) => {
  if (!/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(response.id_token)) {
    return {error: 'Grant: OpenID Connect invalid id_token format'}
  }

  var [header, payload, signature] = response.id_token.split('.')

  try {
    header = JSON.parse(Buffer.from(header, 'base64').toString('binary'))
    payload = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'))
  }
  catch (err) {
    return {error: 'Grant: OpenID Connect error decoding id_token'}
  }

  if (![].concat(payload.aud).includes(provider.key)) {
    return {error: 'Grant: OpenID Connect invalid id_token audience'}
  }
  else if ((payload.nonce && session.nonce) && (payload.nonce !== session.nonce)) {
    return {error: 'Grant: OpenID Connect nonce mismatch'}
  }

  return {header, payload, signature}
}

var data = ({provider, input, input:{session}, output}) => {
  if (output.error) {
    return {provider, input, output}
  }

  var data = tokens(provider, output)

  if (output.id_token) {
    var jwt = oidc(provider, session, output)
    if (jwt.error) {
      return {provider, input, output: jwt}
    }
  }

  if (provider.response) {
    var response = [].concat(provider.response)
    if (response.includes('jwt') && jwt) {
      data.jwt = {id_token: jwt}
    }
    if (response.includes('raw')) {
      data.raw = output
    }
  }
  else {
    data.raw = output
  }

  return {provider, input, output: data}
}

var transport = ({provider, input, input:{params, state, session}, output}) => ({
  location:
      (params.override !== 'callback' && !output.error)
    ? output

    : (!provider.transport || provider.transport === 'querystring')
    ? `${provider.callback || ''}?${qs.stringify(output)}`

    : provider.transport === 'session'
    ? provider.callback

    : undefined,
  session: (
    provider.transport === 'session' ? session.response = output : null,
    session
  ),
  state: (
    provider.transport === 'state' ? state.response = output : null,
    state
  ),
})

module.exports = {data, transport}
<commit_msg>Update response configuration<commit_after>
var qs = require('qs')

var tokens = (provider, response) => {
  var data = {}

  if (provider.concur) {
    data.access_token = response.replace(
      /[\s\S]+<Token>([^<]+)<\/Token>[\s\S]+/, '$1')
    data.refresh_token = response.replace(
      /[\s\S]+<Refresh_Token>([^<]+)<\/Refresh_Token>[\s\S]+/, '$1')
  }
  else if (provider.getpocket) {
    data.access_token = response.access_token
  }
  else if (provider.yammer) {
    data.access_token = response.access_token.token
  }

  else if (provider.oauth === 1) {
    if (response.oauth_token) {
      data.access_token = response.oauth_token
    }
    if (response.oauth_token_secret) {
      data.access_secret = response.oauth_token_secret
    }
  }
  else if (provider.oauth === 2) {
    if (response.id_token) {
      data.id_token = response.id_token
    }
    if (response.access_token) {
      data.access_token = response.access_token
    }
    if (response.refresh_token) {
      data.refresh_token = response.refresh_token
    }
  }

  return data
}

var oidc = (provider, session, response) => {
  if (!/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(response.id_token)) {
    return {error: 'Grant: OpenID Connect invalid id_token format'}
  }

  var [header, payload, signature] = response.id_token.split('.')

  try {
    header = JSON.parse(Buffer.from(header, 'base64').toString('binary'))
    payload = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'))
  }
  catch (err) {
    return {error: 'Grant: OpenID Connect error decoding id_token'}
  }

  if (![].concat(payload.aud).includes(provider.key)) {
    return {error: 'Grant: OpenID Connect invalid id_token audience'}
  }
  else if ((payload.nonce && session.nonce) && (payload.nonce !== session.nonce)) {
    return {error: 'Grant: OpenID Connect nonce mismatch'}
  }

  return {header, payload, signature}
}

var data = ({provider, input, input:{session}, output}) => {
  if (output.error) {
    return {provider, input, output}
  }

  if (output.id_token) {
    var jwt = oidc(provider, session, output)
    if (jwt.error) {
      return {provider, input, output: jwt}
    }
  }

  if (!provider.response) {
    var data = tokens(provider, output)
    data.raw = output
  }
  else {
    var data = {}
    var response = [].concat(provider.response)
    if (response.find((key) => /token/.test(key))) {
      data = tokens(provider, output)
    }
    if (response.includes('jwt') && jwt) {
      data.jwt = {id_token: jwt}
    }
    if (response.includes('raw')) {
      data.raw = output
    }
  }

  return {provider, input, output: data}
}

var transport = ({provider, input, input:{params, state, session}, output}) => ({
  location:
      (params.override !== 'callback' && !output.error)
    ? output

    : (!provider.transport || provider.transport === 'querystring')
    ? `${provider.callback || ''}?${qs.stringify(output)}`

    : provider.transport === 'session'
    ? provider.callback

    : undefined,
  session: (
    provider.transport === 'session' ? session.response = output : null,
    session
  ),
  state: (
    provider.transport === 'state' ? state.response = output : null,
    state
  ),
})

module.exports = {data, transport}
<|endoftext|>