<commit_before>'use strict'

const Promise = require('bluebird')
const t = require('../lib/translator')
const Logger = require('../lib/logger')
const languages = require('../lib/lang')
const mixpanel = require('../lib/mixpanel')
const state = require('../lib/state')
const repo = require('../db/repo')
const config = require('../config')
const _ = require('lodash')

const maxTextReplyLength = 320

const responseType = {
  help: '#help',
  getStarted: '#getstarted',
  reset: '#reset',
  switch: '#switch',
  list: '#list'
}

const helpQuickReply = {
  'content_type': 'text',
  'title': 'Need help?',
  'payload': '#help'
}

const examples = [
  'English', 'German', 'Italian', 'Korean', 'Dutch', 'Japanese', 'Hindi', 'Spanish', 'French', 'Indonesian', 'Russian', 'Chinese', 'Greek'
]

class MessageHandler {

  constructor(bot) {
    this.bot = bot
  }

  // next middleware
  next(sender) {
    let next = this.bot
    const profile = state.get(sender.id)
    if (profile !== undefined) {
      // pseudo middleware handler
      next = {
        getProfile: (id, cb) => {
          cb(null, profile)
        }
      }
    }
    return next
  }

  // typing sends a typing response to the recipient
  typing(sender) {
    this.bot.setTyping(sender.id, true)
  }

  // handleMessage is our main handler
  handleMessage(payload, reply) {

    const { sender, message, postback } = payload

    // we don't care about handling our own responses
    if (sender.id === config.fb_page_id) return

    return new Promise((resolve) => {

      const next = this.next(sender)
      next.getProfile(sender.id, (err, profile) => {
        if (err) return Logger.log(`getProfileError: ${JSON.stringify(err)}`)
        // cache the user for subsequent requests
        state.set(sender.id, profile)

        repo(sender.id).get().then((response, err) => {
          if (err) Logger.log(`getAsyncError: ${JSON.stringify(err)}`)

          const { context } = response

          // check for postbacks
          if (postback && postback.payload) {
            return resolve(this.handlePostBack(context, postback, profile, sender, reply))
          }
          // check if a quick reply (comes in as a message)
          if (message && message.quick_reply && message.quick_reply.payload) {
            return resolve(this.handlePostBack(context, message.quick_reply, profile, sender, reply))
          }

          // not a postback and not a text message, return
          if (!message.text) return resolve(false)

          if (_.includes(message.text.toLowerCase(), 'help') && !context) {
            return resolve(this.handleHelp(context, profile, sender, reply))
          }
          // reset or switch command
          if (message.text === responseType.reset || message.text === responseType.switch) {
            return resolve(this.handlePostBack(context, {payload: message.text}, profile, sender, reply))
          }
          // no context
          if (!context) {
            return resolve(this.handleNoContext(sender, profile, message, reply))
          }
          // we have context and the user has sent a text message, before translation check if this is
          // a possible direct change command: {lang} to {lang}
          if (isPossibleChangeCommand(message.text)) {
            const context = getContextFromMessage(message.text, true)
            if (context.hasTwo && context.from !== context.to) {
              return (resolve(this.handleSetContext(context.code, context.from, context.to, sender, null, reply)))
            }
          }
          // we made it! translate the message
          return resolve(this.handleTranslation(context, sender, message, reply))
        })
      })
    })
  }

  // handlePostBack
  handlePostBack(context, postback, profile, sender, reply) {
    Logger.log('handlePostBack')
    if (postback.payload === responseType.getStarted) {
      return this.handleGetStarted(sender, profile, reply)
    }
    if (postback.payload === responseType.help) {
      return this.handleHelp(context, profile, sender, reply)
    }
    if (postback.payload === responseType.reset) {
      return this.handleReset(sender, reply)
    }
    if (postback.payload === responseType.switch) {
      return this.handleSwitch(context, sender, reply)
    }
    if (postback.payload === responseType.list) {
      return this.handleShowAllLanguages(sender, reply)
    }
  }

  // handleGetStarted
  handleGetStarted(sender, profile, reply) {
    Logger.log('handleGetStarted')
    reply({
      text: `Hola ${profile && profile.first_name}! Start by telling me what languages to translate for you. Say something like ${getSmartExample(profile)}\n\nAsk me for "help" at any time`
    }, () => {
      mixpanel.setPerson(sender, profile)
      mixpanel.track('I click to get started', sender)
    })
  }

  // handleHelp
  handleHelp(context, profile, sender, reply) {
    Logger.log('handleHelp')
    let text = `Hola. I see you've asked for some help... \n\nTell me what languages to translate by saying something like ${getSmartExample(profile)}`
    let options = [
      {
        'type': 'postback',
        'title': 'Show all languages',
        'payload': responseType.list
      }
    ]

    if (context) {
      context = getContextFromCode(context)
      text = `Hola again. I see you've asked for some help...\n\nI'm currently translating everything you say from ${context.from} to ${context.to}\n\n`
      options.unshift(
        {
          'type': 'postback',
          'title': 'Reset',
          'payload': responseType.reset
        },
        {
          'type': 'postback',
          'title': `${context.to} to ${context.from}`,
          'payload': responseType.switch
        }
      )
    }

    return reply({
      'attachment': {
        'type': 'template',
        'payload': {
          'template_type': 'button',
          'text': text,
          'buttons': options
        }
      }
    }, () => {
      mixpanel.track('I ask for help', sender)
    })
  }

  // handleShowAllLanguages
  // due to FBs payload size limit we need to chunk the list into seprate bits
  handleShowAllLanguages(sender, reply) {
    const list = getAllLanguageNames()
    const first = list.splice(0, list.length / 3)
    const second = list.splice(0, list.length / 2)
    reply({text: `OK, here goes...\n\n${first.toString().replace(/,/g, ', ')}`}, () => {
      reply({text: `${second.toString().replace(/,/g, ', ')}`}, () => {
        reply({text: `${list.toString().replace(/,/g, ', ')}\n\n *GASP*`})
      })
    })
  }

  // handleReset
  handleReset(sender, reply) {
    repo(sender.id).set('').then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: 'OK, what should I translate for you next?'
      }, () => {
        mixpanel.track('I reset context', sender)
      })
    })
  }

  // handleSwitch
  handleSwitch(context, sender, reply) {
    context = switchContext(getContextFromCode(context))
    return this.handleSetContext(context.code, context.from, context.to, sender, null, reply)
  }

  // handleNoContext
  handleNoContext(sender, profile, message, reply) {
    Logger.log('handleNoContext')
    const context = getContextFromMessage(message.text)
    if (context.hasTwo && context.from !== context.to) {
      return this.handleSetContext(context.code, context.from, context.to, sender, message, reply)
    }
    if (this.handleGeneralResponse(sender, profile, message, reply) !== false) {
      return
    }

    let text = 'Oops, I didn\'t quite catch that. Ask me for "help" at anytime'
    if (context.hasOne) text = `I only caught ${context.from} in there. Try again, or ask me for "help"`
    return reply({
      text: text
    }, () => {
      mixpanel.track('I incorrectly set context', sender, message)
    })
  }

  // handleGeneralResponse such as hello, goodbye etc
  handleGeneralResponse(sender, profile, message, reply) {
    if (_.includes(['goodbye', 'bye', 'adios', 'ciao', 'seeya', 'see you', 'later'], message.text.toLowerCase())) {
      return reply({
        text: `Adiós ${profile.gender === 'male' ? 'muchacho' : 'muchacha'}`
      }, () => {
        mixpanel.track('I say goodbye without context', sender, message)
      })
    }
    if (_.includes(['hello', 'hi', 'howdy', 'hallo', 'yo', 'hey', 'sup', 'hiya'], message.text.toLowerCase())) {
      return reply({
        text: getRandom([`Hi ${profile.first_name}!`, 'Hello', '¡Hola!', `Hey ${profile.first_name}!`, 'Oh hey there!', 'Hallo', 'Howdy', 'Oh Hiiiiii'])
      }, () => {
        mixpanel.track('I say hello without context', sender, message)
      })
    }
    if (_.includes(message.text.toLowerCase(), 'hmm')) {
      return reply({ text: 'hmmmm...' })
    }
    return false
  }

  // handleSetContext
  handleSetContext(code, from, to, sender, message, reply) {
    Logger.log('handleSetContext')
    repo(sender.id).set(code).then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: `${from} to ${to}. Got it! Now go ahead and tell me what to say.`
      }, () => {
        mixpanel.track('I set context', sender, message)
      })
    })
  }

  // handleTranslation
  handleTranslation(context, sender, message, reply) {
    let response = {}
    this.typing(sender)
    t.translate(message.text, context).
      then((result) => {
        if (result.length > maxTextReplyLength) {
          return reply({text: 'I\'m sorry, I can\'t translate all of that in one go. Try again with a smaller message'})
        }
        response.text = result
        // check if the user actually wants help by passing a quick reply button with the translated text
        if (_.includes(message.text.toLowerCase(), 'help')) {
          response.quick_replies = [helpQuickReply]
        }
        reply(response, (err) => {
          if (err) Logger.log(err)
          mixpanel.track('I send a message to be translated', sender, message)
        })
      }).
      catch((err) => {
        if (err) Logger.log(err)
        reply({ text: 'Oh no, something has gone wrong. Please try that again' })
      })
  }

  // _privates
  // expose these for unit testing
  _privates() {
    return {
      getContextFromMessage: getContextFromMessage,
      getContextFromCode: getContextFromCode,
      switchContext: switchContext,
      getLanguageName: getLanguageName,
      getAllLanguageNames: getAllLanguageNames,
      getLanguageNameLocale: getLanguageNameLocale,
      getSmartExample: getSmartExample,
      isPossibleChangeCommand: isPossibleChangeCommand
    }
  }
}

// private methods
// getContextFromMessage
function getContextFromMessage(message, strict) {
  const ctxMatches = getContextMatches(message, strict)
  const { hasTwo, hasOne, hasNone } = ctxMatches
  let code, from, to
  if (hasOne || hasTwo) {
    from = _.capitalize(ctxMatches.matches[0].name)
  }
  if (hasTwo) {
    code = `${ctxMatches.matches[0].code}:${ctxMatches.matches[1].code}`
    to = _.capitalize(ctxMatches.matches[1].name)
  }
  return { code, from, to, hasTwo, hasOne, hasNone }
}

// getContextFromCode
function getContextFromCode(code) {
  const codes = code.split(':')
  return {
    code,
    from: getLanguageName(codes[0]),
    to: getLanguageName(codes[1]),
    hasTwo: true
  }
}

// getContext
function getContextMatches(message, strict) {
  let matches = []
  const words = message.split(' ')
  if (!strict || (strict && words.length === 3)) {
    words.forEach((word) => {
      languages.filter(item => item.name === _.lowerCase(word)).map((lang) => {
        if (matches.length === 2) return
        matches.push(lang)
      })
    })
  }
  return {
    hasTwo: matches.length === 2,
    hasOne: matches.length === 1,
    hasNone: matches.length === 0,
    matches: matches
  }
}

// switchContext
function switchContext(context) {
  const values = context.code.split(':')
  const from = context.to
  const to = context.from

  context.code = `${values[1]}:${values[0]}`
  context.from = from
  context.to = to

  return context
}

