<commit_before>// # Choices component

/*
Render customized (non-native) dropdown choices.
*/

'use strict';

var React = require('react');
var R = React.DOM;
var _ = require('../../undash');
var ScrollLock = require('react-scroll-lock');

var magicChoiceRe = /^\/\/\/[^\/]+\/\/\/$/;

module.exports = React.createClass({

  displayName: 'Choices',

  mixins: [
    require('../../mixins/helper'),
    //plugin.require('mixin.resize'),
    //plugin.require('mixin.scroll'),
    require('../../mixins/click-outside'),
    ScrollLock
  ],

  getInitialState: function () {
    return {
      maxHeight: null,
      open: this.props.open,
      searchString: ''
    };
  },

  getIgnoreCloseNodes: function () {
    if (!this.props.ignoreCloseNodes) {
      return [];
    }
    var nodes = this.props.ignoreCloseNodes();
    if (!_.isArray(nodes)) {
      nodes = [nodes];
    }
    return nodes;
  },

  componentDidMount: function () {
    this.setOnClickOutside('container', function (event) {

      // Make sure we don't find any nodes to ignore.
      if (!_.find(this.getIgnoreCloseNodes(), function (node) {
        return this.isNodeInside(event.target, node);
      }.bind(this))) {
        this.onClose();
      }
    }.bind(this));

    this.adjustSize();
    this.updateListeningToWindow();
  },

  componentWillUnmount: function () {
    this.stopListeningToWindow();
  },

  onSelect: function (choice, event) {
    this.setState({
      openSection: null,
      searchString: ''
    });
    this.props.onSelect(choice.value, event);
  },

  onChoiceAction: function (choice) {
    this.setState({
      openSection: null,
      searchString: ''
    });
    this.props.onChoiceAction(choice);
  },

  onClose: function () {
    this.setState({
      openSection: null,
      searchString: ''
    });
    this.props.onClose();
  },

  // Doing something a little crazy... measuring on every frame. Makes this smoother than using an interval.
  // Can't using scrolling events, because we might be scrolling inside a container instead of the body.
  // Shouldn't be any more costly than animations though, I think. And only one of these is open at a time.
  updateListeningToWindow: function () {
    if (this.refs.choices) {
      if (!this.isListening) {
        const listenToFrame = () => {
          requestAnimationFrame(() => {
            this.adjustSize();
            if (this.isListening) {
              listenToFrame();
            }
          });
        };
        this.isListening = true;
        listenToFrame();
      }
    } else {
      if (this.isListening) {
        this.stopListeningToWindow();
      }
    }
  },

  stopListeningToWindow: function () {
    if (this.isListening) {
      this.isListening = false;
    }
  },

  adjustSize: function () {
    if (this.refs.choices) {
      var node = this.refs.container;
      var rect = node.getBoundingClientRect();
      var top = rect.top;
      var windowHeight = window.innerHeight;
      var height = windowHeight - top;
      if (height !== this.state.maxHeight) {
        this.setState({
          maxHeight: height
        });
      }
    }
  },

  componentWillReceiveProps: function (nextProps) {
    var nextState = {
      open: nextProps.open
    };

    this.setState(nextState, function () {
      this.adjustSize();
      this.updateListeningToWindow();
    }.bind(this));
  },

  onHeaderClick: function (choice) {
    if (this.state.openSection === choice.sectionKey) {
      this.setState({openSection: null});
    } else {
      this.setState({openSection: choice.sectionKey}, this.adjustSize);
    }
  },

  hasOneSection: function () {
    var sectionHeaders = this.props.choices.filter(function (c) { return c.sectionKey; });
    return sectionHeaders.length === 1;
  },

  visibleChoices: function () {
    var choices = this.props.choices;
    var config = this.props.config;

    if (choices && choices.length === 0) {
      return [{value: '///empty///'}];
    }

    if (this.state.searchString) {
      choices = choices.filter((choice) => {
        if (choice.sectionKey) {
          return true;
        }
        return config.isSearchStringInChoice(this.state.searchString, choice);
      });
    }

    if (!this.props.isAccordion) {
      return choices;
    }

    var openSection = this.state.openSection;
    var alwaysExanded = this.hasOneSection() || this.state.searchString;
    var visibleChoices = [];
    var inSection;

    choices.forEach(function (choice) {
      if (choice.value && choice.value.match(magicChoiceRe)) {
        visibleChoices.push(choice);
      }
      if (choice.sectionKey) {
        inSection = choice.sectionKey === openSection;
      }
      if (alwaysExanded || choice.sectionKey || inSection) {
        visibleChoices.push(choice);
      }
    });

    return visibleChoices;
  },

  render: function () {
    return this.renderWithConfig();
  },

  onClick: function (event) {
    // swallow clicks
    event.stopPropagation();
  },

  onChangeSearch: function (event) {
    this.setState({
      searchString: event.target.value
    });
  },

  renderDefault: function () {
    var config = this.props.config;

    var choices = this.visibleChoices();

    var search = null;

    var hasSearch = !!this.state.searchString;

    if (!hasSearch) {
      if (this.props.choices.length > 2) {
        if (_.find(choices, choice => !choice.action && choice.value !== '///loading///')) {
          hasSearch = true;
        }
      }
    }

    if (hasSearch) {
      search = config.createElement('choices-search', {key: 'choices-search', field: this.props.field, onChange: this.onChangeSearch});
    }

    if (this.props.open) {
      return R.div({
        ref: 'container', onClick: this.onClick, className: 'choices-container', style: {
          userSelect: 'none', WebkitUserSelect: 'none', position: 'absolute',
          maxHeight: this.state.maxHeight ? this.state.maxHeight : null
        }
      },
        config.cssTransitionWrapper(

          search,

          R.ul({key: 'choices', ref: 'choices', className: 'choices'},

            choices.map(function (choice, i) {

              var choiceElement = null;

              if (choice.value === '///loading///') {
                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onClose},
                  R.span({className: 'choice-label'},
                    config.createElement('loading-choice', {field: this.props.field})
                  )
                );
              } else if (choice.value === '///empty///') {
                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onClose},
                  R.span({className: 'choice-label'},
                    'No choices available.'
                  )
                );
              } else if (choice.action) {
                var labelClasses = 'choice-label ' + choice.action;

                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onChoiceAction.bind(this, choice)},
                  R.span({className: labelClasses},
                    choice.label || this.props.config.actionChoiceLabel(choice.action)
                  ),
                  this.props.config.createElement('choice-action-sample', {action: choice.action, choice: choice})
                );
              } else if (choice.sectionKey) {
                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onHeaderClick.bind(this, choice)},
                  config.createElement('choice-section-header', {choice: choice})
                );
              }
              else {
                choiceElement = config.createElement('choice', {
                  onSelect: this.onSelect, choice: choice, field: this.props.field
                });
              }

              return R.li({key: i, className: 'choice'},
                choiceElement
              );
            }.bind(this))
          )
        )
      );
    }

    // not open
    return null;
  }
});
<commit_msg>make sure animation frame doesn't back up update queue<commit_after>// # Choices component

