<commit_before>(function(window, $){
  if(!$){throw new Error('jQuery required by Sortable')};

  function PositionsUpdater(node, current_node, new_position, old_position) {
    this.node = node;
    this.current_node = current_node;
    this.new_position = new_position;
    this.old_position = old_position;
  };

  PositionsUpdater.prototype.update = function() {
    var step = this.new_position > this.old_position ? -1 : 1;
    var start_position = step === -1 ? this.old_position + 1 : this.new_position;
    var stop_position = step === -1 ? this.new_position : this.old_position - 1;
    var target, data_position;
    var current_node_role = this.current_node.data('role');

    $('*[data-position]', this.node)
      .filter(function(){
        var position = $(this).data('position');
        var role = $(this).data('role');

        return (current_node_role !== role) && position >= start_position && position <= stop_position;
      })
      .each(function(){
        var target = $(this);

        var data_position = target.data('position');
        var new_position = data_position + step;

        target.attr('data-position', new_position).data('position', new_position);

        var position_input_node = $('*[data-role~="position"]', target);
        if ( position_input_node.length > 0 ) {
          position_input_node.val(new_position);
        }
      });
  };

  function ServerSorter(node, current_node, new_position, positions_updater) {
    this.node = node;
    this.current_node = current_node;
    this.new_position = new_position;
    this.positions_updater = positions_updater;
  };

  ServerSorter.prototype.sort = function() {
    $.ajax({
      type: 'POST',
      dataType: 'script',
      url: this.current_node.data('move-url'),
      data: {
        position: this.new_position
      },
      success: $.proxy(this.sort_by_server_success, this),
      error: $.proxy(this.sort_by_server_error, this)
    });
  };

  ServerSorter.prototype.sort_by_server_success = function() {
    this.positions_updater.update();
    this.node.trigger('sortable:sort_success').trigger('sortable:stop').sortable('enable');
  };

  ServerSorter.prototype.sort_by_server_error = function() {
    this.node.sortable('cancel').trigger('sortable:sort_error').trigger('sortable:stop').sortable('enable');
  };

  function ClientSorter(node, current_node, new_position, positions_updater) {
    this.node = node;
    this.current_node = current_node;
    this.new_position = new_position;
    this.positions_updater = positions_updater;
  };

  ClientSorter.prototype.sort = function() {
    var position_input_node = $('*[data-role~="position"]', this.current_node);
    if ( position_input_node.length > 0 ) {
      this.positions_updater.update();

      this.current_node.data('position', this.new_position);
      position_input_node.val(this.new_position);
    }
    else {
      console && console.error && console.error('Could not found any *[data-role~="position"] element for', this.current_node);
    }
    this.node.trigger('sortable:sort_success').trigger('sortable:stop').sortable('enable');
  };

  function Sortable(node, options) {
    this.node = node;

    if (!options) options = {};
    options['update'] = $.proxy(this.sortable_stop_handler, this);

    var defaults = { axis: 'y' };
    var settings = $.extend({}, defaults, options);

    this.node.sortable(settings);
  };

  Sortable.prototype.sortable_stop_handler = function(e, ui) {
    this.node.sortable('disable').trigger('sortable:start');

    var node = ui.item;
    var new_position = node.index();
    var old_position = parseInt(node.data('position'));

    var positions_updater = new PositionsUpdater(this.node, node, new_position, old_position);

    var sorter_factory = (node.data('move-url') ? ServerSorter : ClientSorter);

    var sorter = new sorter_factory(this.node, node, new_position, positions_updater);
    sorter.sort();
  };

  $.fn.activerecord_sortable = function() {
    var target = $(this);
    target.each(function(){
      var self = $(this);
      var sortable_instance = new Sortable(self);

      self.data('sortable-instance', sortable_instance);
    });
  }
})(window, window.jQuery);
<commit_msg>added options to js activerecord_sortable<commit_after>(function(window, $){
  if(!$){throw new Error('jQuery required by Sortable')};

  function PositionsUpdater(node, current_node, new_position, old_position) {
    this.node = node;
    this.current_node = current_node;
    this.new_position = new_position;
    this.old_position = old_position;
  };

  PositionsUpdater.prototype.update = function() {
    var step = this.new_position > this.old_position ? -1 : 1;
    var start_position = step === -1 ? this.old_position + 1 : this.new_position;
    var stop_position = step === -1 ? this.new_position : this.old_position - 1;
    var target, data_position;
    var current_node_role = this.current_node.data('role');

    $('*[data-position]', this.node)
      .filter(function(){
        var position = $(this).data('position');
        var role = $(this).data('role');

        return (current_node_role !== role) && position >= start_position && position <= stop_position;
      })
      .each(function(){
        var target = $(this);

        var data_position = target.data('position');
        var new_position = data_position + step;

        target.attr('data-position', new_position).data('position', new_position);

        var position_input_node = $('*[data-role~="position"]', target);
        if ( position_input_node.length > 0 ) {
          position_input_node.val(new_position);
        }
      });
  };

  function ServerSorter(node, current_node, new_position, positions_updater) {
    this.node = node;
    this.current_node = current_node;
    this.new_position = new_position;
    this.positions_updater = positions_updater;
  };

  ServerSorter.prototype.sort = function() {
    $.ajax({
      type: 'POST',
      dataType: 'script',
      url: this.current_node.data('move-url'),
      data: {
        position: this.new_position
      },
      success: $.proxy(this.sort_by_server_success, this),
      error: $.proxy(this.sort_by_server_error, this)
    });
  };

  ServerSorter.prototype.sort_by_server_success = function() {
    this.positions_updater.update();
    this.node.trigger('sortable:sort_success').trigger('sortable:stop').sortable('enable');
  };

  ServerSorter.prototype.sort_by_server_error = function() {
    this.node.sortable('cancel').trigger('sortable:sort_error').trigger('sortable:stop').sortable('enable');
  };

  function ClientSorter(node, current_node, new_position, positions_updater) {
    this.node = node;
    this.current_node = current_node;
    this.new_position = new_position;
    this.positions_updater = positions_updater;
  };

  ClientSorter.prototype.sort = function() {
    var position_input_node = $('*[data-role~="position"]', this.current_node);
    if ( position_input_node.length > 0 ) {
      this.positions_updater.update();

      this.current_node.data('position', this.new_position);
      position_input_node.val(this.new_position);
    }
    else {
      console && console.error && console.error('Could not found any *[data-role~="position"] element for', this.current_node);
    }
    this.node.trigger('sortable:sort_success').trigger('sortable:stop').sortable('enable');
  };

  function Sortable(node, options) {
    this.node = node;

    if (!options) options = {};
    options['update'] = $.proxy(this.sortable_stop_handler, this);

    var defaults = { axis: 'y' };
    var settings = $.extend({}, defaults, options);

    this.node.sortable(settings);
  };

  Sortable.prototype.sortable_stop_handler = function(e, ui) {
    this.node.sortable('disable').trigger('sortable:start');

    var node = ui.item;
    var new_position = node.index();
    var old_position = parseInt(node.data('position'));

    var positions_updater = new PositionsUpdater(this.node, node, new_position, old_position);

    var sorter_factory = (node.data('move-url') ? ServerSorter : ClientSorter);

    var sorter = new sorter_factory(this.node, node, new_position, positions_updater);
    sorter.sort();
  };

  $.fn.activerecord_sortable = function(options) {
    var target = $(this);
    target.each(function(){
      var self = $(this);
      var sortable_instance = new Sortable(self, options);

      self.data('sortable-instance', sortable_instance);
    });
  }
})(window, window.jQuery);
<|endoftext|>


<commit_before>/* eslint-disable react/prop-types */
import R from 'ramda';
import React, { Component } from 'react';
import Select from 'material-ui/Select';
import TextField from 'material-ui/TextField';
import Input from 'material-ui/Input';
import Checkbox from 'material-ui/Checkbox';
import Autosuggest from 'react-autosuggest';
import { FormControl, FormControlLabel, FormHelperText, FormLabel } from 'material-ui/Form';
import Attachment from 'material-ui-icons/Attachment';
import DateRange from 'material-ui-icons/DateRange';
import DatePicker from 'material-ui-old/DatePicker';
import Typography from 'material-ui/Typography';
import RadioGroupRow from '../components/common/radio/radioGroupRow';
import RadioHeight from '../components/common/radio/radioHeight';
import FieldLabelWithTooltipIcon from '../components/common/fieldLabelWithTooltip';
import { formatDateForPrint, formatDateForDatePicker } from './dates';
import { numerical } from '../helpers/validation';
import {
  renderInput,
  renderMultipleInput,
  renderSuggestion,
  renderSuggestionsContainer,
} from '../components/forms/autocompleteHelpers/autocompleteRenders';
import {
  getSuggestionValue,
  setSuggestionValue,
  setMultipleSuggestionValue,
  handleClear,
} from '../components/forms//autocompleteHelpers/autocompleteFunctions';
import { RenderMultipleSelections, RenderPlaceholder } from '../components/forms/selectHelpers/selectRenderers';

export const fileNameFromUrl = (url) => {
  if (url) {
    return url.split('/').pop();
  }

  return '-';
};

export const BOOL_VAL = [
  {
    value: true,
    label: 'Yes',
  },
  {
    value: false,
    label: 'No',
  },
];

const transformBool = (value) => {
  if (typeof (value) === 'boolean' && value || value === 'true') {
    return BOOL_VAL[0].value;
  } else if (typeof (value) === 'boolean' && !value || value === 'false') {
    return BOOL_VAL[1].value;
  }

  return value;
};