// getLanguageName
function getLanguageName(code) {
  const name = languages.filter(item => item.code === code).map((lang) => {
    return lang.name
  })
  if (name && name.length) return _.capitalize(name[0])
}

// listAllLanguages
function getAllLanguageNames() {
  let list = []
  languages.forEach(function(lang) {
    list.push(_.capitalize(lang.name))
  })
  return list
}

// getLanguageNameLocale
function getLanguageNameLocale(profile) {
  if (!profile || (profile && !profile.locale || profile.locale.indexOf('_') === -1)) return
  const code = profile.locale.split('_')[0]
  return getLanguageName(code)
}

// getRandom
function getRandom(responses) {
  return responses[Math.floor(Math.random()*responses.length)]
}

// getSmartExample
function getSmartExample(profile) {
  const locale = getLanguageNameLocale(profile)
  let shuffled = shuffleArray(_.clone(examples))
  if (!locale) return `"${shuffled[0]} to ${shuffled[1]}"`

  shuffled = _.remove(shuffled, (n) => { return n !== locale })
  return `"${locale} to ${shuffled[0]}"`
}

// shuffleArray
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

// isPossibleChangeCommand
function isPossibleChangeCommand(message) {
  const words = message.split(' ')
  return words.length === 3 && words[1] === 'to'
}

module.exports = MessageHandler
<commit_msg>code cleanup<commit_after>'use strict'

const Promise = require('bluebird')
const t = require('../lib/translator')
const Logger = require('../lib/logger')
const languages = require('../lib/lang')
const mixpanel = require('../lib/mixpanel')
const state = require('../lib/state')
const repo = require('../db/repo')
const config = require('../config')
const _ = require('lodash')

const maxTextReplyLength = 320

const responseType = {
  help: '#help',
  getStarted: '#getstarted',
  reset: '#reset',
  switch: '#switch',
  list: '#list'
}

const baseHelpOptions = [
  {
    'type': 'postback',
    'title': 'Show all languages',
    'payload': responseType.list
  }
]

const helpQuickReply = {
  'content_type': 'text',
  'title': 'Need help?',
  'payload': '#help'
}

const examples = [
  'English', 'German', 'Italian', 'Korean', 'Dutch', 'Japanese', 'Hindi', 'Spanish', 'French', 'Indonesian', 'Russian', 'Chinese', 'Greek'
]

class MessageHandler {

  constructor(bot) {
    this.bot = bot
  }

  // pseudo middleware handler
  next(sender) {
    let next = this.bot
    const profile = state.get(sender.id)
    if (profile !== undefined) {
      next = {
        getProfile: (id, cb) => {
          cb(null, profile)
        }
      }
    }
    return next
  }

  // typing sends a typing response to the recipient
  typing(sender) {
    this.bot.setTyping(sender.id, true)
  }

  // handleMessage is our main handler
  handleMessage(payload, reply) {

    const { sender, message, postback } = payload

    // we don't care about handling our own responses
    if (sender.id === config.fb_page_id) return

    return new Promise((resolve) => {

      this.next(sender).getProfile(sender.id, (err, profile) => {
        if (err) return Logger.log(`getProfileError: ${JSON.stringify(err)}`)
        // cache the user for subsequent requests
        state.set(sender.id, profile)

        repo(sender.id).get().then((response, err) => {
          if (err) Logger.log(`getAsyncError: ${JSON.stringify(err)}`)

          const { context } = response

          // check for postbacks
          if (postback && postback.payload) {
            return resolve(this.handlePostBack(context, postback, profile, sender, reply))
          }
          // check if a quick reply (comes in as a message)
          else if (message && message.quick_reply && message.quick_reply.payload) {
            return resolve(this.handlePostBack(context, message.quick_reply, profile, sender, reply))
          }

          // not a postback and not a text message, return
          else if (!message.text) return resolve(false)

          else if (_.includes(message.text.toLowerCase(), 'help') && !context) {
            return resolve(this.handleHelp(context, profile, sender, reply))
          }
          // reset or switch command
          else if (message.text === responseType.reset || message.text === responseType.switch) {
            return resolve(this.handlePostBack(context, {payload: message.text}, profile, sender, reply))
          }
          // no context
          else if (!context) {
            return resolve(this.handleNoContext(sender, profile, message, reply))
          }
          // we have context and the user has sent a text message, before translation check if this is
          // a possible direct change command: {lang} to {lang}
          else if (isPossibleChangeCommand(message.text)) {
            const context = getContextFromMessage(message.text, true)
            if (context.hasTwo && context.from !== context.to) {
              return (resolve(this.handleSetContext(context.code, context.from, context.to, sender, null, reply)))
            }
          }
          // we made it! translate the message
          return resolve(this.handleTranslation(context, sender, message, reply))
        })
      })
    })
  }

  // handlePostBack
  handlePostBack(context, postback, profile, sender, reply) {
    Logger.log('handlePostBack')
    if (postback.payload === responseType.getStarted) {
      return this.handleGetStarted(sender, profile, reply)
    }
    else if (postback.payload === responseType.help) {
      return this.handleHelp(context, profile, sender, reply)
    }
    else if (postback.payload === responseType.reset) {
      return this.handleReset(sender, reply)
    }
    else if (postback.payload === responseType.switch) {
      return this.handleSwitch(context, sender, reply)
    }
    else if (postback.payload === responseType.list) {
      return this.handleShowAllLanguages(sender, reply)
    }
  }

  // handleGetStarted
  handleGetStarted(sender, profile, reply) {
    Logger.log('handleGetStarted')
    reply({
      text: `Hola ${profile && profile.first_name}! Start by telling me what languages to translate for you. Say something like ${getSmartExample(profile)}\n\nAsk me for "help" at any time`
    }, () => {
      mixpanel.setPerson(sender, profile)
      mixpanel.track('I click to get started', sender)
    })
  }

  // handleHelp
  handleHelp(context, profile, sender, reply) {
    Logger.log('handleHelp')
    let text = `Hola. I see you've asked for some help... \n\nTell me what languages to translate by saying something like ${getSmartExample(profile)}`
    let options = baseHelpOptions

    if (context) {
      context = getContextFromCode(context)
      text = `Hola again. I see you've asked for some help...\n\nI'm currently translating everything you say from ${context.from} to ${context.to}\n\n`
      options.unshift(
        {
          'type': 'postback',
          'title': 'Reset',
          'payload': responseType.reset
        },
        {
          'type': 'postback',
          'title': `${context.to} to ${context.from}`,
          'payload': responseType.switch
        }
      )
    }

    return reply({
      'attachment': {
        'type': 'template',
        'payload': {
          'template_type': 'button',
          'text': text,
          'buttons': options
        }
      }
    }, () => {
      mixpanel.track('I ask for help', sender)
    })
  }

  // handleShowAllLanguages
  // due to Facebook's payload size limit we need to chunk the list into seprate bits
  handleShowAllLanguages(sender, reply) {
    const list = getAllLanguageNames()
    const first = list.splice(0, list.length / 3)
    const second = list.splice(0, list.length / 2)
    reply({text: `OK, here goes...\n\n${first.toString().replace(/,/g, ', ')}`}, () => {
      reply({text: `${second.toString().replace(/,/g, ', ')}`}, () => {
        reply({text: `${list.toString().replace(/,/g, ', ')}\n\n *GASP*`})
      })
    })
  }

  // handleReset
  handleReset(sender, reply) {
    repo(sender.id).set('').then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: 'OK, what should I translate for you next?'
      }, () => {
        mixpanel.track('I reset context', sender)
      })
    })
  }

  // handleSwitch
  handleSwitch(context, sender, reply) {
    context = switchContext(getContextFromCode(context))
    return this.handleSetContext(context.code, context.from, context.to, sender, null, reply)
  }

  // handleNoContext
  handleNoContext(sender, profile, message, reply) {
    Logger.log('handleNoContext')
    const context = getContextFromMessage(message.text)
    if (context.hasTwo && context.from !== context.to) {
      return this.handleSetContext(context.code, context.from, context.to, sender, message, reply)
    }
    if (this.handleGeneralResponse(sender, profile, message, reply) !== false) {
      return
    }

    let text = 'Oops, I didn\'t quite catch that. Ask me for "help" at anytime'
    if (context.hasOne) text = `I only caught ${context.from} in there. Try again, or ask me for "help"`
    return reply({
      text: text
    }, () => {
      mixpanel.track('I incorrectly set context', sender, message)
    })
  }

  // handleGeneralResponse such as hello, goodbye etc
  handleGeneralResponse(sender, profile, message, reply) {
    if (_.includes(['goodbye', 'bye', 'adios', 'ciao', 'seeya', 'see you', 'later'], message.text.toLowerCase())) {
      return reply({
        text: `Adiós ${profile.gender === 'male' ? 'muchacho' : 'muchacha'}`
      }, () => {
        mixpanel.track('I say goodbye without context', sender, message)
      })
    }
    if (_.includes(['hello', 'hi', 'howdy', 'hallo', 'yo', 'hey', 'sup', 'hiya'], message.text.toLowerCase())) {
      return reply({
        text: getRandom([`Hi ${profile.first_name}!`, 'Hello', '¡Hola!', `Hey ${profile.first_name}!`, 'Oh hey there!', 'Hallo', 'Howdy', 'Oh Hiiiiii'])
      }, () => {
        mixpanel.track('I say hello without context', sender, message)
      })
    }
    if (_.includes(message.text.toLowerCase(), 'hmm')) {
      return reply({ text: 'hmmmm...' })
    }
    return false
  }

  // handleSetContext
  handleSetContext(code, from, to, sender, message, reply) {
    Logger.log('handleSetContext')
    repo(sender.id).set(code).then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: `${from} to ${to}. Got it! Now go ahead and tell me what to say.`
      }, () => {
        mixpanel.track('I set context', sender, message)
      })
    })
  }

  // handleTranslation
  handleTranslation(context, sender, message, reply) {
    let response = {}
    t.translate(message.text, context).
      then((result) => {
        if (result.length > maxTextReplyLength) {
          return reply({text: 'I\'m sorry, I can\'t translate all of that in one go. Try again with a smaller message'})
        }
        response.text = result
        // check if the user actually wants help by passing a quick reply button with the translated text
        if (_.includes(message.text.toLowerCase(), 'help')) {
          response.quick_replies = [helpQuickReply]
        }
        reply(response, (err) => {
          if (err) Logger.log(err)
          mixpanel.track('I send a message to be translated', sender, message)
        })
      }).
      catch((err) => {
        if (err) Logger.log(err)
        reply({ text: 'Oh no, something has gone wrong. Please try that again' })
      })
  }

  // _privates
  // expose these for tests
  _privates() {
    return {
      getContextFromMessage: getContextFromMessage,
      getContextFromCode: getContextFromCode,
      switchContext: switchContext,
      getLanguageName: getLanguageName,
      getAllLanguageNames: getAllLanguageNames,
      getLanguageNameLocale: getLanguageNameLocale,
      getSmartExample: getSmartExample,
      isPossibleChangeCommand: isPossibleChangeCommand
    }
  }
}

