<commit_before>// Template.editSampleGroup

Template.editSampleGroup.onCreated(function () {
  let instance = this;

  // load all available data sets
  instance.subscribe("dataSets");

  instance.sampleGroup = instance.data.sampleGroup;

  // make sure it's initialized
  if (!instance.sampleGroup.get()) {
    instance.sampleGroup.set({
      name: "",
      version: 1,
      collaborations: [ Meteor.user().collaborations.personal ],
      data_sets: []
    });
  }

  // store this seperately so that we don't look for a version every time
  // something that's not the name changes
  instance.name = new ReactiveVar("");

  // look up the name in the sample groups this person has access to
  // and figure out what version this one should be
  let updateVersion = _.debounce((name) => {
    Meteor.call("getSampleGroupVersion", name, (error, result) => {
      let sampleGroup = instance.sampleGroup.get();
      sampleGroup.version = result;
      instance.sampleGroup.set(sampleGroup);
    });
  }, 250);
  instance.autorun(() => {
    updateVersion(instance.name.get());
  });

  // update the sample group with the name
  instance.autorun(() => {
    // don't rerun when sample group changes
    let sampleGroup = Tracker.nonreactive(() => {
      return instance.sampleGroup.get();
    });
    sampleGroup.name = instance.name.get();
    instance.sampleGroup.set(sampleGroup);
  });
});

Template.editSampleGroup.onRendered(function () {
  let instance = this;

  instance.$(".sample-group-version").popup({
    position : "bottom right",
  });
});

Template.editSampleGroup.helpers({
  sampleGroup: function () {
    return Template.instance().sampleGroup; // returns ReactiveVar
  },
  getSampleGroup: function () {
    return Template.instance().sampleGroup.get();
  },
  addableDataSets: function () {
    let addedDataSets = Template.instance().sampleGroup.get().data_sets;

    // only return data sets that haven't already been added
    return DataSets.find({
      _id: { $nin: _.pluck(addedDataSets, "data_set_id") },
    });
  },
  dataSetName: function () {
    let dataSet = DataSets.findOne(this.data_set_id);

    if (dataSet) {
      return dataSet.name;
    } else {
      return "You don't have access to this data set.";
    }
  },
});

Template.editSampleGroup.events({
  "keyup .sample-group-name": function (event, instance) {
    instance.name.set(event.target.value);
  },
  "click .remove-data-set": function (event, instance) {
    let sampleGroup = instance.sampleGroup.get();

    sampleGroup.data_sets = _.filter(sampleGroup.data_sets, (dataSet) => {
      return dataSet.data_set_id !== this.data_set_id
    });

    instance.sampleGroup.set(sampleGroup);
  },
});

// Template.addDataSetMenu

Template.addDataSetMenu.onRendered(function () {
  let instance = this;

  instance.$(".dropdown").popup({
    hoverable: true,
    on: "click",
    position: "bottom left",
  });
});

Template.addDataSetMenu.events({
  "click .add-data-set-to-sample-group": function (event, instance) {
    let sampleGroup = instance.data.sampleGroup.get();

    sampleGroup.data_sets.push({
      data_set_id: this._id,
      filters: [],
    });

    instance.data.sampleGroup.set(sampleGroup);
  },
});

// Template.addFilterButton

Template.addFilterButton.onCreated(function () {
  let instance = this;

  instance.addFilter = function(filterObject) {
    // the popup moves down weirdly, so hide it
    instance.$(".dropdown").popup("hide");

    // add the filter to the data set
    let sampleGroup = instance.data.sampleGroup.get();
    sampleGroup.data_sets[instance.data.dataSetIndex].filters.push(filterObject);
    instance.data.sampleGroup.set(sampleGroup);
  }
  // Only allow one form values filter
});

Template.addFilterButton.onRendered(function () {
  let instance = this;

  instance.$(".dropdown").popup({
    hoverable: true,
    on: "click",
  });
});

Template.addFilterButton.helpers({
  isAFormValuesFilterActive: function(){
    let sampleGroup = Template.instance().data.sampleGroup.get();
    let currentDataSet = sampleGroup.data_sets[Template.instance().data.dataSetIndex];
    // No data sets -> no form values filters
    if( typeof(currentDataSet) === "undefined"){ return false; }
    let allFilters = currentDataSet.filters ;
    let wasFilter = (_.pluck(allFilters, "type").indexOf("form_values") !== -1);
    return wasFilter;
  },
});

Template.addFilterButton.events({
  "click .add-form-values-filter": function (event, instance) {
    instance.addFilter({
      type: "form_values",
      options : {
        form_id: "",
        mongo_query: "",
      },
    });
  },
  "click .add-sample-label-list-filter": function (event, instance) {
    instance.addFilter({
      type: "include_sample_list",
      options: {
        sample_labels: []
      },
    });
  },
  "click .add-exclude-sample-label-list-filter": function (event, instance) {
    instance.addFilter({
      type: "exclude_sample_list",
      options: {
        sample_labels: []
      },
    });
  },
});

// Template.showFilter

Template.showFilter.onCreated(function () {
  let instance = this;

  instance.sampleGroup = instance.data.sampleGroup;

  instance.setOptions = function (newOptions) {
    let sampleGroup = instance.sampleGroup.get();

    let { filterIndex, dataSetIndex } = instance.data;
    sampleGroup.data_sets[dataSetIndex].filters[filterIndex].options = newOptions;

    instance.sampleGroup.set(sampleGroup);
  };
});

Template.showFilter.helpers({
  getFilter: function () {
    let { sampleGroup, data } = Template.instance();

    let dataSet = sampleGroup.get().data_sets[data.dataSetIndex];
    if (dataSet) { // remove error on remove dataSet
      return dataSet.filters[data.filterIndex];
    }
  },
  setOptions: function () {
    return Template.instance().setOptions;
  },
  data_set_id: function () {
    let instance = Template.instance();
    return instance.sampleGroup.get()
        .data_sets[instance.data.dataSetIndex].data_set_id;
  },
});

Template.showFilter.events({
  "click .remove-filter": function (event, instance) {
    // define a button with this class in a sub-template to make it work
    let sampleGroup = instance.sampleGroup.get();

    let { filterIndex, dataSetIndex } = instance.data;
    sampleGroup.data_sets[dataSetIndex].filters.splice(filterIndex, 1);

    instance.sampleGroup.set(sampleGroup);
  },
});

// Template.sampleLabelListFilter

Template.sampleLabelListFilter.onCreated(function () {
  let instance = this;

  instance.editing = new ReactiveVar(false);
  instance.invalidSampleLabels = new ReactiveVar(null);
});

Template.sampleLabelListFilter.helpers({
  sampleLabelsToText: function () {
    let sampleObjs =
        MedBook.utility.sampleArrStrToObj(this.options.sample_labels);

    return _.pluck(sampleObjs, "uq_sample_label").join("\n");
  },
  getInvalidSampleLabels: function () {
    return Template.instance().invalidSampleLabels.get();
  },
  getEditing: function () {
    return Template.instance().editing.get();
  },
});

Template.sampleLabelListFilter.events({
  "click .done-editing": function (event, instance) {
    event.preventDefault();

    // clear errors
    instance.invalidSampleLabels.set(null);

    // TODO: for now only allow data sets that have one study
    let dataSet = DataSets.findOne(instance.data.data_set_id);
    let sampleStudyObjs =
        MedBook.utility.sampleArrStrToObj(dataSet.sample_labels);
    let uniqueStudyLabels = _.uniq(_.pluck(sampleStudyObjs, "study_label"));

    if (uniqueStudyLabels.length !== 1) {
      alert("multiple study data sets not supported... yet!");
      instance.editing.set(false);
      return;
    }

    let study_label = uniqueStudyLabels[0];

    // let's gooo (split by whitespace characters, get rid of spaces)
    let textareaSampleLabels = instance.$("textarea").val().split(/[\s,;]+/);
    let sample_labels = _.chain(textareaSampleLabels)
      .filter((value) => { return value; }) // remove falsey
      .uniq() // uniques only
      .map((uq_sample_label) => {
        // convert into sample_labels
        return study_label + "/" + uq_sample_label;
      })
      .value();

    // make sure we don't have any bad values
    let badValues = _.filter(sample_labels, function (sample_label) {
      return dataSet.sample_label_index[sample_label] === undefined;
    });

    // if we do, display them to the user
    if (badValues.length) {
      let sampleObjs = MedBook.utility.sampleArrStrToObj(badValues);

      instance.invalidSampleLabels.set(_.pluck(sampleObjs, "uq_sample_label"));
      return;
    }

    // nicely done! set the options and return to non-editing
    instance.data.setOptions({
      sample_labels
    });
    instance.editing.set(false);
  },
  "click .edit-filter": function (event, instance) {
    // unclear why we need this, but otherwise it submits the form
    event.preventDefault();

    instance.editing.set(true);
  },
  "click .close-sample-error-message": function (event, instance) {
    instance.invalidSampleLabels.set(null);
  },
});

Template.formValuesFilter.onCreated(function(){
  // Find forms that share samples with this data set
  // let them be options for which form to filter on
  let instance = this;

  instance.editing = new ReactiveVar(false);

  let dataset_id = instance.data.data_set_id ;
  instance.available_filter_forms = new ReactiveVar();
  instance.available_filter_forms.set([{name: "Loading forms...", urlencodedId: "placeholder_loadingforms"}]);
  instance.filter_forms_options = new ReactiveVar();
  instance.filter_forms_options.set({});

  instance.active_querybuilder = new ReactiveVar("");
  instance.active_crf = new ReactiveVar("");

  // Store forms for this data set in a reactive var for later use.
  Meteor.call("getFormsMatchingDataSet", dataset_id, function(err, res){
    if(err) {
      instance.available_filter_forms.set([{name:'Error loading forms!', formId: 'Errorloadingforms'}]);
      console.log("Error getting forms for this data set", err);
      throw err;
    } else {
      instance.available_filter_forms.set(res);
    }
  });
});

Template.formValuesFilterMenu.onRendered(function(){
  let instance = this;
  instance.$(".ui.dropdown").dropdown();
});

Template.formValuesFilter.helpers({
  getFilterFormsOptions: function() {

  },
  getAvailableFilterForms: function() {
    return Template.instance().available_filter_forms.get();
  },
  getEditing: function(){
    return Template.instance().editing.get();
  },
});

