<commit_before>'use strict'

/*  BLE Configuration Service
    deviceNameCharacteristicUUID - write/write without response - max 10 byte - ascii string
    advertisingParamCharacteristicUUID - write/write without response - 3 bytes - uint16_t adv interval in ms - uint8_t adv timeout in s
    appearanceCharacteristicUUID - write/write without response - 2 bytes - uint16_t appearance
    connectionParamCharacteristicUUID - write/write without response - 8 bytes - uint16_t min conn interval - uint16_t max conn interval - uint16_t slave latency - uint16_t supervision timeout
*/
var configurationServiceUUID = 'ef680001-9b35-4933-9b10-52ffa9740042';
var deviceNameCharacteristicUUID = 'ef680002-9b35-4933-9b10-52ffa9740042';
var advertisingParamCharacteristicUUID = 'ef680003-9b35-4933-9b10-52ffa9740042';
var appearanceCharacteristicUUID = 'ef680004-9b35-4933-9b10-52ffa9740042';
var connectionParamCharacteristicUUID = 'ef680005-9b35-4933-9b10-52ffa9740042';

/*  Weather Station Service
    temperatureCharacteristicUUID - notify/read - 2 bytes - uint8_t integer - uint8_t decimal
    pressureCharacteristicUUID - notify/read - 5 bytes - int32_t integer - uint8_t decimal
    humidityCharacteristicUUID - notify/read - 1 byte - uint8_t
    configurationCharacteristicUUID - write/write without response - 7 bytes - uint16_t temp interval in ms - uint16_t pressure interval in ms - uint16_t humidity interval in ms - uint8_t pressure mode (0=barometer, 1=altimeter)
*/
var weatherStationServiceUUID = '20080001-e36f-4648-91c6-9e86ead38764';
var temperatureCharacteristicUUID = '20080002-e36f-4648-91c6-9e86ead38764';
var pressureCharacteristicUUID = '20080003-e36f-4648-91c6-9e86ead38764';
var humidityCharacteristicUUID = '20080004-e36f-4648-91c6-9e86ead38764';
var configurationCharacteristicUUID = '20080005-e36f-4648-91c6-9e86ead38764';

/*  User Interface Service
    ledCharacteristicUUID - write/read - 4 bytes - uint32_t - LED ID - Red - Green - Blue (LSB)
    buttonCharacteristicUUID - write/read - 2 bytes - uint16_t - Button 2 state - Button 1 state (LSB)
*/
var userInterfaceServiceUUID = 'C7AE0001-3266-4A5C-859F-0F4799146BB5';
var ledCharacteristicUUID = 'C7AE0002-3266-4A5C-859F-0F4799146BB5';
var buttonCharacteristicUUID = 'C7AE0003-3266-4A5C-859F-0F4799146BB5';

/*  Default values for weather station configuration*/
var weatherStationConfig = {
    pressureInterval:1000,
    temperatureInterval:1000,
    humidityInterval:1000,
    pressureMode:0
};

var isConnecting = false;
var isConnected = false;
var sInterval;
var sTimeout;
var humidity;
var temperature;
var pressure;
var bleDevice;
var bleServer;
var bleService;
var pressureChar;
var humidityChar;
var temperatureChar;
var pressureString;
var humidityString;
var temperatureString;

window.onload = function(){
  document.querySelector('#connect').addEventListener('click', getAll);
  document.querySelector('#disconnect').addEventListener('click', stopAll);
  document.querySelector('#load-configuration').addEventListener('click', loadConfiguration);
  document.querySelector('#apply-configuration').addEventListener('click', applyConfiguration);
};

function applyConfiguration(){
  log('> applyConfiguration()');
  log(weatherStationConfig);

  let pInterval = parseFloat(document.getElementById("pressure-interval").value);
  log(pInterval);
  let tInterval = document.getElementById("temperature-interval").value;
  log(tInterval);
  let hInterval = document.getElementById("humidity-interval").value;
  log(hInterval);
  let pMode;

  //  get the checked button toggle
  let temp = $("input[name='options']:checked").val();
  if(temp == 'barometer'){
    pMode = 0;
  }
  else{
    pMode = 1;
  }

  if (!navigator.bluetooth) {
    log('Web Bluetooth API is not available.\n' +
        'Please make sure the Web Bluetooth flag is enabled.');
    return;
  }
  log('Requesting Bluetooth Device...');
  navigator.bluetooth.requestDevice({filters: [{services: [configurationServiceUUID]}]})
  .then(device => {
    log(device);
    return device.connectGATT();
  })
  .then(server => {
    log(server);
    return server.getPrimaryService(weatherStationServiceUUID);
  })
  .then(service => {
    log(service);
    return service.getCharacteristic(configurationCharacteristicUUID);
  })
  .then(characteristic => {
    log(characteristic);
    let data2 = new Uint8Array([0,1,0,1,0,1,1]);
    log(data2);
    for(let i = 0; i<7; i++){
      log(data2[i]);
    }
    let data = new Uint8Array(7);
    data[0] = tInterval & 0xff;
    data[1] = (tInterval >> 8) & 0xff;
    data[2] = pInterval & 0xff;
    data[3] = (pInterval  >> 8) & 0xff;
    data[4] = hInterval & 0xff;
    data[5] = (hInterval  >> 8) & 0xff;
    data[6] = pMode;

    log('data: ' + data);
    for(let i = 0; i<7; i++){
      log(i + ': ' + data[i]);
    }
    return characteristic.writeValue(data);
  })
  .then(value => {
    log('This data was sent: ' + value);
  return value;
  })
  .catch(error => {
  log('> applyConfiguration() ' + error);
  });
}

function loadConfiguration(){
  log('> loadConfiguration()');
  log(weatherStationConfig);
  log(weatherStationConfig.pressureInterval);
  if (!navigator.bluetooth) {
    log('Web Bluetooth API is not available.\n' +
        'Please make sure the Web Bluetooth flag is enabled.');
    return;
  }
  log('Requesting Bluetooth Device...');
  navigator.bluetooth.requestDevice({filters: [{services: [configurationServiceUUID]}]})
  .then(device => {
    log(device);
    return device.connectGATT();
  })
  .then(server => {
    log(server);
    return server.getPrimaryService(weatherStationServiceUUID);
  })
  .then(service => {
    log(service);
    return service.getCharacteristic(configurationCharacteristicUUID);
  })
  .then(characteristic => {
    log(characteristic);
    return characteristic.readValue();
  })
  .then(value => {
    log(value);
    let temp = value.buffer ? value : new DataView(value);
    log(temp);
  return handleConfiguration(value);
  })
  .catch(error => {
  log('> loadConfiguration() ' + error);
    });
}

function handleConfiguration(value){
  value = value.buffer ? value : new DataView(value);
  weatherStationConfig.temperatureInterval = value.getUint8(0) | ((value.getUint8(1) << 8 )&0xff00);
  weatherStationConfig.pressureInterval = value.getUint8(2) | ((value.getUint8(3) << 8 )&0xff00);
  weatherStationConfig.humidityInterval = value.getUint8(4) | ((value.getUint8(5) << 8 )&0xff00);
  weatherStationConfig.pressureMode = value.getUint8(6);
  log('weatherStationConfig.temperatureInterval: ' + weatherStationConfig.temperatureInterval);
  log('weatherStationConfig.pressureInterval: ' + weatherStationConfig.pressureInterval);
  log('weatherStationConfig.humidityInterval: ' + weatherStationConfig.humidityInterval);
  log('weatherStationConfig.pressureMode: ' + weatherStationConfig.pressureMode);
  document.getElementById("pressure-interval").value = weatherStationConfig.pressureInterval;
  document.getElementById("humidity-interval").value = weatherStationConfig.humidityInterval;
  document.getElementById("temperature-interval").value = weatherStationConfig.temperatureInterval;

  if(weatherStationConfig.pressureMode == 0){
    $('#pressure-barometer').toggleClass('active');
  }
  else{
    $('#pressure-altimeter').toggleClass('active');
  }
}

function log(text) {
    document.querySelector('#log').textContent += text + '\n';
    console.log(text);
}

function setConnecting(connecting) {
    isConnecting = connecting;
    if (connecting) {
        document.querySelector('#connect').style.display = "none";
    }
    else {
        document.querySelector('#connect').style.display = "float";
    }
}

function setConnected(connected) {
    isConnected = connected;
    if (connected) {
        document.querySelector('#connect').style.display = "none";
        document.querySelector('#disconnect').style.display = "float";
    }
    else {
        document.querySelector('#connect').style.display = "float";
        document.querySelector('#disconnect').style.display = "none";
        sServer.disconnect();
    }
}

function getAll() {
  if (!navigator.bluetooth) {
      log('Web Bluetooth API is not available.\n' +
          'Please make sure the Web Bluetooth flag is enabled.');
      return;
  }
  log('Requesting Bluetooth Device...');
  navigator.bluetooth.requestDevice({filters: [
    {services: [configurationServiceUUID]},
    {services: [weatherStationServiceUUID]}
    ]
  })
  .then(device => device.connectGATT())
  .then(server => {
    bleServer = server;
    return server.getPrimaryService(weatherStationServiceUUID);
  })
  .then(service => {
    bleService = service;
  })
  .then( bleService.getCharacteristic(pressureCharacteristicUUID))
  .then( characteristic => {
    characteristic.addEventListener('characteristicvaluechanged',handleNotifyPressure);
    characteristic.startNotifications();
  })
  .then( bleService.getCharacteristic(humidityCharacteristicUUID))
  .then( characteristic => {
    characteristic.addEventListener('characteristicvaluechanged',handleNotifyHumidity);
    characteristic.startNotifications();
  })
  .then( bleService.getCharacteristic(temperatureCharacteristicUUID))
  .then( characteristic => {
    characteristic.addEventListener('characteristicvaluechanged',handleNotifyTemperature);
    characteristic.startNotifications();
  })

    /**
    return Promise.all([
      service.getCharacteristic(pressureCharacteristicUUID)
      .then(handlePressure),
      service.getCharacteristic(humidityCharacteristicUUID)
      .then(handleHumidity),
      service.getCharacteristic(temperatureCharacteristicUUID)
      .then(handleTemperature)
    ])
    **/

  .then(startCloudLogging())
  .catch(error => {
    log('> getAll() ' + error);
  });
}

function handlePressure(characteristic){
  log('> handlePressure()');
  pressureChar = characteristic;
  characteristic.addEventListener('characteristicvaluechanged',handleNotifyPressure);
  return characteristic.startNotifications();
}

function handleTemperature(characteristic){
  log('> handleTemperature()');
  temperatureChar = characteristic;
  characteristic.addEventListener('characteristicvaluechanged',handleNotifyTemperature);
  return characteristic.startNotifications();
}