// private methods
// getContextFromMessage
function getContextFromMessage(message, strict) {
  const ctxMatches = getContextMatches(message, strict)
  const { hasTwo, hasOne, hasNone } = ctxMatches
  let code, from, to
  if (hasOne || hasTwo) {
    from = _.capitalize(ctxMatches.matches[0].name)
  }
  if (hasTwo) {
    code = `${ctxMatches.matches[0].code}:${ctxMatches.matches[1].code}`
    to = _.capitalize(ctxMatches.matches[1].name)
  }
  return { code, from, to, hasTwo, hasOne, hasNone }
}

// getContextFromCode
function getContextFromCode(code) {
  const codes = code.split(':')
  return {
    code,
    from: getLanguageName(codes[0]),
    to: getLanguageName(codes[1]),
    hasTwo: true
  }
}

// getContext
function getContextMatches(message, strict) {
  let matches = []
  const words = message.split(' ')
  if (!strict || (strict && words.length === 3)) {
    words.forEach((word) => {
      languages.filter(item => item.name === _.lowerCase(word)).map((lang) => {
        if (matches.length === 2) return
        matches.push(lang)
      })
    })
  }
  return {
    hasTwo: matches.length === 2,
    hasOne: matches.length === 1,
    hasNone: matches.length === 0,
    matches: matches
  }
}

// switchContext
function switchContext(context) {
  const values = context.code.split(':')
  const from = context.to
  const to = context.from

  context.code = `${values[1]}:${values[0]}`
  context.from = from
  context.to = to

  return context
}

// getLanguageName
function getLanguageName(code) {
  const name = languages.filter(item => item.code === code).map((lang) => {
    return lang.name
  })
  if (name && name.length) return _.capitalize(name[0])
}

// listAllLanguages
function getAllLanguageNames() {
  let list = []
  languages.forEach(function(lang) {
    list.push(_.capitalize(lang.name))
  })
  return list
}

// getLanguageNameLocale
function getLanguageNameLocale(profile) {
  if (!profile || (profile && !profile.locale || profile.locale.indexOf('_') === -1)) return
  const code = profile.locale.split('_')[0]
  return getLanguageName(code)
}

// getRandom
function getRandom(responses) {
  return responses[Math.floor(Math.random()*responses.length)]
}

// getSmartExample
function getSmartExample(profile) {
  const locale = getLanguageNameLocale(profile)
  let shuffled = shuffleArray(_.clone(examples))
  if (!locale) return `"${shuffled[0]} to ${shuffled[1]}"`

  shuffled = _.remove(shuffled, (n) => { return n !== locale })
  return `"${locale} to ${shuffled[0]}"`
}

// shuffleArray
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

// isPossibleChangeCommand
function isPossibleChangeCommand(message) {
  const words = message.split(' ')
  return words.length === 3 && words[1] === 'to'
}

module.exports = MessageHandler
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import Breadcrumbs from '../components/Breadcrumbs';
import MapLeaf from '../components/Map';
// import ExampleCalendar from '../components/Calendar';

class MapLocations extends Component {
	constructor(props){
		super(props);
		this.state={
			selectedTraining: '',
			tableRow: 3,
			tableCol: 3,
			locations: [{
				name: 'Training 1',
				address: 'Aloha State',
				date: 'February 21, 2018',
				times: '9:00am - 12:00pm'
			}, {
				name: 'Training 2',
				address: 'Sunshine State',
				date: 'March 2, 2018',
				times: '6:00pm - 8:00pm'
			}]
		};
		this.handleOptionChange = this.handleOptionChange.bind(this);
		this.handleFormSubmit = this.handleFormSubmit.bind(this);
	}

	handleOptionChange (e){
		this.setState({selectedTraining: e.target.value});
		console.log(e.target.value);
	}

	handleFormSubmit(submit){
		submit.preventDefault();
		console.log('You signed up for', this.state.selectedTraining);
	}

	render(){
		let loc = this.state.locations;
		let info = [];
		let infoValue;
		let infoBlocks;
		let times = [];
		let locArr = loc.map(function(data){
			let dataArr = [data];
			for (var keyValue in dataArr){
				infoValue = dataArr[keyValue];
				infoBlocks = [infoValue.name, infoValue.address, infoValue.date, infoValue.times].join(' ');
				times.push(infoValue.times);
			}
			return infoBlocks;
		});

		for (var trainings in locArr){
			let signUps =

											{locArr[trainings]}

													<input type="radio" value={times[trainings]}
														checked={this.state.selectedTraining===times[trainings]}
														onChange={this.handleOptionChange}
														/>
													{times[trainings]}

;
			info.push(signUps);
		}

		return(

					<Breadcrumbs/>

					What part of the island do you want to be trained at?

						MAP GOES HERE
						<MapLeaf />

							Name: Address: Date: Times:

							<form onSubmit={this.handleFormSubmit}>
								{info}
								<button type="submit" className="btn btn-lg">Sign Up</button>
							</form>

		);
	}
}

export default MapLocations;<commit_msg>Minor additions and fixes<commit_after>import React, { Component } from 'react';
import Breadcrumbs from '../components/Breadcrumbs';
import MapLeaf from '../components/Map';
// import ExampleCalendar from '../components/Calendar';

class MapLocations extends Component {
	constructor(props){
		super(props);
		this.state={
			selectedTraining: '',
			tableRow: 3,
			tableCol: 3,
			locations: [{
				name: 'Training 1',
				address: 'Aloha State',
				date: 'February 21, 2018',
				times: '9:00am - 12:00pm'
			}, {
				name: 'Training 2',
				address: 'Sunshine State',
				date: 'March 2, 2018',
				times: '6:00pm - 8:00pm'
			},{
				name: 'Training 3',
				address: 'Aloha State',
				date: 'June 14, 2018',
				times: '6:30pm - 8:00pm'
			},{
				name: 'Training 4',
				address: 'Sunshine State',
				date: 'June 16, 2018',
				times: '9:00am - 10:30am'
			}]
		};
		this.handleOptionChange = this.handleOptionChange.bind(this);
		this.handleFormSubmit = this.handleFormSubmit.bind(this);
	}

	handleOptionChange (e){
		this.setState({selectedTraining: e.target.value});
		console.log(e.target.value);
	}

	handleFormSubmit(submit){
		submit.preventDefault();
		console.log('You signed up for', this.state.selectedTraining);
	}

	render(){
		let loc = this.state.locations;
		let info = [];
		let infoValue;
		let infoBlocks;
		let times = [];
		let locArr = loc.map(function(data){
			let dataArr = [data];
			for (var keyValue in dataArr){
				infoValue = dataArr[keyValue];
				infoBlocks = [infoValue.name, infoValue.address, infoValue.date, infoValue.times].join(' ');
				times.push(infoValue.times);
			}
			return infoBlocks;
		});

		for (var trainings in locArr){
			let signUps =

											{locArr[trainings]}

													<input type="radio" value={times[trainings]}
														checked={this.state.selectedTraining===times[trainings]}
														onChange={this.handleOptionChange}
														/>
													{times[trainings]}

;
			info.push(signUps);
		}

		return(

					<Breadcrumbs/>

					What part of the island do you want to be trained at?

						<MapLeaf />

							Name: Address: Date: Times:

							<form onSubmit={this.handleFormSubmit}>
								{info}
								<button type="submit" className="btn btn-lg">Sign Up</button>
							</form>

		);
	}
}

export default MapLocations;
<|endoftext|>