Template.formValuesFilter.events({
  "click .chosen-form-filter": function(event, instance) {
    // Find the ids for the selected form from the dropdown
    let clicked_dataset_id = event.target.dataset.dataset_id ;
    let clicked_form_id = event.target.dataset.form_id ;

    // Find the form that matches the current dataset and form id
    let forms = instance.available_filter_forms.get();
    let chosenForm = _.find(forms, function(form){
      return (form.formId === clicked_form_id) && (form.dataSetId === clicked_dataset_id)});
    let formFields = chosenForm.fields ;

    // Then build the filters for the querybuilder
    let queryFilters = [];
    for(let field of formFields){
      // field will be object with keys
      // name, value_type, values
      queryFilters.push(
        { id: field.name,
          label: field.name,
          type: "string", // TODO use value_type & propagate this through
          input: "select",
          values: field.values,
          operators: ['equal', 'not_equal', 'is_null', 'is_not_null'],
        }
      );
    }

  // If there's already an active querybuilder for this dataset, hide it
  if(instance.active_querybuilder.get() !== ""){
    $(instance.active_querybuilder.get()).hide();
    instance.active_crf.set("");
  }

  // Find the empty querybuilder div we prepared in the formValuesFilter template
  // and attach a querybuilder object to it
  let queryBuilderDivId = "#" + clicked_dataset_id + "_" + clicked_form_id + "_querybuilder";
  $(queryBuilderDivId).show()
  $(queryBuilderDivId).queryBuilder({
    filters: queryFilters,
    });

  // And set it as active so we can find it later
  instance.active_querybuilder.set(queryBuilderDivId);
  instance.active_crf.set(clicked_form_id);
  },

  "click .done-editing": function(event, instance){
    event.preventDefault();

    let queryBuilderDivId = instance.active_querybuilder.get();

    let query = $(queryBuilderDivId).queryBuilder('getMongo');
    let serialized_query = JSON.stringify(query);
    let sampleCrfId = decodeURIComponent(instance.active_crf.get());
     let dataset_id = instance.data.data_set_id;

    instance.editing.set(false);

    // Populate the filter info
    // TODO: rename variables -- sampleCrfId no longer refers to a CRF
    // but to a Form; & it's the ID of the actual Form (not a Record
    // of that form)
    instance.data.setOptions({
      form_id: sampleCrfId,
      mongo_query: serialized_query,
    });
   },
  "click .edit-filter": function(event, instance){
    event.preventDefault();
    instance.editing.set(true);
  },
});
<commit_msg>fix merge error located by @e-k-t<commit_after>// Template.editSampleGroup

Template.editSampleGroup.onCreated(function () {
  let instance = this;

  // load all available data sets
  instance.subscribe("dataSets");

  instance.sampleGroup = instance.data.sampleGroup;

  // make sure it's initialized
  if (!instance.sampleGroup.get()) {
    instance.sampleGroup.set({
      name: "",
      version: 1,
      collaborations: [ Meteor.user().collaborations.personal ],
      data_sets: []
    });
  }

  // store this seperately so that we don't look for a version every time
  // something that's not the name changes
  instance.name = new ReactiveVar("");

  // look up the name in the sample groups this person has access to
  // and figure out what version this one should be
  let updateVersion = _.debounce((name) => {
    Meteor.call("getSampleGroupVersion", name, (error, result) => {
      let sampleGroup = instance.sampleGroup.get();
      sampleGroup.version = result;
      instance.sampleGroup.set(sampleGroup);
    });
  }, 250);
  instance.autorun(() => {
    updateVersion(instance.name.get());
  });

  // update the sample group with the name
  instance.autorun(() => {
    // don't rerun when sample group changes
    let sampleGroup = Tracker.nonreactive(() => {
      return instance.sampleGroup.get();
    });
    sampleGroup.name = instance.name.get();
    instance.sampleGroup.set(sampleGroup);
  });
});

Template.editSampleGroup.onRendered(function () {
  let instance = this;

  instance.$(".sample-group-version").popup({
    position : "bottom right",
  });
});

Template.editSampleGroup.helpers({
  sampleGroup: function () {
    return Template.instance().sampleGroup; // returns ReactiveVar
  },
  getSampleGroup: function () {
    return Template.instance().sampleGroup.get();
  },
  addableDataSets: function () {
    let addedDataSets = Template.instance().sampleGroup.get().data_sets;

    // only return data sets that haven't already been added
    return DataSets.find({
      _id: { $nin: _.pluck(addedDataSets, "data_set_id") },
    });
  },
  dataSetName: function () {
    let dataSet = DataSets.findOne(this.data_set_id);

    if (dataSet) {
      return dataSet.name;
    } else {
      return "You don't have access to this data set.";
    }
  },
});

Template.editSampleGroup.events({
  "keyup .sample-group-name": function (event, instance) {
    instance.name.set(event.target.value);
  },
  "click .remove-data-set": function (event, instance) {
    let sampleGroup = instance.sampleGroup.get();

    sampleGroup.data_sets = _.filter(sampleGroup.data_sets, (dataSet) => {
      return dataSet.data_set_id !== this.data_set_id;
    });

    instance.sampleGroup.set(sampleGroup);
  },
});

// Template.addDataSetMenu

Template.addDataSetMenu.onRendered(function () {
  let instance = this;

  instance.$(".dropdown").popup({
    hoverable: true,
    on: "click",
    position: "bottom left",
  });
});

Template.addDataSetMenu.events({
  "click .add-data-set-to-sample-group": function (event, instance) {
    let sampleGroup = instance.data.sampleGroup.get();

    sampleGroup.data_sets.push({
      data_set_id: this._id,
      filters: [],
    });

    instance.data.sampleGroup.set(sampleGroup);
  },
});

// Template.addFilterButton

Template.addFilterButton.onCreated(function () {
  let instance = this;

  instance.addFilter = function(filterObject) {
    // the popup moves down weirdly, so hide it
    instance.$(".dropdown").popup("hide");

    // add the filter to the data set
    let sampleGroup = instance.data.sampleGroup.get();
    sampleGroup.data_sets[instance.data.dataSetIndex].filters.push(filterObject);
    instance.data.sampleGroup.set(sampleGroup);
  };
  // Only allow one form values filter
});

Template.addFilterButton.onRendered(function () {
  let instance = this;

  instance.$(".dropdown").popup({
    hoverable: true,
    on: "click",
  });
});

Template.addFilterButton.helpers({
  isAFormValuesFilterActive: function(){
    let sampleGroup = Template.instance().data.sampleGroup.get();
    let currentDataSet = sampleGroup.data_sets[Template.instance().data.dataSetIndex];
    // No data sets -> no form values filters
    if( typeof(currentDataSet) === "undefined"){ return false; }
    let allFilters = currentDataSet.filters ;
    let wasFilter = (_.pluck(allFilters, "type").indexOf("form_values") !== -1);
    return wasFilter;
  },
});

Template.addFilterButton.events({
  "click .add-form-values-filter": function (event, instance) {
    instance.addFilter({
      type: "form_values",
      options : {
        form_id: "",
        mongo_query: "",
      },
    });
  },
  "click .add-sample-label-list-filter": function (event, instance) {
    instance.addFilter({
      type: "include_sample_list",
      options: {
        sample_labels: []
      },
    });
  },
  "click .add-exclude-sample-label-list-filter": function (event, instance) {
    instance.addFilter({
      type: "exclude_sample_list",
      options: {
        sample_labels: []
      },
    });
  },
});

// Template.showFilter

Template.showFilter.onCreated(function () {
  let instance = this;

  instance.sampleGroup = instance.data.sampleGroup;

  instance.setOptions = function (newOptions) {
    let sampleGroup = instance.sampleGroup.get();

    let { filterIndex, dataSetIndex } = instance.data;
    sampleGroup.data_sets[dataSetIndex].filters[filterIndex].options = newOptions;

    instance.sampleGroup.set(sampleGroup);
  };
});

Template.showFilter.helpers({
  getFilter: function () {
    let { sampleGroup, data } = Template.instance();

    let dataSet = sampleGroup.get().data_sets[data.dataSetIndex];
    if (dataSet) { // remove error on remove dataSet
      return dataSet.filters[data.filterIndex];
    }
  },
  setOptions: function () {
    return Template.instance().setOptions;
  },
  data_set_id: function () {
    let instance = Template.instance();
    return instance.sampleGroup.get()
        .data_sets[instance.data.dataSetIndex].data_set_id;
  },
});

Template.showFilter.events({
  "click .remove-filter": function (event, instance) {
    // define a button with this class in a sub-template to make it work
    let sampleGroup = instance.sampleGroup.get();

    let { filterIndex, dataSetIndex } = instance.data;
    sampleGroup.data_sets[dataSetIndex].filters.splice(filterIndex, 1);

    instance.sampleGroup.set(sampleGroup);
  },
});

// Template.sampleLabelListFilter

Template.sampleLabelListFilter.onCreated(function () {
  let instance = this;

  instance.editing = new ReactiveVar(false);
  instance.invalidSampleLabels = new ReactiveVar(null);
});

Template.sampleLabelListFilter.helpers({
  sampleLabelsToText: function () {
    let sampleObjs =
        MedBook.utility.sampleArrStrToObj(this.options.sample_labels);

    return _.pluck(sampleObjs, "uq_sample_label").join("\n");
  },
  getInvalidSampleLabels: function () {
    return Template.instance().invalidSampleLabels.get();
  },
  getEditing: function () {
    return Template.instance().editing.get();
  },
});

Template.sampleLabelListFilter.events({
  "click .done-editing": function (event, instance) {
    event.preventDefault();

    // clear errors
    instance.invalidSampleLabels.set(null);

    // TODO: for now only allow data sets that have one study
    let dataSet = DataSets.findOne(instance.data.data_set_id);
    let sampleStudyObjs =
        MedBook.utility.sampleArrStrToObj(dataSet.sample_labels);
    let uniqueStudyLabels = _.uniq(_.pluck(sampleStudyObjs, "study_label"));

    if (uniqueStudyLabels.length !== 1) {
      alert("multiple study data sets not supported... yet!");
      instance.editing.set(false);
      return;
    }

    let study_label = uniqueStudyLabels[0];

    // let's gooo (split by whitespace characters, get rid of spaces)
    let textareaSampleLabels = instance.$("textarea").val().split(/[\s,;]+/);
    let sample_labels = _.chain(textareaSampleLabels)
      .filter((value) => { return value; }) // remove falsey
      .uniq() // uniques only
      .map((uq_sample_label) => {
        // convert into sample_labels
        return study_label + "/" + uq_sample_label;
      })
      .value();

    // make sure we don't have any bad values
    let badValues = _.filter(sample_labels, function (sample_label) {
      return dataSet.sample_label_index[sample_label] === undefined;
    });

    // if we do, display them to the user
    if (badValues.length) {
      let sampleObjs = MedBook.utility.sampleArrStrToObj(badValues);

      instance.invalidSampleLabels.set(_.pluck(sampleObjs, "uq_sample_label"));
      return;
    }

    // nicely done! set the options and return to non-editing
    instance.data.setOptions({
      sample_labels
    });
    instance.editing.set(false);
  },
  "click .edit-filter": function (event, instance) {
    // unclear why we need this, but otherwise it submits the form
    event.preventDefault();

    instance.editing.set(true);
  },
  "click .close-sample-error-message": function (event, instance) {
    instance.invalidSampleLabels.set(null);
  },
});