function handleHumidity(characteristic){
  log('> handleHumidity()');
  humidityChar = characteristic;
  characteristic.addEventListener('characteristicvaluechanged',handleNotifyHumidity);
  return characteristic.startNotifications();
}

function stopAll() {
  log('> stopAll()')
  if (pressureChar) {
    pressureChar.stopNotifications().then(() => {
      pressureChar.removeEventListener('characteristicvaluechanged',handleNotifyPressure);
      log('> Pressure notifications stopped');
    });
  }
  if (humidityChar) {
    humidityChar.stopNotifications().then(() => {
      humidityChar.removeEventListener('characteristicvaluechanged', handleNotifyHumidity);
      log('> Humidity notifications stopped');
    });
  }
  if (temperatureChar) {
    temperatureChar.stopNotifications().then(() => {
      temperatureChar.removeEventListener('characteristicvaluechanged',handleNotifyTemperature);
      log('> Temperature notifications stopped');
    });
  }
  stopCloudLogging();
  // Disconnect only for Chrome OS 50+
  //log('Disconnecting from Bluetooth Device...');
  //if (bleServer) {
  //  if (bleServer.connected) {
  //    bleServer.disconnect();
  //    log('Bluetooth Device connected: ' + bleServer.connected);
  //  } else {
  //    log('Bluetooth Device is already disconnected');
  //  }
}

function handleNotifyHumidity(event) {
  let value = event.target.value;
  value = value.buffer ? value : new DataView(value);
  let humidity_int = value.getUint8(0);
  humidityString = humidity_int.toString();
  log('Humidity is ' + humidity_int + '%');
  document.getElementById("humidity_reading").innerHTML = humidity_int +"%";
}

function handleNotifyTemperature(event) {
  let value = event.target.value;
  value = value.buffer ? value : new DataView(value);
  let temperature_int = value.getUint8(0);
  let temperature_dec = value.getUint8(1);
  temperatureString = temperature_int.toString() + '.' + temperature_dec.toString();
  log('Temperature is ' + temperature_int + '.' + temperature_dec + 'C');
  document.getElementById("temperature_reading").innerHTML = temperature_int + '.' + temperature_dec + '°C';
}

function handleNotifyPressure(event) {
  let value = event.target.value;
  value = value.buffer ? value : new DataView(value);
  let pressure_pascal = value.getInt32(0, true);
  let pressure_kpascal = pressure_pascal / 1000;
  let pressure_decimal = value.getUint8(4);
  pressureString = pressure_kpascal.toString();
  log('Pressure is ' + pressure_pascal + 'Pa');
  document.getElementById("pressure_reading").innerHTML = pressure_pascal + 'Pa';
}

// Swap byte order of 32bit value
function swap32(val) {
    return ((val & 0xFF) << 24)
           | ((val & 0xFF00) << 8)
           | ((val >> 8) & 0xFF00)
           | ((val >> 24) & 0xFF);
}

function stop(event) {
    log("stop(" + event + ")");
    try {
        sCharacteristic.writeValue(new Uint8Array([0, 0, 0, 0, 0, 0]));
        event.preventDefault();
    } catch (error) {
        log(error);
    }
}

function disconnectDevice(){
  document.querySelector('#connect').style.display = "block";
  document.querySelector('#disconnect').style.display = "none";
  sServer.disconnect();
  log("Disconnected.");
}<commit_msg>Testing multiple notifications.<commit_after>'use strict'

/*  BLE Configuration Service
    deviceNameCharacteristicUUID - write/write without response - max 10 byte - ascii string
    advertisingParamCharacteristicUUID - write/write without response - 3 bytes - uint16_t adv interval in ms - uint8_t adv timeout in s
    appearanceCharacteristicUUID - write/write without response - 2 bytes - uint16_t appearance
    connectionParamCharacteristicUUID - write/write without response - 8 bytes - uint16_t min conn interval - uint16_t max conn interval - uint16_t slave latency - uint16_t supervision timeout
*/
var configurationServiceUUID = 'ef680001-9b35-4933-9b10-52ffa9740042';
var deviceNameCharacteristicUUID = 'ef680002-9b35-4933-9b10-52ffa9740042';
var advertisingParamCharacteristicUUID = 'ef680003-9b35-4933-9b10-52ffa9740042';
var appearanceCharacteristicUUID = 'ef680004-9b35-4933-9b10-52ffa9740042';
var connectionParamCharacteristicUUID = 'ef680005-9b35-4933-9b10-52ffa9740042';

/*  Weather Station Service
    temperatureCharacteristicUUID - notify/read - 2 bytes - uint8_t integer - uint8_t decimal
    pressureCharacteristicUUID - notify/read - 5 bytes - int32_t integer - uint8_t decimal
    humidityCharacteristicUUID - notify/read - 1 byte - uint8_t
    configurationCharacteristicUUID - write/write without response - 7 bytes - uint16_t temp interval in ms - uint16_t pressure interval in ms - uint16_t humidity interval in ms - uint8_t pressure mode (0=barometer, 1=altimeter)
*/
var weatherStationServiceUUID = '20080001-e36f-4648-91c6-9e86ead38764';
var temperatureCharacteristicUUID = '20080002-e36f-4648-91c6-9e86ead38764';
var pressureCharacteristicUUID = '20080003-e36f-4648-91c6-9e86ead38764';
var humidityCharacteristicUUID = '20080004-e36f-4648-91c6-9e86ead38764';
var configurationCharacteristicUUID = '20080005-e36f-4648-91c6-9e86ead38764';

/*  User Interface Service
    ledCharacteristicUUID - write/read - 4 bytes - uint32_t - LED ID - Red - Green - Blue (LSB)
    buttonCharacteristicUUID - write/read - 2 bytes - uint16_t - Button 2 state - Button 1 state (LSB)
*/
var userInterfaceServiceUUID = 'C7AE0001-3266-4A5C-859F-0F4799146BB5';
var ledCharacteristicUUID = 'C7AE0002-3266-4A5C-859F-0F4799146BB5';
var buttonCharacteristicUUID = 'C7AE0003-3266-4A5C-859F-0F4799146BB5';

/*  Default values for weather station configuration*/
var weatherStationConfig = {
    pressureInterval:1000,
    temperatureInterval:1000,
    humidityInterval:1000,
    pressureMode:0
};

var isConnecting = false;
var isConnected = false;
var sInterval;
var sTimeout;
var humidity;
var temperature;
var pressure;
var bleDevice;
var bleServer;
var bleService;
var pressureChar;
var humidityChar;
var temperatureChar;
var pressureString;
var humidityString;
var temperatureString;
var myCharacteristic;

window.onload = function(){
  document.querySelector('#connect').addEventListener('click', getAll);
  document.querySelector('#disconnect').addEventListener('click', stopAll);
  document.querySelector('#load-configuration').addEventListener('click', loadConfiguration);
  document.querySelector('#apply-configuration').addEventListener('click', applyConfiguration);
};

function applyConfiguration(){
  log('> applyConfiguration()');
  log(weatherStationConfig);

  let pInterval = parseFloat(document.getElementById("pressure-interval").value);
  log(pInterval);
  let tInterval = document.getElementById("temperature-interval").value;
  log(tInterval);
  let hInterval = document.getElementById("humidity-interval").value;
  log(hInterval);
  let pMode;

  //  get the checked button toggle
  let temp = $("input[name='options']:checked").val();
  if(temp == 'barometer'){
    pMode = 0;
  }
  else{
    pMode = 1;
  }

  if (!navigator.bluetooth) {
    log('Web Bluetooth API is not available.\n' +
        'Please make sure the Web Bluetooth flag is enabled.');
    return;
  }
  log('Requesting Bluetooth Device...');
  navigator.bluetooth.requestDevice({filters: [{services: [configurationServiceUUID]}]})
  .then(device => {
    log(device);
    return device.connectGATT();
  })
  .then(server => {
    log(server);
    return server.getPrimaryService(weatherStationServiceUUID);
  })
  .then(service => {
    log(service);
    return service.getCharacteristic(configurationCharacteristicUUID);
  })
  .then(characteristic => {
    log(characteristic);
    let data2 = new Uint8Array([0,1,0,1,0,1,1]);
    log(data2);
    for(let i = 0; i<7; i++){
      log(data2[i]);
    }
    let data = new Uint8Array(7);
    data[0] = tInterval & 0xff;
    data[1] = (tInterval >> 8) & 0xff;
    data[2] = pInterval & 0xff;
    data[3] = (pInterval  >> 8) & 0xff;
    data[4] = hInterval & 0xff;
    data[5] = (hInterval  >> 8) & 0xff;
    data[6] = pMode;

    log('data: ' + data);
    for(let i = 0; i<7; i++){
      log(i + ': ' + data[i]);
    }
    return characteristic.writeValue(data);
  })
  .then(value => {
    log('This data was sent: ' + value);
  return value;
  })
  .catch(error => {
  log('> applyConfiguration() ' + error);
  });
}

function loadConfiguration(){
  log('> loadConfiguration()');
  log(weatherStationConfig);
  log(weatherStationConfig.pressureInterval);
  if (!navigator.bluetooth) {
    log('Web Bluetooth API is not available.\n' +
        'Please make sure the Web Bluetooth flag is enabled.');
    return;
  }
  log('Requesting Bluetooth Device...');
  navigator.bluetooth.requestDevice({filters: [{services: [configurationServiceUUID]}]})
  .then(device => {
    log(device);
    return device.connectGATT();
  })
  .then(server => {
    log(server);
    return server.getPrimaryService(weatherStationServiceUUID);
  })
  .then(service => {
    log(service);
    return service.getCharacteristic(configurationCharacteristicUUID);
  })
  .then(characteristic => {
    log(characteristic);
    return characteristic.readValue();
  })
  .then(value => {
    log(value);
    let temp = value.buffer ? value : new DataView(value);
    log(temp);
  return handleConfiguration(value);
  })
  .catch(error => {
  log('> loadConfiguration() ' + error);
    });
}

function handleConfiguration(value){
  value = value.buffer ? value : new DataView(value);
  weatherStationConfig.temperatureInterval = value.getUint8(0) | ((value.getUint8(1) << 8 )&0xff00);
  weatherStationConfig.pressureInterval = value.getUint8(2) | ((value.getUint8(3) << 8 )&0xff00);
  weatherStationConfig.humidityInterval = value.getUint8(4) | ((value.getUint8(5) << 8 )&0xff00);
  weatherStationConfig.pressureMode = value.getUint8(6);
  log('weatherStationConfig.temperatureInterval: ' + weatherStationConfig.temperatureInterval);
  log('weatherStationConfig.pressureInterval: ' + weatherStationConfig.pressureInterval);
  log('weatherStationConfig.humidityInterval: ' + weatherStationConfig.humidityInterval);
  log('weatherStationConfig.pressureMode: ' + weatherStationConfig.pressureMode);
  document.getElementById("pressure-interval").value = weatherStationConfig.pressureInterval;
  document.getElementById("humidity-interval").value = weatherStationConfig.humidityInterval;
  document.getElementById("temperature-interval").value = weatherStationConfig.temperatureInterval;

  if(weatherStationConfig.pressureMode == 0){
    $('#pressure-barometer').toggleClass('active');
  }
  else{
    $('#pressure-altimeter').toggleClass('active');
  }
}