<commit_before>(function () {
  'use strict';

  var uiSelectModule = angular.module('fng.uiSelect', ['ui.select']);

  uiSelectModule.factory('uiSelectHelper', ['$rootScope', '$q', 'SubmissionsService', function ($rootScope, $q, SubmissionsService) {
    var lastW, lastH;
    return {
      windowChanged: function(w,h) {
        var result = false;
        if (w !== lastW || h !== lastH) {
          lastW = w;
          lastH = h;
          result = true;
        }
        return result;
      },
      lookupFunc: function(value, formSchema, cb) {
        if (formSchema.array) {
          // TODO extend back end to do multiple lookups in one hit

          var promises = [];
          var results = [];
          angular.forEach(value, function(obj) {
            promises.push(SubmissionsService.getListAttributes(formSchema.ref, obj.x));
          });
          $q.all(promises).then(function(responses) {
            angular.forEach(responses, function(response) {
                results.push({x:{id:value.shift().x, text:response.data.list}});
              });
            cb(formSchema, results);
            setTimeout(function() {
              $rootScope.$digest();
            });
          });
        } else {
          SubmissionsService.getListAttributes(formSchema.ref, value).then(function(response) {
            cb(formSchema, {id: value, text: response.data.list});
          });
        }
      }
    };
  }]);

  uiSelectModule.controller('FngUISelectCtrl',function($scope, $window, $timeout, $http, uiSelectHelper) {

    $scope.uiSelect = $scope.uiSelect || [];

    $scope.windowResizeUiSelect = function(checkResize) {
      // This is to stop the resize firing too often.  Really horrible way of doing it caused by some very regrettable design choices which will be factored out later
      // Actually not choices - just not knowing any better
      if (!checkResize || uiSelectHelper.windowChanged($window.innerWidth, $window.innerHeight)) {
        for (var i = 0; i < $scope.uiSelect.length; i++) {
          var selectId = $scope.uiSelect[i].selectId;
          var select = document.getElementById(selectId);
          if (select) {
            var helper = document.getElementById(selectId + '_width-helper');
            if (helper) {
              var w = helper.offsetWidth;
              if (select.offsetLeft + w > $window.innerWidth) {
                w = $window.innerWidth - select.offsetLeft - 20;
              }
              angular.element('#' + selectId).css('width', w);
            } else {
              console.log('No helper element found for ' + selectId);
            }
          }
        }
      }
    };

    $scope.refreshOptions = function(searchString, id) {
      var elem = _.find($scope.uiSelect, function(elem) {return elem.selectId === id});
      if (elem) {
        if (searchString.length === 0) {
          $scope[id + '_options'] = [];
        } else {
          return $http.get('/api/search/' + elem.ref + '?q=' + searchString).then(function(response) {
            $scope[id + '_options'] = response.data.results;
          });
        }
      } else {
        throw new Error('Could not find uiSelect element for ' + id);
      }
    };

    angular.element($window).bind('resize', function() {
      $scope.windowResizeUiSelect(true);
      return $scope.$apply();
    });

    $timeout($scope.windowResizeUiSelect);
  });

  uiSelectModule.directive('fngUiSelect', ['$compile','$window','pluginHelper', 'cssFrameworkService', 'formMarkupHelper', 'uiSelectHelper',
    function ($compile, $window, pluginHelper, cssFrameworkService, formMarkupHelper, uiSelectHelper) {
    return {
      restrict: 'E',
      controller: 'FngUISelectCtrl',
      link: function (scope, element, attr) {
        var processedAttr = pluginHelper.extractFromAttr(attr, 'fngUiSelect');
        var elemScope = {selectId: processedAttr.info.id};
        var multi = (processedAttr.info.array === 'true');
        var elementHtml;
        var input='';

        scope.uiSelect.push(elemScope);
        scope.conversions[processedAttr.info.name] = processedAttr.directiveOptions;

        // Sort out the theme, defaulting to select2 (so old users won't see the change).  Bootstrap theme only works with Bootstrap 3
        var theme = processedAttr.directiveOptions.theme || 'select2';
        if (processedAttr.info.theme === 'bootstrap' && cssFrameworkService.framework !== 'bs3') {
          theme = 'select2';
        }

        var multiControl = false;
        var multiStr = '';
        if (multi && (processedAttr.directiveOptions.fngajax === 'true' || processedAttr.directiveOptions.forcemultiple === 'true')) {
          // We need the array to be an array of objects with a x property.  This tells forms-angular to convert it by
          // adding an attribute to the schema.
          pluginHelper.findIdInSchemaAndFlagNeedX(scope.baseSchema(), processedAttr.info.id);
          multiControl = true;
        } else {
          multiStr = multi ? 'multiple close-on-select reset-search-input ' : '';
        }

        var requiredStr = '';
        var allowClearStr = '';
        if (processedAttr.info.required) {
          requiredStr = ' ng-required="true"';
        } else {
          allowClearStr = ' allow-clear';
        }

        // First of all add a hidden input field which we will use to set the width of the select
        var hiddenInputInfo = {
          id: processedAttr.info.id + '_width-helper',
          label: ''
        };
        input = pluginHelper.buildInputMarkup(scope, attr.model, hiddenInputInfo, processedAttr.options, multiControl, multiControl, function (buildingBlocks) {
          return '<input id="' + hiddenInputInfo.id + '" type="text" class="form-control" disabled="" style="position: absolute; left: -1200px;">';
        });
        elementHtml = pluginHelper.buildInputMarkup(scope, attr.model, processedAttr.info, processedAttr.options, multiControl, multiControl, function (buildingBlocks) {
          // set up the ui-select directives
          var select = '<ui-select ' + multiStr + buildingBlocks.common + requiredStr + ' theme="' + theme + '" ng-disabled="disabled" style="width:300px;">';
          select += '<ui-select-match' + allowClearStr + ' placeholder="' + (processedAttr.info.placeholder || 'Select an option...') + '">';

          if (processedAttr.directiveOptions.fngajax === 'true') {
            // Set up lookup function
            scope.conversions[processedAttr.info.name].fngajax = uiSelectHelper.lookupFunc;
            // Use the forms-angular API to query the referenced collection
            elemScope.ref = processedAttr.info.ref;
            scope[processedAttr.info.id + '_options'] = [];
            if (multiControl) {
              select += '{{$select.selected.text}}';
            } else {
              select += '{{' + attr.model + '.' + processedAttr.info.name + '.text}}';
            }
            select += '</ui-select-match>';
            select += '<ui-select-choices repeat="option in ' + processedAttr.info.id + '_options track by $index" ';
            select += 'refresh="refreshOptions($select.search, \'' + processedAttr.info.id + '\')" ';
            select += 'refresh-delay="0"> ';
            select += '

';
          } else if (processedAttr.info.options) {
            // Simple case - enumerated options on the form scope
            if (multiControl) {
              select += '{{$select.selected}}';
            } else {
              select += multi ? '{{$item}}' : '{{$select.selected}}';
            }
            select += '</ui-select-match>';
            select += '<ui-select-choices repeat="option in ' + processedAttr.info.options + ' | filter:$select.search">';
            select += '

';
          }
          select += '</ui-select-choices>';
          select += '</ui-select>';
          return select;
        });
        element.replaceWith($compile(input + elementHtml)(scope));
      }
    }
  }]);
})();
<commit_msg>fix(gen) Add name to width helper element<commit_after>(function () {
  'use strict';

  var uiSelectModule = angular.module('fng.uiSelect', ['ui.select']);

  uiSelectModule.factory('uiSelectHelper', ['$rootScope', '$q', 'SubmissionsService', function ($rootScope, $q, SubmissionsService) {
    var lastW, lastH;
    return {
      windowChanged: function(w,h) {
        var result = false;
        if (w !== lastW || h !== lastH) {
          lastW = w;
          lastH = h;
          result = true;
        }
        return result;
      },
      lookupFunc: function(value, formSchema, cb) {
        if (formSchema.array) {
          // TODO extend back end to do multiple lookups in one hit

          var promises = [];
          var results = [];
          angular.forEach(value, function(obj) {
            promises.push(SubmissionsService.getListAttributes(formSchema.ref, obj.x));
          });
          $q.all(promises).then(function(responses) {
            angular.forEach(responses, function(response) {
                results.push({x:{id:value.shift().x, text:response.data.list}});
              });
            cb(formSchema, results);
            setTimeout(function() {
              $rootScope.$digest();
            });
          });
        } else {
          SubmissionsService.getListAttributes(formSchema.ref, value).then(function(response) {
            cb(formSchema, {id: value, text: response.data.list});
          });
        }
      }
    };
  }]);

  uiSelectModule.controller('FngUISelectCtrl',function($scope, $window, $timeout, $http, uiSelectHelper) {

    $scope.uiSelect = $scope.uiSelect || [];

    $scope.windowResizeUiSelect = function(checkResize) {
      // This is to stop the resize firing too often.  Really horrible way of doing it caused by some very regrettable design choices which will be factored out later
      // Actually not choices - just not knowing any better
      if (!checkResize || uiSelectHelper.windowChanged($window.innerWidth, $window.innerHeight)) {
        for (var i = 0; i < $scope.uiSelect.length; i++) {
          var selectId = $scope.uiSelect[i].selectId;
          var select = document.getElementById(selectId);
          if (select) {
            var helper = document.getElementById(selectId + '_width-helper');
            if (helper) {
              var w = helper.offsetWidth;
              if (select.offsetLeft + w > $window.innerWidth) {
                w = $window.innerWidth - select.offsetLeft - 20;
              }
              angular.element('#' + selectId).css('width', w);
            } else {
              console.log('No helper element found for ' + selectId);
            }
          }
        }
      }
    };

    $scope.refreshOptions = function(searchString, id) {
      var elem = _.find($scope.uiSelect, function(elem) {return elem.selectId === id});
      if (elem) {
        if (searchString.length === 0) {
          $scope[id + '_options'] = [];
        } else {
          return $http.get('/api/search/' + elem.ref + '?q=' + searchString).then(function(response) {
            $scope[id + '_options'] = response.data.results;
          });
        }
      } else {
        throw new Error('Could not find uiSelect element for ' + id);
      }
    };

    angular.element($window).bind('resize', function() {
      $scope.windowResizeUiSelect(true);
      return $scope.$apply();
    });

    $timeout($scope.windowResizeUiSelect);
  });

  uiSelectModule.directive('fngUiSelect', ['$compile','$window','pluginHelper', 'cssFrameworkService', 'formMarkupHelper', 'uiSelectHelper',
    function ($compile, $window, pluginHelper, cssFrameworkService, formMarkupHelper, uiSelectHelper) {
    return {
      restrict: 'E',
      controller: 'FngUISelectCtrl',
      link: function (scope, element, attr) {
        var processedAttr = pluginHelper.extractFromAttr(attr, 'fngUiSelect');
        var elemScope = {selectId: processedAttr.info.id};
        var multi = (processedAttr.info.array === 'true');
        var elementHtml;
        var input='';

        scope.uiSelect.push(elemScope);
        scope.conversions[processedAttr.info.name] = processedAttr.directiveOptions;

        // Sort out the theme, defaulting to select2 (so old users won't see the change).  Bootstrap theme only works with Bootstrap 3
        var theme = processedAttr.directiveOptions.theme || 'select2';
        if (processedAttr.info.theme === 'bootstrap' && cssFrameworkService.framework !== 'bs3') {
          theme = 'select2';
        }

        var multiControl = false;
        var multiStr = '';
        if (multi && (processedAttr.directiveOptions.fngajax === 'true' || processedAttr.directiveOptions.forcemultiple === 'true')) {
          // We need the array to be an array of objects with a x property.  This tells forms-angular to convert it by
          // adding an attribute to the schema.
          pluginHelper.findIdInSchemaAndFlagNeedX(scope.baseSchema(), processedAttr.info.id);
          multiControl = true;
        } else {
          multiStr = multi ? 'multiple close-on-select reset-search-input ' : '';
        }

        var requiredStr = '';
        var allowClearStr = '';
        if (processedAttr.info.required) {
          requiredStr = ' ng-required="true"';
        } else {
          allowClearStr = ' allow-clear';
        }

        // First of all add a hidden input field which we will use to set the width of the select
        var hiddenInputInfo = {
          id: processedAttr.info.id + '_width-helper',
          name: processedAttr.info.name + '_width-helper',
          label: ''
        };
        input = pluginHelper.buildInputMarkup(scope, attr.model, hiddenInputInfo, processedAttr.options, multiControl, multiControl, function (buildingBlocks) {
          return '<input id="' + hiddenInputInfo.id + '" type="text" class="form-control" disabled="" style="position: absolute; left: -1200px;">';
        });
        elementHtml = pluginHelper.buildInputMarkup(scope, attr.model, processedAttr.info, processedAttr.options, multiControl, multiControl, function (buildingBlocks) {
          // set up the ui-select directives
          var select = '<ui-select ' + multiStr + buildingBlocks.common + requiredStr + ' theme="' + theme + '" ng-disabled="disabled" style="width:300px;">';
          select += '<ui-select-match' + allowClearStr + ' placeholder="' + (processedAttr.info.placeholder || 'Select an option...') + '">';

          if (processedAttr.directiveOptions.fngajax === 'true') {
            // Set up lookup function
            scope.conversions[processedAttr.info.name].fngajax = uiSelectHelper.lookupFunc;
            // Use the forms-angular API to query the referenced collection
            elemScope.ref = processedAttr.info.ref;
            scope[processedAttr.info.id + '_options'] = [];
            if (multiControl) {
              select += '{{$select.selected.text}}';
            } else {
              select += '{{' + attr.model + '.' + processedAttr.info.name + '.text}}';
            }
            select += '</ui-select-match>';
            select += '<ui-select-choices repeat="option in ' + processedAttr.info.id + '_options track by $index" ';
            select += 'refresh="refreshOptions($select.search, \'' + processedAttr.info.id + '\')" ';
            select += 'refresh-delay="0"> ';
            select += '

';
          } else if (processedAttr.info.options) {
            // Simple case - enumerated options on the form scope
            if (multiControl) {
              select += '{{$select.selected}}';
            } else {
              select += multi ? '{{$item}}' : '{{$select.selected}}';
            }
            select += '</ui-select-match>';
            select += '<ui-select-choices repeat="option in ' + processedAttr.info.options + ' | filter:$select.search">';
            select += '

';
          }
          select += '</ui-select-choices>';
          select += '</ui-select>';
          return select;
        });
        element.replaceWith($compile(input + elementHtml)(scope));
      }
    }
  }]);
})();
<|endoftext|>