const convertBool = (value) => {
  if (typeof (value) === 'boolean' && value || value === 'true') {
    return BOOL_VAL[0].label;
  } else if (typeof (value) === 'boolean' && !value || value === 'false') {
    return BOOL_VAL[1].label;
  }

  return value;
};

export const visibleIfNo = (value) => {
  if (value === BOOL_VAL[1].value || typeof (value) === 'boolean' && !value) { return true; }

  return false;
};

export const visibleIfYes = (value) => {
  if (value === BOOL_VAL[0].value || typeof (value) === 'boolean' && value) { return true; }

  return false;
};

export const renderFormControlWithLabel = ({
  className,
  label,
  meta: { touched, error, warning },
  input,
  ...other
}) => (

    <FormLabel>{label}</FormLabel>
    <FormControl
      className={className}
      {...input}
      {...other}
    />
    {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderFormControl = ({
  className,
  label,
  meta: { touched, error, warning },
  input,
  ...other
}) => (

    <FormControl
      className={className}
      {...input}
      {...other}
    />
    {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderSelectField = ({
  input,
  defaultValue,
  meta: { touched, error, warning },
  children,
  multiple,
  label,
  values,
  placeholder,
  infoText,
  ...other
}) => {
  let valueForSelect;
  if (multiple) {
    valueForSelect = (!R.isEmpty(input.value) && input.value) || defaultValue || ['placeholder_none'];
  } else {
    valueForSelect = input.value || defaultValue || 'placeholder_none';
  }
  return (<FormControl fullWidth error={(touched && error) || warning}>
    <FieldLabelWithTooltipIcon
      infoText={infoText}
      tooltipIconProps={{
        name: input.name,
      }}
    >
      {label}
    </FieldLabelWithTooltipIcon>
    <Select
      {...input}
      value={valueForSelect}
      multiple={multiple}
      style={{ marginTop: 0 }}
      renderValue={(value) => {
        if (value === 'placeholder_none' || R.indexOf('placeholder_none', value) !== -1) {
          return (<RenderPlaceholder placeholder={placeholder} />);
        }
        if (Array.isArray(value)) {
          const selectedValues = R.filter(
            R.propSatisfies(prop => value.includes(prop), 'value'),
            values,
          );
          return (<RenderMultipleSelections
            fieldName={input.name}
            onSelectionRemove={(removedValue) => {
              input.onChange(R.without([removedValue], value));
            }}
            selectedValues={selectedValues}
          />);
        }
        const selectedValue = R.find(R.propEq('value', value))(values) || {};
        const selectedLabel = R.prop('label', selectedValue);
        return selectedLabel;
      }}
      onBlur={(event) => {
        event.preventDefault();
      }}
      {...other}
    >
      {children}
    </Select>
    {((touched && error) || warning) && <FormHelperText>{error}</FormHelperText>}
  </FormControl>);
};

export const renderRadioField = ({ input,
  label,
  defaultValue,
  classes,
  infoText,
  meta: { touched, error, warning },
  options, ...other
}) => (

    <FormControl fullWidth>
      <FieldLabelWithTooltipIcon
        infoText={infoText}
        tooltipIconProps={{
          name: input.name,
        }}
      >
        {label}
      </FieldLabelWithTooltipIcon>
      <RadioGroupRow
        selectedValue={!R.isEmpty(input.value) ? transformBool(input.value) : defaultValue}
        onChange={(event, value) => { input.onChange(transformBool(value)); }}
        {...other}
      >
        {options.map((value, index) => (
          <FormControlLabel
            key={index}
            value={`${value.value}`}
            control={<RadioHeight />}
            label={value.label}
          />))}</RadioGroupRow>
    </FormControl>
    {((touched && error) || warning) &&
    <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderCheckbox = ({
  name,
  disabled,
  label,
  meta: { touched, error, warning },
  input,
}) => (

      <Checkbox
        id={name}
        disabled={disabled}
        checked={input.value}
        onChange={(event, value) => { input.onChange(transformBool(value)); }}
      />
      <Typography color="inherit" type="caption">
        {label}
      </Typography>

    {((touched && error) || error || warning)
      && <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderFileDownload = () => ({ input, label, infoText }) => (<FormControl fullWidth>
  <FieldLabelWithTooltipIcon
    infoText={infoText}
    tooltipIconProps={{
      name: input.name,
    }}
  >
    {label}
  </FieldLabelWithTooltipIcon>

    {input.value && <Attachment style={{ marginRight: 5 }} />}

 { window.open(input.value); }}
      style={{
        cursor: 'pointer',
        overflow: 'hidden',
        whiteSpace: 'nowrap',
        textOverflow: 'ellipsis',
      }}
    >
      {fileNameFromUrl(input.value)}

</FormControl>);

export const renderTextField = ({
  name,
  className,
  meta: { touched, error, warning },
  input,
  label,
  infoText,
  ...other
}) => (<FormControl fullWidth>
  <FieldLabelWithTooltipIcon
    infoText={infoText}
    tooltipIconProps={{
      name: input.name,
    }}
  >
    {label}
  </FieldLabelWithTooltipIcon>
  <TextField
    className={className}
    id={input.name}
    error={(touched && !!error) || !!warning}
    fullWidth
    {...input}
    {...other}
  />

    {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}
    {/* show limit of characters
       {other.inputProps && other.inputProps.maxLength &&
        <FormHelperText style={{ marginLeft: 'auto' }}>
        {input.value.length}/{other.inputProps.maxLength}
        </FormHelperText>} */}

</FormControl>);

export const renderNumberField = ({
  name,
  className,
  meta: { touched, error, warning },
  input,
  ...other
}) => {
  const rangeError = numerical(other.inputProps.min, other.inputProps.max)(input.value);

  return (

      <Input
        className={className}
        id={name}
        error={(touched && !!error) || !!warning || !!rangeError}
        fullWidth
        {...input}
        {...other}
      />

        {((touched && error) || warning || rangeError) &&
          <FormHelperText error>{error || warning || rangeError}</FormHelperText>}
        {other.inputProps && other.inputProps.maxLength && <FormHelperText style={{ marginLeft: 'auto' }}>{input.value.length}/{other.inputProps.maxLength}</FormHelperText>}

);
};

export const renderDatePicker = ({
  input: { value, onChange, ...inputOther },
  meta: { touched, error, warning },
  ...other
}) => {
  const datePickerProps = {};

  if (!value) {
    // noop
  } else if (typeof value === 'object' && value.valueOf()) {
    datePickerProps.value = value;
  } else if (typeof value === 'string' && value !== 'Invalid date') {
    datePickerProps.value = formatDateForDatePicker(value);
  }

  return (

      <DatePicker
        errorText={(touched && error) || warning}
        {...datePickerProps}
        {...inputOther}
        onChange={(event, val) => onChange(val)}
        {...other}
      />

  );
};

export const renderText = ({
  className,
  input,
  values,
  optional,
  label,
  infoText,
  date,
  meta,
  multiline,
  inputProps,
  ...other
}) => {
  let value = (!R.isNil(input.value) && !R.isEmpty(input.value))
    ? input.value
    : (inputProps
      ? inputProps.initial
      : null);

  if (!value) value = '-';

  if (values) {
    value = R.filter((val) => {
      if (Array.isArray(value)) return value.includes(val.value);
      return value === val.value;
    }, values).map(matchedValue => matchedValue.label).join(', ');
  }

  if (R.isEmpty(value) || R.isNil(value)) {
    value = (!R.isNil(input.value) && !R.isEmpty(input.value))
      ? input.value
      : (inputProps
        ? inputProps.initial
        : null);
  }
  if (date) value = formatDateForPrint(value);
  if (R.isEmpty(value) || R.isNil(value)) value = '-';

  return (
    <FormControl fullWidth>
      {label && <FieldLabelWithTooltipIcon
        infoText={infoText}
        tooltipIconProps={{
          name: input.name,
        }}
      >
        {label}
      </FieldLabelWithTooltipIcon>}

        {date && <DateRange style={{
          marginRight: 5,
          width: 22,
          height: 22,
        }}
        />}
        <Typography
          className={className}
          {...other}
        >
          {value}
        </Typography>

    </FormControl>
  );
};

export const renderBool = ({
  className,
  input,
  values,
  optional,
  label,
  meta,
  infoText,
  ...other
}) => (
  <FormControl fullWidth>
    <FormLabel>{label}</FormLabel>
    <Typography
      className={className}
      {...other}
    >
      {convertBool(input.value) ? convertBool(input.value) : '-'}
    </Typography>
  </FormControl>
);

export class AutocompleteRenderer extends Component {
  componentWillReceiveProps({ input, multiple, handleChange }) {
    if (!multiple && this.props.input.value && !input.value) {
      handleChange({}, { newValue: '' });
    }
  }

  render() {
    const {
      meta: { touched, error, warning },
      input: { name, onChange: onFormChange, value: formValue, onBlur: _onBlur, ...inputProps },
      suggestions,
      handleSuggestionsFetchRequested,
      handleSuggestionsClearRequested,
      classes,
      label,
      placeholder,
      handleChange,
      handleMultiChange,
      handleMultiClear,
      multiValues,
      fieldValue,
      multiple,
      overlap,
      suggestionsPool,
      infoText,
    } = this.props;

    return (

        <Autosuggest
          id={`autosuggest-${name}`}
          theme={{
            container: classes.container,
            suggestionsContainerOpen: overlap
              ? `${classes.suggestionsContainerOpen} ${classes.suggestionsContainerOpenOverlap}`
              : `${classes.suggestionsContainerOpen} ${classes.suggestionsContainerOpenExpand}`,
            suggestionsList: classes.suggestionsList,
            suggestion: classes.suggestion,
          }}
          renderInputComponent={multiple ? renderMultipleInput : renderInput}
          suggestions={suggestions}
          onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
          onSuggestionsClearRequested={handleSuggestionsClearRequested}
          renderSuggestionsContainer={renderSuggestionsContainer}
          getSuggestionValue={getSuggestionValue}
          onSuggestionSelected={multiple
            ? R.curry(setMultipleSuggestionValue)(formValue, handleChange, onFormChange, handleMultiChange)
            : R.curry(setSuggestionValue)(onFormChange)
          }
          highlightFirstSuggestion
          renderSuggestion={renderSuggestion}
          inputProps={{
            name,
            error: (touched && (!!error || !!warning)),
            label,
            type: 'text',
            placeholder,
            value: fieldValue,
            multiValues,
            onChange: handleChange,
            onBlur(...args) {
              if (typeof _onBlur === 'function') {
                _onBlur(...args);
              }

              if (multiple) {
                return;
              }

              const ev = args[0];

              ev.persist();

              setTimeout(() => {
                const prevSelection = (suggestionsPool || []).filter(s => s.value === formValue)[0];
                const newValue = prevSelection ? prevSelection.label : '';

                ev.target.value = newValue;

                handleChange(ev, { newValue });
              });
            },
            handleClear: R.curry(handleClear)(onFormChange, handleMultiClear),
            infoText,
            ...inputProps,
          }}
        />

          {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}

    );
  }
}
<commit_msg>Autocomplete fix<commit_after>/* eslint-disable react/prop-types */
import R from 'ramda';
import React, { Component } from 'react';
import Select from 'material-ui/Select';
import TextField from 'material-ui/TextField';
import Input from 'material-ui/Input';
import Checkbox from 'material-ui/Checkbox';
import Autosuggest from 'react-autosuggest';
import { FormControl, FormControlLabel, FormHelperText, FormLabel } from 'material-ui/Form';
import Attachment from 'material-ui-icons/Attachment';
import DateRange from 'material-ui-icons/DateRange';
import DatePicker from 'material-ui-old/DatePicker';
import Typography from 'material-ui/Typography';
import RadioGroupRow from '../components/common/radio/radioGroupRow';
import RadioHeight from '../components/common/radio/radioHeight';
import FieldLabelWithTooltipIcon from '../components/common/fieldLabelWithTooltip';
import { formatDateForPrint, formatDateForDatePicker } from './dates';
import { numerical } from '../helpers/validation';
import {
  renderInput,
  renderMultipleInput,
  renderSuggestion,
  renderSuggestionsContainer,
} from '../components/forms/autocompleteHelpers/autocompleteRenders';
import {
  getSuggestionValue,
  setSuggestionValue,
  setMultipleSuggestionValue,
  handleClear,
} from '../components/forms//autocompleteHelpers/autocompleteFunctions';
import { RenderMultipleSelections, RenderPlaceholder } from '../components/forms/selectHelpers/selectRenderers';

export const fileNameFromUrl = (url) => {
  if (url) {
    return url.split('/').pop();
  }

  return '-';
};

export const BOOL_VAL = [
  {
    value: true,
    label: 'Yes',
  },
  {
    value: false,
    label: 'No',
  },
];

const transformBool = (value) => {
  if (typeof (value) === 'boolean' && value || value === 'true') {
    return BOOL_VAL[0].value;
  } else if (typeof (value) === 'boolean' && !value || value === 'false') {
    return BOOL_VAL[1].value;
  }

  return value;
};

const convertBool = (value) => {
  if (typeof (value) === 'boolean' && value || value === 'true') {
    return BOOL_VAL[0].label;
  } else if (typeof (value) === 'boolean' && !value || value === 'false') {
    return BOOL_VAL[1].label;
  }

  return value;
};

export const visibleIfNo = (value) => {
  if (value === BOOL_VAL[1].value || typeof (value) === 'boolean' && !value) { return true; }

  return false;
};

export const visibleIfYes = (value) => {
  if (value === BOOL_VAL[0].value || typeof (value) === 'boolean' && value) { return true; }

  return false;
};

export const renderFormControlWithLabel = ({
  className,
  label,
  meta: { touched, error, warning },
  input,
  ...other
}) => (

    <FormLabel>{label}</FormLabel>
    <FormControl
      className={className}
      {...input}
      {...other}
    />
    {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderFormControl = ({
  className,
  label,
  meta: { touched, error, warning },
  input,
  ...other
}) => (

    <FormControl
      className={className}
      {...input}
      {...other}
    />
    {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderSelectField = ({
  input,
  defaultValue,
  meta: { touched, error, warning },
  children,
  multiple,
  label,
  values,
  placeholder,
  infoText,
  ...other
}) => {
  let valueForSelect;
  if (multiple) {
    valueForSelect = (!R.isEmpty(input.value) && input.value) || defaultValue || ['placeholder_none'];
  } else {
    valueForSelect = input.value || defaultValue || 'placeholder_none';
  }
  return (<FormControl fullWidth error={(touched && error) || warning}>
    <FieldLabelWithTooltipIcon
      infoText={infoText}
      tooltipIconProps={{
        name: input.name,
      }}
    >
      {label}
    </FieldLabelWithTooltipIcon>
    <Select
      {...input}
      value={valueForSelect}
      multiple={multiple}
      style={{ marginTop: 0 }}
      renderValue={(value) => {
        if (value === 'placeholder_none' || R.indexOf('placeholder_none', value) !== -1) {
          return (<RenderPlaceholder placeholder={placeholder} />);
        }
        if (Array.isArray(value)) {
          const selectedValues = R.filter(
            R.propSatisfies(prop => value.includes(prop), 'value'),
            values,
          );
          return (<RenderMultipleSelections
            fieldName={input.name}
            onSelectionRemove={(removedValue) => {
              input.onChange(R.without([removedValue], value));
            }}
            selectedValues={selectedValues}
          />);
        }
        const selectedValue = R.find(R.propEq('value', value))(values) || {};
        const selectedLabel = R.prop('label', selectedValue);
        return selectedLabel;
      }}
      onBlur={(event) => {
        event.preventDefault();
      }}
      {...other}
    >
      {children}
    </Select>
    {((touched && error) || warning) && <FormHelperText>{error}</FormHelperText>}
  </FormControl>);
};

export const renderRadioField = ({ input,
  label,
  defaultValue,
  classes,
  infoText,
  meta: { touched, error, warning },
  options, ...other
}) => (

    <FormControl fullWidth>
      <FieldLabelWithTooltipIcon
        infoText={infoText}
        tooltipIconProps={{
          name: input.name,
        }}
      >
        {label}
      </FieldLabelWithTooltipIcon>
      <RadioGroupRow
        selectedValue={!R.isEmpty(input.value) ? transformBool(input.value) : defaultValue}
        onChange={(event, value) => { input.onChange(transformBool(value)); }}
        {...other}
      >
        {options.map((value, index) => (
          <FormControlLabel
            key={index}
            value={`${value.value}`}
            control={<RadioHeight />}
            label={value.label}
          />))}</RadioGroupRow>
    </FormControl>
    {((touched && error) || warning) &&
    <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderCheckbox = ({
  name,
  disabled,
  label,
  meta: { touched, error, warning },
  input,
}) => (

      <Checkbox
        id={name}
        disabled={disabled}
        checked={input.value}
        onChange={(event, value) => { input.onChange(transformBool(value)); }}
      />
      <Typography color="inherit" type="caption">
        {label}
      </Typography>

    {((touched && error) || error || warning)
      && <FormHelperText error>{error || warning}</FormHelperText>}

);

export const renderFileDownload = () => ({ input, label, infoText }) => (<FormControl fullWidth>
  <FieldLabelWithTooltipIcon
    infoText={infoText}
    tooltipIconProps={{
      name: input.name,
    }}
  >
    {label}
  </FieldLabelWithTooltipIcon>

    {input.value && <Attachment style={{ marginRight: 5 }} />}

 { window.open(input.value); }}
      style={{
        cursor: 'pointer',
        overflow: 'hidden',
        whiteSpace: 'nowrap',
        textOverflow: 'ellipsis',
      }}
    >
      {fileNameFromUrl(input.value)}

</FormControl>);

export const renderTextField = ({
  name,
  className,
  meta: { touched, error, warning },
  input,
  label,
  infoText,
  ...other
}) => (<FormControl fullWidth>
  <FieldLabelWithTooltipIcon
    infoText={infoText}
    tooltipIconProps={{
      name: input.name,
    }}
  >
    {label}
  </FieldLabelWithTooltipIcon>
  <TextField
    className={className}
    id={input.name}
    error={(touched && !!error) || !!warning}
    fullWidth
    {...input}
    {...other}
  />

    {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}
    {/* show limit of characters
       {other.inputProps && other.inputProps.maxLength &&
        <FormHelperText style={{ marginLeft: 'auto' }}>
        {input.value.length}/{other.inputProps.maxLength}
        </FormHelperText>} */}

</FormControl>);

export const renderNumberField = ({
  name,
  className,
  meta: { touched, error, warning },
  input,
  ...other
}) => {
  const rangeError = numerical(other.inputProps.min, other.inputProps.max)(input.value);

  return (

      <Input
        className={className}
        id={name}
        error={(touched && !!error) || !!warning || !!rangeError}
        fullWidth
        {...input}
        {...other}
      />

        {((touched && error) || warning || rangeError) &&
          <FormHelperText error>{error || warning || rangeError}</FormHelperText>}
        {other.inputProps && other.inputProps.maxLength && <FormHelperText style={{ marginLeft: 'auto' }}>{input.value.length}/{other.inputProps.maxLength}</FormHelperText>}

);
};

export const renderDatePicker = ({
  input: { value, onChange, ...inputOther },
  meta: { touched, error, warning },
  ...other
}) => {
  const datePickerProps = {};

  if (!value) {
    // noop
  } else if (typeof value === 'object' && value.valueOf()) {
    datePickerProps.value = value;
  } else if (typeof value === 'string' && value !== 'Invalid date') {
    datePickerProps.value = formatDateForDatePicker(value);
  }

  return (

      <DatePicker
        errorText={(touched && error) || warning}
        {...datePickerProps}
        {...inputOther}
        onChange={(event, val) => onChange(val)}
        {...other}
      />

  );
};

export const renderText = ({
  className,
  input,
  values,
  optional,
  label,
  infoText,
  date,
  meta,
  multiline,
  inputProps,
  ...other
}) => {
  let value = (!R.isNil(input.value) && !R.isEmpty(input.value))
    ? input.value
    : (inputProps
      ? inputProps.initial
      : null);

  if (!value) value = '-';

  if (values) {
    value = R.filter((val) => {
      if (Array.isArray(value)) return value.includes(val.value);
      return value === val.value;
    }, values).map(matchedValue => matchedValue.label).join(', ');
  }

  if (R.isEmpty(value) || R.isNil(value)) {
    value = (!R.isNil(input.value) && !R.isEmpty(input.value))
      ? input.value
      : (inputProps
        ? inputProps.initial
        : null);
  }
  if (date) value = formatDateForPrint(value);
  if (R.isEmpty(value) || R.isNil(value)) value = '-';

  return (
    <FormControl fullWidth>
      {label && <FieldLabelWithTooltipIcon
        infoText={infoText}
        tooltipIconProps={{
          name: input.name,
        }}
      >
        {label}
      </FieldLabelWithTooltipIcon>}

        {date && <DateRange style={{
          marginRight: 5,
          width: 22,
          height: 22,
        }}
        />}
        <Typography
          className={className}
          {...other}
        >
          {value}
        </Typography>

    </FormControl>
  );
};

export const renderBool = ({
  className,
  input,
  values,
  optional,
  label,
  meta,
  infoText,
  ...other
}) => (
  <FormControl fullWidth>
    <FormLabel>{label}</FormLabel>
    <Typography
      className={className}
      {...other}
    >
      {convertBool(input.value) ? convertBool(input.value) : '-'}
    </Typography>
  </FormControl>
);

export class AutocompleteRenderer extends Component {
  componentWillReceiveProps({ input, multiple, handleChange }) {
    if (!multiple && this.props.input.value && !input.value) {
      handleChange({}, { newValue: '' });
    }
  }

  render() {
    const {
      meta: { touched, error, warning },
      input: { name, onChange: onFormChange, value: formValue, onBlur: _onBlur, ...inputProps },
      suggestions,
      handleSuggestionsFetchRequested,
      handleSuggestionsClearRequested,
      classes,
      label,
      placeholder,
      handleChange,
      handleMultiChange,
      handleMultiClear,
      multiValues,
      fieldValue,
      multiple,
      overlap,
      suggestionsPool,
      infoText,
    } = this.props;

    return (

        <Autosuggest
          id={`autosuggest-${name}`}
          theme={{
            container: classes.container,
            suggestionsContainerOpen: overlap
              ? `${classes.suggestionsContainerOpen} ${classes.suggestionsContainerOpenOverlap}`
              : `${classes.suggestionsContainerOpen} ${classes.suggestionsContainerOpenExpand}`,
            suggestionsList: classes.suggestionsList,
            suggestion: classes.suggestion,
          }}
          renderInputComponent={multiple ? renderMultipleInput : renderInput}
          suggestions={suggestions}
          onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
          onSuggestionsClearRequested={handleSuggestionsClearRequested}
          renderSuggestionsContainer={renderSuggestionsContainer}
          getSuggestionValue={getSuggestionValue}
          onSuggestionSelected={multiple
            ? R.curry(setMultipleSuggestionValue)(formValue, handleChange, onFormChange, handleMultiChange)
            : R.curry(setSuggestionValue)(onFormChange)
          }
          highlightFirstSuggestion
          renderSuggestion={renderSuggestion}
          inputProps={{
            name,
            error: (touched && (!!error || !!warning)),
            label,
            type: 'text',
            placeholder,
            value: fieldValue,
            multiValues,
            onChange: handleChange,
            onBlur(...args) {
              if (typeof _onBlur === 'function') {
                _onBlur(...args);
              }

              if (multiple || !suggestionsPool) {
                return;
              }

              const ev = args[0];

              ev.persist();

              setTimeout(() => {
                const prevSelection = suggestionsPool.filter(s => s.value === formValue)[0];
                const newValue = prevSelection ? prevSelection.label : '';

                ev.target.value = newValue;

                handleChange(ev, { newValue });
              });
            },
            handleClear: R.curry(handleClear)(onFormChange, handleMultiClear),
            infoText,
            ...inputProps,
          }}
        />

          {((touched && error) || warning) && <FormHelperText error>{error || warning}</FormHelperText>}

    );
  }
}
<|endoftext|>


<commit_before>/**
 * Cloud9 Python support
 *
 * @copyright 2015, Ajax.org B.V.
 */
define(function(require, exports, module) {
    main.consumes = [
        "Plugin", "language", "jsonalyzer", "settings",
        "preferences", "preferences.experimental"
    ];
    main.provides = ["language.python"];
    return main;

    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var language = imports.language;
        var jsonalyzer = imports["jsonalyzer"];
        var experimental = imports["preferences.experimental"];
        var prefs = imports.preferences;
        var settings = imports.settings;
        var plugin = new Plugin("Ajax.org", main.consumes);
        var jediServer = require("text!./server/jedi_server.py").replace(/ {4}/g, " ");
        var launchCommand = require("text!./server/launch_command.sh").replace(/ +/g, " ");

        var enabled = experimental.addExperiment("python_worker", false, "Language/Python Code Completion");

        plugin.on("load", function() {
            jsonalyzer.registerWorkerHandler("plugins/c9.ide.language.python/worker/python_jsonalyzer_worker");
            jsonalyzer.registerServerHandler("plugins/c9.ide.language.python/server/python_jsonalyzer_server_worker");

            prefs.add({
                "Project": {
                    "Language Support" : {
                        position: 800,
                        "Python Version" : {
                            position: 300,
                            type: "dropdown",
                            path: "project/python/@version",
                            items: [
                                { caption: "Python 2", value: "python2" },
                                { caption: "Python 3", value: "python3" },
                            ]
                        },
                    }
                }
            }, plugin);

            settings.on("read", function(e) {
                settings.setDefaults("project/python", [
                    ["version", "python2"]
                ]);
            });

            if (!enabled)
                return;

            settings.on("project/python", function(e) {
                language.getWorker(function(err, worker) {
                    if (err) return console.error(err);
                    var version = settings.get("project/python/@version");
                    worker.emit("set_python_version", { data: version });
                });
            }, plugin);

            language.registerLanguageHandler("plugins/c9.ide.language.python/worker/python_worker", function(err, worker) {
                if (err) return console.error(err);
                var version = settings.get("project/python/@version");
                worker.emit("set_python_version", { data: version });
                worker.emit("set_python_scripts", { data: { jediServer: jediServer, launchCommand: launchCommand } });
            });
        });

        plugin.on("unload", function() {
            jsonalyzer.unregisterWorkerHandler("plugins/c9.ide.language.python/worker/python_jsonalyzer_worker");
            jsonalyzer.unregisterServerHandler("plugins/c9.ide.language.python/server/python_jsonalyzer_server_worker");
            language.unregisterLanguageHandler("plugins/c9.ide.language.python/worker/python_worker");
        });

        /** @ignore */
        register(null, {
            "language.python": plugin
        });
    }
});<commit_msg>Fix plugin reloading<commit_after>/**
 * Cloud9 Python support
 *
 * @copyright 2015, Ajax.org B.V.
 */
define(function(require, exports, module) {
    main.consumes = [
        "Plugin", "language", "jsonalyzer", "settings",
        "preferences", "preferences.experimental"
    ];
    main.provides = ["language.python"];
    return main;

    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var language = imports.language;
        var jsonalyzer = imports["jsonalyzer"];
        var experimental = imports["preferences.experimental"];
        var prefs = imports.preferences;
        var settings = imports.settings;
        var plugin = new Plugin("Ajax.org", main.consumes);
        var jediServer = require("text!./server/jedi_server.py").replace(/ {4}/g, " ");
        var launchCommand = require("text!./server/launch_command.sh").replace(/ +/g, " ");

        var enabled = experimental.addExperiment("python_worker", false, "Language/Python Code Completion");

        plugin.on("load", function() {
            jsonalyzer.registerWorkerHandler("plugins/c9.ide.language.python/worker/python_jsonalyzer_worker");
            jsonalyzer.registerServerHandler("plugins/c9.ide.language.python/server/python_jsonalyzer_server_worker");

            prefs.add({
                "Project": {
                    "Language Support" : {
                        position: 800,
                        "Python Version" : {
                            position: 300,
                            type: "dropdown",
                            path: "project/python/@version",
                            items: [
                                { caption: "Python 2", value: "python2" },
                                { caption: "Python 3", value: "python3" },
                            ]
                        },
                    }
                }
            }, plugin);

            settings.on("read", function(e) {
                settings.setDefaults("project/python", [
                    ["version", "python2"]
                ]);
            }, plugin);

            if (!enabled)
                return;

            settings.on("project/python", function(e) {
                language.getWorker(function(err, worker) {
                    if (err) return console.error(err);
                    var version = settings.get("project/python/@version");
                    worker.emit("set_python_version", { data: version });
                });
            }, plugin);

            language.registerLanguageHandler("plugins/c9.ide.language.python/worker/python_worker", function(err, worker) {
                if (err) return console.error(err);
                var version = settings.get("project/python/@version");
                worker.emit("set_python_version", { data: version });
                worker.emit("set_python_scripts", { data: { jediServer: jediServer, launchCommand: launchCommand } });
            });
        });

        plugin.on("unload", function() {
            jsonalyzer.unregisterWorkerHandler("plugins/c9.ide.language.python/worker/python_jsonalyzer_worker");
            jsonalyzer.unregisterServerHandler("plugins/c9.ide.language.python/server/python_jsonalyzer_server_worker");
            language.unregisterLanguageHandler("plugins/c9.ide.language.python/worker/python_worker");
        });

        /** @ignore */
        register(null, {
            "language.python": plugin
        });
    }
});<|endoftext|>


<commit_before>'use strict';

var Component = require('substance/ui/Component');
var getXRefTargets = require('./getXRefTargets');
var find = require('lodash/find');
var without = require('lodash/without');

/*
  Editing of XRefTargets
*/
function XRefTargets() {
  XRefTargets.super.apply(this, arguments);
  console.log('Created XRefTargets', this.__id__);
}

XRefTargets.Prototype = function() {

  this.getInitialState = function() {
    return {
      targets: getXRefTargets(this.props.node)
    };
  };

  this.willReceiveProps = function() {
    console.log('XRefTargets.willReceiveProps', this.__id__);
  };

  this.dispose = function() {
    console.log('XRefTargets.dispose', this.__id__);
  };

  this.render = function($$) {
    var el = $$('div').addClass('sc-xref-targets');
    var componentRegistry = this.context.componentRegistry;

    this.state.targets.forEach(function(target) {
      var TargetComponent = componentRegistry.get(target.node.type+'-target');
      el.append(
        $$(TargetComponent, target)
          .on('click', this._toggleTarget.bind(this, target.node))
      );
    }.bind(this));
    return el;
  };

  this._toggleTarget = function(targetNode) {
    var node = this.props.node;
    var surface = this.context.surfaceManager.getFocusedSurface();

    // Update model
    var newTargets = node.targets;
    if (newTargets.indexOf(targetNode.id) > 0) {
      newTargets = without(newTargets, targetNode.id);
    } else {
      newTargets.push(targetNode.id);
    }

    // Compute visual feedback
    var targets = this.state.targets;
    var target = find(this.state.targets, function(t) {
      return t.node === targetNode;
    });

    // Flip the selected flag
    target.selected = !target.selected;

    // Triggers a rerender
    this.setState({
      targets: targets
    });

    // ATTENTION: still we need to use surface.transaction()
    surface.transaction(function(tx) {
      tx.set([node.id, 'targets'], newTargets);
    });
  };
};

Component.extend(XRefTargets);

module.exports = XRefTargets;<commit_msg>Make FigureTargets non editable.<commit_after>'use strict';

var clone = require('lodash/clone');
var find = require('lodash/find');
var without = require('lodash/without');
var Component = require('substance/ui/Component');
var getXRefTargets = require('./getXRefTargets');

/*
  Editing of XRefTargets
*/
function XRefTargets() {
  XRefTargets.super.apply(this, arguments);
  console.log('Created XRefTargets', this.__id__);
}

XRefTargets.Prototype = function() {

  this.getInitialState = function() {
    return {
      targets: getXRefTargets(this.props.node)
    };
  };

  this.willReceiveProps = function() {
    console.log('XRefTargets.willReceiveProps', this.__id__);
  };

  this.dispose = function() {
    console.log('XRefTargets.dispose', this.__id__);
  };

  this.render = function($$) {
    var el = $$('div').addClass('sc-xref-targets');
    var componentRegistry = this.context.componentRegistry;

    this.state.targets.forEach(function(target) {
      var TargetComponent = componentRegistry.get(target.node.type+'-target');
      var props = clone(target);
      // disable editing in TargetComponent
      props.disabled = true;
      el.append(
        $$(TargetComponent, props)
          .on('click', this._toggleTarget.bind(this, target.node))
      );
    }.bind(this));
    return el;
  };

  this._toggleTarget = function(targetNode) {
    var node = this.props.node;
    var surface = this.context.surfaceManager.getFocusedSurface();

    // Update model
    var newTargets = node.targets;
    if (newTargets.indexOf(targetNode.id) > 0) {
      newTargets = without(newTargets, targetNode.id);
    } else {
      newTargets.push(targetNode.id);
    }

    // Compute visual feedback
    var targets = this.state.targets;
    var target = find(this.state.targets, function(t) {
      return t.node === targetNode;
    });

    // Flip the selected flag
    target.selected = !target.selected;

    // Triggers a rerender
    this.setState({
      targets: targets
    });

    // ATTENTION: still we need to use surface.transaction()
    surface.transaction(function(tx) {
      tx.set([node.id, 'targets'], newTargets);
    });
  };
};

Component.extend(XRefTargets);

module.exports = XRefTargets;<|endoftext|>


<commit_before>/*
 * grunt-appcache
 * http://canvace.com/
 *
 * Copyright (c) 2013 Canvace Srl
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function (grunt) {

    var path = require('path');
    var appcache = require('./lib/appcache').init(grunt);

    function identity(input) {
        return input;
    }

    function isAbsolutePath(path) {
        return (/^(?:https?:)?\/\//i).test(path);
    }

    function expand(input) {
        var list = [];
        input.forEach(function (pattern) {
            if(isAbsolutePath(pattern)) {
                list.push(pattern);
            }
            else {
                list.push.apply(list, grunt.file.expand(pattern).filter(function (filepath) {
                    return grunt.file.exists(filepath) && grunt.file.isFile(filepath);
                }));
            }
        });
        return list;
    }

    function filter(input, action) {
        if (!Array.isArray(input)) {
            input = [input];
        }
        return action(input);
    }

    grunt.registerMultiTask('appcache', 'Automatically generates an HTML5 AppCache manifest from a list of files.', function () {
        var options = this.options({
            basePath: process.cwd(),
            ignoreManifest: true,
            preferOnline: false
        });

        options.basePath = path.normalize(options.basePath);

        var ignored = [];
        if (this.data.ignored) {
            ignored = filter(this.data.ignored, expand).map(function (filepath) {
                return path.normalize(filepath);
            });
        }

        if (options.ignoreManifest) {
            ignored.push(path.normalize(this.data.dest));
        }

        var manifest = {
            version: {
                revision: 1,
                date: new Date()
            },
            cache: filter(this.data.cache, expand).filter(function (filepath) {
                return (-1 === ignored.indexOf(path.normalize(filepath)));
            }).map(function (filepath) {
                if (isAbsolutePath(filepath)) {
                    return filepath;
                }
                return path.relative(options.basePath, path.normalize(filepath));
            }),
            network: filter(this.data.network || [], identity),
            fallback: filter(this.data.fallback || [], identity)
        };

        if (grunt.file.exists(this.data.dest)) {
            var original = appcache.readManifest(this.data.dest);
            manifest.version.revision = (1 + original.version.revision);
        }

        if (!appcache.writeManifest(this.data.dest, manifest)) {
            grunt.log.error('AppCache manifest creation failed.');
            return false;
        }

        grunt.log.writeln('AppCache manifest "' + this.data.dest + '" created.');
    });

};
<commit_msg>Improve file expansion.<commit_after>/*
 * grunt-appcache
 * http://canvace.com/
 *
 * Copyright (c) 2013 Canvace Srl
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function (grunt) {

    var path = require('path');
    var appcache = require('./lib/appcache').init(grunt);

    function array(input) {
        return Array.isArray(input) ? input : [input];
    }

    function isUrl(path) {
        return (/^(?:https?:)?\/\//i).test(path);
    }

    function relative(basePath, filePath) {
        return path.relative(
                path.normalize(basePath),
                path.normalize(filePath));
    }

    function expand(pattern, basePath) {
        var matches = grunt.file.expand({
            filter: function (src) {
                return grunt.file.isFile(src) || isUrl(src);
            }
        }, pattern);
        if (typeof basePath === 'string') {
            matches = matches.map(function (filePath) {
                return relative(basePath, filePath);
            });
        }
        return matches;
    }

    grunt.registerMultiTask('appcache', 'Automatically generates an HTML5 AppCache manifest from a list of files.', function () {
        var output = path.normalize(this.data.dest);
        var options = this.options({
            basePath: process.cwd(),
            ignoreManifest: true,
            preferOnline: false
        });

        var ignored = [];
        if (this.data.ignored) {
            ignored = expand(this.data.ignored, options.basePath);
        }
        if (options.ignoreManifest) {
            ignored.push(relative(options.basePath, output));
        }

        var cache = expand(this.data.cache, options.basePath).filter(function (path) {
            return ignored.indexOf(path) === -1;
        });

        var manifest = {
            version: {
                revision: 1,
                date: new Date()
            },
            cache: cache,
            network: array(this.data.network || []),
            fallback: array(this.data.fallback || [])
        };

        if (grunt.file.exists(output)) {
            var original = appcache.readManifest(output);
            manifest.version.revision = (1 + original.version.revision);
        }

        if (!appcache.writeManifest(output, manifest)) {
            grunt.log.error('AppCache manifest creation failed.');
            return false;
        }

        grunt.log.writeln('AppCache manifest "' +
                path.basename(output) +
                '" created.');
    });

};
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2021 Google Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

goog.provide('GoogleSmartCard.LibusbToWebusbAdaptor');

goog.require('GoogleSmartCard.Logging');
goog.require('goog.asserts');
goog.require('goog.log');

goog.scope(function() {

const GSC = GoogleSmartCard;
const LibusbJsDevice = GSC.LibusbProxyDataModel.LibusbJsDevice;
const LibusbJsConfigurationDescriptor =
    GSC.LibusbProxyDataModel.LibusbJsConfigurationDescriptor;
const LibusbJsEndpointType = GSC.LibusbProxyDataModel.LibusbJsEndpointType;
const LibusbJsEndpointDescriptor =
    GSC.LibusbProxyDataModel.LibusbJsEndpointDescriptor;
const LibusbJsDirection = GSC.LibusbProxyDataModel.LibusbJsDirection;
const LibusbJsInterfaceDescriptor =
    GSC.LibusbProxyDataModel.LibusbJsInterfaceDescriptor;

const logger = GSC.Logging.getScopedLogger('LibusbToWebusbAdaptor');

/**
 * Implements the Libusb requests via the WebUSB API.
 */
GSC.LibusbToWebusbAdaptor = class extends GSC.LibusbToJsApiAdaptor {
  constructor() {
    super();
    /**
     * Mapping from IDs (generated by us) into USBDevice objects (returned by
     * the API).
     * @type {!Map<number, !Object>}
     */
    this.idToWebusbDeviceMap_ = new Map();
    /**
     * The counter that's used for generating new device IDs.
     * @type {number}
     */
    this.nextFreeDeviceId_ = 1;
  }

  /** @override */
  async listDevices() {
    const webusbDevices =
        /** @type {!Array<!Object>} */ (await navigator['usb']['getDevices']());

    // Keep references to all current USBDevice objects, since other WebUSB
    // functions take USBDevice as parameter. It's not possible to
    // programmatically recreate a USBDevice object, so we need to store them.
    this.updateDeviceMap_(webusbDevices);

    return webusbDevices.map(
        webusbDevice => this.convertWebusbDeviceToLibusb_(webusbDevice));
  }

  /** @override */
  async getConfigurations(deviceId) {
    const webusbDevice = this.getDeviceByIdOrThrow_(deviceId);
    // Note: It's incorrect to check whether the configuration is active by
    // comparing it via "===" against `webusbDevice['configuration']`, because
    // Chrome produces different objects for USBDevice::configuration and for
    // USBDevice::configurations: see crbug.com/1274922. Hence the need to
    // compare by the configurationValue fields.
    const activeConfigurationValue = webusbDevice['configuration'] ?
        webusbDevice['configuration']['configurationValue'] :
        null;
    return webusbDevice['configurations'].map(
        config => getLibusbJsConfigurationDescriptor(
            config, activeConfigurationValue));
  }

  /** @override */
  async openDeviceHandle(deviceId) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async closeDeviceHandle(deviceId, deviceHandle) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async claimInterface(deviceId, deviceHandle, interfaceNumber) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async releaseInterface(deviceId, deviceHandle, interfaceNumber) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async resetDevice(deviceId, deviceHandle) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async controlTransfer(deviceId, deviceHandle, parameters) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async bulkTransfer(deviceId, deviceHandle, parameters) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async interruptTransfer(deviceId, deviceHandle, parameters) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /**
   * @private
   * @param {!Object} webusbDevice The USBDevice object.
   * @return {number|null}
   */
  getDeviceId_(webusbDevice) {
    for (const deviceId of this.idToWebusbDeviceMap_.keys()) {
      if (this.idToWebusbDeviceMap_.get(deviceId) === webusbDevice)
        return deviceId;
    }
    return null;
  }

  /**
   * @private
   * @param {number} deviceId
   * @return {!Object} The WebUSB USBDevice object.
   */
  getDeviceByIdOrThrow_(deviceId) {
    const webusbDevice = this.idToWebusbDeviceMap_.get(deviceId);
    if (!webusbDevice)
      throw new Error(`No device with ID ${deviceId}`);
    return webusbDevice;
  }

  /**
   * @private
   * @param {!Array<!Object>} webusbDevices The list of USBDevice objects.
   */
  updateDeviceMap_(webusbDevices) {
    /** @type {!Map<number, !Object>} */
    const newIdToDeviceMap = new Map();
    for (const webusbDevice of webusbDevices) {
      // Reuse the ID if the device was already tracked. Otherwise generate a
      // new one.
      let chosenDeviceId = this.getDeviceId_(webusbDevice);
      if (chosenDeviceId === null) {
        chosenDeviceId = this.nextFreeDeviceId_;
        this.nextFreeDeviceId_++;
      }
      newIdToDeviceMap.set(chosenDeviceId, webusbDevice);
    }
    // Overwrite the map. Note that this drops the devices that disappeared from
    // the list.
    this.idToWebusbDeviceMap_ = newIdToDeviceMap;
  }

  /**
   * @private
   * @param {!Object} webusbDevice
   * @return {!LibusbJsDevice}
   */
  convertWebusbDeviceToLibusb_(webusbDevice) {
    const deviceId = this.getDeviceId_(webusbDevice);
    goog.asserts.assert(deviceId);
    /** @type {!LibusbJsDevice} */
    const libusbJsDevice = {
      'deviceId': deviceId,
      'vendorId': webusbDevice['vendorId'],
      'productId': webusbDevice['productId'],
      'version': (webusbDevice['deviceVersionMajor'] << 8) +
          (webusbDevice['deviceVersionMinor'] << 4) +
          webusbDevice['deviceVersionSubminor'],
    };
    if (webusbDevice['productName'])
      libusbJsDevice['productName'] = webusbDevice['productName'];
    if (webusbDevice['manufacturerName'])
      libusbJsDevice['manufacturerName'] = webusbDevice['manufacturerName'];
    if (webusbDevice['serialNumber'])
      libusbJsDevice['serialNumber'] = webusbDevice['serialNumber'];
    return libusbJsDevice;
  }
};

/**
 * @param {!Object} webusbConfiguration The WebUSB USBConfiguration value.
 * @param {number|null} activeConfigurationValue The active configuration's
 *     value.
 * @return {!LibusbJsConfigurationDescriptor}
 */
function getLibusbJsConfigurationDescriptor(
    webusbConfiguration, activeConfigurationValue) {
  // Note: extraData is skipped, as it's not provided by WebUSB.
  return {
    'active':
        webusbConfiguration['configurationValue'] === activeConfigurationValue,
    'configurationValue': webusbConfiguration['configurationValue'],
    'interfaces': webusbConfiguration['interfaces']
                      .map(
                          webusbInterface =>
                              getLibusbJsInterfaceDescriptor(webusbInterface))
                      .filter(item => item !== null),
  };
}

/**
 * @param {!Object} webusbInterface The WebUSB USBInterface value.
 * @return {!LibusbJsInterfaceDescriptor|null}
 */
function getLibusbJsInterfaceDescriptor(webusbInterface) {
  if (webusbInterface['alternates'].length === 0) {
    // Only log in the Debug mode by default, since these errors might quickly
    // flood the application's logs.
    goog.log.fine(logger, `Ignoring WebUSB interface without alternates`);
    return null;
  }
  // Note: We're not using the "alternate" field here, since, contrary to the
  // WebUSB specification, Chrome's implementation typically sets this field to
  // null. See crbug.com/1093502.
  const webusbAlternateInterface = webusbInterface['alternates'][0];
  // Note: extraData is skipped, as it's not provided by WebUSB.
  return {
    'interfaceNumber': webusbInterface['interfaceNumber'],
    'interfaceClass': webusbAlternateInterface['interfaceClass'],
    'interfaceSubclass': webusbAlternateInterface['interfaceSubclass'],
    'interfaceProtocol': webusbAlternateInterface['interfaceProtocol'],
    'endpoints': webusbAlternateInterface['endpoints']
                     .map(endpoint => getLibusbJsEndpointDescriptor(endpoint))
                     .filter(item => item !== null),
  };
}

/**
 * @param {!Object} webusbEndpoint The WebUSB USBEndpoint value.
 * @return {!LibusbJsEndpointDescriptor|null}
 */
function getLibusbJsEndpointDescriptor(webusbEndpoint) {
  // According to the USB specification, the endpoint address contains the
  // endpoint number in bits 0..3 and the direction in the bit 7.
  const endpointAddress = webusbEndpoint['endpointNumber'] +
      (webusbEndpoint['direction'] === 'in' ? 1 << 7 : 0);
  const endpointType = getLibusbJsEndpointType(webusbEndpoint['type']);
  if (!endpointType)
    return null;
  // Note: extraData is skipped, as it's not provided by WebUSB.
  return {
    'endpointAddress': endpointAddress,
    'direction': webusbEndpoint['direction'] === 'in' ? LibusbJsDirection.IN :
                                                        LibusbJsDirection.OUT,
    'type': endpointType,
    'maxPacketSize': webusbEndpoint['packetSize'],
  };
}

/**
 * @param {string} webusbEndpointType The WebUSB USBEndpointType value.
 * @return {!LibusbJsEndpointType|null}
 */
function getLibusbJsEndpointType(webusbEndpointType) {
  switch (webusbEndpointType) {
    case 'bulk':
      return LibusbJsEndpointType.BULK;
    case 'interrupt':
      return LibusbJsEndpointType.INTERRUPT;
    case 'isochronous':
      return LibusbJsEndpointType.ISOCHRONOUS;
  }
  // Only log in the Debug mode by default, since these errors might quickly
  // flood the application's logs.
  goog.log.fine(logger, `Unknown WebUSB endpoint type: ${webusbEndpointType}`);
  return null;
}

/**
 * Returns whether the API needed for this adaptor to work is available.
 * @static
 * @return {boolean}
 */
GSC.LibusbToWebusbAdaptor.isApiAvailable = function() {
  return navigator !== undefined && navigator['usb'] !== undefined;
};
});  // goog.scope
<commit_msg>[usb] Fetch extraData manually when using WebUSB (#483)<commit_after>/**
 * @license
 * Copyright 2021 Google Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

goog.provide('GoogleSmartCard.LibusbToWebusbAdaptor');

goog.require('GoogleSmartCard.Logging');
goog.require('goog.asserts');
goog.require('goog.log');
goog.require('goog.object');

goog.scope(function() {

const GSC = GoogleSmartCard;
const LibusbJsDevice = GSC.LibusbProxyDataModel.LibusbJsDevice;
const LibusbJsConfigurationDescriptor =
    GSC.LibusbProxyDataModel.LibusbJsConfigurationDescriptor;
const LibusbJsEndpointType = GSC.LibusbProxyDataModel.LibusbJsEndpointType;
const LibusbJsEndpointDescriptor =
    GSC.LibusbProxyDataModel.LibusbJsEndpointDescriptor;
const LibusbJsDirection = GSC.LibusbProxyDataModel.LibusbJsDirection;
const LibusbJsInterfaceDescriptor =
    GSC.LibusbProxyDataModel.LibusbJsInterfaceDescriptor;

const logger = GSC.Logging.getScopedLogger('LibusbToWebusbAdaptor');

/**
 * Implements the Libusb requests via the WebUSB API.
 */
GSC.LibusbToWebusbAdaptor = class extends GSC.LibusbToJsApiAdaptor {
  constructor() {
    super();
    /**
     * Mapping from IDs (generated by us) into USBDevice objects (returned by
     * the API).
     * @type {!Map<number, !Object>}
     */
    this.idToWebusbDeviceMap_ = new Map();
    /**
     * The counter that's used for generating new device IDs.
     * @type {number}
     */
    this.nextFreeDeviceId_ = 1;
  }

  /** @override */
  async listDevices() {
    const webusbDevices =
        /** @type {!Array<!Object>} */ (await navigator['usb']['getDevices']());

    // Keep references to all current USBDevice objects, since other WebUSB
    // functions take USBDevice as parameter. It's not possible to
    // programmatically recreate a USBDevice object, so we need to store them.
    this.updateDeviceMap_(webusbDevices);

    return webusbDevices.map(
        webusbDevice => this.convertWebusbDeviceToLibusb_(webusbDevice));
  }

  /** @override */
  async getConfigurations(deviceId) {
    const webusbDevice = this.getDeviceByIdOrThrow_(deviceId);
    // Note: It's incorrect to check whether the configuration is active by
    // comparing it via "===" against `webusbDevice['configuration']`, because
    // Chrome produces different objects for USBDevice::configuration and for
    // USBDevice::configurations: see crbug.com/1274922. Hence the need to
    // compare by the configurationValue fields.
    const activeConfigurationValue = webusbDevice['configuration'] ?
        webusbDevice['configuration']['configurationValue'] :
        null;
    const libusbJsConfigurations = [];
    for (const webusbConfiguration of webusbDevice['configurations']) {
      const libusbJsConfiguration = getLibusbJsConfigurationDescriptor(
          webusbConfiguration, activeConfigurationValue);
      // WebUSB doesn't return extraData in USBConfiguration, so we need to
      // fetch it.
      /** @preserveTry */
      try {
        await fetchAndFillConfigurationExtraData(
            webusbDevice, libusbJsConfiguration);
      } catch (exc) {
        // Suppress errors of fetching extraData: they should be non-fatal.
        // Log only at the "fine" level, as this can quickly flood the logs.
        goog.log.fine(logger, `Failure fetching extra data: ${exc}`);
      }
      libusbJsConfigurations.push(libusbJsConfiguration);
    }
    return libusbJsConfigurations;
  }

  /** @override */
  async openDeviceHandle(deviceId) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async closeDeviceHandle(deviceId, deviceHandle) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async claimInterface(deviceId, deviceHandle, interfaceNumber) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async releaseInterface(deviceId, deviceHandle, interfaceNumber) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async resetDevice(deviceId, deviceHandle) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async controlTransfer(deviceId, deviceHandle, parameters) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async bulkTransfer(deviceId, deviceHandle, parameters) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /** @override */
  async interruptTransfer(deviceId, deviceHandle, parameters) {
    // TODO(#429): Implement this method.
    throw new Error('Not implemented');
  }

  /**
   * @private
   * @param {!Object} webusbDevice The USBDevice object.
   * @return {number|null}
   */
  getDeviceId_(webusbDevice) {
    for (const deviceId of this.idToWebusbDeviceMap_.keys()) {
      if (this.idToWebusbDeviceMap_.get(deviceId) === webusbDevice)
        return deviceId;
    }
    return null;
  }

  /**
   * @private
   * @param {number} deviceId
   * @return {!Object} The WebUSB USBDevice object.
   */
  getDeviceByIdOrThrow_(deviceId) {
    const webusbDevice = this.idToWebusbDeviceMap_.get(deviceId);
    if (!webusbDevice)
      throw new Error(`No device with ID ${deviceId}`);
    return webusbDevice;
  }

  /**
   * @private
   * @param {!Array<!Object>} webusbDevices The list of USBDevice objects.
   */
  updateDeviceMap_(webusbDevices) {
    /** @type {!Map<number, !Object>} */
    const newIdToDeviceMap = new Map();
    for (const webusbDevice of webusbDevices) {
      // Reuse the ID if the device was already tracked. Otherwise generate a
      // new one.
      let chosenDeviceId = this.getDeviceId_(webusbDevice);
      if (chosenDeviceId === null) {
        chosenDeviceId = this.nextFreeDeviceId_;
        this.nextFreeDeviceId_++;
      }
      newIdToDeviceMap.set(chosenDeviceId, webusbDevice);
    }
    // Overwrite the map. Note that this drops the devices that disappeared from
    // the list.
    this.idToWebusbDeviceMap_ = newIdToDeviceMap;
  }

  /**
   * @private
   * @param {!Object} webusbDevice
   * @return {!LibusbJsDevice}
   */
  convertWebusbDeviceToLibusb_(webusbDevice) {
    const deviceId = this.getDeviceId_(webusbDevice);
    goog.asserts.assert(deviceId);
    /** @type {!LibusbJsDevice} */
    const libusbJsDevice = {
      'deviceId': deviceId,
      'vendorId': webusbDevice['vendorId'],
      'productId': webusbDevice['productId'],
      'version': (webusbDevice['deviceVersionMajor'] << 8) +
          (webusbDevice['deviceVersionMinor'] << 4) +
          webusbDevice['deviceVersionSubminor'],
    };
    if (webusbDevice['productName'])
      libusbJsDevice['productName'] = webusbDevice['productName'];
    if (webusbDevice['manufacturerName'])
      libusbJsDevice['manufacturerName'] = webusbDevice['manufacturerName'];
    if (webusbDevice['serialNumber'])
      libusbJsDevice['serialNumber'] = webusbDevice['serialNumber'];
    return libusbJsDevice;
  }
};

/**
 * @param {!Object} webusbConfiguration The WebUSB USBConfiguration value.
 * @param {number|null} activeConfigurationValue The active configuration's
 *     value.
 * @return {!LibusbJsConfigurationDescriptor}
 */
function getLibusbJsConfigurationDescriptor(
    webusbConfiguration, activeConfigurationValue) {
  return {
    'active':
        webusbConfiguration['configurationValue'] === activeConfigurationValue,
    'configurationValue': webusbConfiguration['configurationValue'],
    'interfaces': webusbConfiguration['interfaces']
                      .map(
                          webusbInterface =>
                              getLibusbJsInterfaceDescriptor(webusbInterface))
                      .filter(item => item !== null),
  };
}

/**
 * @param {!Object} webusbInterface The WebUSB USBInterface value.
 * @return {!LibusbJsInterfaceDescriptor|null}
 */
function getLibusbJsInterfaceDescriptor(webusbInterface) {
  if (webusbInterface['alternates'].length === 0) {
    // Only log in the Debug mode by default, since these errors might quickly
    // flood the application's logs.
    goog.log.fine(logger, `Ignoring WebUSB interface without alternates`);
    return null;
  }
  // Note: We're not using the "alternate" field here, since, contrary to the
  // WebUSB specification, Chrome's implementation typically sets this field to
  // null. See crbug.com/1093502.
  const webusbAlternateInterface = webusbInterface['alternates'][0];
  return {
    'interfaceNumber': webusbInterface['interfaceNumber'],
    'interfaceClass': webusbAlternateInterface['interfaceClass'],
    'interfaceSubclass': webusbAlternateInterface['interfaceSubclass'],
    'interfaceProtocol': webusbAlternateInterface['interfaceProtocol'],
    'endpoints': webusbAlternateInterface['endpoints']
                     .map(endpoint => getLibusbJsEndpointDescriptor(endpoint))
                     .filter(item => item !== null),
  };
}

/**
 * @param {!Object} webusbEndpoint The WebUSB USBEndpoint value.
 * @return {!LibusbJsEndpointDescriptor|null}
 */
function getLibusbJsEndpointDescriptor(webusbEndpoint) {
  // According to the USB specification, the endpoint address contains the
  // endpoint number in bits 0..3 and the direction in the bit 7.
  const endpointAddress = webusbEndpoint['endpointNumber'] +
      (webusbEndpoint['direction'] === 'in' ? 1 << 7 : 0);
  const endpointType = getLibusbJsEndpointType(webusbEndpoint['type']);
  if (!endpointType)
    return null;
  return {
    'endpointAddress': endpointAddress,
    'direction': webusbEndpoint['direction'] === 'in' ? LibusbJsDirection.IN :
                                                        LibusbJsDirection.OUT,
    'type': endpointType,
    'maxPacketSize': webusbEndpoint['packetSize'],
  };
}

/**
 * @param {string} webusbEndpointType The WebUSB USBEndpointType value.
 * @return {!LibusbJsEndpointType|null}
 */
function getLibusbJsEndpointType(webusbEndpointType) {
  switch (webusbEndpointType) {
    case 'bulk':
      return LibusbJsEndpointType.BULK;
    case 'interrupt':
      return LibusbJsEndpointType.INTERRUPT;
    case 'isochronous':
      return LibusbJsEndpointType.ISOCHRONOUS;
  }
  // Only log in the Debug mode by default, since these errors might quickly
  // flood the application's logs.
  goog.log.fine(logger, `Unknown WebUSB endpoint type: ${webusbEndpointType}`);
  return null;
}

/**
 * Fetches class-/vendor-specific configuration/interface/endpoint descriptors
 * using special control transfers. The result is assigned into the
 * corresponding 'extraData' fields inside `libusbJsConfiguration`.
 * @param {!Object} webusbDevice The WebUSB USBDevice value.
 * @param {!LibusbJsConfigurationDescriptor} libusbJsConfiguration
 */
async function fetchAndFillConfigurationExtraData(
    webusbDevice, libusbJsConfiguration) {
  await webusbDevice['open']();
  /** @preserveTry */
  try {
    await fetchAndFillConfigurationExtraDataForOpenedDevice(
        webusbDevice, libusbJsConfiguration);
  } finally {
    // TODO(#429): Don't close if there are opened device handles.
    await webusbDevice['close']();
  }
}

/**
 * Same as `fetchAndFillConfigurationExtraData()`, but assumes the device to be
 * opened.
 * @param {!Object} webusbDevice The WebUSB USBDevice value.
 * @param {!LibusbJsConfigurationDescriptor} libusbJsConfiguration
 */
async function fetchAndFillConfigurationExtraDataForOpenedDevice(
    webusbDevice, libusbJsConfiguration) {
  const GET_DESCRIPTOR_REQUEST = 0x06;
  const CONFIGURATION_DESCRIPTOR_TYPE = 0x02;
  const INTERFACE_DESCRIPTOR_TYPE = 0x04;
  const ENDPOINT_DESCRIPTOR_TYPE = 0x05;
  const CONFIGURATION_DESCRIPTOR_LENGTH = 9;
  const INTERFACE_DESCRIPTOR_LENGTH = 9;
  const ENDPOINT_DESCRIPTOR_LENGTH = 7;

  const controlRequestValue = (CONFIGURATION_DESCRIPTOR_TYPE << 8) |
      (libusbJsConfiguration['configurationValue'] - 1);
  const controlTransferParameters = {
    'requestType': 'standard',
    'recipient': 'device',
    'request': GET_DESCRIPTOR_REQUEST,
    'value': controlRequestValue,
    'index': 0
  };

  // Determine the size of the whole descriptor hierarchy ("wTotalLength").
  let transferResult = await webusbDevice['controlTransferIn'](
      controlTransferParameters, CONFIGURATION_DESCRIPTOR_LENGTH);
  if (transferResult['status'] !== 'ok' || !transferResult['data'])
    return;
  const initialData = /** @type {!DataView} */ (transferResult['data']);
  const totalLength = initialData.getUint16(2, /*littleEndian=*/ true);

  // Read the whole descriptor hierarchy.
  transferResult = await webusbDevice['controlTransferIn'](
      controlTransferParameters, totalLength);
  if (transferResult['status'] !== 'ok' || !transferResult['data'])
    return;
  const descriptors = /** @type {!DataView} */ (transferResult['data']);
  if (descriptors.byteLength !== totalLength)
    return;

  // Parse descriptors one-by-one from the fetched concatenated blob. Maintain
  // pointers to the LibusbJs objects that correspond to the currently parsed
  // interface/endpoint descriptor.
  /** @type {!LibusbJsInterfaceDescriptor|undefined} */
  let currentInterface = undefined;
  /** @type {!LibusbJsEndpointDescriptor|undefined} */
  let currentEndpoint = undefined;
  for (let offset = 0; offset < totalLength;) {
    const descriptorLength = descriptors.getUint8(offset);
    if (descriptorLength < 2) {
      // This is a malformed descriptor - bail out immediately (so that, for
      // example, we don't hang because of a zero in the descriptor length
      // field).
      return;
    }
    if (offset + descriptorLength > totalLength) {
      // Ignore the truncated descriptor.
      break;
    }
    const descriptorType = descriptors.getUint8(offset + 1);
    switch (descriptorType) {
      case CONFIGURATION_DESCRIPTOR_TYPE: {
        if (descriptorLength < CONFIGURATION_DESCRIPTOR_LENGTH) {
          // Ignore the invalid (too short) descriptor.
          break;
        }
        // The current item is the configuration descriptor. There should be
        // only one such item returned and it should refer to
        // `libusbJsConfiguration`, so nothing needs to be done here.
        break;
      }
      case INTERFACE_DESCRIPTOR_TYPE: {
        if (descriptorLength < INTERFACE_DESCRIPTOR_LENGTH) {
          // Ignore the invalid (too short) descriptor.
          break;
        }
        // The current item is the interface descriptor. Parse the interface
        // number and switch the pointer to the corresponding
        // `LibusbJsInterfaceDescriptor`.
        const interfaceNumber = descriptors.getUint8(offset + 2);
        currentInterface = libusbJsConfiguration['interfaces'].find(
            libusbJsInterface =>
                libusbJsInterface['interfaceNumber'] === interfaceNumber);
        currentEndpoint = undefined;
        break;
      }
      case ENDPOINT_DESCRIPTOR_TYPE: {
        if (descriptorLength < ENDPOINT_DESCRIPTOR_LENGTH) {
          // Ignore the invalid (too short) descriptor.
          break;
        }
        // The current item is the endpoint descriptor. Parse the endpoint
        // address and switch the pointer to the corresponding
        // `LibusbJsEndpointDescriptor`.
        const endpointAddress = descriptors.getUint8(offset + 2);
        if (currentInterface) {
          currentEndpoint = currentInterface['endpoints'].find(
              libusbJsEndpoint =>
                  libusbJsEndpoint['endpointAddress'] === endpointAddress);
        } else {
          currentEndpoint = undefined;
        }
        break;
      }
      default: {
        // The current item is an unknown descriptor, so add it as extraData to
        // the current (the most nested one) LibusbJs object.
        const targetObject =
            currentEndpoint || currentInterface || libusbJsConfiguration;
        if (targetObject) {
          appendExtraData(
              new Uint8Array(
                  descriptors.buffer, descriptors.byteOffset + offset,
                  descriptorLength),
              targetObject);
        }
        break;
      }
    }
    // Jump to the next descriptor in the blob.
    offset += descriptorLength;
  }
}

/**
 * Appends the specified data to the 'extraData' property of the given LibusbJs
 * object.
 * @param {!Uint8Array} extraDataToAppend
 * @param {!LibusbJsConfigurationDescriptor|!LibusbJsInterfaceDescriptor|!LibusbJsEndpointDescriptor}
 *     libusbJsObject
 */
function appendExtraData(extraDataToAppend, libusbJsObject) {
  const oldExtraData =
      goog.object.get(libusbJsObject, 'extraData', new ArrayBuffer(0));
  const newData =
      new Uint8Array(oldExtraData.byteLength + extraDataToAppend.byteLength);
  newData.set(new Uint8Array(oldExtraData), 0);
  newData.set(extraDataToAppend, oldExtraData.byteLength);
  libusbJsObject['extraData'] = newData.buffer;
}

/**
 * Returns whether the API needed for this adaptor to work is available.
 * @static
 * @return {boolean}
 */
GSC.LibusbToWebusbAdaptor.isApiAvailable = function() {
  return navigator !== undefined && navigator['usb'] !== undefined;
};
});  // goog.scope
<|endoftext|>