function log(text) {
    document.querySelector('#log').textContent += text + '\n';
    console.log(text);
}

function setConnecting(connecting) {
    isConnecting = connecting;
    if (connecting) {
        document.querySelector('#connect').style.display = "none";
    }
    else {
        document.querySelector('#connect').style.display = "float";
    }
}

function setConnected(connected) {
    isConnected = connected;
    if (connected) {
        document.querySelector('#connect').style.display = "none";
        document.querySelector('#disconnect').style.display = "float";
    }
    else {
        document.querySelector('#connect').style.display = "float";
        document.querySelector('#disconnect').style.display = "none";
        sServer.disconnect();
    }
}

function getAll() {
  if (!navigator.bluetooth) {
      log('Web Bluetooth API is not available.\n' +
          'Please make sure the Web Bluetooth flag is enabled.');
      return;
  }
  log('Requesting Bluetooth Device...');
  navigator.bluetooth.requestDevice({filters: [
    {services: [configurationServiceUUID]},
    {services: [weatherStationServiceUUID]}
    ]
  })
  .then(device => device.connectGATT())
  .then(server => {
    bleServer = server;
    log('Got bleServer');
    return server.getPrimaryService(weatherStationServiceUUID);
  })
  .then(service => {
    log('Got bleService');
    bleService = service;
  })
  .then( bleService.getCharacteristic(pressureCharacteristicUUID))
  .then( characteristic => {
    log('Got pressureCharacteristic');
    myCharacteristic = characteristic;
    return myCharacteristic.startNotifications();
  })
  .then(() => {
    myCharacteristic.addEventListener('characteristicvaluechanged',handleNotifyPressure);
  })
  .then( bleService.getCharacteristic(humidityCharacteristicUUID))
  .then( characteristic => {
    log('Got humidityCharacteristic');
    myCharacteristic = characteristic;
    return myCharacteristic.startNotifications();
  })
  .then(() => {
    myCharacteristic.addEventListener('characteristicvaluechanged',handleNotifyHumidity);
  })
    .then( bleService.getCharacteristic(temperatureCharacteristicUUID))
  .then( characteristic => {
    log('Got TemperatureCharacteristic');
    myCharacteristic = characteristic;
    return myCharacteristic.startNotifications();
  })
  .then(() => {
    myCharacteristic.addEventListener('characteristicvaluechanged',handleNotifyTemperature);
  })

    /** Works in Chrome OS not on Android
    return Promise.all([
      service.getCharacteristic(pressureCharacteristicUUID)
      .then(handlePressure),
      service.getCharacteristic(humidityCharacteristicUUID)
      .then(handleHumidity),
      service.getCharacteristic(temperatureCharacteristicUUID)
      .then(handleTemperature)
    ])
    **/

  .then(startCloudLogging())
  .catch(error => {
    log('> getAll() ' + error);
  });
}

function handlePressure(characteristic){
  log('> handlePressure()');
  pressureChar = characteristic;
  characteristic.addEventListener('characteristicvaluechanged',handleNotifyPressure);
  return characteristic.startNotifications();
}

function handleTemperature(characteristic){
  log('> handleTemperature()');
  temperatureChar = characteristic;
  characteristic.addEventListener('characteristicvaluechanged',handleNotifyTemperature);
  return characteristic.startNotifications();
}

function handleHumidity(characteristic){
  log('> handleHumidity()');
  humidityChar = characteristic;
  characteristic.addEventListener('characteristicvaluechanged',handleNotifyHumidity);
  return characteristic.startNotifications();
}

function stopAll() {
  log('> stopAll()')
  if (pressureChar) {
    pressureChar.stopNotifications().then(() => {
      pressureChar.removeEventListener('characteristicvaluechanged',handleNotifyPressure);
      log('> Pressure notifications stopped');
    });
  }
  if (humidityChar) {
    humidityChar.stopNotifications().then(() => {
      humidityChar.removeEventListener('characteristicvaluechanged', handleNotifyHumidity);
      log('> Humidity notifications stopped');
    });
  }
  if (temperatureChar) {
    temperatureChar.stopNotifications().then(() => {
      temperatureChar.removeEventListener('characteristicvaluechanged',handleNotifyTemperature);
      log('> Temperature notifications stopped');
    });
  }
  stopCloudLogging();
  // Disconnect only for Chrome OS 50+
  //log('Disconnecting from Bluetooth Device...');
  //if (bleServer) {
  //  if (bleServer.connected) {
  //    bleServer.disconnect();
  //    log('Bluetooth Device connected: ' + bleServer.connected);
  //  } else {
  //    log('Bluetooth Device is already disconnected');
  //  }
}

function handleNotifyHumidity(event) {
  let value = event.target.value;
  value = value.buffer ? value : new DataView(value);
  let humidity_int = value.getUint8(0);
  humidityString = humidity_int.toString();
  log('Humidity is ' + humidity_int + '%');
  document.getElementById("humidity_reading").innerHTML = humidity_int +"%";
}

function handleNotifyTemperature(event) {
  let value = event.target.value;
  value = value.buffer ? value : new DataView(value);
  let temperature_int = value.getUint8(0);
  let temperature_dec = value.getUint8(1);
  temperatureString = temperature_int.toString() + '.' + temperature_dec.toString();
  log('Temperature is ' + temperature_int + '.' + temperature_dec + 'C');
  document.getElementById("temperature_reading").innerHTML = temperature_int + '.' + temperature_dec + '°C';
}

function handleNotifyPressure(event) {
  let value = event.target.value;
  value = value.buffer ? value : new DataView(value);
  let pressure_pascal = value.getInt32(0, true);
  let pressure_kpascal = pressure_pascal / 1000;
  let pressure_decimal = value.getUint8(4);
  pressureString = pressure_kpascal.toString();
  log('Pressure is ' + pressure_pascal + 'Pa');
  document.getElementById("pressure_reading").innerHTML = pressure_pascal + 'Pa';
}

// Swap byte order of 32bit value
function swap32(val) {
    return ((val & 0xFF) << 24)
           | ((val & 0xFF00) << 8)
           | ((val >> 8) & 0xFF00)
           | ((val >> 24) & 0xFF);
}

function stop(event) {
    log("stop(" + event + ")");
    try {
        sCharacteristic.writeValue(new Uint8Array([0, 0, 0, 0, 0, 0]));
        event.preventDefault();
    } catch (error) {
        log(error);
    }
}

function disconnectDevice(){
  document.querySelector('#connect').style.display = "block";
  document.querySelector('#disconnect').style.display = "none";
  sServer.disconnect();
  log("Disconnected.");
}<|endoftext|>


<commit_before>/**
 * @URL https://github.com/csetea/magpieJS
 * @license MIT
 */