<commit_before>Editable.events = (function() {
  'use strict';

  return {

    setup: function() {
      var $document = $(document);
      var _this = this;

      $document.on('keydown.editable', '.-js-editable', function(event) {
        if (_this.actOnKeyStroke(event)) {
          event.preventDefault();
          event.stopPropagation();
        }
      }).on('focus.editable', '.-js-editable', function(event) {
        console.log('Focus');
      }).on('blur.editable', '.-js-editable', function(event) {
        console.log('Blur');
      });;

      // cache selectionChanged function for simplicity
      var selectionChanged = Editable.selectionWatcher.selectionChanged;

      if (Editable.browserFeatures.selectionchange) {
        var selectionDirty = false;
        var suppressSelectionChanges = false;

        // fires on mousemove (thats probably a bit too much)
        // catches changes like 'select all' from context menu
        $document.on('selectionchange.editable', function(event) {
          if (suppressSelectionChanges) {
            selectionDirty = true;
          } else {
            selectionChanged();
          }
        });

        // listen for selection changes by mouse so we can
        // suppress the selectionchange event and only fire the
        // change event on mouseup
        $document.on('mousedown.editable', '.-js-editable', function(event) {
          suppressSelectionChanges = true;
          $document.on('mouseup.editableSelection', function(event) {
            $document.off('.editableSelection');
            suppressSelectionChanges = false;

            if (selectionDirty) {
              selectionDirty = false;
              selectionChanged();
            }
          });
        });

      } else {

        // listen for selection changes by mouse
        $document.on('mouseup.editableSelection', function(event) {
          selectionChanged();
        });

        // listen for selection changes by keys
        $document.on('keyup.editable', '.-js-editable', function(event) {
          selectionChanged();
        });

      }

    },

    actOnKeyStroke: function(event) {
      switch (event.keyCode) {

      // Left
      case 37:
        console.log('Left arrow');
        return false;

      // Right
      case 39:
        console.log('Right arrow');
        return false;

      // Up
      case 38:
        console.log('Up arrow');
        return false;

      // Down
      case 40:
        console.log('Down arrow');
        return false;

      // Tab
      case 9:
        if (event.shiftKey) {
          console.log('Shift Tab');
          return false;
        } else {
          console.log('Tab');
          return false;
        }
        break;

      // Escape
      case 27:
        console.log('Escape');
        return false;

      // Backspace
      case 8:
        console.log('Backspace');
        return false;

      // Delete
      case 46:
        console.log('Delete');
        return false;

      // Enter
      case 13:
        if (event.shiftKey) {
          console.log('Shift Enter');
          return false;
        } else {
          console.log('Enter');
          return false;
        }
        break;

      default:
        return false;
      }
    }

  };
})();
<commit_msg>Intercepting copy/cut/paste using jquery event<commit_after>Editable.events = (function() {
  'use strict';

  return {

    setup: function() {
      var $document = $(document);
      var _this = this;

      $document.on('keydown.editable', '.-js-editable', function(event) {
        if (_this.actOnKeyStroke(event)) {
          event.preventDefault();
          event.stopPropagation();
        }
      }).on('focus.editable', '.-js-editable', function(event) {
        console.log('Focus');
      }).on('blur.editable', '.-js-editable', function(event) {
        console.log('Blur');
      }).on('copy.editable', '.-js-editable', function(event) {
        console.log('Copy');
      }).on('cut.editable', '.-js-editable', function(event) {
        console.log('Cut');
      }).on('paste.editable', '.-js-editable', function(event) {
        console.log('Paste');
      });

      // cache selectionChanged function for simplicity
      var selectionChanged = Editable.selectionWatcher.selectionChanged;

      if (Editable.browserFeatures.selectionchange) {
        var selectionDirty = false;
        var suppressSelectionChanges = false;

        // fires on mousemove (thats probably a bit too much)
        // catches changes like 'select all' from context menu
        $document.on('selectionchange.editable', function(event) {
          if (suppressSelectionChanges) {
            selectionDirty = true;
          } else {
            selectionChanged();
          }
        });

        // listen for selection changes by mouse so we can
        // suppress the selectionchange event and only fire the
        // change event on mouseup
        $document.on('mousedown.editable', '.-js-editable', function(event) {
          suppressSelectionChanges = true;
          $document.on('mouseup.editableSelection', function(event) {
            $document.off('.editableSelection');
            suppressSelectionChanges = false;

            if (selectionDirty) {
              selectionDirty = false;
              selectionChanged();
            }
          });
        });

      } else {

        // listen for selection changes by mouse
        $document.on('mouseup.editableSelection', function(event) {
          selectionChanged();
        });

        // listen for selection changes by keys
        $document.on('keyup.editable', '.-js-editable', function(event) {
          selectionChanged();
        });

      }

    },

    actOnKeyStroke: function(event) {
      switch (event.keyCode) {

      // Left
      case 37:
        console.log('Left arrow');
        return false;

      // Right
      case 39:
        console.log('Right arrow');
        return false;

      // Up
      case 38:
        console.log('Up arrow');
        return false;

      // Down
      case 40:
        console.log('Down arrow');
        return false;

      // Tab
      case 9:
        if (event.shiftKey) {
          console.log('Shift Tab');
          return false;
        } else {
          console.log('Tab');
          return false;
        }
        break;

      // Escape
      case 27:
        console.log('Escape');
        return false;

      // Backspace
      case 8:
        console.log('Backspace');
        return false;

      // Delete
      case 46:
        console.log('Delete');
        return false;

      // Enter
      case 13:
        if (event.shiftKey) {
          console.log('Shift Enter');
          return false;
        } else {
          console.log('Enter');
          return false;
        }
        break;

      default:
        return false;
      }
    }

  };
})();
<|endoftext|>


<commit_before>
import _ from 'lodash';
import express from 'express';
import WebSocket from 'ws';
import bodyParser from 'body-parser';
import superagent from 'superagent';
import mongodb from 'mongodb';
import core from './forest-core';
import auth from './auth';

let serverHost=null;
let serverPort=0;

// --------------------------------

const logRequest = (req, res, next) => {
  console.log('---------------------------->');
  if(req.method==='POST') console.log(req.method, req.originalUrl, req.body && req.body.UID, req.headers.authorization||'');
  else                    console.log(req.method, req.originalUrl,                           req.headers.authorization||'');
  next();
};

const logResponse = (req, res, next) => {
  console.log(res.statusCode);
  console.log('<----------------------------');
  next();
};

const CORS = (req, res, next) => {
  res.header('Access-Control-Allow-Origin','*');
  res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, Authorization'); // Accept, X-Requested-By, Origin, Cache-Control
  next();
};

const app = express();

app.use(bodyParser.json());

app.options("/*",
  logRequest,
  CORS,
  (req, res, next) => {
    res.sendStatus(200);
  },
  logResponse,
);

function prefixUIDs(o){
  const s = JSON.stringify(_.omit(o, core.localProps), null, 2);
  return s.replace(/"(uid-[^"]*"[^:])/g, `"http://${serverHost}:${serverPort}/$1`);
}

function checkPKAndReturnObject(Peer, uid, res, r){
  if(!r.PK || r.PK==='FAIL'){
    res.status(404).send('Not found');
  }
  else if(r.PK==='OK' || auth.checkSig(r.PK)){
  // TODO: delete r
    return core.getObject(uid).then(o => {
      res.json(JSON.parse(prefixUIDs(o)));
      if(Peer) core.setNotify(o,Peer);
    });
  }
  else{
    res.status(404).send('Not found');
  }
  // TODO: delete r
}

app.get('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const { Peer, User } = auth.getPeerUser(req);
    const uid = req.originalUrl.substring(1);
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'GET',
      url: uid,
    });
    core.runEvaluator(rc).then(r => {
      if(r.PK) return checkPKAndReturnObject(Peer, uid, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndReturnObject(Peer, uid, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(404).send('Not found');
      next();
    });
  },
  logResponse,
);

function checkPKAndSaveObject(User, Peer, json, path, res, r){
  if(!r.PK || r.PK==='FAIL'){
    res.status(403).send('Forbidden');
  }
  else if(r.PK==='OK' || auth.checkSig(r.PK)){
    core.incomingObject(Object.assign({ User }, Peer && { Peer }, json), path!=='notify' && path);
    res.json({ });
  }
  else{
    res.status(403).send('Forbidden');
  }
  // TODO: delete r
}

app.post('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const json=req.body;
    if(!json || !json.UID) next();
    const { Peer, User } = auth.getPeerUser(req);
    const path = req.originalUrl.substring(1);
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'POST',
      url: json.UID,
    });
    core.runEvaluator(rc).then(r => {
      if(r.PK) return checkPKAndSaveObject(User, Peer, json, path, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndSaveObject(User, Peer, json, path, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(403).send('Forbidden');
      next();
    });
  },
  logResponse,
);

function doGet(url){
  return superagent.get(url)
    .timeout({ response: 9000, deadline: 10000 })
    .set(auth.makeHTTPAuth())
    .then(x => x.body);
}

const pendingWSpackets = {};

function doPost(o, u){
  if(core.isURL(u)) return Promise.resolve(false);
  const Peer = core.isNotify(u)? u: o.Peer;
  if(!pendingWSpackets[Peer]) pendingWSpackets[Peer] = [];
  pendingWSpackets[Peer].push(prefixUIDs(o));
  wsFlush(Peer);
  return Promise.resolve(true);
}

const peer2ws = {};

function wsInit(config){
  const wss = new WebSocket.Server(config);
  wss.on('connection', (ws) => {
    ws.on('message', (data) => {
      const json = JSON.parse(data);
      if(json.Peer){
        console.log('ws init:', json);
        peer2ws[json.Peer]=ws;
        ws.send(auth.makeWSAuth());
        wsFlush(json.Peer);
      }
      else{
        console.log('ws incoming json:', json);
      }
    });
  });
}

function wsFlush(Peer){
  const ws = peer2ws[Peer];
  if(!ws){
    console.log('websocket not found', Peer);
    return;
  }
  let packet;
  while((packet=(pendingWSpackets[Peer]||[]).shift())){
    try{
      console.log('<<----------ws---------------', Peer);
      if(ws.readyState === ws.OPEN) ws.send(packet);
      else console.log('WebSocket closed sending\n', packet, '\nto', Peer);
    }
    catch(e){
      console.error('error sending\n', packet, '\nto', Peer, '\n', e);
    }
  }
}

core.setNetwork({ doGet, doPost });

// --------------------------------

let forestdb;

function persistenceInit(mongoHostPort){
  return mongodb.MongoClient.connect(mongoHostPort)
    .then((client) => {
      forestdb = client.db('forest');
    });
}

function persist(o){
  if(!o.is) return Promise.resolve();
  const collectionName = (o.is.constructor===String)? o.is:
                        ((o.is.constructor===Array)? o.is.join('-'): null);
  if(!collectionName) return Promise.resolve();
  return forestdb.collection(collectionName)
    .update({ UID: o.UID }, o, { upsert: true })
    .then(result => result.result)
    .catch(err => { console.log(err, `Failed to insert ${o.UID} in ${collectionName}`); return err });
}

function fetch(uid){
  return forestdb.collections().then(colls=>findOneFirst(colls, uid));
}

function findOneFirst(colls, uid){
  if(!(colls && colls.length)) return Promise.resolve(null);
  return colls[0].findOne({ UID: uid }, { projection: { _id: 0 }}).then(o => o || findOneFirst(colls.slice(1), uid));
}

function recache(){
  return forestdb.collections()
    .then(colls=>Promise.all(colls.map(coll=>coll.find({ Cache: 'keep-active' }, { projection: { _id: 0 }}).toArray()))
                  .then(actives => [].concat(...actives)));
}

function query(is, scope, query){
  return forestdb.collection(is.join('-'))
    .find(toMongo(scope, query.match), { projection: { _id: 0 }})
    .toArray()
    .then(r => r.map(o => query.inline? getInlineVals(o, query.inline): o.UID))
    .catch(e => console.error(e));
}