Template.formValuesFilter.onCreated(function(){
  // Find forms that share samples with this data set
  // let them be options for which form to filter on
  let instance = this;

  instance.editing = new ReactiveVar(false);

  let dataset_id = instance.data.data_set_id ;
  instance.available_filter_forms = new ReactiveVar();
  instance.available_filter_forms.set([{name: "Loading forms...", formId: "placeholder_loadingforms"}]);
  instance.filter_forms_options = new ReactiveVar();
  instance.filter_forms_options.set({});

  instance.active_querybuilder = new ReactiveVar("");
  instance.active_crf = new ReactiveVar("");

  // Store forms for this data set in a reactive var for later use.
  Meteor.call("getFormsMatchingDataSet", dataset_id, function(err, res){
    if(err) {
      instance.available_filter_forms.set([{name:'Error loading forms!', formId: 'Errorloadingforms'}]);
      console.log("Error getting forms for this data set", err);
      throw err;
    } else {
      instance.available_filter_forms.set(res);
    }
  });
});

Template.formValuesFilterMenu.onRendered(function(){
  let instance = this;
  instance.$(".ui.dropdown").dropdown();
});

Template.formValuesFilter.helpers({
  getFilterFormsOptions: function() {

  },
  getAvailableFilterForms: function() {
    return Template.instance().available_filter_forms.get();
  },
  getEditing: function(){
    return Template.instance().editing.get();
  },
});

Template.formValuesFilter.events({
  "click .chosen-form-filter": function(event, instance) {
    // Find the ids for the selected form from the dropdown
    let clicked_dataset_id = event.target.dataset.dataset_id ;
    let clicked_form_id = event.target.dataset.form_id ;

    // Find the form that matches the current dataset and form id
    let forms = instance.available_filter_forms.get();
    let chosenForm = _.find(forms, function(form){
      return (form.formId === clicked_form_id) && (form.dataSetId === clicked_dataset_id);});
    let formFields = chosenForm.fields ;

    // Then build the filters for the querybuilder
    let queryFilters = [];
    for(let field of formFields){
      // field will be object with keys
      // name, value_type, values
      queryFilters.push(
        { id: field.name,
          label: field.name,
          type: "string", // TODO use value_type & propagate this through
          input: "select",
          values: field.values,
          operators: ['equal', 'not_equal', 'is_null', 'is_not_null'],
        }
      );
    }

  // If there's already an active querybuilder for this dataset, hide it
  if(instance.active_querybuilder.get() !== ""){
    $(instance.active_querybuilder.get()).hide();
    instance.active_crf.set("");
  }

  // Find the empty querybuilder div we prepared in the formValuesFilter template
  // and attach a querybuilder object to it
  let queryBuilderDivId = "#" + clicked_dataset_id + "_" + clicked_form_id + "_querybuilder";
  $(queryBuilderDivId).show();
  $(queryBuilderDivId).queryBuilder({
    filters: queryFilters,
    });

  // And set it as active so we can find it later
  instance.active_querybuilder.set(queryBuilderDivId);
  instance.active_crf.set(clicked_form_id);
  },

  "click .done-editing": function(event, instance){
    event.preventDefault();

    let queryBuilderDivId = instance.active_querybuilder.get();

    let query = $(queryBuilderDivId).queryBuilder('getMongo');
    let serialized_query = JSON.stringify(query);
    let sampleCrfId = decodeURIComponent(instance.active_crf.get());
     let dataset_id = instance.data.data_set_id;

    instance.editing.set(false);

    // Populate the filter info
    // TODO: rename variables -- sampleCrfId no longer refers to a CRF
    // but to a Form; & it's the ID of the actual Form (not a Record
    // of that form)
    instance.data.setOptions({
      form_id: sampleCrfId,
      mongo_query: serialized_query,
    });
   },
  "click .edit-filter": function(event, instance){
    event.preventDefault();
    instance.editing.set(true);
  },
});
<|endoftext|>


<commit_before>/**
 * @file ES6 decorator implementation
 * @since 0.2.9
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfArraySlice*/ // [].slice.call
/*global _gpfDefineClassImport*/ // Retrieves or import entity definition from instance builder
/*global _gpfDefClassAttrCheck*/ // Check attribute
/*global _gpfDefClassAttrBuild*/ // _gpfDefClassAttrBuild
/*global _GPF_DEFINE_CLASS_ATTRIBUTES_NAME*/ // $attributes
/*#endif*/

function _gpfAttributesDecoratorGetAttributesKeyFromMember (member) {
    if (!member) {
        return _GPF_DEFINE_CLASS_ATTRIBUTES_NAME;
    }
    return member;
}

function _gpfAttributesDecoratorProcessEachAttribute (entityDefinition, member, attributes) {
    attributes.forEach(function (attribute) {
        _gpfDefClassAttrCheck.call(entityDefinition, member, attribute);
        _gpfDefClassAttrBuild(member, attribute, entityDefinition._instanceBuilder.prototype);
    });
}

function _gpfAttributesDecoratorAddAttributes (entityDefinition, member, attributes) {
    var key = _gpfAttributesDecoratorGetAttributesKeyFromMember(member);
    entityDefinition._attributes[key] = (entityDefinition._attributes[key] || []).concat(attributes);
    _gpfAttributesDecoratorProcessEachAttribute(entityDefinition, member, attributes);
}

/**
 * Bridge ES6 decorators with attributes
 *
 * @param {...gpf.attributes.Attribute} attribute Attributes to add
 * @return {Function} decorator function
 * @throws {gpf.Error.InvalidParameter}
 * @since 0.2.9
 */
function _gpfAttributesDecorator () {
    var attributes = _gpfArraySlice(arguments);
    return function (ClassConstructor, member/*, descriptor*/) {
        _gpfAttributesDecoratorAddAttributes(_gpfDefineClassImport(ClassConstructor), member, attributes);
    };
}

/**
 * @gpf:sameas _gpfAttributesDecorator
 * @since 0.2.9
 */
gpf.attributes.decorator = _gpfAttributesDecorator;
<commit_msg>Decorators are for ES6 class only (#296)<commit_after>/**
 * @file ES6 decorator implementation
 * @since 0.2.9
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfIsClass*/ // Check if the parameter is an ES6 class
/*global _gpfArraySlice*/ // [].slice.call
/*global _gpfDefineClassImport*/ // Retrieves or import entity definition from instance builder
/*global _gpfDefClassAttrCheck*/ // Check attribute
/*global _gpfDefClassAttrBuild*/ // _gpfDefClassAttrBuild
/*global _GPF_DEFINE_CLASS_ATTRIBUTES_NAME*/ // $attributes
/*#endif*/

_gpfErrorDeclare("attributes/decorator", {

    /**
     * ### Summary
     *
     * ES6 class only
     *
     * ### Description
     *
     * Decorators can be used on ES6 class only
     */
    es6classOnly: "ES6 class only"

});

function _gpfAttributesDecoratorGetAttributesKeyFromMember (member) {
    if (!member) {
        return _GPF_DEFINE_CLASS_ATTRIBUTES_NAME;
    }
    return member;
}

function _gpfAttributesDecoratorProcessEachAttribute (entityDefinition, member, attributes) {
    attributes.forEach(function (attribute) {
        _gpfDefClassAttrCheck.call(entityDefinition, member, attribute);
        _gpfDefClassAttrBuild(member, attribute, entityDefinition._instanceBuilder.prototype);
    });
}

function _gpfAttributesDecoratorAddAttributes (entityDefinition, member, attributes) {
    var key = _gpfAttributesDecoratorGetAttributesKeyFromMember(member);
    entityDefinition._attributes[key] = (entityDefinition._attributes[key] || []).concat(attributes);
    _gpfAttributesDecoratorProcessEachAttribute(entityDefinition, member, attributes);
}

/**
 * Bridge ES6 decorators with attributes
 *
 * @param {...gpf.attributes.Attribute} attribute Attributes to add
 * @return {Function} decorator function
 * @throws {gpf.Error.InvalidParameter}
 * @since 0.2.9
 */
function _gpfAttributesDecorator () {
    var attributes = _gpfArraySlice(arguments);
    return function (ClassConstructor, member/*, descriptor*/) {
        if (!_gpfIsClass(ClassConstructor)) {
            gpf.Error.es6classOnly();
        }
        _gpfAttributesDecoratorAddAttributes(_gpfDefineClassImport(ClassConstructor), member, attributes);
    };
}

/**
 * @gpf:sameas _gpfAttributesDecorator
 * @since 0.2.9
 */
gpf.attributes.decorator = _gpfAttributesDecorator;
<|endoftext|>


<commit_before>'use strict'

import './polyfill'

// API implementations
import allDocs from './all_docs'
import bulkDocs from './bulk_docs'
import changes from './changes'
import destroy from './destroy'
import doCompaction from './do_compaction'
import get from './get'
import getAttachment from './get_attachment'
import getRevisionTree from './get_revision_tree'
import info from './info'

import { get as getDatabase, close as closeDatabase } from './databases'

const ADAPTER_NAME = 'asyncstorage'

function AsyncStoragePouch (dbOpts, constuctorCallback) {
  const api = this

  api.type = () => ADAPTER_NAME

  api._id = callback => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => cb(null, database.meta.db_uuid), callback))
      .catch(callback)
  }
  api._info = callback => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => info(database, cb), callback))
      .catch(callback)
  }
  api._get = (id, opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => get(database, id, opts, cb), callback))
      .catch(callback)
  }
  api._getAttachment = (docId, attachId, attachment, opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => getAttachment(database, docId, attachId, attachment, opts, cb), callback))
      .catch(callback)
  }
  api._getRevisionTree = (id, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => getRevisionTree(database, id, cb), callback))
      .catch(callback)
  }
  api._allDocs = (opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => allDocs(database, opts, cb), callback))
      .catch(callback)
  }
  api._bulkDocs = (req, opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => bulkDocs(database, req, opts, cb), callback))
      .catch(callback)
  }
  api._changes = opts => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => {
        changes(database, api, opts)
        cb()
      }))
      .catch(error => opts.complete && opts.complete(error))
  }
  api._doCompaction = (id, revs, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => doCompaction(database, id, revs, cb), callback))
      .catch(callback)
  }
  api._destroy = (opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => destroy(database, opts, cb), callback))
      .catch(callback)
  }
  api._close = callback => {
    sequence(cb => {
      closeDatabase(dbOpts.name)
      cb()
    }, callback)
  }

  constuctorCallback(null, api)

  const queue = []
  let isRunning = false
  const sequence = (func, callback) => {
    const run = () => {
      if (isRunning || queue.length === 0) return

      isRunning = true
      const task = queue.shift()
      setImmediate(() => {
        task.func((error, result) => {
          task.callback && task.callback(error, result)
          isRunning = false
          run()
        })
      })
    }

    queue.push({func, callback})
    run()
  }
}