// Module for handling custom element registrations
define([ 'module','magpie/util/config','require', 'magpie/log!magpie/html5/customElement', 'magpie/dom/inject','./_ieVersion', './_registerElement!' ], function(module,config, require, log, inject, ieVersion) {

	// TODO doc config options
	/*jshint -W004 */
	var config =config(module,{
		templateLoaderPlugin:'text',
		strictDefinition: false
	});

	//FIXME handle erroros with throw? errors['call? ????
	function _checkTagName(customElementDef) {
		var validTagName=/^[a-zA-Z][\w\_\.\:]*\-[\w\_\-\.\:]*$/g;

		if (!('tag' in customElementDef)){
			if (config.strictDefinition){
				log.error('missing mandantory field in customElementDef: tag');
				return false;
			}else{
				log.warn('missing mandantory field in customElementDef: tag');
				log.warn('Now try to use the module name as alternative value for: tag');
				log.debug('Module path:',customElementDef.m_customElementPath);
				var moduleName = customElementDef.m_customElementPath.replace(/.*\//,'');
				log.debug('Module name:',moduleName);
				if (!validTagName.test(moduleName)){
					log.error('Module name can not be set as tag name while does not fit to custom element tag name convention: ',validTagName);
					return false;
				}

				customElementDef.tag = moduleName;
				return true;
			}
		}else{
			if (typeof customElementDef.tag !== 'string'){
				log.error('wrong type of tag, it must be string, but current type is:', typeof customElementDef.tag, customElementDef);
				return false;
			}

			if (!validTagName.test(customElementDef.tag)){
				log.error('invalid tag value:',customElementDef.tag);
				log.error(customElementDef.tag + ' does not fit to custom element tag name convention:',validTagName);
				log.debug('Note: tag name must contain a dash(-) character');
				return false;
			}

			return true;
		}
	}

	function _checkCustomElementDef(customElementDef, callbackFn){
		if (customElementDef instanceof Function){
			customElementDef=new customElementDef();
		}

		//
		// Validate tag name
		//
		if (!_checkTagName(customElementDef)){
			return false;
		}else{
			if (ieVersion < 9) {
				document.createElement(customElementDef.tag);
			}
		}

		//
		// Check prototype
		//
		var prototype = customElementDef.prototype;

		// for IE8 and below
		var xHTMLElement = typeof HTMLElement !== "undefined" ? HTMLElement : Element;

		if (!( prototype == xHTMLElement.prototype ||
				( xHTMLElement instanceof Function && prototype instanceof xHTMLElement)
				)){
			if ( typeof prototype === 'undefined' ){
				log.debug('prototype is not defined, set it to default: HTMLElement.prototype');
				prototype = xHTMLElement.prototype;
			}else{
				log.error('prototype must be at least HTMLElement.prototype or direved from it');
				return false;
			}
		}

		customElementDef.m_proto=Object.create(prototype);
		//
		// Check extends
		//
		if ('extends' in customElementDef){
			if (typeof customElementDef['extends'] !== 'string'){
				log.error('extends property must be string',customElementDef);
			}
		}

		//
		// Check callback function definitions
		//
		var callbacks=['createdCallback','attachedCallback','detachedCallback','attributeChangedCallback'];

		// add custom attributes and properties to proto
		for(var p in customElementDef){
			customElementDef.m_proto[p]=customElementDef[p];
		}
		//
		// Check callback definitions
		//
		for (var i=0; i< callbacks.length; i++){
			var callback = callbacks[i];
			if (callback in customElementDef){
				log.trace('check callback definition of customElement definition: '+callback);
				if (customElementDef[callback] instanceof Function){
					customElementDef.m_proto[callback]=customElementDef[callback];
				}else{
					customElementDef.m_proto[callback]= undefined;
					log.warn(callback+' is not function, ignore it');
				}
			}
		}
		// add default createdCallback impl
		if (!customElementDef.m_proto.createdCallback){
			if ('template' in customElementDef.m_proto){
				//
				// concept: inject template and call 'created' callback method
				//
				customElementDef.m_proto.createdCallback= function(){
					log.trace('create default createdCallback impl. to inject template into custom element');
					inject(this, this.template, this.append);

//					if (this.bind instanceof Function){
//						this.bind(this);
//					}

					if (this.created instanceof Function){
						this.created(this);
					}
				};
			}else{
				log.debug('load template for '+customElementDef.tag+':',config.templateLoaderPlugin+'!'+customElementDef.m_customElementPath+'.html');
					require([config.templateLoaderPlugin+'!'+customElementDef.m_customElementPath+'.html'],function(template){

						customElementDef.m_proto.template=customElementDef.template=template;
						customElementDef.m_proto.createdCallback= function(){
							log.trace('create default createdCallback impl. to inject template into custom element');
							inject(this, this.template, this.append);

							if (this.created instanceof Function){
								this.created(this);
							}
						};

						callbackFn(customElementDef);
					}, function(err){
						//...
					});
				return;
			}

		}
		callbackFn(customElementDef);
	}

	function _registerElement(customElementDef){
		log(customElementDef.tag,'proto',customElementDef.m_proto);
		customElementDef.m_proto.m_customElementDef=customElementDef;
		try{
			if (customElementDef['extends']){
				return customElementDef.m_proto['magpie/html5/customElement']=document.registerElement(customElementDef.tag,{
					prototype: customElementDef.m_proto,
					'extends': customElementDef['extends']
					});
			}else{
				return customElementDef.m_proto['magpie/html5/customElement']=document.registerElement(customElementDef.tag,{
					prototype: customElementDef.m_proto
//					prototype: Object.create(
//						      HTMLImageElement.prototype,
//						      {
//						        createdCallback: {
//						          value: function () {}}})
//					});
//				return 'he?';
				});
			}
		}catch(e){
			log.error('Failed registrations of customElement: '+customElementDef.tag,e);
		}
	}

	return {

		config: config,
		//
		// loader plugin for customElement definitions
		//
		load: function(customElementPath, parentRequire, onload) {
			log.info('Load customElement definition:',customElementPath);
			parentRequire([customElementPath],function(customElementDef){
				customElementDef.m_customElementPath=customElementPath;
				_checkCustomElementDef(customElementDef,
					function(customElementDef){
						onload( _registerElement(customElementDef));
					});
				});

		}
		};

});
<commit_msg>use customElementDef.append instead of this.append //New: inject(this, this.template, customElementDef.append == true); //Old: inject(this, this.template, this.append);<commit_after>/**
 * @URL https://github.com/csetea/magpieJS
 * @license MIT
 */

// Module for handling custom element registrations
define([ 'module','magpie/util/config','require', 'magpie/log!magpie/html5/customElement', 'magpie/dom/inject','./_ieVersion', './_registerElement!' ], function(module,config, require, log, inject, ieVersion) {

	// TODO doc config options
	/*jshint -W004 */
	var config =config(module,{
		templateLoaderPlugin:'text',
		strictDefinition: false
	});

	//FIXME handle erroros with throw? errors['call? ????
	function _checkTagName(customElementDef) {
		var validTagName=/^[a-zA-Z][\w\_\.\:]*\-[\w\_\-\.\:]*$/g;

		if (!('tag' in customElementDef)){
			if (config.strictDefinition){
				log.error('missing mandantory field in customElementDef: tag');
				return false;
			}else{
				log.warn('missing mandantory field in customElementDef: tag');
				log.warn('Now try to use the module name as alternative value for: tag');
				log.debug('Module path:',customElementDef.m_customElementPath);
				var moduleName = customElementDef.m_customElementPath.replace(/.*\//,'');
				log.debug('Module name:',moduleName);
				if (!validTagName.test(moduleName)){
					log.error('Module name can not be set as tag name while does not fit to custom element tag name convention: ',validTagName);
					return false;
				}

				customElementDef.tag = moduleName;
				return true;
			}
		}else{
			if (typeof customElementDef.tag !== 'string'){
				log.error('wrong type of tag, it must be string, but current type is:', typeof customElementDef.tag, customElementDef);
				return false;
			}

			if (!validTagName.test(customElementDef.tag)){
				log.error('invalid tag value:',customElementDef.tag);
				log.error(customElementDef.tag + ' does not fit to custom element tag name convention:',validTagName);
				log.debug('Note: tag name must contain a dash(-) character');
				return false;
			}

			return true;
		}
	}

	function addDefaultCreatedCallBackImplementation(customElementDef){
		//
		// concept: inject template and call 'created' callback method
		//
		customElementDef.m_proto.createdCallback= function(){
			// FIXME maybe express like next () solution??? for createdCallback instead of  instanceInitializationBlocks callback
			if (customElementDef.m_proto.instanceInitializationBlocks instanceof Function){
//				 customElementDef.m_proto.initCallback();
				this.instanceInitializationBlocks = customElementDef.m_proto.instanceInitializationBlocks;
				this.instanceInitializationBlocks (this);
			}
			log.trace('create default createdCallback impl. to inject template into custom element');

			inject(this, this.template, customElementDef.append == true);

			if (this.created instanceof Function){
				this.created(this);
			}
		};
	}

	function _checkCustomElementDef(customElementDef, callbackFn){
		if (customElementDef instanceof Function){
			customElementDef=new customElementDef();
		}

		//
		// Validate tag name
		//
		if (!_checkTagName(customElementDef)){
			return false;
		}else{
			if (ieVersion < 9) {
				document.createElement(customElementDef.tag);
			}
		}

		//
		// Check prototype
		//
		var prototype = customElementDef.prototype;

		// for IE8 and below
		var xHTMLElement = typeof HTMLElement !== "undefined" ? HTMLElement : Element;

		if (!( prototype == xHTMLElement.prototype ||
				( xHTMLElement instanceof Function && prototype instanceof xHTMLElement)
				)){
			if ( typeof prototype === 'undefined' ){
				log.debug('prototype is not defined, set it to default: HTMLElement.prototype');
				prototype = xHTMLElement.prototype;
			}else{
				log.error('prototype must be at least HTMLElement.prototype or direved from it');
				return false;
			}
		}

		customElementDef.m_proto=Object.create(prototype);
		//
		// Check extends
		//
		if ('extends' in customElementDef){
			if (typeof customElementDef['extends'] !== 'string'){
				log.error('extends property must be string',customElementDef);
			}
		}

		//
		// Check callback function definitions
		//
		var callbacks=['createdCallback','attachedCallback','detachedCallback','attributeChangedCallback'];

		// add custom attributes and properties to proto
		for(var p in customElementDef){
			customElementDef.m_proto[p]=customElementDef[p];
		}
		//
		// Check callback definitions
		//
		for (var i=0; i< callbacks.length; i++){
			var callback = callbacks[i];
			if (callback in customElementDef){
				log.trace('check callback definition of customElement definition: '+callback);
				if (customElementDef[callback] instanceof Function){
					customElementDef.m_proto[callback]=customElementDef[callback];
				}else{
					customElementDef.m_proto[callback]= undefined;
					log.warn(callback+' is not function, ignore it');
				}
			}
		}

		// add default createdCallback impl
		if (!customElementDef.m_proto.createdCallback){
			if ('template' in customElementDef.m_proto){
				addDefaultCreatedCallBackImplementation(customElementDef);
			}else{
				log.debug('load template for '+customElementDef.tag+':',config.templateLoaderPlugin+'!'+customElementDef.m_customElementPath+'.html');
					require([config.templateLoaderPlugin+'!'+customElementDef.m_customElementPath+'.html'],function(template){

						customElementDef.m_proto.template=customElementDef.template=template;
						addDefaultCreatedCallBackImplementation(customElementDef);

						callbackFn(customElementDef);
					}, function(err){
						//...
					});
				return;
			}

		}
		callbackFn(customElementDef);
	}

	function _registerElement(customElementDef){
		log(customElementDef.tag,'proto',customElementDef.m_proto);
		customElementDef.m_proto.m_customElementDef=customElementDef;
		try{
			if (customElementDef['extends']){
				return customElementDef.m_proto['magpie/html5/customElement']=document.registerElement(customElementDef.tag,{
					prototype: customElementDef.m_proto,
					'extends': customElementDef['extends']
					});
			}else{
				return customElementDef.m_proto['magpie/html5/customElement']=document.registerElement(customElementDef.tag,{
					prototype: customElementDef.m_proto
				});
			}
		}catch(e){
			log.error('Failed registrations of customElement: '+customElementDef.tag,e);
		}
	}

	return {

		config: config,
		//
		// loader plugin for customElement definitions
		//
		load: function(customElementPath, parentRequire, onload) {
			log.info('Load customElement definition:',customElementPath);
			parentRequire([customElementPath],function(customElementDef){
				customElementDef.m_customElementPath=customElementPath;
				_checkCustomElementDef(customElementDef,
					function(customElementDef){
						onload( _registerElement(customElementDef));
					});
				});

		}
		};

});
<|endoftext|>


<commit_before>var _ = require('lodash'),
  Q = require('bluebird');

var utils = require('../testutils'),
  assert = utils.assert,
  expect = utils.expect,
  should = utils.should,
  sinon = utils.sinon;

var Robe = utils.Robe,
  Database = Robe.Database;

var test = module.exports = {};

test['default options'] = function*() {
  Robe.DEFAULT_CONNECTION_OPTIONS.should.eql({
    timeout: 3000
  });
};

test['connect'] = {
  'valid': function*() {
    var db = yield Robe.connect('127.0.0.1/robe-test');

    db.should.be.instanceOf(Robe.Database);
  },
  /**
   * TODO: Fix
   *
   * Test completes successfully but process hangs because handles not yet
   * cleaned up...why not!? Initial investigation points to something in either
   * Monk or mongodb node.js package - needs looking into
   */
  // 'timeout': {
  //   'default': function*() {
  //     this.timeout(Robe.DEFAULT_CONNECTION_OPTIONS.timeout * 2);

  //     try {
  //       yield Robe.connect('127.123.121.233');

  //       throw new Error('Should not be here');
  //     } catch (err) {
  //       err.message.should.eql('Timed out connecting to db');
  //     }
  //   },
  //   'custom': function*() {
  //     try {
  //       yield Robe.connect('127.123.121.233', {
  //         timeout: 100
  //       });

  //       throw new Error('Should not be here');
  //     } catch (err) {
  //       err.message.should.eql('Timed out connecting to db');
  //     }
  //   },
  // },
  'replica set': function*() {
    var db = yield Robe.connect(['127.0.0.1/robe-test/robe-test','localhost/robe-test']);

    db.should.be.instanceOf(Robe.Database);
  },
};

<commit_msg>make it clear that tests need fixing<commit_after>var _ = require('lodash'),
  Q = require('bluebird');

var utils = require('../testutils'),
  assert = utils.assert,
  expect = utils.expect,
  should = utils.should,
  sinon = utils.sinon;

var Robe = utils.Robe,
  Database = Robe.Database;

var test = module.exports = {};

test['default options'] = function*() {
  Robe.DEFAULT_CONNECTION_OPTIONS.should.eql({
    timeout: 3000
  });
};

test['connect'] = {
  'valid': function*() {
    var db = yield Robe.connect('127.0.0.1/robe-test');

    db.should.be.instanceOf(Robe.Database);
  },
  /**
   * TODO: Fix
   *
   * Test completes successfully but process hangs because handles not yet
   * cleaned up...why not!? Initial investigation points to something in either
   * Monk or mongodb node.js package - needs looking into
   */
  'timeout': {
    'default': function*() {
      console.log('TODO');
  //     this.timeout(Robe.DEFAULT_CONNECTION_OPTIONS.timeout * 2);

  //     try {
  //       yield Robe.connect('127.123.121.233');

  //       throw new Error('Should not be here');
  //     } catch (err) {
  //       err.message.should.eql('Timed out connecting to db');
  //     }
    },
    'custom': function*() {
      console.log('TODO');
  //     try {
  //       yield Robe.connect('127.123.121.233', {
  //         timeout: 100
  //       });

  //       throw new Error('Should not be here');
  //     } catch (err) {
  //       err.message.should.eql('Timed out connecting to db');
  //     }
    },
  },
  'replica set': function*() {
    var db = yield Robe.connect(['127.0.0.1/robe-test/robe-test','localhost/robe-test']);

    db.should.be.instanceOf(Robe.Database);
  },
};

<|endoftext|>


<commit_before>const expect = require('chai').expect
const urlSlug = require('../src/index')

describe('module', () => {

  it('should include constructor as a property', () => {
    expect(urlSlug.UrlSlug.constructor)
      .to.be.equal(urlSlug.constructor)
  })

  it('should contain convert and revert methods', () => {
    expect(urlSlug.convert)
      .to.be.a('function')
    expect(urlSlug.revert)
      .to.be.a('function')
  })

  it('should call convert if called as a function', () => {
    expect(urlSlug)
      .to.be.equal(urlSlug.convert)
  })

  describe('instance', () => {

    const instance = new urlSlug.UrlSlug()

    it(
      'should contain lowercase, uppercase and titlecase builtin transformers',
      () => {
        expect(urlSlug.transformers)
          .to.contain.all.keys('lowercase', 'uppercase', 'titlecase')
      }
    )

    it('should set "-" as default separator', () => {
      expect(instance.separator)
        .to.be.equal('-')
    })

    it('should set "lowercase" as default transformer', () => {
      expect(instance.transformer)
        .to.be.equal(urlSlug.transformers.lowercase)
    })

    describe('transformers', () => {

      it('should contain a working lowercase', () => {
        expect(urlSlug.transformers.lowercase(['TEST', 'STRING'], ' '))
          .to.be.equal('test string')
      })

      it('should contain a working uppercase', () => {
        expect(urlSlug.transformers.uppercase(['test', 'string'], ' '))
          .to.be.equal('TEST STRING')
      })

      it('should contain a working titlecase', () => {
        expect(urlSlug.transformers.titlecase(['tesT', 'strinG'], ' '))
          .to.be.equal('Test String')
      })

    })

    describe('options', () => {

      it('should not accept a separator that is not a string', () => {
        expect(instance.convert.bind(instance, '', 123))
          .to.throw(/^The separator must be a string/)
      })

      it(
        'should only accept a separator defined as unreserved character in ' +
        'RFC 3986',
        () => {
          expect(instance.convert.bind(instance, '', '-._~'))
            .to.not.throw(/^The separator has invalid characters/)
        }
      )

      it(
        'should not accept a separator not defined as unreserved character ' +
        'in RFC 3986',
        () => {
          expect(instance.convert.bind(instance, '', '+'))
            .to.throw(/^The separator has invalid characters/)
        }
      )

      it('should accept false as a transformer option', () => {
        expect(instance.convert.bind(instance, '', '', false))
          .to.not.throw(/^The transformer must be a function/)
      })

      it('should accept all builtin presets as transform', () => {
        expect(instance.convert.bind(instance, '', '', instance.lowercase))
          .to.not.throw(/^The transformer must be a function/)
        expect(instance.convert.bind(instance, '', '', instance.uppercase))
          .to.not.throw(/^The transformer must be a function/)
        expect(instance.convert.bind(instance, '', '', instance.titlecase))
          .to.not.throw(/^The transformer must be a function/)
      })

      it('should accept a function as transform', () => {
        expect(instance.convert.bind(instance, '', () => {}))
          .to.not.throw(/^The transformer must be a function/)
      })

      it(
        'should only accept false, a function or a builtin preset as transform',
        () => {
          expect(instance.convert.bind(instance, '', '', true))
            .to.throw(/^The transformer must be a function/)
          expect(instance.convert.bind(instance, '', '', 'nonexistent'))
            .to.throw(/^The transformer must be a function/)
          expect(instance.convert.bind(instance, '', '', {}))
            .to.throw(/^The transformer must be a function/)
        }
      )

    })

    describe('convert', () => {

      it('should convert input to string', () => {
        expect(instance.convert(123))
          .to.be.equal('123')
      })

      it('should return a default slug if no options are set', () => {
        expect(instance.convert('Url Slug'))
          .to.be.equal('url-slug')
      })

      it('should remove accents', () => {
        expect(instance.convert('á é í ó ú'))
          .to.be.equal('a-e-i-o-u')
      })

      it('should convert to upper case and use default separator', () => {
        expect(instance.convert('a bronx tale', 'uppercase'))
          .to.be.equal('A-BRONX-TALE')
      })

      it('should use underscore separators and title case', () => {
        expect(instance.convert('tom jobim', '_', 'titlecase'))
          .to.be.equal('Tom_Jobim')
      })

      it(
        'should allow multiple characters in separator and not change the case',
        () => {
          expect(instance.convert('Charly García', '-._~-._~', false))
            .to.be.equal('Charly-._~-._~Garcia')
        }
      )

      it('should return a camel case string', () => {
        expect(instance.convert('java script', '', 'titlecase'))
          .to.be.equal('JavaScript')
      })

      it('should break a camel case string', () => {
        expect(instance.convert('javaScript'))
          .to.be.equal('java-script')
      })

      it('should return only consonants', () => {
        const transform = (fragments, separator) => fragments
          .join(separator)
          .replace(/[aeiou]/gi, '')
        expect(instance.convert('React', '', transform))
          .to.be.equal('Rct')
      })

      it('should handle empty strings', () => {
        expect(instance.convert(''))
          .to.be.equal('')
      })

      it('should handle strings with no alphanumeric characters', () => {
        expect(instance.convert('- ( ) [ ]'))
          .to.be.equal('')
      })
    })

    describe('revert', () => {

      it('should convert input to string', () => {
        expect(instance.revert(123))
          .to.be.equal('123')
      })

      it('should use unknown reversion and maintain input case', () => {
        expect(instance.revert('UrlSlug-_url.~slug'))
          .to.be.equal('Url Slug url slug')
      })

      it(
        'should break only on camel case and convert input to upper case',
        () => {
          expect(instance.revert('ClaudioBaglioni_is-Italian', '', 'uppercase'))
            .to.be.equal('CLAUDIO BAGLIONI_IS-ITALIAN')
        }
      )

      it('should return the title of a Pink Floyd track', () => {
        expect(instance.revert('comfortably-._~numb', '-._~', 'titlecase'))
          .to.be.equal('Comfortably Numb')
      })

      it('should empty strings revert to another empty string', () => {
        expect(instance.revert(''))
          .to.be.equal('')
      })

    })

  })

})
<commit_msg>Improved test descriptions<commit_after>const expect = require('chai').expect
const urlSlug = require('../src/index')

describe('module', () => {
  it('includes UrlSlug constructor as a property', () => {
    expect(urlSlug.UrlSlug.constructor)
      .to.be.equal(urlSlug.constructor)
  })

  it('contains convert and revert methods', () => {
    expect(urlSlug.convert)
      .to.be.a('function')
    expect(urlSlug.revert)
      .to.be.a('function')
  })

  it('calls convert if called as a function', () => {
    expect(urlSlug)
      .to.be.equal(urlSlug.convert)
  })
})

describe('instance', () => {
  const instance = new urlSlug.UrlSlug()

  it('contains lowercase, uppercase and titlecase builtin transformers', () => {
    expect(urlSlug.transformers)
      .to.contain.all.keys('lowercase', 'uppercase', 'titlecase')
  })

  it('uses "-" as default separator', () => {
    expect(instance.separator)
      .to.be.equal('-')
  })

  it('sets "lowercase" as default transformer', () => {
    expect(instance.transformer)
      .to.be.equal(urlSlug.transformers.lowercase)
  })

  describe('transformers', () => {
    it('contains a lowercase transformer', () => {
      expect(urlSlug.transformers.lowercase(['TEST', 'STRING'], ' '))
        .to.be.equal('test string')
    })

    it('contains an uppercase transformer', () => {
      expect(urlSlug.transformers.uppercase(['test', 'string'], ' '))
        .to.be.equal('TEST STRING')
    })

    it('contains a titlecase transformer', () => {
      expect(urlSlug.transformers.titlecase(['tesT', 'strinG'], ' '))
        .to.be.equal('Test String')
    })
  })

  describe('options', () => {
    it('does not accept a separator that is not a string', () => {
      expect(instance.convert.bind(instance, '', 123))
        .to.throw(/^The separator must be a string/)
    })

    it('accepts unreserved characters in RFC 3986 as separator', () => {
      expect(instance.convert.bind(instance, '', '-._~'))
        .to.not.throw(/^The separator has invalid characters/)
    })

    it('does not accept separators not defined as unreserved character', () => {
      expect(instance.convert.bind(instance, '', '+'))
        .to.throw(/^The separator has invalid characters/)
    })

    it('accepts false as a transformer', () => {
      expect(instance.convert.bind(instance, '', '', false))
        .to.not.throw(/^The transformer must be a function/)
    })

    it('accepts all builtin presets as a transformer', () => {
      expect(instance.convert.bind(instance, '', '', instance.lowercase))
        .to.not.throw(/^The transformer must be a function/)
      expect(instance.convert.bind(instance, '', '', instance.uppercase))
        .to.not.throw(/^The transformer must be a function/)
      expect(instance.convert.bind(instance, '', '', instance.titlecase))
        .to.not.throw(/^The transformer must be a function/)
    })

    it('accepts a function as a transformer', () => {
      expect(instance.convert.bind(instance, '', () => {}))
        .to.not.throw(/^The transformer must be a function/)
    })

    it('accepts false, a function or a builtin preset as a transformer', () => {
      expect(instance.convert.bind(instance, '', '', true))
        .to.throw(/^The transformer must be a function/)
      expect(instance.convert.bind(instance, '', '', 'nonexistent'))
        .to.throw(/^The transformer must be a function/)
      expect(instance.convert.bind(instance, '', '', {}))
        .to.throw(/^The transformer must be a function/)
    })
  })

  describe('convert', () => {
    it('converts input to string', () => {
      expect(instance.convert(123))
        .to.be.equal('123')
    })

    it('uses lowercase transformer and hyphen separator as default', () => {
      expect(instance.convert('Url Slug'))
        .to.be.equal('url-slug')
    })

    it('removes accents', () => {
      expect(instance.convert('á é í ó ú'))
        .to.be.equal('a-e-i-o-u')
    })

    it('uses upper case as transformer and use the default separator', () => {
      expect(instance.convert('a bronx tale', 'uppercase'))
        .to.be.equal('A-BRONX-TALE')
    })

    it('uses underscore as separator and title case as transformer', () => {
      expect(instance.convert('tom jobim', '_', 'titlecase'))
        .to.be.equal('Tom_Jobim')
    })

    it('allows multiple characters as separator and maintain the case', () => {
      expect(instance.convert('Charly García', '-._~-._~', false))
        .to.be.equal('Charly-._~-._~Garcia')
    })

    it('returns a camel case string', () => {
      expect(instance.convert('java script', '', 'titlecase'))
        .to.be.equal('JavaScript')
    })

    it('splits a camel case string', () => {
      expect(instance.convert('javaScript'))
        .to.be.equal('java-script')
    })

    it('returns only consonants', () => {
      const transform = (fragments, separator) => fragments
        .join(separator)
        .replace(/[aeiou]/gi, '')
      expect(instance.convert('React', '', transform))
        .to.be.equal('Rct')
    })

    it('handles empty strings', () => {
      expect(instance.convert(''))
        .to.be.equal('')
    })

    it('handles strings with no alphanumeric characters', () => {
      expect(instance.convert('- ( ) [ ]'))
        .to.be.equal('')
    })
  })

  describe('revert', () => {
    it('converts input to string', () => {
      expect(instance.revert(123))
        .to.be.equal('123')
    })

    it('uses unknown reversion and maintain input case', () => {
      expect(instance.revert('UrlSlug-_url.~slug'))
        .to.be.equal('Url Slug url slug')
    })

    it('splits on camel case and convert input to upper case', () => {
      expect(instance.revert('ClaudioBaglioni_is-Italian', '', 'uppercase'))
        .to.be.equal('CLAUDIO BAGLIONI_IS-ITALIAN')
    })

    it('returns the title of a Pink Floyd track', () => {
      expect(instance.revert('comfortably-._~numb', '-._~', 'titlecase'))
        .to.be.equal('Comfortably Numb')
    })

    it('reverts an empty string to another empty string', () => {
      expect(instance.revert(''))
        .to.be.equal('')
    })
  })
})
<|endoftext|>


<commit_before>// @flow

import Icon from '@conveyal/woonerf/components/icon'
import React, { Component } from 'react'
import { Button, FormControl, InputGroup } from 'react-bootstrap'

import type {
  Substitution
} from '../../../types'

type SubstitutionRowProps = {
  index: number,
  onChange: (Substitution, number) => void,
  onRemove: number => void,
  substitution: Substitution
}

// (Encoded) test string to be included to URL to regex test tool (regexr).
const STRING_FOR_REGEXR_TEST = encodeURIComponent('Route 10 to Bay 1 @ Train Station (10th+West)')

/**
 * Renders and lets user edit a substitution.
 */
export default class SubstitutionRow extends Component<SubstitutionRowProps> {
  _onEditPattern = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ pattern: evt.target.value })
  }

  _onEditReplacement = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ replacement: evt.target.value })
  }

  _onEditNormalizeSpace = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ normalizeSpace: evt.target.checked })
  }

  _onEditDescription = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ description: evt.target.value })
  }

  _onRemove = () => this.props.onRemove(this.props.index)

  _updateSubtitution (partialState: any) {
    const { index, onChange, substitution } = this.props
    const newSubstitution = {
      ...substitution,
      ...partialState
    }
    onChange(newSubstitution, index)
  }

  render () {
    const { substitution } = this.props
    const { description, normalizeSpace, pattern, replacement } = substitution
    const inputStyle = { height: 'unset', padding: '2px' }
    const regexTestLink = `https://regexr.com/?expression=${encodeURIComponent(pattern)}&text=${STRING_FOR_REGEXR_TEST}`

    return (

          <InputGroup style={{ padding: 0 }}>
            <FormControl
              onChange={this._onEditPattern}
              placeholder='Text (regex) to find'
              style={inputStyle}
              value={pattern}
            />
            <InputGroup.Addon style={inputStyle}>

                <Icon type='flask' />

            </InputGroup.Addon>
          </InputGroup>

          <InputGroup style={{ padding: 0 }}>
            <FormControl
              onChange={this._onEditReplacement}
              placeholder='Replacement'
              style={inputStyle}
              value={replacement}
            />
            <InputGroup.Addon style={{ padding: '0 6px' }}>
              <input
                ariaLabel='Normalize space'
                checked={normalizeSpace}
                onChange={this._onEditNormalizeSpace}
                style={{ padding: '0 6px' }}
                type='checkbox'
              />
            </InputGroup.Addon>
          </InputGroup>

          <FormControl
            onChange={this._onEditDescription}
            placeholder='Optional description'
            style={inputStyle}
            value={description || ''}
          />

          <Button
            bsSize='xsmall'
            onClick={this._onRemove}
            title='Remove this substitution'
          >
            <Icon type='remove' />
          </Button>

    )
  }
}
<commit_msg>refactor(SubstitutionRow): Add dangling refactor<commit_after>// @flow