function toMongo(scope, match){
  return Object.assign({}, ...Object.keys(match).map(k => ({[k]: toMongoProp(k,match[k])})));
}

function toMongoProp(key, val){
  if(val.length===3 && val[1]==='..'){
    return { $gt: val[0], $lt: val[2] };
  }
  return val;
}

function getInlineVals(o, inline){
  return Object.assign({}, ...inline.map(k => o[k] && { [k]: o[k] }), { More: o.UID });
}

function dropAll(){
  return forestdb.collections()
            .then(colls=>colls.map(coll=>coll.stats()
              .then(s=>{
                  if(!/system.indexes/.test(s.ns)){
                    console.log('*************** dropping', s.ns, s.count);
                    coll.drop()
                  }
              })
            ));
}

core.setPersistence({ persist, fetch, recache, query });

// --------------------------------

function init({httpHost, httpPort, wsPort, mongoHostPort}){
  serverHost=httpHost; serverPort=httpPort;
  return new Promise((resolve, reject) => {
    persistenceInit(mongoHostPort)
      .then(() => {
        app.listen(httpPort, ()=>{
          console.log(`Server started on port ${httpPort}`);
          wsInit({ port: wsPort });
          resolve();
        }).on('error', (err) => reject(err));
      })
      .catch((err) => reject(err));
  });
}

export default {
  init,
  dropAll,
  cacheObjects:        core.cacheObjects,
  reCacheObjects:      core.reCacheObjects,
  setEvaluator:        core.setEvaluator,
  getObject:           core.getObject,
  spawnObject:         core.spawnObject,
  makeUID:             core.makeUID,
  setLogging:          core.setLogging,
  setPeerIdentityUser: auth.setPeerIdentityUser,
}

<commit_msg>dropAll proper promise handling<commit_after>
import _ from 'lodash';
import express from 'express';
import WebSocket from 'ws';
import bodyParser from 'body-parser';
import superagent from 'superagent';
import mongodb from 'mongodb';
import core from './forest-core';
import auth from './auth';

let serverHost=null;
let serverPort=0;

// --------------------------------

const logRequest = (req, res, next) => {
  console.log('---------------------------->');
  if(req.method==='POST') console.log(req.method, req.originalUrl, req.body && req.body.UID, req.headers.authorization||'');
  else                    console.log(req.method, req.originalUrl,                           req.headers.authorization||'');
  next();
};

const logResponse = (req, res, next) => {
  console.log(res.statusCode);
  console.log('<----------------------------');
  next();
};

const CORS = (req, res, next) => {
  res.header('Access-Control-Allow-Origin','*');
  res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, Authorization'); // Accept, X-Requested-By, Origin, Cache-Control
  next();
};

const app = express();

app.use(bodyParser.json());

app.options("/*",
  logRequest,
  CORS,
  (req, res, next) => {
    res.sendStatus(200);
  },
  logResponse,
);

function prefixUIDs(o){
  const s = JSON.stringify(_.omit(o, core.localProps), null, 2);
  return s.replace(/"(uid-[^"]*"[^:])/g, `"http://${serverHost}:${serverPort}/$1`);
}

function checkPKAndReturnObject(Peer, uid, res, r){
  if(!r.PK || r.PK==='FAIL'){
    res.status(404).send('Not found');
  }
  else if(r.PK==='OK' || auth.checkSig(r.PK)){
  // TODO: delete r
    return core.getObject(uid).then(o => {
      res.json(JSON.parse(prefixUIDs(o)));
      if(Peer) core.setNotify(o,Peer);
    });
  }
  else{
    res.status(404).send('Not found');
  }
  // TODO: delete r
}

app.get('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const { Peer, User } = auth.getPeerUser(req);
    const uid = req.originalUrl.substring(1);
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'GET',
      url: uid,
    });
    core.runEvaluator(rc).then(r => {
      if(r.PK) return checkPKAndReturnObject(Peer, uid, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndReturnObject(Peer, uid, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(404).send('Not found');
      next();
    });
  },
  logResponse,
);

function checkPKAndSaveObject(User, Peer, json, path, res, r){
  if(!r.PK || r.PK==='FAIL'){
    res.status(403).send('Forbidden');
  }
  else if(r.PK==='OK' || auth.checkSig(r.PK)){
    core.incomingObject(Object.assign({ User }, Peer && { Peer }, json), path!=='notify' && path);
    res.json({ });
  }
  else{
    res.status(403).send('Forbidden');
  }
  // TODO: delete r
}

app.post('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const json=req.body;
    if(!json || !json.UID) next();
    const { Peer, User } = auth.getPeerUser(req);
    const path = req.originalUrl.substring(1);
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'POST',
      url: json.UID,
    });
    core.runEvaluator(rc).then(r => {
      if(r.PK) return checkPKAndSaveObject(User, Peer, json, path, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndSaveObject(User, Peer, json, path, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(403).send('Forbidden');
      next();
    });
  },
  logResponse,
);

function doGet(url){
  return superagent.get(url)
    .timeout({ response: 9000, deadline: 10000 })
    .set(auth.makeHTTPAuth())
    .then(x => x.body);
}

const pendingWSpackets = {};

function doPost(o, u){
  if(core.isURL(u)) return Promise.resolve(false);
  const Peer = core.isNotify(u)? u: o.Peer;
  if(!pendingWSpackets[Peer]) pendingWSpackets[Peer] = [];
  pendingWSpackets[Peer].push(prefixUIDs(o));
  wsFlush(Peer);
  return Promise.resolve(true);
}

const peer2ws = {};

function wsInit(config){
  const wss = new WebSocket.Server(config);
  wss.on('connection', (ws) => {
    ws.on('message', (data) => {
      const json = JSON.parse(data);
      if(json.Peer){
        console.log('ws init:', json);
        peer2ws[json.Peer]=ws;
        ws.send(auth.makeWSAuth());
        wsFlush(json.Peer);
      }
      else{
        console.log('ws incoming json:', json);
      }
    });
  });
}

function wsFlush(Peer){
  const ws = peer2ws[Peer];
  if(!ws){
    console.log('websocket not found', Peer);
    return;
  }
  let packet;
  while((packet=(pendingWSpackets[Peer]||[]).shift())){
    try{
      console.log('<<----------ws---------------', Peer);
      if(ws.readyState === ws.OPEN) ws.send(packet);
      else console.log('WebSocket closed sending\n', packet, '\nto', Peer);
    }
    catch(e){
      console.error('error sending\n', packet, '\nto', Peer, '\n', e);
    }
  }
}

core.setNetwork({ doGet, doPost });

// --------------------------------

let forestdb;

function persistenceInit(mongoHostPort){
  return mongodb.MongoClient.connect(mongoHostPort)
    .then((client) => {
      forestdb = client.db('forest');
    });
}

function persist(o){
  if(!o.is) return Promise.resolve();
  const collectionName = (o.is.constructor===String)? o.is:
                        ((o.is.constructor===Array)? o.is.join('-'): null);
  if(!collectionName) return Promise.resolve();
  return forestdb.collection(collectionName)
    .update({ UID: o.UID }, o, { upsert: true })
    .then(result => result.result)
    .catch(err => { console.log(err, `Failed to insert ${o.UID} in ${collectionName}`); return err });
}

function fetch(uid){
  return forestdb.collections().then(colls=>findOneFirst(colls, uid));
}

function findOneFirst(colls, uid){
  if(!(colls && colls.length)) return Promise.resolve(null);
  return colls[0].findOne({ UID: uid }, { projection: { _id: 0 }}).then(o => o || findOneFirst(colls.slice(1), uid));
}

function recache(){
  return forestdb.collections()
    .then(colls=>Promise.all(colls.map(coll=>coll.find({ Cache: 'keep-active' }, { projection: { _id: 0 }}).toArray()))
                  .then(actives => [].concat(...actives)));
}

function query(is, scope, query){
  return forestdb.collection(is.join('-'))
    .find(toMongo(scope, query.match), { projection: { _id: 0 }})
    .toArray()
    .then(r => r.map(o => query.inline? getInlineVals(o, query.inline): o.UID))
    .catch(e => console.error(e));
}

function toMongo(scope, match){
  return Object.assign({}, ...Object.keys(match).map(k => ({[k]: toMongoProp(k,match[k])})));
}

function toMongoProp(key, val){
  if(val.length===3 && val[1]==='..'){
    return { $gt: val[0], $lt: val[2] };
  }
  return val;
}

function getInlineVals(o, inline){
  return Object.assign({}, ...inline.map(k => o[k] && { [k]: o[k] }), { More: o.UID });
}

function dropAll(){
  return forestdb.collections()
            .then(colls=>Promise.all(colls.map(coll=>coll.stats()
              .then(s=>{
                  if(!/system.indexes/.test(s.ns)){
                    console.log('*************** dropping', s.ns, s.count);
                    return coll.drop();
                  }
              })
            )));
}

core.setPersistence({ persist, fetch, recache, query });

// --------------------------------

function init({httpHost, httpPort, wsPort, mongoHostPort}){
  serverHost=httpHost; serverPort=httpPort;
  return new Promise((resolve, reject) => {
    persistenceInit(mongoHostPort)
      .then(() => {
        app.listen(httpPort, ()=>{
          console.log(`Server started on port ${httpPort}`);
          wsInit({ port: wsPort });
          resolve();
        }).on('error', (err) => reject(err));
      })
      .catch((err) => reject(err));
  });
}

export default {
  init,
  dropAll,
  cacheObjects:        core.cacheObjects,
  reCacheObjects:      core.reCacheObjects,
  setEvaluator:        core.setEvaluator,
  getObject:           core.getObject,
  spawnObject:         core.spawnObject,
  makeUID:             core.makeUID,
  setLogging:          core.setLogging,
  setPeerIdentityUser: auth.setPeerIdentityUser,
}

<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { connect } from 'react-redux';

class WeatherList extends Component {
  render() {
    return(

            City
            Temperature
            Pressure
            Humidity

    );
  }
}

function mapStateToProps({ weather }) {
  return { weather }; //equal to { weather: state.weather } but using fancy ES6 syntax
}

export default connect(mapStateToProps)(WeatherList);
<commit_msg>Made a list with the weather of cities<commit_after>import React, { Component } from 'react';
import { connect } from 'react-redux';

class WeatherList extends Component {
  renderWeather(cityData) {
    return(

        {cityData.city.name}

    );
  }

  render() {
    return(

            City
            Temperature
            Pressure
            Humidity

          {this.props.weather.map(this.renderWeather)}

    );
  }
}

function mapStateToProps({ weather }) {
  return { weather }; //equal to { weather: state.weather } but using fancy ES6 syntax
}

export default connect(mapStateToProps)(WeatherList);
<|endoftext|>


<commit_before>import {PropTypes} from 'react'
import {getUnfulfilledRoleComponent} from './components/UnfulfilledRole'