/*
Render customized (non-native) dropdown choices.
*/

'use strict';

var React = require('react');
var R = React.DOM;
var _ = require('../../undash');
var ScrollLock = require('react-scroll-lock');

var magicChoiceRe = /^\/\/\/[^\/]+\/\/\/$/;

module.exports = React.createClass({

  displayName: 'Choices',

  mixins: [
    require('../../mixins/helper'),
    //plugin.require('mixin.resize'),
    //plugin.require('mixin.scroll'),
    require('../../mixins/click-outside'),
    ScrollLock
  ],

  getInitialState: function () {
    return {
      maxHeight: null,
      open: this.props.open,
      searchString: ''
    };
  },

  getIgnoreCloseNodes: function () {
    if (!this.props.ignoreCloseNodes) {
      return [];
    }
    var nodes = this.props.ignoreCloseNodes();
    if (!_.isArray(nodes)) {
      nodes = [nodes];
    }
    return nodes;
  },

  componentDidMount: function () {
    this.setOnClickOutside('container', function (event) {

      // Make sure we don't find any nodes to ignore.
      if (!_.find(this.getIgnoreCloseNodes(), function (node) {
        return this.isNodeInside(event.target, node);
      }.bind(this))) {
        this.onClose();
      }
    }.bind(this));

    this.adjustSize();
    this.updateListeningToWindow();
  },

  componentWillUnmount: function () {
    this.stopListeningToWindow();
  },

  onSelect: function (choice, event) {
    this.setState({
      openSection: null,
      searchString: ''
    });
    this.props.onSelect(choice.value, event);
  },

  onChoiceAction: function (choice) {
    this.setState({
      openSection: null,
      searchString: ''
    });
    this.props.onChoiceAction(choice);
  },

  onClose: function () {
    this.setState({
      openSection: null,
      searchString: ''
    });
    this.props.onClose();
  },

  // Doing something a little crazy... measuring on every frame. Makes this smoother than using an interval.
  // Can't using scrolling events, because we might be scrolling inside a container instead of the body.
  // Shouldn't be any more costly than animations though, I think. And only one of these is open at a time.
  updateListeningToWindow: function () {
    if (this.refs.choices) {
      if (!this.isListening) {
        const listenToFrame = () => {
          requestAnimationFrame(() => {
            if (this.isListening) {
              // Make sure we don't adjust again before rendering.
              this.adjustSize(() => {
                if (this.isListening) {
                  listenToFrame();
                }
              });
            }
          });
        };
        this.isListening = true;
        listenToFrame();
      }
    } else {
      if (this.isListening) {
        this.stopListeningToWindow();
      }
    }
  },

  stopListeningToWindow: function () {
    if (this.isListening) {
      this.isListening = false;
    }
  },

  adjustSize: function (cb) {
    let didSetState = false;
    if (this.refs.choices) {
      var node = this.refs.container;
      var rect = node.getBoundingClientRect();
      var top = rect.top;
      var windowHeight = window.innerHeight;
      var height = windowHeight - top;
      if (height !== this.state.maxHeight) {
        didSetState = true;
        this.setState({
          maxHeight: height
        }, cb);
      }
    }
    if (!didSetState) {
      cb();
    }
  },

  componentWillReceiveProps: function (nextProps) {
    var nextState = {
      open: nextProps.open
    };

    this.setState(nextState, function () {
      this.adjustSize();
      this.updateListeningToWindow();
    }.bind(this));
  },

  onHeaderClick: function (choice) {
    if (this.state.openSection === choice.sectionKey) {
      this.setState({openSection: null});
    } else {
      this.setState({openSection: choice.sectionKey}, this.adjustSize);
    }
  },

  hasOneSection: function () {
    var sectionHeaders = this.props.choices.filter(function (c) { return c.sectionKey; });
    return sectionHeaders.length === 1;
  },

  visibleChoices: function () {
    var choices = this.props.choices;
    var config = this.props.config;

    if (choices && choices.length === 0) {
      return [{value: '///empty///'}];
    }

    if (this.state.searchString) {
      choices = choices.filter((choice) => {
        if (choice.sectionKey) {
          return true;
        }
        return config.isSearchStringInChoice(this.state.searchString, choice);
      });
    }

    if (!this.props.isAccordion) {
      return choices;
    }

    var openSection = this.state.openSection;
    var alwaysExanded = this.hasOneSection() || this.state.searchString;
    var visibleChoices = [];
    var inSection;

    choices.forEach(function (choice) {
      if (choice.value && choice.value.match(magicChoiceRe)) {
        visibleChoices.push(choice);
      }
      if (choice.sectionKey) {
        inSection = choice.sectionKey === openSection;
      }
      if (alwaysExanded || choice.sectionKey || inSection) {
        visibleChoices.push(choice);
      }
    });

    return visibleChoices;
  },

  render: function () {
    return this.renderWithConfig();
  },

  onClick: function (event) {
    // swallow clicks
    event.stopPropagation();
  },

  onChangeSearch: function (event) {
    this.setState({
      searchString: event.target.value
    });
  },

  renderDefault: function () {
    var config = this.props.config;

    var choices = this.visibleChoices();

    var search = null;

    var hasSearch = !!this.state.searchString;

    if (!hasSearch) {
      if (this.props.choices.length > 2) {
        if (_.find(choices, choice => !choice.action && choice.value !== '///loading///')) {
          hasSearch = true;
        }
      }
    }

    if (hasSearch) {
      search = config.createElement('choices-search', {key: 'choices-search', field: this.props.field, onChange: this.onChangeSearch});
    }

    if (this.props.open) {
      return R.div({
        ref: 'container', onClick: this.onClick, className: 'choices-container', style: {
          userSelect: 'none', WebkitUserSelect: 'none', position: 'absolute',
          maxHeight: this.state.maxHeight ? this.state.maxHeight : null
        }
      },
        config.cssTransitionWrapper(

          search,

          R.ul({key: 'choices', ref: 'choices', className: 'choices'},

            choices.map(function (choice, i) {

              var choiceElement = null;

              if (choice.value === '///loading///') {
                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onClose},
                  R.span({className: 'choice-label'},
                    config.createElement('loading-choice', {field: this.props.field})
                  )
                );
              } else if (choice.value === '///empty///') {
                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onClose},
                  R.span({className: 'choice-label'},
                    'No choices available.'
                  )
                );
              } else if (choice.action) {
                var labelClasses = 'choice-label ' + choice.action;

                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onChoiceAction.bind(this, choice)},
                  R.span({className: labelClasses},
                    choice.label || this.props.config.actionChoiceLabel(choice.action)
                  ),
                  this.props.config.createElement('choice-action-sample', {action: choice.action, choice: choice})
                );
              } else if (choice.sectionKey) {
                choiceElement = R.a({href: 'JavaScript' + ':', onClick: this.onHeaderClick.bind(this, choice)},
                  config.createElement('choice-section-header', {choice: choice})
                );
              }
              else {
                choiceElement = config.createElement('choice', {
                  onSelect: this.onSelect, choice: choice, field: this.props.field
                });
              }

              return R.li({key: i, className: 'choice'},
                choiceElement
              );
            }.bind(this))
          )
        )
      );
    }

    // not open
    return null;
  }
});

{'fraction_non_alphanumeric': 0.1423406862745098, 'fraction_numerical': 0.0003676470588235294, 'mean_word_length': 2.0449626865671644, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 14, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4990698', 'n_tokens_mistral': 4816, 'n_tokens_neox': 4616, 'n_words': 980}