import Icon from '@conveyal/woonerf/components/icon'
import React, { Component } from 'react'
import { Button, FormControl, InputGroup } from 'react-bootstrap'

import type {
  Substitution
} from '../../../types'

type SubstitutionRowProps = {
  index: number,
  onChange: (Substitution, number) => void,
  onRemove: number => void,
  substitution: Substitution
}

// (Encoded) test string to be included to URL to regex test tool (regexr).
const STRING_FOR_REGEXR_TEST = encodeURIComponent('Route 10 to Bay 1 @ Train Station (10th+West)')

/**
 * Renders and lets user edit a substitution.
 */
export default class SubstitutionRow extends Component<SubstitutionRowProps> {
  _onEditPattern = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ pattern: evt.target.value })
  }

  _onEditReplacement = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ replacement: evt.target.value })
  }

  _onEditNormalizeSpace = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ normalizeSpace: evt.target.checked })
  }

  _onEditDescription = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateSubtitution({ description: evt.target.value })
  }

  _onRemove = () => this.props.onRemove(this.props.index)

  _updateSubtitution (partialState: any) {
    const { index, onChange, substitution } = this.props
    const newSubstitution = {
      ...substitution,
      ...partialState
    }
    onChange(newSubstitution, index)
  }

  render () {
    const { substitution } = this.props
    const { description, normalizeSpace, pattern, replacement } = substitution
    const inputStyle = { height: 'unset', padding: '2px' }
    const inputGroupStyle = { padding: 0 }
    const regexTestLink = `https://regexr.com/?expression=${encodeURIComponent(pattern)}&text=${STRING_FOR_REGEXR_TEST}`

    return (

          <InputGroup style={inputGroupStyle}>
            <FormControl
              onChange={this._onEditPattern}
              placeholder='Text (regex) to find'
              style={inputStyle}
              value={pattern}
            />
            <InputGroup.Addon style={inputStyle}>

                <Icon type='flask' />

            </InputGroup.Addon>
          </InputGroup>

          <InputGroup style={inputGroupStyle}>
            <FormControl
              onChange={this._onEditReplacement}
              placeholder='Replacement'
              style={inputStyle}
              value={replacement}
            />
            <InputGroup.Addon style={{ padding: '0 6px' }}>
              <input
                ariaLabel='Normalize space'
                checked={normalizeSpace}
                onChange={this._onEditNormalizeSpace}
                style={{ padding: '0 6px' }}
                type='checkbox'
              />
            </InputGroup.Addon>
          </InputGroup>

          <FormControl
            onChange={this._onEditDescription}
            placeholder='Optional description'
            style={inputStyle}
            value={description || ''}
          />

          <Button
            bsSize='xsmall'
            onClick={this._onRemove}
            title='Remove this substitution'
          >
            <Icon type='remove' />
          </Button>

    )
  }
}
<|endoftext|>