function Sanity({roles}) {
  function getRole(roleName) {
    const fulfiller = roles[roleName]
    if (!fulfiller) {
      return null
    }

    return fulfiller.module.__esModule && fulfiller.module.default
      ? fulfiller.module.default
      : fulfiller.module
  }

  function getComponents(wanted) {
    return Object.keys(wanted).reduce((target, key) => {
      const role = typeof wanted[key] === 'string'
        ? {name: wanted[key]}
        : wanted[key]

      target[key] = getRole(role.name) || getUnfulfilledRoleComponent(role)

      return target
    }, {})
  }

  function getPluginForRole(roleName) {
    return roles[roleName] && roles[roleName].plugin
  }

  return {
    getRole,
    getComponents,
    getPluginForRole
  }
}

export const sanityShape = {
  sanity: PropTypes.shape({
    getRole: PropTypes.func.isRequired,
    getComponents: PropTypes.func.isRequired,
    getPluginForRole: PropTypes.func.isRequired
  })
}

export default Sanity
<commit_msg>Resolve ES6-modules in multi-fulfiller roles<commit_after>import {PropTypes} from 'react'
import {getUnfulfilledRoleComponent} from './components/UnfulfilledRole'

function Sanity({roles}) {
  function getRole(roleName) {
    const fulfiller = roles[roleName]
    if (!fulfiller) {
      return null
    }

    return Array.isArray(fulfiller)
      ? fulfiller.map(getModule)
      : getModule(fulfiller)
  }

  function getComponents(wanted) {
    return Object.keys(wanted).reduce((target, key) => {
      const role = typeof wanted[key] === 'string'
        ? {name: wanted[key]}
        : wanted[key]

      target[key] = getRole(role.name) || getUnfulfilledRoleComponent(role)

      return target
    }, {})
  }

  function getPluginForRole(roleName) {
    return roles[roleName] && roles[roleName].plugin
  }

  return {
    getRole,
    getComponents,
    getPluginForRole
  }
}

function getModule(fulfiller) {
  return fulfiller.module.__esModule && fulfiller.module.default
    ? fulfiller.module.default
    : fulfiller.module
}

export const sanityShape = {
  sanity: PropTypes.shape({
    getRole: PropTypes.func.isRequired,
    getComponents: PropTypes.func.isRequired,
    getPluginForRole: PropTypes.func.isRequired
  })
}

export default Sanity
<|endoftext|>


<commit_before>(function($) {
  // events to register as functions on tree
  var events = ["click", "dblclick", "mousedown", "mousemove", "mouseup",
                "mouseenter", "mouseleave"];
  // returns a function for the passed eventType that binds a passed
  // function to that eventType nodes/edges in the tree
  var eventhandler = function(eventType) {
    return function(data, handler, options) {
      // default options; not enabled for edges by default
      var defaultopts = {node: true, edge: false},
          opts = defaultopts; // by default, go with default options
      if (typeof options === "object") { // 3 arguments, last one is options
        opts = jQuery.extend(defaultopts, options);
      } else if (typeof handler === "object") { // 2 arguments, 2nd is options
        opts = jQuery.extend(defaultopts, handler);
      }
      if ((opts.node && !opts.edge) || (options.node && opts.edge)) {
        // bind an event handler for nodes in this tree
        this.element.on(eventType, ".jsavnode", function(e) {
          var node = $(this).data("node"); // get the JSAV node object
          if ($.isFunction(data)) { // if no data -> 1st arg is the handler function
            // bind this to the node and call handler
            // with the event as parameter
            data.call(node, e);
          } else if ($.isFunction(handler)) { // data provided, 2nd arg is the handler function
            var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array
            params.push(e); // jQuery event as the last parameter
            handler.apply(node, params); // apply the given handler function
          }
        });
      }
      if (opts.edge) { // if supposed to attach the handler to edges
        // find the SVG elements matching this tree's container
        this.jsav.canvas.on(eventType, '.jsavedge[data-container="' + self.id() + '"]', function(e) {
          var edge = $(this).data("edge"); // get the JSAV edge object
          if ($.isFunction(data)) { // no data
            // bind this to the edge and call handler
            // with the event as parameter
            data.call(edge, e);
          } else if ($.isFunction(handler)) { // data provided
            var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array
            params.push(e); // jQuery event as the last parameter
            handler.apply(self, params); // apply the function
          }
        });
      }
      return this; // enable chaining of calls
    };
  };
  var on = function(eventName, data, handler, options) {
    eventhandler(eventName).call(this, data, handler, options);
    return this;
  };

  JSAV.utils._events = {
    _addEventSupport: function(proto) {
      // create the event binding functions and add to the given prototype
      for (var i = events.length; i--; ) {
        proto[events[i]] = eventhandler(events[i]);
      }
      proto.on = on;
    }
  };
})(jQuery);<commit_msg>Fixed bug in edge event handling<commit_after>(function($) {
  // events to register as functions on tree
  var events = ["click", "dblclick", "mousedown", "mousemove", "mouseup",
                "mouseenter", "mouseleave"];
  // returns a function for the passed eventType that binds a passed
  // function to that eventType nodes/edges in the tree
  var eventhandler = function(eventType) {
    return function(data, handler, options) {
      // default options; not enabled for edges by default
      var defaultopts = {edge: false},
          opts = defaultopts; // by default, go with default options
      if (typeof options === "object") { // 3 arguments, last one is options
        opts = $.extend(defaultopts, options);
      } else if (typeof handler === "object") { // 2 arguments, 2nd is options
        opts = $.extend(defaultopts, handler);
      }
      if (!opts.edge || opts.node) {
        // bind an event handler for nodes in this tree
        this.element.on(eventType, ".jsavnode", function(e) {
          var node = $(this).data("node"); // get the JSAV node object
          if ($.isFunction(data)) { // if no data -> 1st arg is the handler function
            // bind this to the node and call handler
            // with the event as parameter
            data.call(node, e);
          } else if ($.isFunction(handler)) { // data provided, 2nd arg is the handler function
            var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array
            params.push(e); // jQuery event as the last parameter
            handler.apply(node, params); // apply the given handler function
          }
        });
      }
      if (opts.edge) { // if supposed to attach the handler to edges
        // find the SVG elements matching this tree's container
        this.jsav.canvas.on(eventType, '.jsavedge[data-container="' + this.id() + '"]', function(e) {
          var edge = $(this).data("edge"); // get the JSAV edge object
          if ($.isFunction(data)) { // no data
            // bind this to the edge and call handler
            // with the event as parameter
            data.call(edge, e);
          } else if ($.isFunction(handler)) { // data provided
            var params = $.isArray(data)?data.slice(0):[data]; // get a cloned array or data as array
            params.push(e); // jQuery event as the last parameter
            handler.apply(edge, params); // apply the function
          }
        });
      }
      return this; // enable chaining of calls
    };
  };
  var on = function(eventName, data, handler, options) {
    eventhandler(eventName).call(this, data, handler, options);
    return this;
  };

  JSAV.utils._events = {
    _addEventSupport: function(proto) {
      // create the event binding functions and add to the given prototype
      for (var i = events.length; i--; ) {
        proto[events[i]] = eventhandler(events[i]);
      }
      proto.on = on;
    }
  };
})(jQuery);<|endoftext|>


<commit_before>// API methods
import fetch from './fetch';

/**
 * API class, has a function for each method of the Telegram API which take
 * an object argument, and send request to the API server
 *
 * Methods: getMe, sendMessage, forwardMessage, sendPhoto, sendAudio,
 * sendDocument, sendSticker, sendVideo, sendLocation, sendChatAction,
 * getUserProfilePhotos, getUpdates
 */
export default class API {
  /**
   * Create a new api object with the given token
   * @param  {string} token
   */
  constructor(token) {
    this.token = token;
  }
}

API.prototype.request = function request(method, data) {
  return fetch(`${this.token}/${method}`, data);
};

const methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto',
'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo',
'sendLocation', 'sendChatAction', 'getUserProfilePhotos',
'getUpdates', 'setWebhook', 'deleteMessage'];

methods.forEach(method => {
  API.prototype[method] = function(data) { //eslint-disable-line
    return this.request(method, data);
  };
});
<commit_msg>Introduce internal API call queueing mechanism to send messages sequentially (no more random order)<commit_after>// API methods
import fetch from './fetch';

/**
 * Simple replacement for Bluebird's Promise.mapSeries() implementation
 * @param {Array} tasks to run serially
 * @param {Function} task function to execute
 * @return {Promise}
 */
function sequence(tasks, fn) {
  return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

/**
 * API class, has a function for each method of the Telegram API which take
 * an object argument, and send request to the API server
 *
 * Methods: getMe, sendMessage, forwardMessage, sendPhoto, sendAudio,
 * sendDocument, sendSticker, sendVideo, sendLocation, sendChatAction,
 * getUserProfilePhotos, getUpdates
 *
 */
export default class API {
  /**
   * Create a new api object with the given token
   * @param  {string} token
   */
  constructor(token) {
    this.token = token;
    this._queue = [];
    this._inUseQueue = [];
  }

  /**
   * Run Telegram API calls serially using internal queueing mechanism
   * @private
   */
  _runQueue() {
    // implementation taken from https://github.com/yagop/node-telegram-bot-api/issues/192#issuecomment-249488807
    if (this._inUseQueue.length || !this._queue.length) return;

    this._inUseQueue = this._queue;
    this._queue = [];

    sequence(this._inUseQueue, request => { //eslint-disable-line
      return this.request(request.method, request.data)
        .then(request.resolve)
        .catch(request.reject);
    }).then(() => {
      this._inUseQueue = [];
      this._runQueue();
    });
  }
}

API.prototype.request = function request(method, data) {
  return fetch(`${this.token}/${method}`, data);
};

const methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto',
  'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo',
  'sendLocation', 'sendChatAction', 'getUserProfilePhotos',
  'getUpdates', 'setWebhook', 'deleteMessage'];

methods.forEach(method => {
  API.prototype[method] = function (data) { //eslint-disable-line
    // implementation taken from https://github.com/yagop/node-telegram-bot-api/issues/192#issuecomment-249488807
    let resolve;
    let reject;

    const promise = new Promise((_resolve, _reject) => {
      resolve = _resolve;
      reject = _reject;
    });

    this._queue.push({ method, data, resolve, reject });
    process.nextTick(this._runQueue.bind(this));

    return promise;
  };
});
<|endoftext|>


<commit_before>import { LineUtil, Point, Polygon, DomEvent } from 'leaflet';
import { defaultOptions, edgesKey, modesKey, polygons } from '../FreeDraw';
import { updateFor } from './Layer';
import createEdges from './Edges';
import { DELETE, APPEND } from './Flags';
import handlePolygonClick from './Polygon';
import concavePolygon from './Concave';
import mergePolygons from './Merge';

/**
 * @method appendEdgeFor
 * @param {Object} map
 * @param {Object} polygon
 * @param {Object} options
 * @param {Array} parts
 * @param {Object} newPoint
 * @param {Object} startPoint
 * @param {Object} endPoint
 * @return {void}
 */
const appendEdgeFor = (map, polygon, options, { parts, newPoint, startPoint, endPoint }) => {

    const latLngs = parts.reduce((accumulator, point, index) => {

        const nextPoint = parts[index + 1] || parts[0];

        if (point === startPoint && nextPoint === endPoint) {

            return [

                // We've found the location to add the new polygon.
                ...accumulator,
                map.containerPointToLatLng(point),
                map.containerPointToLatLng(newPoint)

            ];

        }

        return [ ...accumulator, map.containerPointToLatLng(point) ];

    }, []);

    // Update the lat longs with the newly inserted edge.
    polygon.setLatLngs(latLngs);

    // Remove the current set of edges for the polygon, and then recreate them, assigning the
    // new set of edges back into the polygon.
    polygon[edgesKey].map(edge => map.removeLayer(edge));
    polygon[edgesKey] = createEdges(map, polygon, options);

};