AsyncStoragePouch.valid = () => {
  try {
    return require('react-native').AsyncStorage !== null
  } catch (error) {
    return false
  }
}

AsyncStoragePouch.use_prefix = false

export default function (PouchDB) {
  PouchDB.adapter(ADAPTER_NAME, AsyncStoragePouch, true)
}
<commit_msg>Adding _remote to comply with PouchDB deprecation notice.<commit_after>'use strict'

import './polyfill'

// API implementations
import allDocs from './all_docs'
import bulkDocs from './bulk_docs'
import changes from './changes'
import destroy from './destroy'
import doCompaction from './do_compaction'
import get from './get'
import getAttachment from './get_attachment'
import getRevisionTree from './get_revision_tree'
import info from './info'

import { get as getDatabase, close as closeDatabase } from './databases'

const ADAPTER_NAME = 'asyncstorage'

function AsyncStoragePouch (dbOpts, constuctorCallback) {
  const api = this

  api._remote = false
  api.type = () => ADAPTER_NAME

  api._id = callback => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => cb(null, database.meta.db_uuid), callback))
      .catch(callback)
  }
  api._info = callback => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => info(database, cb), callback))
      .catch(callback)
  }
  api._get = (id, opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => get(database, id, opts, cb), callback))
      .catch(callback)
  }
  api._getAttachment = (docId, attachId, attachment, opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => getAttachment(database, docId, attachId, attachment, opts, cb), callback))
      .catch(callback)
  }
  api._getRevisionTree = (id, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => getRevisionTree(database, id, cb), callback))
      .catch(callback)
  }
  api._allDocs = (opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => allDocs(database, opts, cb), callback))
      .catch(callback)
  }
  api._bulkDocs = (req, opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => bulkDocs(database, req, opts, cb), callback))
      .catch(callback)
  }
  api._changes = opts => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => {
        changes(database, api, opts)
        cb()
      }))
      .catch(error => opts.complete && opts.complete(error))
  }
  api._doCompaction = (id, revs, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => doCompaction(database, id, revs, cb), callback))
      .catch(callback)
  }
  api._destroy = (opts, callback) => {
    getDatabase(dbOpts)
      .then(database => sequence(cb => destroy(database, opts, cb), callback))
      .catch(callback)
  }
  api._close = callback => {
    sequence(cb => {
      closeDatabase(dbOpts.name)
      cb()
    }, callback)
  }

  constuctorCallback(null, api)

  const queue = []
  let isRunning = false
  const sequence = (func, callback) => {
    const run = () => {
      if (isRunning || queue.length === 0) return

      isRunning = true
      const task = queue.shift()
      setImmediate(() => {
        task.func((error, result) => {
          task.callback && task.callback(error, result)
          isRunning = false
          run()
        })
      })
    }

    queue.push({func, callback})
    run()
  }
}

AsyncStoragePouch.valid = () => {
  try {
    return require('react-native').AsyncStorage !== null
  } catch (error) {
    return false
  }
}

AsyncStoragePouch.use_prefix = false

export default function (PouchDB) {
  PouchDB.adapter(ADAPTER_NAME, AsyncStoragePouch, true)
}
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';

// Utils
import { getDatasetDefaultWidget, getDatasetDefaultEditableWidget } from 'components/explore-detail/explore-detail-helpers';
import { breakpoints } from 'utils/responsive';

// Components
import MediaQuery from 'react-responsive';

// Widget editor
import WidgetEditor from 'widget-editor';

// Modal
import Modal from 'components/modal/modal-component';
import SaveWidgetModal from 'components/modal/SaveWidgetModal';

// Constants
class ExploreDetailWidgetEditor extends PureComponent {
  static propTypes = {
    dataset: PropTypes.object,
    responsive: PropTypes.object
  }

  state = {
    showSaveModal: false
  }

  handleToggleSaveWidget = (bool) => {
    this.setState({ showSaveModal: bool });
  }

  render() {
    const { dataset, responsive } = this.props;
    const defaultWidget = getDatasetDefaultWidget(dataset);
    const defaultEditableWidget = getDatasetDefaultEditableWidget(dataset);

    return (

        <MediaQuery
          minDeviceWidth={breakpoints.large}
          values={{ deviceWidth: responsive.fakeWidth }}
        >
          <WidgetEditor
            datasetId={dataset.id}
            widgetId={defaultEditableWidget && defaultEditableWidget.id}
            saveButtonMode="auto"
            embedButtonMode="auto"
            titleMode="auto"
            provideWidgetConfig={(func) => { this.onGetWidgetConfig = func; }}
            onSave={() => this.handleToggleSaveWidget(true)}
          />

          <Modal
            isOpen={this.state.showSaveModal}
            className="-medium"
            onRequestClose={() => this.handleToggleSaveWidget(false)}
          >
            <SaveWidgetModal
              dataset={dataset.id}
              getWidgetConfig={this.onGetWidgetConfig}
              onRequestClose={() => this.handleToggleSaveWidget(false)}
            />
          </Modal>
        </MediaQuery>

        {defaultWidget &&
          <MediaQuery
            maxDeviceWidth={breakpoints.large - 1}
            values={{ deviceWidth: responsive.fakeWidth }}
          >
            {/* Widget Card */}
          </MediaQuery>
        }

    );
  }
}

export default ExploreDetailWidgetEditor;
<commit_msg>Explore detail: use Vega chart from widgetEditor<commit_after>import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';

// Utils
import { getDatasetDefaultEditableWidget } from 'components/explore-detail/explore-detail-helpers';
import { breakpoints } from 'utils/responsive';

// Components
import MediaQuery from 'react-responsive';

// Widget editor
import WidgetEditor, { VegaChart } from 'widget-editor';

// Modal
import Modal from 'components/modal/modal-component';
import SaveWidgetModal from 'components/modal/SaveWidgetModal';

// Constants
class ExploreDetailWidgetEditor extends PureComponent {
  static propTypes = {
    dataset: PropTypes.object,
    responsive: PropTypes.object
  }

  state = {
    showSaveModal: false
  }

  handleToggleSaveWidget = (bool) => {
    this.setState({ showSaveModal: bool });
  }

  render() {
    const { dataset, responsive } = this.props;
    const defaultEditableWidget = getDatasetDefaultEditableWidget(dataset);

    return (

        <MediaQuery
          minDeviceWidth={breakpoints.large}
          values={{ deviceWidth: responsive.fakeWidth }}
        >
          <WidgetEditor
            datasetId={dataset.id}
            widgetId={defaultEditableWidget && defaultEditableWidget.id}
            saveButtonMode="auto"
            embedButtonMode="auto"
            titleMode="auto"
            provideWidgetConfig={(func) => { this.onGetWidgetConfig = func; }}
            onSave={() => this.handleToggleSaveWidget(true)}
          />

          <Modal
            isOpen={this.state.showSaveModal}
            className="-medium"
            onRequestClose={() => this.handleToggleSaveWidget(false)}
          >
            <SaveWidgetModal
              dataset={dataset.id}
              getWidgetConfig={this.onGetWidgetConfig}
              onRequestClose={() => this.handleToggleSaveWidget(false)}
            />
          </Modal>
        </MediaQuery>

        {defaultEditableWidget &&
          <MediaQuery
            maxDeviceWidth={breakpoints.large - 1}
            values={{ deviceWidth: responsive.fakeWidth }}
          >
            <VegaChart
              data={defaultEditableWidget.widgetConfig}
              reloadOnResize
            />
          </MediaQuery>
        }

    );
  }
}

export default ExploreDetailWidgetEditor;
<|endoftext|>