<commit_before>import React, { Component } from "react";
import { connect } from "react-redux";
import Downshift from "downshift";

import {
	addComponent,
	removeComponent,
	watchComponent,
	updateQuery,
	setQueryOptions
} from "@appbaseio/reactivecore/lib/actions";
import {
	debounce,
	pushToAndClause,
	checkValueChange,
	checkPropChange,
	checkSomePropChange
} from "@appbaseio/reactivecore/lib/utils/helper";

import types from "@appbaseio/reactivecore/lib/utils/types";
import Title from "../../styles/Title";
import Input, { suggestionsContainer, suggestions } from "../../styles/Input";

class DataSearch extends Component {
	constructor(props) {
		super(props);

		this.state = {
			currentValue: "",
			suggestions: [],
			isOpen: false
		};
		this.internalComponent = props.componentId + "__internal";
	}

	componentWillMount() {
		this.props.addComponent(this.props.componentId);
		this.props.addComponent(this.internalComponent);

		if (this.props.highlight) {
			const queryOptions = this.highlightQuery(this.props);
			this.props.setQueryOptions(this.props.componentId, queryOptions);
		}
		this.setReact(this.props);

		if (this.props.selectedValue) {
			this.setValue(this.props.selectedValue, true);
		} else if (this.props.defaultSelected) {
			this.setValue(this.props.defaultSelected, true);
		}
	}

	componentWillReceiveProps(nextProps) {
		checkSomePropChange(
			this.props,
			nextProps,
			["highlight", "dataField", "highlightField"],
			() => {
				const queryOptions = this.highlightQuery(nextProps);
				this.props.setQueryOptions(nextProps.componentId, queryOptions);
			}
		);

		checkPropChange(
			this.props.react,
			nextProps.react,
			() => this.setReact(nextProps)
		);

		if (Array.isArray(nextProps.suggestions) && this.state.currentValue.trim().length) {
			checkPropChange(
				this.props.suggestions,
				nextProps.suggestions,
				() => {
					this.setState({
						suggestions: this.onSuggestions(nextProps.suggestions)
					});
				}
			);
		}

		checkSomePropChange(
			this.props,
			nextProps,
			["fieldWeights", "fuzziness", "queryFormat"],
			() => {
				this.updateQuery(nextProps.componentId, this.state.currentValue, nextProps)
			}
		);

		if (this.props.defaultSelected !== nextProps.defaultSelected) {
			this.setValue(nextProps.defaultSelected, true, nextProps);
		} else if (
			this.props.selectedValue !== nextProps.selectedValue &&
			this.state.currentValue !== nextProps.selectedValue
		) {
			// check for selected value prop change or selectedValue will never match currentValue
			this.setValue(nextProps.selectedValue || "", true, nextProps);
		}
	}