/**
 * @method createFor
 * @param {Object} map
 * @param {Array} latLngs
 * @param {Object} [options = defaultOptions]
 * @param {Boolean} [preventMutations = false]
 * @return {Array|Boolean}
 */
export const createFor = (map, latLngs, options = defaultOptions, preventMutations = false) => {

    // Determine whether we've reached the maximum polygons.
    const limitReached = polygons.get(map).size === options.maximumPolygons;

    // Apply the concave hull algorithm to the created polygon if the options allow.
    const concavedLatLngs = !preventMutations && options.concavePolygon ? concavePolygon(map, latLngs) : latLngs;

    // Simplify the polygon before adding it to the map.
    const addedPolygons = limitReached ? [] : map.simplifyPolygon(map, concavedLatLngs, options).map(latLngs => {

        const polygon = new Polygon(latLngs, {
            ...defaultOptions, ...options, className: 'leaflet-polygon'
        }).addTo(map);

        // Attach the edges to the polygon.
        polygon[edgesKey] = createEdges(map, polygon, options);

        // Disable the propagation when you click on the marker.
        DomEvent.disableClickPropagation(polygon);

        // Yield the click handler to the `handlePolygonClick` function.
        polygon.off('click');
        polygon.on('click', handlePolygonClick(map, polygon, options));

        return polygon;

    });

    // Append the current polygon to the master set.
    addedPolygons.forEach(polygon => polygons.get(map).add(polygon));

    if (!limitReached && !preventMutations && polygons.get(map).size > 1 && options.mergePolygons) {

        // Attempt a merge of all the polygons if the options allow, and the polygon count is above one.
        const addedMergedPolygons = mergePolygons(map, Array.from(polygons.get(map)), options);

        // Clear the set, and added all of the merged polygons into the master set.
        polygons.get(map).clear();
        addedMergedPolygons.forEach(polygon => polygons.get(map).add(polygon));

        return addedMergedPolygons;

    }

    return addedPolygons;

};

/**
 * @method removeFor
 * @param {Object} map
 * @param {Object} polygon
 * @return {void}
 */
export const removeFor = (map, polygon) => {

    // Remove polygon and all of its associated edges.
    map.removeLayer(polygon);
    edgesKey in polygon && polygon[edgesKey].map(edge => map.removeLayer(edge));

    // Remove polygon from the master set.
    polygons.get(map).delete(polygon);

};

/**
 * @method clearFor
 * @param {Object} map
 * @return {void}
 */
export const clearFor = map => {
    Array.from(polygons.get(map).values()).forEach(polygon => removeFor(map, polygon));
};

/**
 * @param {Object} map
 * @param {Object} polygon
 * @param {Object} options
 * @return {Function}
 */
export default (map, polygon, options) => {

    return event => {

        // Gather all of the points from the lat longs of the current polygon.
        const newPoint = map.mouseEventToContainerPoint('originalEvent' in event ? event.originalEvent : event);
        const parts = polygon.getLatLngs()[0].map(latLng => map.latLngToContainerPoint(latLng));

        const { startPoint, endPoint, lowestDistance } = parts.reduce((accumulator, point, index) => {

            const startPoint = point;
            const endPoint = parts[index + 1] || parts[0];
            const distance = LineUtil.pointToSegmentDistance(newPoint, startPoint, endPoint);

            if (distance < accumulator.lowestDistance) {

                // If the distance is less than the previous then we'll update the accumulator.
                return { lowestDistance: distance, startPoint, endPoint };

            }

            // Otherwise we'll simply yield the previous accumulator.
            return accumulator;

        }, { lowestDistance: Infinity, startPoint: new Point(), endPoint: Point() });

        // Setup the conditions for the switch statement to make the cases clearer.
        const mode = map[modesKey];
        const isDelete = Boolean(mode & DELETE);
        const isAppend = Boolean(mode & APPEND);
        const isDeleteAndAppend = Boolean(mode & DELETE && mode & APPEND);

        // Partially apply the remove and append functions.
        const removePolygon = () => removeFor(map, polygon);
        const appendEdge = () => appendEdgeFor(map, polygon, options, { parts, newPoint, startPoint, endPoint });

        switch (true) {

            // If both modes DELETE and APPEND are active then we need to do a little work to determine
            // which action to take based on where the user clicked on the polygon.
            case isDeleteAndAppend:
                lowestDistance > options.elbowDistance ? removePolygon() : appendEdge();
                break;

            case isDelete:
                removePolygon();
                break;

            case isAppend:
                appendEdge();
                break;

        }

        // Trigger the event for having deleted a polygon or appended an edge.
        (isDelete || isAppend) && updateFor(map, isDelete ? 'delete' : 'append');

    };

};
<commit_msg>Changed "delete" to "remove"<commit_after>import { LineUtil, Point, Polygon, DomEvent } from 'leaflet';
import { defaultOptions, edgesKey, modesKey, polygons } from '../FreeDraw';
import { updateFor } from './Layer';
import createEdges from './Edges';
import { DELETE, APPEND } from './Flags';
import handlePolygonClick from './Polygon';
import concavePolygon from './Concave';
import mergePolygons from './Merge';

/**
 * @method appendEdgeFor
 * @param {Object} map
 * @param {Object} polygon
 * @param {Object} options
 * @param {Array} parts
 * @param {Object} newPoint
 * @param {Object} startPoint
 * @param {Object} endPoint
 * @return {void}
 */
const appendEdgeFor = (map, polygon, options, { parts, newPoint, startPoint, endPoint }) => {

    const latLngs = parts.reduce((accumulator, point, index) => {

        const nextPoint = parts[index + 1] || parts[0];

        if (point === startPoint && nextPoint === endPoint) {

            return [

                // We've found the location to add the new polygon.
                ...accumulator,
                map.containerPointToLatLng(point),
                map.containerPointToLatLng(newPoint)

            ];

        }

        return [ ...accumulator, map.containerPointToLatLng(point) ];

    }, []);

    // Update the lat longs with the newly inserted edge.
    polygon.setLatLngs(latLngs);

    // Remove the current set of edges for the polygon, and then recreate them, assigning the
    // new set of edges back into the polygon.
    polygon[edgesKey].map(edge => map.removeLayer(edge));
    polygon[edgesKey] = createEdges(map, polygon, options);

};

/**
 * @method createFor
 * @param {Object} map
 * @param {Array} latLngs
 * @param {Object} [options = defaultOptions]
 * @param {Boolean} [preventMutations = false]
 * @return {Array|Boolean}
 */
export const createFor = (map, latLngs, options = defaultOptions, preventMutations = false) => {

    // Determine whether we've reached the maximum polygons.
    const limitReached = polygons.get(map).size === options.maximumPolygons;

    // Apply the concave hull algorithm to the created polygon if the options allow.
    const concavedLatLngs = !preventMutations && options.concavePolygon ? concavePolygon(map, latLngs) : latLngs;

    // Simplify the polygon before adding it to the map.
    const addedPolygons = limitReached ? [] : map.simplifyPolygon(map, concavedLatLngs, options).map(latLngs => {

        const polygon = new Polygon(latLngs, {
            ...defaultOptions, ...options, className: 'leaflet-polygon'
        }).addTo(map);

        // Attach the edges to the polygon.
        polygon[edgesKey] = createEdges(map, polygon, options);

        // Disable the propagation when you click on the marker.
        DomEvent.disableClickPropagation(polygon);

        // Yield the click handler to the `handlePolygonClick` function.
        polygon.off('click');
        polygon.on('click', handlePolygonClick(map, polygon, options));

        return polygon;

    });

    // Append the current polygon to the master set.
    addedPolygons.forEach(polygon => polygons.get(map).add(polygon));

    if (!limitReached && !preventMutations && polygons.get(map).size > 1 && options.mergePolygons) {

        // Attempt a merge of all the polygons if the options allow, and the polygon count is above one.
        const addedMergedPolygons = mergePolygons(map, Array.from(polygons.get(map)), options);

        // Clear the set, and added all of the merged polygons into the master set.
        polygons.get(map).clear();
        addedMergedPolygons.forEach(polygon => polygons.get(map).add(polygon));

        return addedMergedPolygons;

    }

    return addedPolygons;

};

/**
 * @method removeFor
 * @param {Object} map
 * @param {Object} polygon
 * @return {void}
 */
export const removeFor = (map, polygon) => {

    // Remove polygon and all of its associated edges.
    map.removeLayer(polygon);
    edgesKey in polygon && polygon[edgesKey].map(edge => map.removeLayer(edge));

    // Remove polygon from the master set.
    polygons.get(map).delete(polygon);

};

/**
 * @method clearFor
 * @param {Object} map
 * @return {void}
 */
export const clearFor = map => {
    Array.from(polygons.get(map).values()).forEach(polygon => removeFor(map, polygon));
};

/**
 * @param {Object} map
 * @param {Object} polygon
 * @param {Object} options
 * @return {Function}
 */
export default (map, polygon, options) => {

    return event => {

        // Gather all of the points from the lat longs of the current polygon.
        const newPoint = map.mouseEventToContainerPoint('originalEvent' in event ? event.originalEvent : event);
        const parts = polygon.getLatLngs()[0].map(latLng => map.latLngToContainerPoint(latLng));

        const { startPoint, endPoint, lowestDistance } = parts.reduce((accumulator, point, index) => {

            const startPoint = point;
            const endPoint = parts[index + 1] || parts[0];
            const distance = LineUtil.pointToSegmentDistance(newPoint, startPoint, endPoint);

            if (distance < accumulator.lowestDistance) {

                // If the distance is less than the previous then we'll update the accumulator.
                return { lowestDistance: distance, startPoint, endPoint };

            }

            // Otherwise we'll simply yield the previous accumulator.
            return accumulator;

        }, { lowestDistance: Infinity, startPoint: new Point(), endPoint: Point() });

        // Setup the conditions for the switch statement to make the cases clearer.
        const mode = map[modesKey];
        const isDelete = Boolean(mode & DELETE);
        const isAppend = Boolean(mode & APPEND);
        const isDeleteAndAppend = Boolean(mode & DELETE && mode & APPEND);

        // Partially apply the remove and append functions.
        const removePolygon = () => removeFor(map, polygon);
        const appendEdge = () => appendEdgeFor(map, polygon, options, { parts, newPoint, startPoint, endPoint });

        switch (true) {

            // If both modes DELETE and APPEND are active then we need to do a little work to determine
            // which action to take based on where the user clicked on the polygon.
            case isDeleteAndAppend:
                lowestDistance > options.elbowDistance ? removePolygon() : appendEdge();
                break;

            case isDelete:
                removePolygon();
                break;

            case isAppend:
                appendEdge();
                break;

        }

        // Trigger the event for having deleted a polygon or appended an edge.
        (isDelete || isAppend) && updateFor(map, isDelete ? 'remove' : 'append');

    };

};
<|endoftext|>