<commit_before>var Lang = A.Lang,
	isArray = Lang.isArray,
	isFunction = Lang.isFunction,
	isString = Lang.isString,

	AArray = A.Array,
	LString = A.namespace('Lang.String'),
	arrayIndexOf = AArray.indexOf,

	STR_BLANK = '',

	DOC = A.config.doc,
	FIRST_CHILD = 'firstChild',
	INNER_HTML = 'innerHTML',
	NODE_VALUE = 'nodeValue',
	NORMALIZE = 'normalize',
	REGEX_DASH = /-([a-z])/gi,
	REGEX_ESCAPE_REGEX = /([.*+?^$(){}|[\]\/\\])/g,
	REGEX_STRIP_SCRIPTS = /(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/gi,
	REGEX_STRIP_TAGS = /<\/?[^>]+>/gi,
	REGEX_UNCAMELIZE = /([a-zA-Z][a-zA-Z])([A-Z])([a-z])/g,
	REGEX_UNCAMELIZE_REPLACE_SEPARATOR = /([a-zA-Z][a-zA-Z])([A-Z])([a-z])/g,

	STR_AMP = '&',
	STR_CHEVRON_LEFT = '<',
	STR_ELLIPSIS = '...',
	STR_END = 'end',
	STR_HASH = '#',
	STR_MIDDLE = 'middle',
	STR_START = 'start',
	STR_ZERO = '0',

	STR_G = 'g',
	STR_S = 's',

	mathBuffer = ['return value ', null, ';'],
	htmlUnescapedValues = [],

	cachedMathFn = A.cached(
		function(mathArgs) {
			mathBuffer[1] = mathArgs;

			return new Function('value', mathBuffer.join(STR_BLANK));
		}
	),

	MAP_HTML_CHARS_ESCAPED = {
		'&': '&',
		'<': '<',
		'>': '>',
		'"': '"',
		'\'': ''',
		'/': '/',
		'`': '`'
	},
	MAP_HTML_CHARS_UNESCAPED = {};

	for (var i in MAP_HTML_CHARS_ESCAPED) {
		if (MAP_HTML_CHARS_ESCAPED.hasOwnProperty(i)) {
			var escapedValue = MAP_HTML_CHARS_ESCAPED[i];

			MAP_HTML_CHARS_UNESCAPED[escapedValue] = i;

			htmlUnescapedValues.push(i);
		}
	}

	var REGEX_HTML_ESCAPE = new RegExp('[' + htmlUnescapedValues.join(STR_BLANK) + ']', 'g'),
		REGEX_HTML_UNESCAPE = /&([^;]+);/g;

A.mix(
	LString,
	{
		camelize: A.cached(
			function(str, separator) {
				var regex = REGEX_DASH;

				str = String(str);

				if (separator) {
					regex = new RegExp(separator + '([a-z])', 'gi');
				}

				return str.replace(regex, LString._camelize);
			}
		),

		capitalize: A.cached(
			function(str) {
				if (str) {
					str = String(str);

					str = str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
				}

				return str;
			}
		),

		contains: function(s, ss) {
		  return s.indexOf(ss) != -1;
		},

		defaultValue: function(str, defaultValue) {
			if (Lang.isUndefined(str) || str == STR_BLANK) {
				if (Lang.isUndefined(defaultValue)) {
					defaultValue = STR_BLANK;
				}

				str = defaultValue;
			}

			return str;
		},

		endsWith: function(str, suffix) {
			var length = (str.length - suffix.length);

			return ((length >= 0) && (str.indexOf(suffix, length) == length));
		},

		escapeHTML: function(str) {
			return str.replace(REGEX_HTML_ESCAPE, LString._escapeHTML);
		},

		// Courtesy of: http://simonwillison.net/2006/Jan/20/escape/
		escapeRegEx: function(str) {
			return str.replace(REGEX_ESCAPE_REGEX, '\\$1');
		},

		math: function(value, mathArgs) {
			return cachedMathFn(mathArgs)(value);
		},

		padNumber: function(num, length, precision) {
			var str = precision ? Number(num).toFixed(precision) : String(num);
			var index = str.indexOf('.');

			if (index == -1) {
				index = str.length;
			}

			return LString.repeat(STR_ZERO, Math.max(0, length - index)) + str;
		},

		pluralize: function(count, singularVersion, pluralVersion) {
			var suffix;

			if (count == 1) {
				suffix = singularVersion;
			}
			else {
				suffix = pluralVersion || singularVersion + STR_S;
			}

			return count + ' ' + suffix;
		},

		remove: function(s, substitute, all) {
			var re = new RegExp(LString.escapeRegEx(substitute), all ? STR_G : STR_BLANK);

			return s.replace(re, STR_BLANK);
		},

		removeAll: function(s, substitute) {
			return LString.remove(s, substitute, true);
		},

		repeat: function(string, length) {
			return new Array(length + 1).join(string);
		},

		round: function(value, precision) {
			value = Number(value);

			if (Lang.isNumber(precision)) {
				precision = Math.pow(10, precision);
				value = Math.round(value * precision) / precision;
			}

			return value;
		},

		startsWith: function(str, prefix) {
			return (str.lastIndexOf(prefix, 0) == 0);
		},

		stripScripts: function(str) {
			if (str) {
				str = String(str).replace(REGEX_STRIP_SCRIPTS, STR_BLANK);
			}

			return str;
		},

		stripTags: function(str) {
			var instance = this;

			if (str) {
				str = String(str).replace(REGEX_STRIP_TAGS, STR_BLANK);
			}

			return str;
		},

		substr: function(str, start, length) {
			return String(str).substr(start, length);
		},

		uncamelize: A.cached(
			function(str, separator) {
				separator = separator || ' ';

				str = String(str);

				str = str.replace(REGEX_UNCAMELIZE, '$1' + separator + '$2$3');
				str = str.replace(REGEX_UNCAMELIZE_REPLACE_SEPARATOR, '$1' + separator + '$2');

				return str;
			}
		),

		toLowerCase: function(str) {
			return String(str).toLowerCase();
		},

		toUpperCase: function(str) {
			return String(str).toUpperCase();
		},

		trim: Lang.trim,

		truncate: function(str, length, where) {
			str = String(str);

			var strLength = str.length;

			if (str && strLength > length) {
				where = where || STR_END;

				if (where == STR_END) {
					str = str.substr(0, length - STR_ELLIPSIS.length) + STR_ELLIPSIS;
				}
				else if (where == STR_MIDDLE) {
					var middlePoint = Math.floor(length / 2);

					str = str.substr(0, middlePoint) + STR_ELLIPSIS + str.substr(strLength - middlePoint);
				}
				else if (where == STR_START) {
					str = STR_ELLIPSIS + str.substr(strLength - length);
				}
			}

			return str;
		},

		undef: function(str) {
			if (Lang.isUndefined(str)) {
				str = STR_BLANK;
			}

			return str;
		},

		// inspired from Google unescape entities
		unescapeEntities: function(str) {
			if (LString.contains(str, STR_AMP)) {
				if (DOC && !LString.contains(str, STR_CHEVRON_LEFT)) {
					str = LString._unescapeEntitiesUsingDom(str);
				}
				else {
					str = LString.unescapeHTML(str);
				}
			}

			return str;
		},

		unescapeHTML: function(str) {
			return str.replace(REGEX_HTML_UNESCAPE, LString._unescapeHTML);
		},

		_camelize: function(match0, match1) {
			return match1.toUpperCase();
		},

		_escapeHTML: function(match) {
			return MAP_HTML_CHARS_ESCAPED[match];
		},

		_unescapeHTML: function(match) {
			var value = MAP_HTML_CHARS_UNESCAPED[match];

			if (!value && entity.charAt(0) == STR_HASH) {
				var charCode = Number(STR_ZERO + entity.substr(1));

				if (!isNaN(charCode)) {
					value = String.fromCharCode(charCode);
				}
			}

			return value;
		},

		_unescapeEntitiesUsingDom: function(str) {
			var el = LString._unescapeNode;

			el[INNER_HTML] = str;

			if (el[NORMALIZE]) {
				el[NORMALIZE]();
			}

			str = el.firstChild.nodeValue;

			el[INNER_HTML] = STR_BLANK;

			return str;
		},

		_unescapeNode: DOC.createElement('a')
	}
);

A.mix(
	AArray,
	{
		remove: function(a, from, to) {
		  var rest = a.slice((to || from) + 1 || a.length);
		  a.length = (from < 0) ? (a.length + from) : from;

		  return a.push.apply(a, rest);
		},

		removeItem: function(a, item) {
			var index = arrayIndexOf(a, item);

			return AArray.remove(a, index);
		}
	}
);

A.mix(
	Lang,
	{
		emptyFn: function() {},

		emptyFnFalse: function() {
			return false;
		},

		emptyFnTrue: function() {
			return true;
		},

		isGuid: function(id) {
			var instance = this;

			return String(id).indexOf(A.Env._guidp) === 0;
		}
	}
);<commit_msg>AUI-484 - Adding nl2br to string formatting methods<commit_after>var Lang = A.Lang,
	isArray = Lang.isArray,
	isFunction = Lang.isFunction,
	isString = Lang.isString,

	AArray = A.Array,
	LString = A.namespace('Lang.String'),
	arrayIndexOf = AArray.indexOf,

	STR_BLANK = '',

	DOC = A.config.doc,
	FIRST_CHILD = 'firstChild',
	INNER_HTML = 'innerHTML',
	NODE_VALUE = 'nodeValue',
	NORMALIZE = 'normalize',
	REGEX_DASH = /-([a-z])/gi,
	REGEX_ESCAPE_REGEX = /([.*+?^$(){}|[\]\/\\])/g,
	REGEX_NL2BR = /\r?\n/g,
	REGEX_STRIP_SCRIPTS = /(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/gi,
	REGEX_STRIP_TAGS = /<\/?[^>]+>/gi,
	REGEX_UNCAMELIZE = /([a-zA-Z][a-zA-Z])([A-Z])([a-z])/g,
	REGEX_UNCAMELIZE_REPLACE_SEPARATOR = /([a-zA-Z][a-zA-Z])([A-Z])([a-z])/g,

	STR_AMP = '&',
	STR_CHEVRON_LEFT = '<',
	STR_ELLIPSIS = '...',
	STR_END = 'end',
	STR_HASH = '#',
	STR_MIDDLE = 'middle',
	STR_START = 'start',
	STR_ZERO = '0',

	STR_G = 'g',
	STR_S = 's',

	mathBuffer = ['return value ', null, ';'],
	htmlUnescapedValues = [],

	cachedMathFn = A.cached(
		function(mathArgs) {
			mathBuffer[1] = mathArgs;

			return new Function('value', mathBuffer.join(STR_BLANK));
		}
	),

	MAP_HTML_CHARS_ESCAPED = {
		'&': '&',
		'<': '<',
		'>': '>',
		'"': '"',
		'\'': ''',
		'/': '/',
		'`': '`'
	},
	MAP_HTML_CHARS_UNESCAPED = {};

	for (var i in MAP_HTML_CHARS_ESCAPED) {
		if (MAP_HTML_CHARS_ESCAPED.hasOwnProperty(i)) {
			var escapedValue = MAP_HTML_CHARS_ESCAPED[i];

			MAP_HTML_CHARS_UNESCAPED[escapedValue] = i;

			htmlUnescapedValues.push(i);
		}
	}

	var REGEX_HTML_ESCAPE = new RegExp('[' + htmlUnescapedValues.join(STR_BLANK) + ']', 'g'),
		REGEX_HTML_UNESCAPE = /&([^;]+);/g;

A.mix(
	LString,
	{
		camelize: A.cached(
			function(str, separator) {
				var regex = REGEX_DASH;

				str = String(str);

				if (separator) {
					regex = new RegExp(separator + '([a-z])', 'gi');
				}

				return str.replace(regex, LString._camelize);
			}
		),

		capitalize: A.cached(
			function(str) {
				if (str) {
					str = String(str);

					str = str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
				}

				return str;
			}
		),

		contains: function(s, ss) {
		  return s.indexOf(ss) != -1;
		},

		defaultValue: function(str, defaultValue) {
			if (Lang.isUndefined(str) || str == STR_BLANK) {
				if (Lang.isUndefined(defaultValue)) {
					defaultValue = STR_BLANK;
				}

				str = defaultValue;
			}

			return str;
		},

		endsWith: function(str, suffix) {
			var length = (str.length - suffix.length);

			return ((length >= 0) && (str.indexOf(suffix, length) == length));
		},

		escapeHTML: function(str) {
			return str.replace(REGEX_HTML_ESCAPE, LString._escapeHTML);
		},

		// Courtesy of: http://simonwillison.net/2006/Jan/20/escape/
		escapeRegEx: function(str) {
			return str.replace(REGEX_ESCAPE_REGEX, '\\$1');
		},

		math: function(value, mathArgs) {
			return cachedMathFn(mathArgs)(value);
		},

		nl2br: function(str) {
			var instance = this;

			return String(str).replace(REGEX_NL2BR, '<br />');
		},

		padNumber: function(num, length, precision) {
			var str = precision ? Number(num).toFixed(precision) : String(num);
			var index = str.indexOf('.');

			if (index == -1) {
				index = str.length;
			}

			return LString.repeat(STR_ZERO, Math.max(0, length - index)) + str;
		},

		pluralize: function(count, singularVersion, pluralVersion) {
			var suffix;

			if (count == 1) {
				suffix = singularVersion;
			}
			else {
				suffix = pluralVersion || singularVersion + STR_S;
			}

			return count + ' ' + suffix;
		},

		remove: function(s, substitute, all) {
			var re = new RegExp(LString.escapeRegEx(substitute), all ? STR_G : STR_BLANK);

			return s.replace(re, STR_BLANK);
		},

		removeAll: function(s, substitute) {
			return LString.remove(s, substitute, true);
		},

		repeat: function(string, length) {
			return new Array(length + 1).join(string);
		},

		round: function(value, precision) {
			value = Number(value);

			if (Lang.isNumber(precision)) {
				precision = Math.pow(10, precision);
				value = Math.round(value * precision) / precision;
			}

			return value;
		},

		startsWith: function(str, prefix) {
			return (str.lastIndexOf(prefix, 0) == 0);
		},

		stripScripts: function(str) {
			if (str) {
				str = String(str).replace(REGEX_STRIP_SCRIPTS, STR_BLANK);
			}

			return str;
		},

		stripTags: function(str) {
			var instance = this;

			if (str) {
				str = String(str).replace(REGEX_STRIP_TAGS, STR_BLANK);
			}

			return str;
		},

		substr: function(str, start, length) {
			return String(str).substr(start, length);
		},

		uncamelize: A.cached(
			function(str, separator) {
				separator = separator || ' ';

				str = String(str);

				str = str.replace(REGEX_UNCAMELIZE, '$1' + separator + '$2$3');
				str = str.replace(REGEX_UNCAMELIZE_REPLACE_SEPARATOR, '$1' + separator + '$2');

				return str;
			}
		),

		toLowerCase: function(str) {
			return String(str).toLowerCase();
		},

		toUpperCase: function(str) {
			return String(str).toUpperCase();
		},

		trim: Lang.trim,

		truncate: function(str, length, where) {
			str = String(str);

			var strLength = str.length;

			if (str && strLength > length) {
				where = where || STR_END;

				if (where == STR_END) {
					str = str.substr(0, length - STR_ELLIPSIS.length) + STR_ELLIPSIS;
				}
				else if (where == STR_MIDDLE) {
					var middlePoint = Math.floor(length / 2);

					str = str.substr(0, middlePoint) + STR_ELLIPSIS + str.substr(strLength - middlePoint);
				}
				else if (where == STR_START) {
					str = STR_ELLIPSIS + str.substr(strLength - length);
				}
			}

			return str;
		},

		undef: function(str) {
			if (Lang.isUndefined(str)) {
				str = STR_BLANK;
			}

			return str;
		},

		// inspired from Google unescape entities
		unescapeEntities: function(str) {
			if (LString.contains(str, STR_AMP)) {
				if (DOC && !LString.contains(str, STR_CHEVRON_LEFT)) {
					str = LString._unescapeEntitiesUsingDom(str);
				}
				else {
					str = LString.unescapeHTML(str);
				}
			}

			return str;
		},

		unescapeHTML: function(str) {
			return str.replace(REGEX_HTML_UNESCAPE, LString._unescapeHTML);
		},

		_camelize: function(match0, match1) {
			return match1.toUpperCase();
		},

		_escapeHTML: function(match) {
			return MAP_HTML_CHARS_ESCAPED[match];
		},

		_unescapeHTML: function(match) {
			var value = MAP_HTML_CHARS_UNESCAPED[match];

			if (!value && entity.charAt(0) == STR_HASH) {
				var charCode = Number(STR_ZERO + entity.substr(1));

				if (!isNaN(charCode)) {
					value = String.fromCharCode(charCode);
				}
			}

			return value;
		},

		_unescapeEntitiesUsingDom: function(str) {
			var el = LString._unescapeNode;

			el[INNER_HTML] = str;

			if (el[NORMALIZE]) {
				el[NORMALIZE]();
			}

			str = el.firstChild.nodeValue;

			el[INNER_HTML] = STR_BLANK;

			return str;
		},

		_unescapeNode: DOC.createElement('a')
	}
);

A.mix(
	AArray,
	{
		remove: function(a, from, to) {
		  var rest = a.slice((to || from) + 1 || a.length);
		  a.length = (from < 0) ? (a.length + from) : from;

		  return a.push.apply(a, rest);
		},

		removeItem: function(a, item) {
			var index = arrayIndexOf(a, item);

			return AArray.remove(a, index);
		}
	}
);

A.mix(
	Lang,
	{
		emptyFn: function() {},

		emptyFnFalse: function() {
			return false;
		},

		emptyFnTrue: function() {
			return true;
		},

		isGuid: function(id) {
			var instance = this;

			return String(id).indexOf(A.Env._guidp) === 0;
		}
	}
);<|endoftext|>


<commit_before>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import cx from 'classnames';
import Downshift from 'downshift';
import PropTypes from 'prop-types';
import React, { useEffect, useState, useRef } from 'react';
import { settings } from 'carbon-components';
import {
  Checkmark16,
  WarningAltFilled16,
  WarningFilled16,
} from '@carbon/icons-react';
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
import { ListBoxTrigger, ListBoxSelection } from '../ListBox/next';
import { match, keys } from '../../internal/keyboard';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
import { mapDownshiftProps } from '../../tools/createPropAdapter';
import mergeRefs from '../../tools/mergeRefs';

const { prefix } = settings;

const defaultItemToString = (item) => {
  if (typeof item === 'string') {
    return item;
  }

  return item && item.label;
};

const defaultShouldFilterItem = () => true;

const getInputValue = ({
  initialSelectedItem,
  inputValue,
  itemToString,
  selectedItem,
}) => {
  if (selectedItem) {
    return itemToString(selectedItem);
  }
  // TODO: consistent `initialSelectedItem` behavior with other listbox components in v11
  if (initialSelectedItem) {
    return itemToString(initialSelectedItem);
  }

  return inputValue || '';
};

const findHighlightedIndex = ({ items, itemToString }, inputValue) => {
  if (!inputValue) {
    return -1;
  }

  const searchValue = inputValue.toLowerCase();

  for (let i = 0; i < items.length; i++) {
    const item = itemToString(items[i]).toLowerCase();
    if (item.indexOf(searchValue) !== -1) {
      return i;
    }
  }

  return -1;
};

const getInstanceId = setupGetInstanceId();

const ComboBox = (props) => {
  const {
    ariaLabel,
    className: containerClassName,
    direction,
    disabled,
    downshiftProps,
    helperText,
    id,
    initialSelectedItem,
    invalid,
    invalidText,
    items,
    itemToElement,
    itemToString,
    light,
    onChange,
    onInputChange,
    onToggleClick, // eslint-disable-line no-unused-vars
    placeholder,
    selectedItem,
    shouldFilterItem,
    size,
    titleText,
    translateWithId,
    type, // eslint-disable-line no-unused-vars
    warn,
    warnText,
    ...rest
  } = props;

  const textInput = useRef();
  const comboBoxInstanceId = getInstanceId();
  const [inputValue, setInputValue] = useState(
    getInputValue({
      initialSelectedItem,
      inputValue: '',
      itemToString,
      selectedItem,
    })
  );
  const [prevSelectedItem, setPrevSelectedItem] = useState(null);
  const [doneInitialSelectedItem, setDoneInitialSelectedItem] = useState(null);

  if (!doneInitialSelectedItem || prevSelectedItem !== selectedItem) {
    setDoneInitialSelectedItem(true);
    setPrevSelectedItem(selectedItem);
    setInputValue(
      getInputValue({
        initialSelectedItem,
        inputValue,
        itemToString,
        selectedItem,
      })
    );
  }

  const filterItems = (items, itemToString, inputValue) =>
    items.filter((item) =>
      shouldFilterItem({
        item,
        itemToString,
        inputValue,
      })
    );

  const handleOnChange = (selectedItem) => {
    if (onChange) {
      onChange({ selectedItem });
    }
  };

  const handleOnInputValueChange = (inputValue) => {
    setInputValue(inputValue || '');
  };

  useEffect(() => {
    if (onInputChange) {
      onInputChange(inputValue);
    }
  });

  const handleSelectionClear = () => {
    if (textInput?.current) {
      textInput.current.focus();
    }
  };

  const handleOnStateChange = (newState, { setHighlightedIndex }) => {
    if (Object.prototype.hasOwnProperty.call(newState, 'inputValue')) {
      const { inputValue } = newState;
      const filteredItems = filterItems(items, itemToString, inputValue);
      setHighlightedIndex(
        findHighlightedIndex(
          {
            ...props,
            items: filteredItems,
          },
          inputValue
        )
      );
    }
  };

  const handleToggleClick = (isOpen) => (event) => {
    if (onToggleClick) {
      onToggleClick(event);
    }

    if (event.target === textInput.current && isOpen) {
      event.preventDownshiftDefault = true;
      event.persist();
    }
  };

  const showWarning = !invalid && warn;
  const className = cx(`${prefix}--combo-box`, containerClassName, {
    [`${prefix}--list-box--up`]: direction === 'top',
    [`${prefix}--combo-box--warning`]: showWarning,
  });
  const titleClasses = cx(`${prefix}--label`, {
    [`${prefix}--label--disabled`]: disabled,
  });
  const comboBoxHelperId = !helperText
    ? undefined
    : `combobox-helper-text-${comboBoxInstanceId}`;
  const helperClasses = cx(`${prefix}--form__helper-text`, {
    [`${prefix}--form__helper-text--disabled`]: disabled,
  });
  const wrapperClasses = cx(`${prefix}--list-box__wrapper`);
  const inputClasses = cx(`${prefix}--text-input`, {
    [`${prefix}--text-input--empty`]: !inputValue,
  });

  // needs to be Capitalized for react to render it correctly
  const ItemToElement = itemToElement;
  return (
    <Downshift
      {...mapDownshiftProps(downshiftProps)}
      onChange={handleOnChange}
      onInputValueChange={handleOnInputValueChange}
      onStateChange={handleOnStateChange}
      inputValue={inputValue || ''}
      itemToString={itemToString}
      initialSelectedItem={initialSelectedItem}
      inputId={id}
      selectedItem={selectedItem}>
      {({
        getInputProps,
        getItemProps,
        getLabelProps,
        getMenuProps,
        getRootProps,
        getToggleButtonProps,
        isOpen,
        inputValue,
        selectedItem,
        highlightedIndex,
        clearSelection,
        toggleMenu,
      }) => {
        const rootProps = getRootProps(
          {},
          {
            suppressRefError: true,
          }
        );
        const labelProps = getLabelProps();
        const buttonProps = getToggleButtonProps({
          disabled,
          onClick: handleToggleClick(isOpen),
          // When we moved the "root node" of Downshift to the <input> for
          // ARIA 1.2 compliance, we unfortunately hit this branch for the
          // "mouseup" event that downshift listens to:
          // https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
          //
          // As a result, it will reset the state of the component and so we
          // stop the event from propagating to prevent this. This allows the
          // toggleMenu behavior for the toggleButton to correctly open and
          // close the menu.
          onMouseUp(event) {
            event.stopPropagation();
          },
        });
        const inputProps = getInputProps({
          // Remove excess aria `aria-labelledby`. HTML  provides this aria information.
          'aria-labelledby': null,
          disabled,
          placeholder,
          onClick() {
            toggleMenu();
          },
          onKeyDown: (event) => {
            if (match(event, keys.Space)) {
              event.stopPropagation();
            }

            if (match(event, keys.Enter) && !inputValue) {
              toggleMenu();
            }
          },
        });

        return (

            {titleText && (

                {titleText}

            )}
            <ListBox
              className={className}
              disabled={disabled}
              invalid={invalid}
              invalidText={invalidText}
              isOpen={isOpen}
              light={light}
              size={size}
              warn={warn}
              warnText={warnText}>
              <div className={`${prefix}--list-box__field`}>
                <input
                  role="combobox"
                  disabled={disabled}
                  className={inputClasses}
                  type="text"
                  tabIndex="0"
                  aria-autocomplete="list"
                  aria-expanded={rootProps['aria-expanded']}
                  aria-haspopup="listbox"
                  aria-controls={inputProps['aria-controls']}
                  {...inputProps}
                  {...rest}
                  ref={mergeRefs(textInput, rootProps.ref)}
                />
                {invalid && (
                  <WarningFilled16
                    className={`${prefix}--list-box__invalid-icon`}
                  />
                )}
                {showWarning && (
                  <WarningAltFilled16
                    className={`${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`}
                  />
                )}
                {inputValue && (
                  <ListBoxSelection
                    clearSelection={clearSelection}
                    translateWithId={translateWithId}
                    disabled={disabled}
                    onClearSelection={handleSelectionClear}
                  />
                )}
                <ListBoxTrigger
                  {...buttonProps}
                  isOpen={isOpen}
                  translateWithId={translateWithId}
                />

              <ListBox.Menu {...getMenuProps({ 'aria-label': ariaLabel })}>
                {isOpen
                  ? filterItems(items, itemToString, inputValue).map(
                      (item, index) => {
                        const itemProps = getItemProps({
                          item,
                          index,
                          ['aria-current']: selectedItem === item ? true : null,
                          ['aria-selected']:
                            highlightedIndex === index ? true : null,
                        });
                        return (
                          <ListBox.MenuItem
                            key={itemProps.id}
                            isActive={selectedItem === item}
                            tabIndex="-1"
                            isHighlighted={
                              highlightedIndex === index ||
                              (selectedItem?.id &&
                                selectedItem?.id === item.id) ||
                              false
                            }
                            title={
                              itemToElement ? item.text : itemToString(item)
                            }
                            {...itemProps}>
                            {itemToElement ? (
                              <ItemToElement key={itemProps.id} {...item} />
                            ) : (
                              itemToString(item)
                            )}
                            {selectedItem === item && (
                              <Checkmark16
                                className={`${prefix}--list-box__menu-item__selected-icon`}
                              />
                            )}
                          </ListBox.MenuItem>
                        );
                      }
                    )
                  : null}
              </ListBox.Menu>
            </ListBox>
            {helperText && !invalid && !warn && (

                {helperText}

            )}

        );
      }}
    </Downshift>
  );
};

ComboBox.propTypes = {
  /**
   * 'aria-label' of the ListBox component.
   */
  ariaLabel: PropTypes.string,

  /**
   * An optional className to add to the container node
   */
  className: PropTypes.string,

  /**
   * Specify the direction of the combobox dropdown. Can be either top or bottom.
   */
  direction: PropTypes.oneOf(['top', 'bottom']),

  /**
   * Specify if the control should be disabled, or not
   */
  disabled: PropTypes.bool,

  /**
   * Additional props passed to Downshift
   */
  downshiftProps: PropTypes.shape(Downshift.propTypes),

  /**
   * Provide helper text that is used alongside the control label for
   * additional help
   */
  helperText: PropTypes.string,

  /**
   * Specify a custom `id` for the input
   */
  id: PropTypes.string.isRequired,

  /**
   * Allow users to pass in an arbitrary item or a string (in case their items are an array of strings)
   * from their collection that are pre-selected
   */
  initialSelectedItem: PropTypes.oneOfType([
    PropTypes.object,
    PropTypes.string,
  ]),

  /**
   * Specify if the currently selected value is invalid.
   */
  invalid: PropTypes.bool,

  /**
   * Message which is displayed if the value is invalid.
   */
  invalidText: PropTypes.node,

  /**
   * Optional function to render items as custom components instead of strings.
   * Defaults to null and is overriden by a getter
   */
  itemToElement: PropTypes.func,

  /**
   * Helper function passed to downshift that allows the library to render a
   * given item to a string label. By default, it extracts the `label` field
   * from a given item to serve as the item label in the list
   */
  itemToString: PropTypes.func,

  /**
   * We try to stay as generic as possible here to allow individuals to pass
   * in a collection of whatever kind of data structure they prefer
   */
  items: PropTypes.array.isRequired,

  /**
   * should use "light theme" (white background)?
   */
  light: PropTypes.bool,

  /**
   * `onChange` is a utility for this controlled component to communicate to a
   * consuming component when a specific dropdown item is selected.
   * @param {{ selectedItem }}
   */
  onChange: PropTypes.func.isRequired,

  /**
   * Callback function to notify consumer when the text input changes.
   * This provides support to change available items based on the text.
   * @param {string} inputText
   */
  onInputChange: PropTypes.func,

  /**
   * Callback function that fires when the combobox menu toggle is clicked
   * @param {MouseEvent} event
   */
  onToggleClick: PropTypes.func,

  /**
   * Used to provide a placeholder text node before a user enters any input.
   * This is only present if the control has no items selected
   */
  placeholder: PropTypes.string.isRequired,

  /**
   * For full control of the selection
   */
  selectedItem: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),

  /**
   * Specify your own filtering logic by passing in a `shouldFilterItem`
   * function that takes in the current input and an item and passes back
   * whether or not the item should be filtered.
   */
  shouldFilterItem: PropTypes.func,

  /**
   * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option.
   */
  size: ListBoxPropTypes.ListBoxSize,

  /**
   * Provide text to be used in a `<label>` element that is tied to the
   * combobox via ARIA attributes.
   */
  titleText: PropTypes.node,

  /**
   * Specify a custom translation function that takes in a message identifier
   * and returns the localized string for the message
   */
  translateWithId: PropTypes.func,

  /**
   * Currently supports either the default type, or an inline variant
   */
  type: ListBoxPropTypes.ListBoxType,

  /**
   * Specify whether the control is currently in warning state
   */
  warn: PropTypes.bool,

  /**
   * Provide the text that is displayed when the control is in warning state
   */
  warnText: PropTypes.node,
};