	componentWillUnmount() {
		this.props.removeComponent(this.props.componentId);
		this.props.removeComponent(this.internalComponent);
	}

	setReact = (props) => {
		const { react } = props;
		if (react) {
			const newReact = pushToAndClause(react, this.internalComponent);
			props.watchComponent(props.componentId, newReact);
		} else {
			props.watchComponent(props.componentId, { and: this.internalComponent });
		}
	}

	highlightQuery = (props) => {
		if (!props.highlight) {
			return null;
		}
		const fields = {};
		const highlightField = props.highlightField ? props.highlightField : props.dataField;

		if (typeof highlightField === "string") {
			fields[highlightField] = {};
		} else if (Array.isArray(highlightField)) {
			highlightField.forEach((item) => {
				fields[item] = {};
			});
		}

		return {
			highlight: {
				pre_tags: [""],
				post_tags: [""],
				fields
			}
		};
	}

	defaultQuery = (value, props) => {
		let finalQuery = null,
			fields;
		if (value) {
			if (Array.isArray(props.dataField)) {
				fields = props.dataField;
			} else {
				fields = [props.dataField];
			}
			finalQuery = {
				bool: {
					should: this.shouldQuery(value, fields, props),
					minimum_should_match: "1"
				}
			};
		}

		if (value === "") {
			finalQuery = {
				"match_all": {}
			}
		}

		return finalQuery;
	};

	shouldQuery = (value, dataFields, props) => {
		const fields = dataFields.map(
			(field, index) => `${field}${(Array.isArray(props.fieldWeights) && props.fieldWeights[index]) ? ("^" + props.fieldWeights[index]) : ""}`
		);

		if (props.queryFormat === "and") {
			return [
				{
					multi_match: {
						query: value,
						fields,
						type: "cross_fields",
						operator: "and",
						fuzziness: props.fuzziness ? props.fuzziness : 0
					}
				},
				{
					multi_match: {
						query: value,
						fields,
						type: "phrase_prefix",
						operator: "and"
					}
				}
			];
		}

		return [
			{
				multi_match: {
					query: value,
					fields,
					type: "best_fields",
					operator: "or",
					fuzziness: props.fuzziness ? props.fuzziness : 0
				}
			},
			{
				multi_match: {
					query: value,
					fields,
					type: "phrase_prefix",
					operator: "or"
				}
			}
		];
	};

	onSuggestions = (suggestions) => {
		if (this.props.onSuggestions) {
			return this.props.onSuggestions(suggestions);
		}

		const fields = Array.isArray(this.props.dataField) ? this.props.dataField : [this.props.dataField];
		let suggestionsList = [];
		let labelsList = [];
		const currentValue = this.state.currentValue.toLowerCase();

		suggestions.forEach(item => {
			fields.forEach(field => {
				const label = item._source[field];
				const val = label.toLowerCase();

				if (val.includes(currentValue) && !labelsList.includes(val)) {
					const option = {
						label,
						value: label
					};
					labelsList = [...labelsList, val];
					suggestionsList = [...suggestionsList, option];
				}
			});
		});

		return suggestionsList;
	};

	setValue = (value, isDefaultValue = false, props = this.props) => {
		const performUpdate = () => {
			this.setState({
				currentValue: value
			});
			if (isDefaultValue) {
				if (this.props.autoSuggest) {
					this.setState({
						isOpen: false
					});
					this.updateQuery(this.internalComponent, value, props);
				}
				this.updateQuery(props.componentId, value, props);
			} else {
				// debounce for handling text while typing
				this.handleTextChange(value);
			}
		}
		checkValueChange(
			props.componentId,
			value,
			props.beforeValueChange,
			props.onValueChange,
			performUpdate
		);
	};

	handleTextChange = debounce((value) => {
		if (this.props.autoSuggest) {
			this.updateQuery(this.internalComponent, value, this.props);
		} else {
			this.updateQuery(this.props.componentId, value, this.props);
		}
	}, 300);

	updateQuery = (componentId, value, props) => {
		const query = props.customQuery || this.defaultQuery;
		let onQueryChange = null;
		if (componentId === props.componentId && props.onQueryChange) {
			onQueryChange = props.onQueryChange;
		}
		props.updateQuery({
			componentId,
			query: query(value, props),
			value,
			label: props.filterLabel,
			showFilter: props.showFilter,
			onQueryChange,
			URLParams: props.URLParams
		});
	};

	handleFocus = (event) => {
		this.setState({
			isOpen: true
		});
		if (this.props.onFocus) {
			this.props.onFocus(event);
		}
	};

	// only works if there's a change in downshift's value
	handleOuterClick = () => {
		this.setValue(this.state.currentValue, true)
	};

	handleKeyDown = (event) => {
		if (event.key === "Enter") {
			event.target.blur();
			this.setValue(event.target.value, true);
		}
		if (this.props.onKeyDown) {
			this.props.onKeyDown(event);
		}
	};

	onInputChange = (e, v) => {
		this.setValue(e.target.value);
	};

	onSuggestionSelected = (suggestion, event) => {
		this.setValue(suggestion.value, true);
		if (this.props.onBlur) {
			this.props.onBlur(event);
		}
	};

	handleStateChange = (changes) => {
		const { isOpen, type } = changes;
		if (type === Downshift.stateChangeTypes.mouseUp) {
			this.setState({
				isOpen
			});
		}
	};

	render() {
		const suggestionsList = this.state.currentValue === "" || this.state.currentValue === null
			? this.props.defaultSuggestions && this.props.defaultSuggestions.length
				? this.props.defaultSuggestions
				: []
			: this.state.suggestions;

		return (

				{this.props.title && <Title>{this.props.title}</Title>}
				{
					this.props.autoSuggest
						? (<Downshift
							onChange={this.onSuggestionSelected}
							onOuterClick={this.handleOuterClick}
							onStateChange={this.handleStateChange}
							isOpen={this.state.isOpen}
							itemToString={i => i.label}
							render={({
								getInputProps,
								getItemProps,
								isOpen,
								highlightedIndex
							}) => (

									<Input {...getInputProps({
										placeholder: this.props.placeholder,
										value: this.state.currentValue === null ? "" : this.state.currentValue,
										onChange: this.onInputChange,
										onBlur: this.props.onBlur,
										onFocus: this.handleFocus,
										onKeyPress: this.props.onKeyPress,
										onKeyDown: this.handleKeyDown,
										onKeyUp: this.props.onKeyUp
									})} />
									{
										isOpen && suggestionsList.length
											? (

													{
														suggestionsList
															.map((item, index) => (

																	{item.label}

															))
													}

)
											: null
									}

							)}
						/>)
						: (<Input
							placeholder={this.props.placeholder}
							value={this.state.currentValue ? this.state.currentValue : ""}
							onChange={(e) => this.setValue(e.target.value)}
							onBlur={this.props.onBlur}
							onFocus={this.props.onFocus}
							onKeyPress={this.props.onKeyPress}
							onKeyDown={this.props.onKeyDown}
							onKeyUp={this.props.onKeyUp}
							autoFocus={this.props.autoFocus}
						/>)
				}

		);
	}
}

DataSearch.propTypes = {
	componentId: types.stringRequired,
	title: types.title,
	addComponent: types.funcRequired,
	highlight: types.bool,
	setQueryOptions: types.funcRequired,
	defaultSelected: types.string,
	dataField: types.dataFieldArray,
	highlightField: types.highlightField,
	react: types.react,
	suggestions: types.suggestions,
	defaultSuggestions: types.suggestions,
	removeComponent: types.funcRequired,
	fieldWeights: types.fieldWeights,
	queryFormat: types.queryFormatSearch,
	fuzziness: types.fuzziness,
	autoSuggest: types.bool,
	beforeValueChange: types.func,
	onValueChange: types.func,
	customQuery: types.func,
	onQueryChange: types.func,
	onSuggestions: types.func,
	updateQuery: types.funcRequired,
	placeholder: types.string,
	onBlur: types.func,
	onFocus: types.func,
	onKeyPress: types.func,
	onKeyDown: types.func,
	onKeyUp: types.func,
	autoFocus: types.bool,
	selectedValue: types.selectedValue,
	URLParams: types.boolRequired,
	showFilter: types.bool,
	filterLabel: types.string,
	style: types.style,
	className: types.string
}

DataSearch.defaultProps = {
	placeholder: "Search",
	autoSuggest: true,
	queryFormat: "or",
	URLParams: false,
	showFilter: true,
	style: {},
	className: null
}

const mapStateToProps = (state, props) => ({
	suggestions: state.hits[props.componentId] && state.hits[props.componentId].hits,
	selectedValue: state.selectedValues[props.componentId] && state.selectedValues[props.componentId].value || null
});

const mapDispatchtoProps = dispatch => ({
	addComponent: component => dispatch(addComponent(component)),
	removeComponent: component => dispatch(removeComponent(component)),
	watchComponent: (component, react) => dispatch(watchComponent(component, react)),
	updateQuery: (updateQueryObject) => dispatch(
		updateQuery(updateQueryObject)
	),
	setQueryOptions: (component, props) => dispatch(setQueryOptions(component, props))
});

export default connect(mapStateToProps, mapDispatchtoProps)(DataSearch);
<commit_msg>Fix DataSearch suggestion selection<commit_after>import React, { Component } from "react";
import { connect } from "react-redux";
import Downshift from "downshift";

import {
	addComponent,
	removeComponent,
	watchComponent,
	updateQuery,
	setQueryOptions
} from "@appbaseio/reactivecore/lib/actions";
import {
	debounce,
	pushToAndClause,
	checkValueChange,
	checkPropChange,
	checkSomePropChange
} from "@appbaseio/reactivecore/lib/utils/helper";

import types from "@appbaseio/reactivecore/lib/utils/types";
import Title from "../../styles/Title";
import Input, { suggestionsContainer, suggestions } from "../../styles/Input";

class DataSearch extends Component {
	constructor(props) {
		super(props);

		this.state = {
			currentValue: "",
			suggestions: [],
			isOpen: false
		};
		this.internalComponent = props.componentId + "__internal";
	}

	componentWillMount() {
		this.props.addComponent(this.props.componentId);
		this.props.addComponent(this.internalComponent);

		if (this.props.highlight) {
			const queryOptions = this.highlightQuery(this.props);
			this.props.setQueryOptions(this.props.componentId, queryOptions);
		}
		this.setReact(this.props);

		if (this.props.selectedValue) {
			this.setValue(this.props.selectedValue, true);
		} else if (this.props.defaultSelected) {
			this.setValue(this.props.defaultSelected, true);
		}
	}

	componentWillReceiveProps(nextProps) {
		checkSomePropChange(
			this.props,
			nextProps,
			["highlight", "dataField", "highlightField"],
			() => {
				const queryOptions = this.highlightQuery(nextProps);
				this.props.setQueryOptions(nextProps.componentId, queryOptions);
			}
		);

		checkPropChange(
			this.props.react,
			nextProps.react,
			() => this.setReact(nextProps)
		);

		if (Array.isArray(nextProps.suggestions) && this.state.currentValue.trim().length) {
			checkPropChange(
				this.props.suggestions,
				nextProps.suggestions,
				() => {
					this.setState({
						suggestions: this.onSuggestions(nextProps.suggestions)
					});
				}
			);
		}

		checkSomePropChange(
			this.props,
			nextProps,
			["fieldWeights", "fuzziness", "queryFormat"],
			() => {
				this.updateQuery(nextProps.componentId, this.state.currentValue, nextProps)
			}
		);

		if (this.props.defaultSelected !== nextProps.defaultSelected) {
			this.setValue(nextProps.defaultSelected, true, nextProps);
		} else if (
			this.props.selectedValue !== nextProps.selectedValue &&
			this.state.currentValue !== nextProps.selectedValue
		) {
			// check for selected value prop change or selectedValue will never match currentValue
			this.setValue(nextProps.selectedValue || "", true, nextProps);
		}
	}

	componentWillUnmount() {
		this.props.removeComponent(this.props.componentId);
		this.props.removeComponent(this.internalComponent);
	}

	setReact = (props) => {
		const { react } = props;
		if (react) {
			const newReact = pushToAndClause(react, this.internalComponent);
			props.watchComponent(props.componentId, newReact);
		} else {
			props.watchComponent(props.componentId, { and: this.internalComponent });
		}
	}

	highlightQuery = (props) => {
		if (!props.highlight) {
			return null;
		}
		const fields = {};
		const highlightField = props.highlightField ? props.highlightField : props.dataField;

		if (typeof highlightField === "string") {
			fields[highlightField] = {};
		} else if (Array.isArray(highlightField)) {
			highlightField.forEach((item) => {
				fields[item] = {};
			});
		}

		return {
			highlight: {
				pre_tags: [""],
				post_tags: [""],
				fields
			}
		};
	}

	defaultQuery = (value, props) => {
		let finalQuery = null,
			fields;
		if (value) {
			if (Array.isArray(props.dataField)) {
				fields = props.dataField;
			} else {
				fields = [props.dataField];
			}
			finalQuery = {
				bool: {
					should: this.shouldQuery(value, fields, props),
					minimum_should_match: "1"
				}
			};
		}

		if (value === "") {
			finalQuery = {
				"match_all": {}
			}
		}

		return finalQuery;
	};

	shouldQuery = (value, dataFields, props) => {
		const fields = dataFields.map(
			(field, index) => `${field}${(Array.isArray(props.fieldWeights) && props.fieldWeights[index]) ? ("^" + props.fieldWeights[index]) : ""}`
		);

		if (props.queryFormat === "and") {
			return [
				{
					multi_match: {
						query: value,
						fields,
						type: "cross_fields",
						operator: "and",
						fuzziness: props.fuzziness ? props.fuzziness : 0
					}
				},
				{
					multi_match: {
						query: value,
						fields,
						type: "phrase_prefix",
						operator: "and"
					}
				}
			];
		}

		return [
			{
				multi_match: {
					query: value,
					fields,
					type: "best_fields",
					operator: "or",
					fuzziness: props.fuzziness ? props.fuzziness : 0
				}
			},
			{
				multi_match: {
					query: value,
					fields,
					type: "phrase_prefix",
					operator: "or"
				}
			}
		];
	};

	onSuggestions = (suggestions) => {
		if (this.props.onSuggestions) {
			return this.props.onSuggestions(suggestions);
		}

		const fields = Array.isArray(this.props.dataField) ? this.props.dataField : [this.props.dataField];
		let suggestionsList = [];
		let labelsList = [];
		const currentValue = this.state.currentValue.toLowerCase();

		suggestions.forEach(item => {
			fields.forEach(field => {
				const label = item._source[field];
				const val = label.toLowerCase();

				if (val.includes(currentValue) && !labelsList.includes(val)) {
					const option = {
						label,
						value: label
					};
					labelsList = [...labelsList, val];
					suggestionsList = [...suggestionsList, option];
				}
			});
		});

		return suggestionsList;
	};

	setValue = (value, isDefaultValue = false, props = this.props) => {
		const performUpdate = () => {
			this.setState({
				currentValue: value
			});
			if (isDefaultValue) {
				if (this.props.autoSuggest) {
					this.setState({
						isOpen: false
					});
					this.updateQuery(this.internalComponent, value, props);
				}
				this.updateQuery(props.componentId, value, props);
			} else {
				// debounce for handling text while typing
				this.handleTextChange(value);
			}
		}
		checkValueChange(
			props.componentId,
			value,
			props.beforeValueChange,
			props.onValueChange,
			performUpdate
		);
	};

	handleTextChange = debounce((value) => {
		if (this.props.autoSuggest) {
			this.updateQuery(this.internalComponent, value, this.props);
		} else {
			this.updateQuery(this.props.componentId, value, this.props);
		}
	}, 300);

	updateQuery = (componentId, value, props) => {
		const query = props.customQuery || this.defaultQuery;
		let onQueryChange = null;
		if (componentId === props.componentId && props.onQueryChange) {
			onQueryChange = props.onQueryChange;
		}
		props.updateQuery({
			componentId,
			query: query(value, props),
			value,
			label: props.filterLabel,
			showFilter: props.showFilter,
			onQueryChange,
			URLParams: props.URLParams
		});
	};

	handleFocus = (event) => {
		this.setState({
			isOpen: true
		});
		if (this.props.onFocus) {
			this.props.onFocus(event);
		}
	};

	// only works if there's a change in downshift's value
	handleOuterClick = () => {
		this.setValue(this.state.currentValue, true)
	};

	handleKeyDown = (event) => {
		if (event.key === "Enter") {
			event.target.blur();
			this.setValue(event.target.value, true);
		}
		if (this.props.onKeyDown) {
			this.props.onKeyDown(event);
		}
	};

	onInputChange = (e, v) => {
		this.setValue(e.target.value);
	};

	onSuggestionSelected = (suggestion, event) => {
		this.setValue(suggestion.value, true);
		if (this.props.onBlur) {
			this.props.onBlur(event);
		}
	};

	handleStateChange = (changes) => {
		const { isOpen, type } = changes;
		if (type === Downshift.stateChangeTypes.mouseUp) {
			this.setState({
				isOpen
			});
		}
	};

	render() {
		const suggestionsList = this.state.currentValue === "" || this.state.currentValue === null
			? this.props.defaultSuggestions && this.props.defaultSuggestions.length
				? this.props.defaultSuggestions
				: []
			: this.state.suggestions;

		return (

				{this.props.title && <Title>{this.props.title}</Title>}
				{
					this.props.autoSuggest
						? (<Downshift
							onChange={this.onSuggestionSelected}
							onOuterClick={this.handleOuterClick}
							onStateChange={this.handleStateChange}
							isOpen={this.state.isOpen}
							itemToString={i => i}
							render={({
								getInputProps,
								getItemProps,
								isOpen,
								highlightedIndex
							}) => (

									<Input {...getInputProps({
										placeholder: this.props.placeholder,
										value: this.state.currentValue === null ? "" : this.state.currentValue,
										onChange: this.onInputChange,
										onBlur: this.props.onBlur,
										onFocus: this.handleFocus,
										onKeyPress: this.props.onKeyPress,
										onKeyDown: this.handleKeyDown,
										onKeyUp: this.props.onKeyUp
									})} />
									{
										isOpen && suggestionsList.length
											? (

													{
														suggestionsList
															.map((item, index) => (

																	{item.label}

															))
													}

)
											: null
									}

							)}
						/>)
						: (<Input
							placeholder={this.props.placeholder}
							value={this.state.currentValue ? this.state.currentValue : ""}
							onChange={(e) => this.setValue(e.target.value)}
							onBlur={this.props.onBlur}
							onFocus={this.props.onFocus}
							onKeyPress={this.props.onKeyPress}
							onKeyDown={this.props.onKeyDown}
							onKeyUp={this.props.onKeyUp}
							autoFocus={this.props.autoFocus}
						/>)
				}

		);
	}
}

DataSearch.propTypes = {
	componentId: types.stringRequired,
	title: types.title,
	addComponent: types.funcRequired,
	highlight: types.bool,
	setQueryOptions: types.funcRequired,
	defaultSelected: types.string,
	dataField: types.dataFieldArray,
	highlightField: types.highlightField,
	react: types.react,
	suggestions: types.suggestions,
	defaultSuggestions: types.suggestions,
	removeComponent: types.funcRequired,
	fieldWeights: types.fieldWeights,
	queryFormat: types.queryFormatSearch,
	fuzziness: types.fuzziness,
	autoSuggest: types.bool,
	beforeValueChange: types.func,
	onValueChange: types.func,
	customQuery: types.func,
	onQueryChange: types.func,
	onSuggestions: types.func,
	updateQuery: types.funcRequired,
	placeholder: types.string,
	onBlur: types.func,
	onFocus: types.func,
	onKeyPress: types.func,
	onKeyDown: types.func,
	onKeyUp: types.func,
	autoFocus: types.bool,
	selectedValue: types.selectedValue,
	URLParams: types.boolRequired,
	showFilter: types.bool,
	filterLabel: types.string,
	style: types.style,
	className: types.string
}

DataSearch.defaultProps = {
	placeholder: "Search",
	autoSuggest: true,
	queryFormat: "or",
	URLParams: false,
	showFilter: true,
	style: {},
	className: null
}

const mapStateToProps = (state, props) => ({
	suggestions: state.hits[props.componentId] && state.hits[props.componentId].hits,
	selectedValue: state.selectedValues[props.componentId] && state.selectedValues[props.componentId].value || null
});

const mapDispatchtoProps = dispatch => ({
	addComponent: component => dispatch(addComponent(component)),
	removeComponent: component => dispatch(removeComponent(component)),
	watchComponent: (component, react) => dispatch(watchComponent(component, react)),
	updateQuery: (updateQueryObject) => dispatch(
		updateQuery(updateQueryObject)
	),
	setQueryOptions: (component, props) => dispatch(setQueryOptions(component, props))
});

export default connect(mapStateToProps, mapDispatchtoProps)(DataSearch);
<|endoftext|>