ComboBox.defaultProps = {
  disabled: false,
  itemToString: defaultItemToString,
  itemToElement: null,
  shouldFilterItem: defaultShouldFilterItem,
  type: 'default',
  ariaLabel: 'Choose an item',
  light: false,
  direction: 'bottom',
};

export default ComboBox;
<commit_msg>fix(ComboBox): add missing dependencies to useEffect (#8308)<commit_after>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import cx from 'classnames';
import Downshift from 'downshift';
import PropTypes from 'prop-types';
import React, { useEffect, useState, useRef } from 'react';
import { settings } from 'carbon-components';
import {
  Checkmark16,
  WarningAltFilled16,
  WarningFilled16,
} from '@carbon/icons-react';
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
import { ListBoxTrigger, ListBoxSelection } from '../ListBox/next';
import { match, keys } from '../../internal/keyboard';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
import { mapDownshiftProps } from '../../tools/createPropAdapter';
import mergeRefs from '../../tools/mergeRefs';

const { prefix } = settings;

const defaultItemToString = (item) => {
  if (typeof item === 'string') {
    return item;
  }

  return item && item.label;
};

const defaultShouldFilterItem = () => true;

const getInputValue = ({
  initialSelectedItem,
  inputValue,
  itemToString,
  selectedItem,
}) => {
  if (selectedItem) {
    return itemToString(selectedItem);
  }
  // TODO: consistent `initialSelectedItem` behavior with other listbox components in v11
  if (initialSelectedItem) {
    return itemToString(initialSelectedItem);
  }

  return inputValue || '';
};

const findHighlightedIndex = ({ items, itemToString }, inputValue) => {
  if (!inputValue) {
    return -1;
  }

  const searchValue = inputValue.toLowerCase();

  for (let i = 0; i < items.length; i++) {
    const item = itemToString(items[i]).toLowerCase();
    if (item.indexOf(searchValue) !== -1) {
      return i;
    }
  }

  return -1;
};

const getInstanceId = setupGetInstanceId();

const ComboBox = (props) => {
  const {
    ariaLabel,
    className: containerClassName,
    direction,
    disabled,
    downshiftProps,
    helperText,
    id,
    initialSelectedItem,
    invalid,
    invalidText,
    items,
    itemToElement,
    itemToString,
    light,
    onChange,
    onInputChange,
    onToggleClick, // eslint-disable-line no-unused-vars
    placeholder,
    selectedItem,
    shouldFilterItem,
    size,
    titleText,
    translateWithId,
    type, // eslint-disable-line no-unused-vars
    warn,
    warnText,
    ...rest
  } = props;

  const textInput = useRef();
  const comboBoxInstanceId = getInstanceId();
  const [inputValue, setInputValue] = useState(
    getInputValue({
      initialSelectedItem,
      inputValue: '',
      itemToString,
      selectedItem,
    })
  );
  const [prevSelectedItem, setPrevSelectedItem] = useState(null);
  const [doneInitialSelectedItem, setDoneInitialSelectedItem] = useState(null);
  const savedOnInputChange = useRef(onInputChange);

  if (!doneInitialSelectedItem || prevSelectedItem !== selectedItem) {
    setDoneInitialSelectedItem(true);
    setPrevSelectedItem(selectedItem);
    setInputValue(
      getInputValue({
        initialSelectedItem,
        inputValue,
        itemToString,
        selectedItem,
      })
    );
  }

  const filterItems = (items, itemToString, inputValue) =>
    items.filter((item) =>
      shouldFilterItem({
        item,
        itemToString,
        inputValue,
      })
    );

  const handleOnChange = (selectedItem) => {
    if (onChange) {
      onChange({ selectedItem });
    }
  };

  const handleOnInputValueChange = (inputValue) => {
    setInputValue(inputValue || '');
  };

  useEffect(() => {
    savedOnInputChange.current = onInputChange;
  }, [onInputChange]);

  useEffect(() => {
    if (savedOnInputChange.current) {
      savedOnInputChange.current(inputValue);
    }
  }, [inputValue]);

  const handleSelectionClear = () => {
    if (textInput?.current) {
      textInput.current.focus();
    }
  };

  const handleOnStateChange = (newState, { setHighlightedIndex }) => {
    if (Object.prototype.hasOwnProperty.call(newState, 'inputValue')) {
      const { inputValue } = newState;
      const filteredItems = filterItems(items, itemToString, inputValue);
      setHighlightedIndex(
        findHighlightedIndex(
          {
            ...props,
            items: filteredItems,
          },
          inputValue
        )
      );
    }
  };

  const handleToggleClick = (isOpen) => (event) => {
    if (onToggleClick) {
      onToggleClick(event);
    }

    if (event.target === textInput.current && isOpen) {
      event.preventDownshiftDefault = true;
      event.persist();
    }
  };

  const showWarning = !invalid && warn;
  const className = cx(`${prefix}--combo-box`, containerClassName, {
    [`${prefix}--list-box--up`]: direction === 'top',
    [`${prefix}--combo-box--warning`]: showWarning,
  });
  const titleClasses = cx(`${prefix}--label`, {
    [`${prefix}--label--disabled`]: disabled,
  });
  const comboBoxHelperId = !helperText
    ? undefined
    : `combobox-helper-text-${comboBoxInstanceId}`;
  const helperClasses = cx(`${prefix}--form__helper-text`, {
    [`${prefix}--form__helper-text--disabled`]: disabled,
  });
  const wrapperClasses = cx(`${prefix}--list-box__wrapper`);
  const inputClasses = cx(`${prefix}--text-input`, {
    [`${prefix}--text-input--empty`]: !inputValue,
  });

  // needs to be Capitalized for react to render it correctly
  const ItemToElement = itemToElement;
  return (
    <Downshift
      {...mapDownshiftProps(downshiftProps)}
      onChange={handleOnChange}
      onInputValueChange={handleOnInputValueChange}
      onStateChange={handleOnStateChange}
      inputValue={inputValue || ''}
      itemToString={itemToString}
      initialSelectedItem={initialSelectedItem}
      inputId={id}
      selectedItem={selectedItem}>
      {({
        getInputProps,
        getItemProps,
        getLabelProps,
        getMenuProps,
        getRootProps,
        getToggleButtonProps,
        isOpen,
        inputValue,
        selectedItem,
        highlightedIndex,
        clearSelection,
        toggleMenu,
      }) => {
        const rootProps = getRootProps(
          {},
          {
            suppressRefError: true,
          }
        );
        const labelProps = getLabelProps();
        const buttonProps = getToggleButtonProps({
          disabled,
          onClick: handleToggleClick(isOpen),
          // When we moved the "root node" of Downshift to the <input> for
          // ARIA 1.2 compliance, we unfortunately hit this branch for the
          // "mouseup" event that downshift listens to:
          // https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065
          //
          // As a result, it will reset the state of the component and so we
          // stop the event from propagating to prevent this. This allows the
          // toggleMenu behavior for the toggleButton to correctly open and
          // close the menu.
          onMouseUp(event) {
            event.stopPropagation();
          },
        });
        const inputProps = getInputProps({
          // Remove excess aria `aria-labelledby`. HTML  provides this aria information.
          'aria-labelledby': null,
          disabled,
          placeholder,
          onClick() {
            toggleMenu();
          },
          onKeyDown: (event) => {
            if (match(event, keys.Space)) {
              event.stopPropagation();
            }

            if (match(event, keys.Enter) && !inputValue) {
              toggleMenu();
            }
          },
        });

        return (

            {titleText && (

                {titleText}

            )}
            <ListBox
              className={className}
              disabled={disabled}
              invalid={invalid}
              invalidText={invalidText}
              isOpen={isOpen}
              light={light}
              size={size}
              warn={warn}
              warnText={warnText}>
              <div className={`${prefix}--list-box__field`}>
                <input
                  role="combobox"
                  disabled={disabled}
                  className={inputClasses}
                  type="text"
                  tabIndex="0"
                  aria-autocomplete="list"
                  aria-expanded={rootProps['aria-expanded']}
                  aria-haspopup="listbox"
                  aria-controls={inputProps['aria-controls']}
                  {...inputProps}
                  {...rest}
                  ref={mergeRefs(textInput, rootProps.ref)}
                />
                {invalid && (
                  <WarningFilled16
                    className={`${prefix}--list-box__invalid-icon`}
                  />
                )}
                {showWarning && (
                  <WarningAltFilled16
                    className={`${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`}
                  />
                )}
                {inputValue && (
                  <ListBoxSelection
                    clearSelection={clearSelection}
                    translateWithId={translateWithId}
                    disabled={disabled}
                    onClearSelection={handleSelectionClear}
                  />
                )}
                <ListBoxTrigger
                  {...buttonProps}
                  isOpen={isOpen}
                  translateWithId={translateWithId}
                />

              <ListBox.Menu {...getMenuProps({ 'aria-label': ariaLabel })}>
                {isOpen
                  ? filterItems(items, itemToString, inputValue).map(
                      (item, index) => {
                        const itemProps = getItemProps({
                          item,
                          index,
                          ['aria-current']: selectedItem === item ? true : null,
                          ['aria-selected']:
                            highlightedIndex === index ? true : null,
                        });
                        return (
                          <ListBox.MenuItem
                            key={itemProps.id}
                            isActive={selectedItem === item}
                            tabIndex="-1"
                            isHighlighted={
                              highlightedIndex === index ||
                              (selectedItem?.id &&
                                selectedItem?.id === item.id) ||
                              false
                            }
                            title={
                              itemToElement ? item.text : itemToString(item)
                            }
                            {...itemProps}>
                            {itemToElement ? (
                              <ItemToElement key={itemProps.id} {...item} />
                            ) : (
                              itemToString(item)
                            )}
                            {selectedItem === item && (
                              <Checkmark16
                                className={`${prefix}--list-box__menu-item__selected-icon`}
                              />
                            )}
                          </ListBox.MenuItem>
                        );
                      }
                    )
                  : null}
              </ListBox.Menu>
            </ListBox>
            {helperText && !invalid && !warn && (

                {helperText}

            )}

        );
      }}
    </Downshift>
  );
};

ComboBox.propTypes = {
  /**
   * 'aria-label' of the ListBox component.
   */
  ariaLabel: PropTypes.string,

  /**
   * An optional className to add to the container node
   */
  className: PropTypes.string,

  /**
   * Specify the direction of the combobox dropdown. Can be either top or bottom.
   */
  direction: PropTypes.oneOf(['top', 'bottom']),

  /**
   * Specify if the control should be disabled, or not
   */
  disabled: PropTypes.bool,

  /**
   * Additional props passed to Downshift
   */
  downshiftProps: PropTypes.shape(Downshift.propTypes),

  /**
   * Provide helper text that is used alongside the control label for
   * additional help
   */
  helperText: PropTypes.string,

  /**
   * Specify a custom `id` for the input
   */
  id: PropTypes.string.isRequired,

  /**
   * Allow users to pass in an arbitrary item or a string (in case their items are an array of strings)
   * from their collection that are pre-selected
   */
  initialSelectedItem: PropTypes.oneOfType([
    PropTypes.object,
    PropTypes.string,
  ]),

  /**
   * Specify if the currently selected value is invalid.
   */
  invalid: PropTypes.bool,

  /**
   * Message which is displayed if the value is invalid.
   */
  invalidText: PropTypes.node,

  /**
   * Optional function to render items as custom components instead of strings.
   * Defaults to null and is overriden by a getter
   */
  itemToElement: PropTypes.func,

  /**
   * Helper function passed to downshift that allows the library to render a
   * given item to a string label. By default, it extracts the `label` field
   * from a given item to serve as the item label in the list
   */
  itemToString: PropTypes.func,

  /**
   * We try to stay as generic as possible here to allow individuals to pass
   * in a collection of whatever kind of data structure they prefer
   */
  items: PropTypes.array.isRequired,

  /**
   * should use "light theme" (white background)?
   */
  light: PropTypes.bool,

  /**
   * `onChange` is a utility for this controlled component to communicate to a
   * consuming component when a specific dropdown item is selected.
   * @param {{ selectedItem }}
   */
  onChange: PropTypes.func.isRequired,

  /**
   * Callback function to notify consumer when the text input changes.
   * This provides support to change available items based on the text.
   * @param {string} inputText
   */
  onInputChange: PropTypes.func,

  /**
   * Callback function that fires when the combobox menu toggle is clicked
   * @param {MouseEvent} event
   */
  onToggleClick: PropTypes.func,

  /**
   * Used to provide a placeholder text node before a user enters any input.
   * This is only present if the control has no items selected
   */
  placeholder: PropTypes.string.isRequired,

  /**
   * For full control of the selection
   */
  selectedItem: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),

  /**
   * Specify your own filtering logic by passing in a `shouldFilterItem`
   * function that takes in the current input and an item and passes back
   * whether or not the item should be filtered.
   */
  shouldFilterItem: PropTypes.func,

  /**
   * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option.
   */
  size: ListBoxPropTypes.ListBoxSize,

  /**
   * Provide text to be used in a `<label>` element that is tied to the
   * combobox via ARIA attributes.
   */
  titleText: PropTypes.node,

  /**
   * Specify a custom translation function that takes in a message identifier
   * and returns the localized string for the message
   */
  translateWithId: PropTypes.func,

  /**
   * Currently supports either the default type, or an inline variant
   */
  type: ListBoxPropTypes.ListBoxType,

  /**
   * Specify whether the control is currently in warning state
   */
  warn: PropTypes.bool,

  /**
   * Provide the text that is displayed when the control is in warning state
   */
  warnText: PropTypes.node,
};

ComboBox.defaultProps = {
  disabled: false,
  itemToString: defaultItemToString,
  itemToElement: null,
  shouldFilterItem: defaultShouldFilterItem,
  type: 'default',
  ariaLabel: 'Choose an item',
  light: false,
  direction: 'bottom',
};

export default ComboBox;
<|endoftext|>