<commit_before>var React = require('react');
var createReactClass = require('create-react-class');
var update = require('react-addons-update');
var types = require('prop-types');
var OptionList = require('./components/option-list');
var ReactCompactMultiselect = require('react-compact-multiselect');
var TagList = require('react-tag-list');
var SkyLight = require('react-skylight').default;
var IsMobileMixin = require('react-ismobile-mixin');
var lunr = require('lunr');
var _ = require('lodash');
var ReactDebounceInput = require('react-debounce-input');
var DebounceInput = ReactDebounceInput.DebounceInput;
require('./react-xzibit-select.scss');

var XzibitSelect = createReactClass({
  getInitialState: function () {
    return {
      mobileTooltipContent: null,
      mobileTooltipTitle: null,
      searchIndex: this.generateSearchIndex(
        this.props.searchFields,
        this.props.refField,
        this.props.options,
        this.props.values,
        this.props.dimensionFilters
      )
    };
  },

  propTypes: {
    addAll: types.bool,
    addAllLimit: types.number,
    filterChangeThrotleMs: types.number,
    filterDimensionOptions: types.array,
    dimensionFilters: types.object,
    searchFilterValue: types.string,
    searchFilterDebounceTime: types.number,
    onDimensionSelectionChange: types.func,
    onDimensionFilterValueChange: types.func,
    onClearDimensionFilterValue: types.func,
    openTipOptions: types.object,
    options: types.array,
    optionsByValue: types.any,
    refField: types.string,
    searchFields: types.array,
    values: types.array
  },

  mixins: [IsMobileMixin],

  getDefaultProps: function () {
    return {
      addAll: true,
      filterChangeThrotleMs: 200,
      dimensionFilters: {},
      searchFilterValue: '',
      searchFilterDebounceTime: 200,
      placeholderText: 'Type here to filter options',
      openTipOptions: {
        offset: [3, 10],
        borderRadius: 2,
        borderColor: '#333333',
        background: '#333333',
        className: 'rxs-tooltip',
        delay: 0,
        hideDelay: 0,
        showEffectDuration: 0,
        hideEffectDuration: 0,
        tipJoint: 'top left',
        stem: false
      },
      refField: 'value',
      searchFields: ['label']
    };
  },

  componentWillReceiveProps: function (nextProps) {
    if (nextProps.searchFilterValue !== this.props.searchFilterValue) {
      this.setLunrResults(nextProps.searchFilterValue);
    }
  },

  generateSearchIndex: function (
    searchFields,
    refField,
    data,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    var componentThis = this;
    var search = lunr(function () {
      var lunrThis = this;
      searchFields.forEach(function (field) {
        var name = field.name || field;
        var weight = field.weight || 1;
        lunrThis.field(name, weight);
      });

      lunrThis.ref(refField);

      componentThis.fillSearch(
        lunrThis,
        data,
        currentlySelectedValues,
        currentDimensionFilters
      );
    });

    return search;
  },

  getAvailableOptions: function (
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    return options.filter(function (opt) {
      var isSelected = currentlySelectedValues.indexOf(opt.value) !== -1;
      var isInDimension = this.dimensionFilterIncludes(
        opt,
        currentDimensionFilters
      );
      return !isSelected && isInDimension;
    }, this);
  },

  fillSearch: function (
    search,
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    // Note that we fill the search index with all of the options, later on if there are less available options, then
    // we must remove those options from search results after the search is done.
    // This is a performance improvement, originally we would recreate the search index on every component update(eg. adding/removing options), which takes a long time for a large number of options
    options.forEach(function (opt) {
      search.add(opt);
    });
  },

  subStringSearch: function (
    searchInput,
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    return this.getAvailableOptions(
      options,
      currentlySelectedValues,
      currentDimensionFilters
    )
      .filter(
        function (opt) {
          var foundValue = false;
          for (var i = 0; i < this.props.searchFields.length; i++) {
            var fieldValue = opt[this.props.searchFields[i]].toLowerCase();
            // If the searchInput exists in the fieldValue
            // break and keep this option in the list
            if (fieldValue.indexOf(searchInput) > -1) {
              foundValue = true;
              break;
            }
          }
          return foundValue;
        }.bind(this)
      )
      .map(function (opt) {
        return opt[this.props.refField];
      }, this);
  },

  removeValue: function (valToRemove) {
    var newValueState = this.props.values.filter(function (val) {
      return val !== valToRemove;
    });
    this.props.onChange(newValueState);
  },

  removeAll: function () {
    this.props.onChange([]);
  },

  addValue: function (valToAdd) {
    var newValueState = this.props.values.slice(0);
    newValueState.push(valToAdd);
    this.props.onChange(newValueState);
  },

  addAllFunc: function () {
    var filteredOptionValues = this.filteredOptions(
      this.props.options,
      this.props.values,
      this.props.dimensionFilters
    ).map(function (opt) {
      return opt.value;
    });
    var newValueState = filteredOptionValues.concat(this.props.values);
    this.props.onChange(newValueState);
  },

  mergeResults: function (array1, array2) {
    // Remove any values from array1 that exist in array2
    var array2MinusArray1 = array2.filter(function (a2Element) {
      return array1.indexOf(a2Element) === -1;
    });
    // Then concat the arrays that now have no duplicates
    // This is more performant than doing the concat upfront
    return array1.concat(array2MinusArray1);
  },

  setLunrResults: function (searchFilterValue) {
    // search using the lunr search index
    var lunrResults = this.state.searchIndex
      .search(searchFilterValue.toLowerCase())
      .map(function (result) {
        return result.ref;
      })
      .map(function (result) {
        return String(result); // make sure the ref is a string for merging results comparison later
      });

    this.setState({ lunrResults: lunrResults });
  },

  filteredOptions: function (
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    var searchFilterValue = this.props.searchFilterValue.toLowerCase();
    if (!searchFilterValue) {
      return this.getAvailableOptions(
        options,
        currentlySelectedValues,
        currentDimensionFilters
      );
    }

    // lunr doesn't filter on a or i
    if (searchFilterValue === 'a' || searchFilterValue === 'i') {
      return this.getAvailableOptions(
        options,
        currentlySelectedValues,
        currentDimensionFilters
      );
    }

    var lunrResults = this.state.lunrResults || [];
    var substringResults = this.subStringSearch(
      searchFilterValue.toLowerCase(),
      options,
      currentlySelectedValues,
      currentDimensionFilters
    ).map(function (result) {
      return String(result); // make sure the ref is a string for merging results comparison later
    });
    var mergedResults = this.mergeResults(lunrResults, substringResults);
    var optionMap = {};
    this.props.options.forEach(function (opt) {
      var ref = opt[this.props.refField];
      optionMap[ref] = opt;
    }, this);
    var lunrResultsAsOptions = mergedResults.map(function (resultRef) {
      return optionMap[resultRef];
    });
    var onlyAvailableResultsOptions = this.getAvailableOptions(
      lunrResultsAsOptions,
      currentlySelectedValues,
      currentDimensionFilters
    );

    return onlyAvailableResultsOptions;
  },

  onMobileTooltip: function (title, content) {
    if (!this.isMobile()) {
      return;
    }

    this.setState(
      { mobileTooltipTitle: title, mobileTooltipContent: content },
      this.refs.tooltip.show
    );
  },

  dimensionFilterIncludes: function (opt, currentDimensionFilters) {
    if (Object.keys(currentDimensionFilters).length < 1) {
      return true;
    }

    var retVal = true;
    var filterHits = this.props.filterDimensionOptions.map(function (
      dimension
    ) {
      var key = dimension.key;
      var name = dimension.name;
      var filterVals = currentDimensionFilters[name]
        ? currentDimensionFilters[name].selectedValues
        : undefined;
      if (filterVals === undefined || filterVals.length < 1) {
        return true;
      }
      if (Array.isArray(opt[key])) {
        var found = false;
        opt[key].forEach(function (optVal) {
          if (filterVals.indexOf(optVal) > -1) {
            found = true;
          }
        });
        return found;
      } else {
        if (filterVals.indexOf(opt[key]) > -1) {
          return true;
        }
      }
      return false;
    },
    this);

    filterHits.forEach(function (fh) {
      if (!fh) {
        retVal = false;
      }
    });

    return retVal;
  },

  handleSearchFilterChange: function (event) {
    this.props.onSearchFilterChange(event.target.value);
  },

  clearSearchFilter: function () {
    this.props.onClearSearchFilter();
  },

  updateSearchIndex: function (props) {
    this.setState({
      searchIndex: this.generateSearchIndex(
        props.searchFields,
        props.refField,
        props.options,
        props.values,
        props.dimensionFilters
      )
    });
  },

  generateUpdateDimensionSelection: function (dimensionName) {
    // Re-generate the search index any time a dimension selection is made so that the index correctly
    // corresponds to the new list of options filtered by the dimension selection
    /**
     *  {'Source' : [], 'Sector' : []}
     */
    return function (values) {
      this.props.onDimensionSelectionChange(dimensionName, values);
    }.bind(this);
  },

  generateUpdateDimensionFilterValue: function (dimensionName) {
    return function (value) {
      this.props.onDimensionFilterValueChange(dimensionName, value);
    }.bind(this);
  },

  generateClearDimensionFilterValue: function (dimensionName) {
    return function (value) {
      this.props.onClearDimensionFilterValue(dimensionName);
    }.bind(this);
  },

  tagListValues: function () {
    var mapFunc = function () {};

    if (this.props.optionsByValue) {
      mapFunc = function (val) {
        return this.props.optionsByValue[val];
      };
    } else {
      mapFunc = function (val) {
        return this.props.options.filter(function (opt) {
          return opt.value === val;
        })[0];
      };
    }

    return this.props.values.map(mapFunc, this);
  },

  getSkylight: function () {
    if (!this.isMobile()) {
      return null;
    }

    return (
      <SkyLight
        ref="tooltip"
        title={this.state.mobileTooltipTitle}
        className="mobile-tooltip"
      >
        {this.state.mobileTooltipContent}
      </SkyLight>
    );
  },

  getSelectFilters: function () {
    return this.props.filterDimensionOptions.map(function (dim) {
      var groupByKey = '';
      if (dim.groupByKey) groupByKey = dim.groupByKey;

      const dimensionFilter = this.props.dimensionFilters[dim.name] || {};
      const selectedValuesForDimension = dimensionFilter.selectedValues || [];
      const filterValueForDimension = dimensionFilter.filterValue || '';

      return (
        <ReactCompactMultiselect
          key={dim.name}
          label={dim.name}
          options={dim.options}
          info={dim.info}
          selectedValues={selectedValuesForDimension}
          onSelectionChange={this.generateUpdateDimensionSelection(dim.name)}
          filterValue={filterValueForDimension}
          onFilterValueChange={this.generateUpdateDimensionFilterValue(
            dim.name
          )}
          onClearFilter={this.generateClearDimensionFilterValue(dim.name)}
          groupBy={groupByKey}
          layoutMode={ReactCompactMultiselect.ALIGN_CONTENT_NE}
        />
      );
    }, this);
  },

  render: function () {

    var filteredOptions = this.filteredOptions(
      this.props.options,
      this.props.values,
      this.props.dimensionFilters
    );
    var selectFilters = this.getSelectFilters();
    var addAll =
      this.props.addAll &&
      !(
        this.props.addAllLimit &&
        filteredOptions.length > this.props.addAllLimit
      );

    return (

                <DebounceInput
                  debounceTimeout={this.props.searchFilterDebounceTime}
                  onChange={this.handleSearchFilterChange}
                  value={this.props.searchFilterValue}
                  placeholder={this.props.placeholderText}
                />
                <button
                  className="rxs-label-filter-clear"
                  name="clear-filter"
                  onClick={this.clearSearchFilter}
                >
                  ×
                </button>

            <TagList
              values={this.tagListValues()}
              onRemove={this.removeValue}
              removeAll={this.removeAll}
              placeholderText=" "
              collapsedRows={1}
            />

          <OptionList
            options={filteredOptions}
            onClick={this.addValue}
            addAll={addAll}
            addAllFunc={this.addAllFunc}
            onMobileTooltip={this.onMobileTooltip}
            openTipOptions={this.props.openTipOptions}
          />

{selectFilters}

        {this.getSkylight()}

    );
  }
});

module.exports = XzibitSelect;
<commit_msg>Update react-xzibit-select.js<commit_after>var React = require('react');
var createReactClass = require('create-react-class');
var update = require('react-addons-update');
var types = require('prop-types');
var OptionList = require('./components/option-list');
var ReactCompactMultiselect = require('react-compact-multiselect');
var TagList = require('react-tag-list');
var SkyLight = require('react-skylight').default;
var IsMobileMixin = require('react-ismobile-mixin');
var lunr = require('lunr');
var _ = require('lodash');
var ReactDebounceInput = require('react-debounce-input');
var DebounceInput = ReactDebounceInput.DebounceInput;
require('./react-xzibit-select.scss');

var XzibitSelect = createReactClass({
  getInitialState: function () {
    return {
      mobileTooltipContent: null,
      mobileTooltipTitle: null,
      searchIndex: this.generateSearchIndex(
        this.props.searchFields,
        this.props.refField,
        this.props.options,
        this.props.values,
        this.props.dimensionFilters
      )
    };
  },

  propTypes: {
    addAll: types.bool,
    addAllLimit: types.number,
    filterChangeThrotleMs: types.number,
    filterDimensionOptions: types.array,
    dimensionFilters: types.object,
    searchFilterValue: types.string,
    searchFilterDebounceTime: types.number,
    onDimensionSelectionChange: types.func,
    onDimensionFilterValueChange: types.func,
    onClearDimensionFilterValue: types.func,
    openTipOptions: types.object,
    options: types.array,
    optionsByValue: types.any,
    refField: types.string,
    searchFields: types.array,
    values: types.array
  },

  mixins: [IsMobileMixin],

  getDefaultProps: function () {
    return {
      addAll: true,
      filterChangeThrotleMs: 200,
      dimensionFilters: {},
      searchFilterValue: '',
      searchFilterDebounceTime: 200,
      placeholderText: 'Type here to filter options',
      openTipOptions: {
        offset: [3, 10],
        borderRadius: 2,
        borderColor: '#333333',
        background: '#333333',
        className: 'rxs-tooltip',
        delay: 0,
        hideDelay: 0,
        showEffectDuration: 0,
        hideEffectDuration: 0,
        tipJoint: 'top left',
        stem: false
      },
      refField: 'value',
      searchFields: ['label']
    };
  },

  componentWillReceiveProps: function (nextProps) {
    if (nextProps.searchFilterValue !== this.props.searchFilterValue) {
      this.setLunrResults(nextProps.searchFilterValue);
    }
  },

  generateSearchIndex: function (
    searchFields,
    refField,
    data,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    var componentThis = this;
    var search = lunr(function () {
      var lunrThis = this;
      searchFields.forEach(function (field) {
        var name = field.name || field;
        var weight = field.weight || 1;
        lunrThis.field(name, weight);
      });

      lunrThis.ref(refField);

      componentThis.fillSearch(
        lunrThis,
        data,
        currentlySelectedValues,
        currentDimensionFilters
      );
    });

    return search;
  },

  getAvailableOptions: function (
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    return options.filter(function (opt) {
      var isSelected = currentlySelectedValues.indexOf(opt.value) !== -1;
      var isInDimension = this.dimensionFilterIncludes(
        opt,
        currentDimensionFilters
      );
      return !isSelected && isInDimension;
    }, this);
  },

  fillSearch: function (
    search,
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    // Note that we fill the search index with all of the options, later on if there are less available options, then
    // we must remove those options from search results after the search is done.
    // This is a performance improvement, originally we would recreate the search index on every component update(eg. adding/removing options), which takes a long time for a large number of options
    options.forEach(function (opt) {
      search.add(opt);
    });
  },

  subStringSearch: function (
    searchInput,
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    return this.getAvailableOptions(
      options,
      currentlySelectedValues,
      currentDimensionFilters
    )
      .filter(
        function (opt) {
          var foundValue = false;
          for (var i = 0; i < this.props.searchFields.length; i++) {
            var fieldValue = opt[this.props.searchFields[i]].toLowerCase();
            // If the searchInput exists in the fieldValue
            // break and keep this option in the list
            if (fieldValue.indexOf(searchInput) > -1) {
              foundValue = true;
              break;
            }
          }
          return foundValue;
        }.bind(this)
      )
      .map(function (opt) {
        return opt[this.props.refField];
      }, this);
  },

  removeValue: function (valToRemove) {
    var newValueState = this.props.values.filter(function (val) {
      return val !== valToRemove;
    });
    this.props.onChange(newValueState);
  },

  removeAll: function () {
    this.props.onChange([]);
  },

  addValue: function (valToAdd) {
    var newValueState = this.props.values.slice(0);
    newValueState.push(valToAdd);
    this.props.onChange(newValueState);
  },

  addAllFunc: function () {
    var filteredOptionValues = this.filteredOptions(
      this.props.options,
      this.props.values,
      this.props.dimensionFilters
    ).map(function (opt) {
      return opt.value;
    });
    var newValueState = filteredOptionValues.concat(this.props.values);
    this.props.onChange(newValueState);
  },

  mergeResults: function (array1, array2) {
    // Remove any values from array1 that exist in array2
    var array2MinusArray1 = array2.filter(function (a2Element) {
      return array1.indexOf(a2Element) === -1;
    });
    // Then concat the arrays that now have no duplicates
    // This is more performant than doing the concat upfront
    return array1.concat(array2MinusArray1);
  },

  setLunrResults: function (searchFilterValue) {
    // search using the lunr search index
    var lunrResults = this.state.searchIndex
      .search(searchFilterValue.toLowerCase())
      .map(function (result) {
        return result.ref;
      })
      .map(function (result) {
        return String(result); // make sure the ref is a string for merging results comparison later
      });

    this.setState({ lunrResults: lunrResults });
  },

  filteredOptions: function (
    options,
    currentlySelectedValues,
    currentDimensionFilters
  ) {
    var searchFilterValue = this.props.searchFilterValue.toLowerCase();
    if (!searchFilterValue) {
      return this.getAvailableOptions(
        options,
        currentlySelectedValues,
        currentDimensionFilters
      );
    }

    // lunr doesn't filter on a or i
    if (searchFilterValue === 'a' || searchFilterValue === 'i') {
      return this.getAvailableOptions(
        options,
        currentlySelectedValues,
        currentDimensionFilters
      );
    }

    var lunrResults = this.state.lunrResults || [];
    var substringResults = this.subStringSearch(
      searchFilterValue.toLowerCase(),
      options,
      currentlySelectedValues,
      currentDimensionFilters
    ).map(function (result) {
      return String(result); // make sure the ref is a string for merging results comparison later
    });
    var mergedResults = this.mergeResults(lunrResults, substringResults);
    var optionMap = {};
    this.props.options.forEach(function (opt) {
      var ref = opt[this.props.refField];
      optionMap[ref] = opt;
    }, this);
    var lunrResultsAsOptions = mergedResults.map(function (resultRef) {
      return optionMap[resultRef];
    });
    var onlyAvailableResultsOptions = this.getAvailableOptions(
      lunrResultsAsOptions,
      currentlySelectedValues,
      currentDimensionFilters
    );

    return onlyAvailableResultsOptions;
  },

  onMobileTooltip: function (title, content) {
    if (!this.isMobile()) {
      return;
    }

    this.setState(
      { mobileTooltipTitle: title, mobileTooltipContent: content },
      this.refs.tooltip.show
    );
  },

  dimensionFilterIncludes: function (opt, currentDimensionFilters) {
    if (Object.keys(currentDimensionFilters).length < 1) {
      return true;
    }

    var retVal = true;
    var filterHits = this.props.filterDimensionOptions.map(function (
      dimension
    ) {
      var key = dimension.key;
      var name = dimension.name;
      var filterVals = currentDimensionFilters[name]
        ? currentDimensionFilters[name].selectedValues
        : undefined;
      if (filterVals === undefined || filterVals.length < 1) {
        return true;
      }
      if (Array.isArray(opt[key])) {
        var found = false;
        opt[key].forEach(function (optVal) {
          if (filterVals.indexOf(optVal) > -1) {
            found = true;
          }
        });
        return found;
      } else {
        if (filterVals.indexOf(opt[key]) > -1) {
          return true;
        }
      }
      return false;
    },
    this);

    filterHits.forEach(function (fh) {
      if (!fh) {
        retVal = false;
      }
    });

    return retVal;
  },

  handleSearchFilterChange: function (event) {
    this.props.onSearchFilterChange(event.target.value);
  },

  clearSearchFilter: function () {
    this.props.onClearSearchFilter();
  },

  updateSearchIndex: function (props) {
    this.setState({
      searchIndex: this.generateSearchIndex(
        props.searchFields,
        props.refField,
        props.options,
        props.values,
        props.dimensionFilters
      )
    });
  },

  generateUpdateDimensionSelection: function (dimensionName) {
    // Re-generate the search index any time a dimension selection is made so that the index correctly
    // corresponds to the new list of options filtered by the dimension selection
    /**
     *  {'Source' : [], 'Sector' : []}
     */
    return function (values) {
      this.props.onDimensionSelectionChange(dimensionName, values);
    }.bind(this);
  },

  generateUpdateDimensionFilterValue: function (dimensionName) {
    return function (value) {
      this.props.onDimensionFilterValueChange(dimensionName, value);
    }.bind(this);
  },

  generateClearDimensionFilterValue: function (dimensionName) {
    return function (value) {
      this.props.onClearDimensionFilterValue(dimensionName);
    }.bind(this);
  },

  tagListValues: function () {
    var mapFunc = function () {};

    if (this.props.optionsByValue) {
      mapFunc = function (val) {
        return this.props.optionsByValue[val];
      };
    } else {
      mapFunc = function (val) {
        return this.props.options.filter(function (opt) {
          return opt.value === val;
        })[0];
      };
    }

    return this.props.values.map(mapFunc, this);
  },

  getSkylight: function () {
    if (!this.isMobile()) {
      return null;
    }

    return (
      <SkyLight
        ref="tooltip"
        title={this.state.mobileTooltipTitle}
        className="mobile-tooltip"
      >
        {this.state.mobileTooltipContent}
      </SkyLight>
    );
  },

  getSelectFilters: function () {
    return this.props.filterDimensionOptions.map(function (dim) {
      var groupByKey = '';
      if (dim.groupByKey) groupByKey = dim.groupByKey;

      const dimensionFilter = this.props.dimensionFilters[dim.name] || {};
      const selectedValuesForDimension = dimensionFilter.selectedValues || [];
      const filterValueForDimension = dimensionFilter.filterValue || '';

      return (
        <ReactCompactMultiselect
          key={dim.name}
          label={dim.name}
          options={dim.options}
          info={dim.info}
          selectedValues={selectedValuesForDimension}
          onSelectionChange={this.generateUpdateDimensionSelection(dim.name)}
          filterValue={filterValueForDimension}
          onFilterValueChange={this.generateUpdateDimensionFilterValue(
            dim.name
          )}
          onClearFilter={this.generateClearDimensionFilterValue(dim.name)}
          groupBy={groupByKey}
          layoutMode={ReactCompactMultiselect.ALIGN_CONTENT_NE}
        />
      );
    }, this);
  },

  render: function () {
    console.log("Render Select");
    console.log(this.props);

    var filteredOptions = this.filteredOptions(
      this.props.options,
      this.props.values,
      this.props.dimensionFilters
    );
    var selectFilters = this.getSelectFilters();
    var addAll =
      this.props.addAll &&
      !(
        this.props.addAllLimit &&
        filteredOptions.length > this.props.addAllLimit
      );

    return (

                <DebounceInput
                  debounceTimeout={this.props.searchFilterDebounceTime}
                  onChange={this.handleSearchFilterChange}
                  value={this.props.searchFilterValue}
                  placeholder={this.props.placeholderText}
                />
                <button
                  className="rxs-label-filter-clear"
                  name="clear-filter"
                  onClick={this.clearSearchFilter}
                >
                  ×
                </button>

            <TagList
              values={this.tagListValues()}
              onRemove={this.removeValue}
              removeAll={this.removeAll}
              placeholderText=" "
              collapsedRows={1}
            />

          <OptionList
            options={filteredOptions}
            onClick={this.addValue}
            addAll={addAll}
            addAllFunc={this.addAllFunc}
            onMobileTooltip={this.onMobileTooltip}
            openTipOptions={this.props.openTipOptions}
          />

{selectFilters}

        {this.getSkylight()}

    );
  }
});

module.exports = XzibitSelect;
<|endoftext|>


<commit_before>var React = require('react');
var update = require('react-addons-update');
var types = React.PropTypes;
var OptionList = require('./components/option-list');
var ReactCompactMultiselect = require('react-compact-multiselect');
var TagList = require('react-tag-list');
var SkyLight = require('react-skylight');
var IsMobileMixin = require('react-ismobile-mixin');

require('./react-xzibit-select.scss');

var XzibitSelect = React.createClass({
	getInitialState: function() {
		return {
			labelFilter: '',
			dimensionFilter: {},
			mobileTooltipContent: null,
			mobileTooltipTitle: null
		};
	},
	propTypes: {
		options: types.array,
		optionsByValue: types.any,
		values: types.array,
		onChange: types.func,
		filterDimensions: types.array,
		addAll: types.bool,
		addAllLimit: types.number
	},
	mixins: [IsMobileMixin],
	getDefaultProps: function() {
		return {
			addAll: true,
			placeholderText: 'Type here to filter options',
      openTipOptions: {
        offset: [3, 10],
        borderRadius: 2,
        borderColor: '#333333',
        background: '#333333',
        className: 'rxs-tooltip',
        delay: 0,
        hideDelay: 0,
        showEffectDuration: 0,
        hideEffectDuration: 0,
        tipJoint: 'top left',
        stem: false
      }
		};
	},
	removeValue: function(valToRemove) {
		var newValueState = this.props.values.filter(function(val){
			return val !== valToRemove;
		});
		this.props.onChange(newValueState);
	},
	removeAll: function() {
		this.props.onChange([]);
	},
	addValue: function(valToAdd){
		var newValueState = this.props.values.slice(0);
		newValueState.push(valToAdd);
		this.props.onChange(newValueState);
	},
	addAllFunc: function() {
		var filteredOptionValues = this.filteredOptions().map(function(opt){ return opt.value;});
		var newValueState = filteredOptionValues.concat(this.props.values);
		this.props.onChange(newValueState);
	},
	filteredOptions: function() {
		return this.props.options.filter(function(opt){
			if(this.props.values.indexOf(opt.value) !== -1 || !this.dimensionFilterIncludes(opt)) return false;
			return (opt.label.toLowerCase().indexOf(this.state.labelFilter.toLowerCase()) > -1);

		}, this);
	},
	onMobileTooltip: function(title, content) {
		if(!this.isMobile()) {
			return;
		}

		this.setState(
			{mobileTooltipTitle: title, mobileTooltipContent: content},
			this.refs.tooltip.show);
	},
	dimensionFilterIncludes: function(opt) {

		if (Object.keys(this.state.dimensionFilter).length < 1){
			return true;
		}

		var retVal = true;

		var filterHits = this.props.filterDimensions.map(function(dimension){
			var key = dimension.key;
			var name = dimension.name;
			var filterVals = this.state.dimensionFilter[name];
			if (filterVals === undefined || filterVals.length < 1) {
				return true;
			}
			if (Array.isArray(opt[key])){
				var found = false;
				opt[key].forEach(function(optVal){
					if (filterVals.indexOf(optVal) > -1) {
						found = true;
					}
				});
				return found;
			} else {
				if (filterVals.indexOf(opt[key]) > -1) {
					return true;
				}
			}
			return false;
		}, this);

		filterHits.forEach(function(fh){
			if (!fh){
				retVal = false;
			}
		});

		return retVal;
	},
	updateLabelFilter: function(event) {
		// TODO: add throttling
		this.setState({labelFilter: event.target.value});
	},
	clearLabelFilter: function() {
	  this.setState({labelFilter: ''});
	},
	generateUpdateDimensionFilter: function(dimensionName) {
		/**
		 *  {'Source' : [], 'Sector' : []}
		 */
		return function(values) {
			var spec = {};
			spec[dimensionName] = {$set: values};
			var newState = update(this.state.dimensionFilter, spec);
			this.setState({dimensionFilter: newState});
		}.bind(this);
	},
	tagListValues: function() {
		var mapFunc = function(){};

		if (this.props.optionsByValue) {
			mapFunc = function(val){
				return this.props.optionsByValue[val];
			};
		} else {
			mapFunc = function(val){
			  return this.props.options.filter(function(opt){
					return opt.value === val;
				})[0];
			};
		}

		return this.props.values.map(mapFunc, this);
	},
	render: function() {
		var filteredOptions = this.filteredOptions();
		var selectFilters = this.props.filterDimensions.map(function(dim){
			var groupByKey = '';
			if(dim.groupByKey)
				groupByKey = dim.groupByKey;

			console.log(dim.info)

			return (<ReactCompactMultiselect
						key={dim.name}
						label={dim.name}
						options={dim.options}
						info={dim.info}
						initialValue={[]}
						groupBy={groupByKey}
						onChange={this.generateUpdateDimensionFilter(dim.name)}
						layoutMode={ReactCompactMultiselect.ALIGN_CONTENT_NE} />);
		}, this);

		var addAll = this.props.addAll;
		if(this.props.addAllLimit && filteredOptions.length > this.props.addAllLimit) {
			addAll=false;
		}

		var skylight = null;
    if(this.isMobile()) {
      skylight = (
        <SkyLight
          ref='tooltip'
          title={this.state.mobileTooltipTitle}
          className='mobile-tooltip'>
          {this.state.mobileTooltipContent}
        </SkyLight>
      );
    }

		return (

							<input
								onChange={this.updateLabelFilter}
								value={this.state.labelFilter}
								placeholder={this.props.placeholderText} />
							<button className='rxs-label-filter-clear' name='clear-filter' onClick={this.clearLabelFilter}>×</button>

						<TagList
							values={this.tagListValues()}
							onRemove={this.removeValue}
							removeAll={this.removeAll}
							placeholderText=' '
							collapsedRows={1} />

					<OptionList
						options={filteredOptions}
						onClick={this.addValue}
						addAll={addAll}
						addAllFunc={this.addAllFunc}
						onMobileTooltip={this.onMobileTooltip}/>

						{selectFilters}

				{skylight}

		);
	}
});

module.exports = XzibitSelect;
<commit_msg>oops console.log<commit_after>var React = require('react');
var update = require('react-addons-update');
var types = React.PropTypes;
var OptionList = require('./components/option-list');
var ReactCompactMultiselect = require('react-compact-multiselect');
var TagList = require('react-tag-list');
var SkyLight = require('react-skylight');
var IsMobileMixin = require('react-ismobile-mixin');

require('./react-xzibit-select.scss');

var XzibitSelect = React.createClass({
	getInitialState: function() {
		return {
			labelFilter: '',
			dimensionFilter: {},
			mobileTooltipContent: null,
			mobileTooltipTitle: null
		};
	},
	propTypes: {
		options: types.array,
		optionsByValue: types.any,
		values: types.array,
		onChange: types.func,
		filterDimensions: types.array,
		addAll: types.bool,
		addAllLimit: types.number
	},
	mixins: [IsMobileMixin],
	getDefaultProps: function() {
		return {
			addAll: true,
			placeholderText: 'Type here to filter options',
      openTipOptions: {
        offset: [3, 10],
        borderRadius: 2,
        borderColor: '#333333',
        background: '#333333',
        className: 'rxs-tooltip',
        delay: 0,
        hideDelay: 0,
        showEffectDuration: 0,
        hideEffectDuration: 0,
        tipJoint: 'top left',
        stem: false
      }
		};
	},
	removeValue: function(valToRemove) {
		var newValueState = this.props.values.filter(function(val){
			return val !== valToRemove;
		});
		this.props.onChange(newValueState);
	},
	removeAll: function() {
		this.props.onChange([]);
	},
	addValue: function(valToAdd){
		var newValueState = this.props.values.slice(0);
		newValueState.push(valToAdd);
		this.props.onChange(newValueState);
	},
	addAllFunc: function() {
		var filteredOptionValues = this.filteredOptions().map(function(opt){ return opt.value;});
		var newValueState = filteredOptionValues.concat(this.props.values);
		this.props.onChange(newValueState);
	},
	filteredOptions: function() {
		return this.props.options.filter(function(opt){
			if(this.props.values.indexOf(opt.value) !== -1 || !this.dimensionFilterIncludes(opt)) return false;
			return (opt.label.toLowerCase().indexOf(this.state.labelFilter.toLowerCase()) > -1);

		}, this);
	},
	onMobileTooltip: function(title, content) {
		if(!this.isMobile()) {
			return;
		}

		this.setState(
			{mobileTooltipTitle: title, mobileTooltipContent: content},
			this.refs.tooltip.show);
	},
	dimensionFilterIncludes: function(opt) {

		if (Object.keys(this.state.dimensionFilter).length < 1){
			return true;
		}

		var retVal = true;

		var filterHits = this.props.filterDimensions.map(function(dimension){
			var key = dimension.key;
			var name = dimension.name;
			var filterVals = this.state.dimensionFilter[name];
			if (filterVals === undefined || filterVals.length < 1) {
				return true;
			}
			if (Array.isArray(opt[key])){
				var found = false;
				opt[key].forEach(function(optVal){
					if (filterVals.indexOf(optVal) > -1) {
						found = true;
					}
				});
				return found;
			} else {
				if (filterVals.indexOf(opt[key]) > -1) {
					return true;
				}
			}
			return false;
		}, this);

		filterHits.forEach(function(fh){
			if (!fh){
				retVal = false;
			}
		});

		return retVal;
	},
	updateLabelFilter: function(event) {
		// TODO: add throttling
		this.setState({labelFilter: event.target.value});
	},
	clearLabelFilter: function() {
	  this.setState({labelFilter: ''});
	},
	generateUpdateDimensionFilter: function(dimensionName) {
		/**
		 *  {'Source' : [], 'Sector' : []}
		 */
		return function(values) {
			var spec = {};
			spec[dimensionName] = {$set: values};
			var newState = update(this.state.dimensionFilter, spec);
			this.setState({dimensionFilter: newState});
		}.bind(this);
	},
	tagListValues: function() {
		var mapFunc = function(){};

		if (this.props.optionsByValue) {
			mapFunc = function(val){
				return this.props.optionsByValue[val];
			};
		} else {
			mapFunc = function(val){
			  return this.props.options.filter(function(opt){
					return opt.value === val;
				})[0];
			};
		}

		return this.props.values.map(mapFunc, this);
	},
	render: function() {
		var filteredOptions = this.filteredOptions();
		var selectFilters = this.props.filterDimensions.map(function(dim){
			var groupByKey = '';
			if(dim.groupByKey)
				groupByKey = dim.groupByKey;

			return (<ReactCompactMultiselect
						key={dim.name}
						label={dim.name}
						options={dim.options}
						info={dim.info}
						initialValue={[]}
						groupBy={groupByKey}
						onChange={this.generateUpdateDimensionFilter(dim.name)}
						layoutMode={ReactCompactMultiselect.ALIGN_CONTENT_NE} />);
		}, this);

		var addAll = this.props.addAll;
		if(this.props.addAllLimit && filteredOptions.length > this.props.addAllLimit) {
			addAll=false;
		}

		var skylight = null;
    if(this.isMobile()) {
      skylight = (
        <SkyLight
          ref='tooltip'
          title={this.state.mobileTooltipTitle}
          className='mobile-tooltip'>
          {this.state.mobileTooltipContent}
        </SkyLight>
      );
    }

		return (

							<input
								onChange={this.updateLabelFilter}
								value={this.state.labelFilter}
								placeholder={this.props.placeholderText} />
							<button className='rxs-label-filter-clear' name='clear-filter' onClick={this.clearLabelFilter}>×</button>

						<TagList
							values={this.tagListValues()}
							onRemove={this.removeValue}
							removeAll={this.removeAll}
							placeholderText=' '
							collapsedRows={1} />

					<OptionList
						options={filteredOptions}
						onClick={this.addValue}
						addAll={addAll}
						addAllFunc={this.addAllFunc}
						onMobileTooltip={this.onMobileTooltip}/>

						{selectFilters}

				{skylight}

		);
	}
});

module.exports = XzibitSelect;
<|endoftext|>


<commit_before>'use strict';

import {Styles} from 'electrum-theme';
import shallowCompare from 'react-addons-shallow-compare';

import E from '../index.js';

/******************************************************************************/

function createComponentClass (component, optionsGetter) {
  const componentClass = class extends component {
    constructor (props) {
      super (props);
      E.inject (this);
      E.autoBindHandlers (this);
    }

    shouldComponentUpdate (nextProps, nextState) {
      const dirty = shallowCompare (this, nextProps, nextState);
      const options = optionsGetter && optionsGetter ();
      if (options && options.log && options.log.shouldComponentUpdate) {
        options.log.shouldComponentUpdate (this, nextProps, nextState, dirty);
      }
      return dirty;
    }

    link (id, overrides) {
      return E.link (this.props, id, overrides);
    }

    read (key) {
      if (key === undefined) {
        key = 'value';
      }
      if (this.props.hasOwnProperty (key)) {
        return this.props[key];
      }
      return E.read (this.props, key);
    }

    get theme () {
      const {theme} = this.props;
      if (!theme) {
        throw new Error (`Component ${component.name} not linked to Electrum`);
      }
      return theme;
    }
  };
  return componentClass;
}

/******************************************************************************/

function injectSingleStyles (componentClass, stylesDef) {
  const stylesResolver = Styles.create (stylesDef);

  componentClass.prototype.mergeStyles = function (...names) {
      const styles = stylesResolver (this.theme, stylesResolver.styles.usesProps && (this.styleProps || this.props));
      return styles.resolve (...names);
    };

  Object.defineProperty (componentClass.prototype, 'styles', {
    get: function styles () {
      const styles = stylesResolver (this.theme, stylesResolver.styles.usesProps && (this.styleProps || this.props));
      let current = styles.resolve ('base', this.props.kind, this.props.styles, this.props.style);

      // We would like to return the style object directly, but then, we
      // could not extend it by attaching the with() function in an invisible
      // manner. Since Radium also accepts array of style objects, we use this
      // trick to provide the with() functionality without interfering with
      // the style object itself.

      const list = [current];

      list.with = function (...names) {
        current = styles.merge (current, ...names);
        list[0] = current;
        return list;
      };

      return list;
    }
  });
}

/******************************************************************************/

function injectMultipleStyles (componentClass, stylesDef) {
  const styleKeys = Object.keys (stylesDef);
  const styleResolvers = {};

  styleKeys.forEach (key => styleResolvers[key] = Styles.create (stylesDef[key]));

  componentClass.prototype.mergeStyles = function (key, ...names) {
    const resolver = styleResolvers[key];
    const styles   = resolver (this.theme, resolver.styles.usesProps && (this.styleProps || this.props));
    return styles.resolve (...names);
  };

  componentClass.prototype.getStyles = function (key) {
    const resolver = styleResolvers[key];
    const styles   = resolver (this.theme, resolver.styles.usesProps && (this.styleProps || this.props));

    let current = styles.resolve ('base', this.props.kind, this.props.styles, this.props.style);

    // We would like to return the style object directly, but then, we
    // could not extend it by attaching the with() function in an invisible
    // manner. Since Radium also accepts array of style objects, we use this
    // trick to provide the with() functionality without interfering with
    // the style object itself.

    const list = [current];

    list.with = function (...names) {
      current = styles.merge (current, ...names);
      list[0] = current;
      return list;
    };

    return list;
  };
}

/******************************************************************************/

export default function extendComponent (component, stylesDef, optionsGetter) {
  const componentClass = createComponentClass (component, optionsGetter);

  if (stylesDef) {
    if (typeof stylesDef === 'object') {
      injectMultipleStyles (componentClass, stylesDef);
    } else {
      injectSingleStyles (componentClass, stylesDef);
    }
  }

  return componentClass;
}

/******************************************************************************/
<commit_msg>Fix for updated Babel ES syntax<commit_after>'use strict';

import {Styles} from 'electrum-theme';
import shallowCompare from 'react-addons-shallow-compare';

import E from '../index.js';

/******************************************************************************/

function createComponentClass (component, optionsGetter) {
  const componentClass = class extends component {
    constructor (props) {
      super (props);
      E.inject (this);
      E.autoBindHandlers (this);
    }

    shouldComponentUpdate (nextProps, nextState) {
      const dirty = shallowCompare (this, nextProps, nextState);
      const options = optionsGetter && optionsGetter ();
      if (options && options.log && options.log.shouldComponentUpdate) {
        options.log.shouldComponentUpdate (this, nextProps, nextState, dirty);
      }
      return dirty;
    }

    link (id, overrides) {
      return E.link (this.props, id, overrides);
    }

    read (key) {
      if (key === undefined) {
        key = 'value';
      }
      if (this.props.hasOwnProperty (key)) {
        return this.props[key];
      }
      return E.read (this.props, key);
    }

    get theme () {
      const {theme} = this.props;
      if (!theme) {
        throw new Error (`Component ${component.name} not linked to Electrum`);
      }
      return theme;
    }
  };
  return componentClass;
}

/******************************************************************************/

function injectSingleStyles (componentClass, stylesDef) {
  const stylesResolver = Styles.create (stylesDef);

  componentClass.prototype.mergeStyles = function (...names) {
      const styles = stylesResolver (this.theme, stylesResolver.styles.usesProps && (this.styleProps || this.props));
      return styles.resolve (...names);
    };

  Object.defineProperty (componentClass.prototype, 'styles', {
    get: function styles () {
      const localStyles = stylesResolver (this.theme, stylesResolver.styles.usesProps && (this.styleProps || this.props));
      let current = localStyles.resolve ('base', this.props.kind, this.props.styles, this.props.style);

      // We would like to return the style object directly, but then, we
      // could not extend it by attaching the with() function in an invisible
      // manner. Since Radium also accepts array of style objects, we use this
      // trick to provide the with() functionality without interfering with
      // the style object itself.

      const list = [current];

      list.with = function (...names) {
        current = localStyles.merge (current, ...names);
        list[0] = current;
        return list;
      };

      return list;
    }
  });
}

/******************************************************************************/

function injectMultipleStyles (componentClass, stylesDef) {
  const styleKeys = Object.keys (stylesDef);
  const styleResolvers = {};

  styleKeys.forEach (key => styleResolvers[key] = Styles.create (stylesDef[key]));

  componentClass.prototype.mergeStyles = function (key, ...names) {
    const resolver = styleResolvers[key];
    const styles   = resolver (this.theme, resolver.styles.usesProps && (this.styleProps || this.props));
    return styles.resolve (...names);
  };

  componentClass.prototype.getStyles = function (key) {
    const resolver = styleResolvers[key];
    const styles   = resolver (this.theme, resolver.styles.usesProps && (this.styleProps || this.props));

    let current = styles.resolve ('base', this.props.kind, this.props.styles, this.props.style);

    // We would like to return the style object directly, but then, we
    // could not extend it by attaching the with() function in an invisible
    // manner. Since Radium also accepts array of style objects, we use this
    // trick to provide the with() functionality without interfering with
    // the style object itself.

    const list = [current];

    list.with = function (...names) {
      current = styles.merge (current, ...names);
      list[0] = current;
      return list;
    };

    return list;
  };
}

/******************************************************************************/

export default function extendComponent (component, stylesDef, optionsGetter) {
  const componentClass = createComponentClass (component, optionsGetter);

  if (stylesDef) {
    if (typeof stylesDef === 'object') {
      injectMultipleStyles (componentClass, stylesDef);
    } else {
      injectSingleStyles (componentClass, stylesDef);
    }
  }

  return componentClass;
}

/******************************************************************************/
<|endoftext|>


<commit_before>'use strict';

/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
  Schema = mongoose.Schema;

/**
 * Article Schema
 */
var ArticleSchema = new Schema({
  created: {
    type: Date,
    default: Date.now
  },
  title: {
    type: String,
    required: true,
    trim: true
  },
  content: {
    type: String,
    required: true,
    trim: true
  },
  user: {
    type: Schema.ObjectId,
    ref: 'User'
  },
  permissions: {
    type: Array
  },
  updated: {
    type: Array
  },
  parent: {
    type: String,
    default: '0'
  }
});

/**
 * Validations
 */
ArticleSchema.path('title').validate(function(title) {
  return !!title;
}, 'Title cannot be blank');

ArticleSchema.path('content').validate(function(content) {
  return !!content;
}, 'Content cannot be blank');

/**
 * Statics
 */
ArticleSchema.statics.load = function(id, cb) {
  this.findOne({
    _id: id
  }).populate('user', 'name username').exec(cb);
};

mongoose.model('Article', ArticleSchema);
<commit_msg>change Article.parent type: Schema.ObjectId, ref: 'Article'<commit_after>'use strict';

/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
  Schema = mongoose.Schema;

/**
 * Article Schema
 */
var ArticleSchema = new Schema({
  created: {
    type: Date,
    default: Date.now
  },
  title: {
    type: String,
    required: true,
    trim: true
  },
  content: {
    type: String,
    required: true,
    trim: true
  },
  user: {
    type: Schema.ObjectId,
    ref: 'User'
  },
  permissions: {
    type: Array
  },
  updated: {
    type: Array
  },
  parent: {
    type: Schema.ObjectId,
    ref: 'Article'
  }
});

/**
 * Validations
 */
ArticleSchema.path('title').validate(function(title) {
  return !!title;
}, 'Title cannot be blank');

ArticleSchema.path('content').validate(function(content) {
  return !!content;
}, 'Content cannot be blank');

/**
 * Statics
 */
ArticleSchema.statics.load = function(id, cb) {
  this.findOne({
    _id: id
  }).populate('user', 'name username').exec(cb);
};

mongoose.model('Article', ArticleSchema);
<|endoftext|>


<commit_before>'use strict';

/*
 * Twig PHP pattern engine for patternlab-node
 *
 * Evan Lovely
 * Licensed under the MIT license.
 *
 * Many thanks to Brad Frost and Dave Olsen for inspiration, encouragement, and advice.
 *
 */

/*
 * ENGINE SUPPORT LEVEL: Experimental
 */

const TwigRenderer = require('@basalt/twig-renderer');
const fs = require('fs-extra');
const path = require('path');

let twigRenderer;
let patternLabConfig = {};

const engine_twig_php = {
  engine: TwigRenderer,
  engineName: 'twig-php',
  engineFileExtension: '.twig',
  expandPartials: false,
  findPartialsRE: /{%\s*(?:extends|include|embed)\s+('[^']+'|"[^"]+").*?(with|%}|\s*%})/g,
  findPartialKeyRE: /"((?:\\.|[^"\\])*)"|'((?:\\.|[^"\\])*)'/,
  namespaces: [],

  /**
   * Accept a Pattern Lab config object from the core and put it in
   * this module's closure scope so we can configure engine behavior.
   *
   * @param {object} config - the global config object from core
   */
  usePatternLabConfig: function(config) {
    patternLabConfig = config;

    if (!config.engines.twig) {
      console.error('Missing "twig" in Pattern Lab config file; exiting...');
      process.exit(1);
    }

    const { namespaces, alterTwigEnv, relativeFrom } = config.engines.twig;

    // Schema on config object being passed in:
    // https://github.com/basaltinc/twig-renderer/blob/master/config.schema.json
    twigRenderer = new TwigRenderer({
      src: {
        roots: [config.paths.source.root, config.paths.source.patterns],
        namespaces,
      },
      relativeFrom,
      alterTwigEnv,
    });

    // Preserve the namespaces (after recursively adding nested folders) from the config so we can use them later to evaluate partials.
    this.namespaces = twigRenderer.config.src.namespaces;
  },

  renderPattern(pattern, data) {
    return new Promise((resolve, reject) => {
      // If this is a pseudo pattern the relPath will be incorrect.
      // i.e. /path/to/pattern.json
      // Twig can't render that file so we need to use the base patterns
      // relPath instead.
      const relPath = pattern.isPseudoPattern
        ? pattern.basePattern.relPath
        : pattern.relPath;

      const patternPath = path.isAbsolute(relPath)
        ? path.relative(patternLabConfig.paths.source.root, relPath)
        : relPath;
      let details = '';
      if (patternLabConfig.logLevel === 'debug') {
        details = `
${JSON.stringify(
          { pattern, data },
          null,
          '  '
        )}
`;
      }

      twigRenderer
        .render(patternPath, data)
        .then(results => {
          if (results.ok) {
            resolve(results.html + details);
          } else {
            reject(results.message);
          }
        })
        .catch(error => {
          reject(error);
        });
    });
  },

  /**
   * Checks to see if the _meta directory has engine-specific head and foot files,
   * spawning them if not found.
   *
   * @param {object} config - the global config object from core, since we won't
   * assume it's already present
   */
  spawnMeta(config) {
    const { paths } = config;
    ['_00-head.twig', '_01-foot.twig'].forEach(fileName => {
      const metaFilePath = path.resolve(paths.source.meta, fileName);
      try {
        fs.statSync(metaFilePath);
      } catch (err) {
        //not a file, so spawn it from the included file
        const metaFileContent = fs.readFileSync(
          path.resolve(__dirname, '..', '_meta/', fileName),
          'utf8'
        );
        fs.outputFileSync(metaFilePath, metaFileContent);
      }
    });
  },

  // Below exists several functions that core uses to build lineage through RegEx
  // @todo Add all functions that get called even if disabled to ease implementing engine further
  // Currently all of them return `null` as I'm not totally sure there absence will be ok. Additionally, future improvements may be implemented in this functions.

  // Find and return any {% extends|include|embed 'template-name' %} within pattern.
  // The regex should match the following examples:
  // {%
  //   include '@molecules/teaser-card/teaser-card.twig' with {
  //     teaser_card: card
  //   } only
  // %}
  // OR
  // {% include '@molecules/teaser-card/teaser-card.twig' %}
  // OR
  // {%
  //   include '@molecules/teaser-card/teaser-card.twig'
  // %}
  findPartials: function(pattern) {
    const matches = pattern.template.match(this.findPartialsRE);
    const filteredMatches =
      matches &&
      matches.filter(match => {
        // Filter out programmatically created includes.
        // i.e. {% include '@namespace/icons/assets/' ~ name ~ '.svg' %}
        return match.indexOf('~') === -1;
      });
    return filteredMatches;
  },

  findPartialsWithStyleModifiers(pattern) {
    return null;
  },

  findPartialsWithPatternParameters(pattern) {
    return null;
  },

  findListItems(pattern) {
    return null;
  },

  findPartial_new(partialString) {
    return null;
  },

  // Given a pattern, and a partial string, tease out the "pattern key" and
  // return it.
  findPartial: function(partialString) {
    try {
      let partial = partialString.match(this.findPartialKeyRE)[0];
      partial = partial.replace(/"/g, '');
      partial = partial.replace(/'/g, '');

      // Check if namespaces is not empty.
      const selectedNamespace = this.namespaces.filter(namespace => {
        // Check to see if this partial contains within the namespace id.
        return partial.indexOf(`@${namespace.id}`) !== -1;
      });

      let namespaceResolvedPartial = '';

      if (selectedNamespace.length > 0) {
        // Loop through all namespaces and try to resolve the namespace to a file path.
        for (
          let index = 0;
          index < selectedNamespace[0].paths.length;
          index++
        ) {
          const patternPath = path.isAbsolute(selectedNamespace[0].paths[index])
            ? path.relative(
                patternLabConfig.paths.source.root,
                selectedNamespace[0].paths[index]
              )
            : selectedNamespace[0].paths[index];

          // Replace the name space with the actual path.
          // i.e. @atoms -> source/_patterns/00-atoms
          const tempPartial = path.join(
            process.cwd(),
            partial.replace(`@${selectedNamespace[0].id}`, patternPath)
          );

          try {
            // Check to see if the file actually exists.
            if (fs.existsSync(tempPartial)) {
              // get the path to the top-level folder of this pattern
              // ex. /Users/bradfrost/sites/pattern-lab/packages/edition-twig/source/_patterns/00-atoms
              const fullFolderPath = `${
                tempPartial.split(selectedNamespace[0].id)[0]
              }${selectedNamespace[0].id}`;

              // then tease out the folder name itself (including the # prefix)
              // ex. 00-atoms
              const folderName = fullFolderPath.substring(
                fullFolderPath.lastIndexOf('/') + 1,
                fullFolderPath.length
              );

              // finally, return the Twig path we created from the full file path
              // ex. 00-atoms/05-buttons/button.twig
              const fullIncludePath = tempPartial.replace(
                tempPartial.split(
                  `${folderName}${tempPartial.split(folderName)[1]}`
                )[0],
                ''
              );

              namespaceResolvedPartial = fullIncludePath;

              // After it matches one time, set the resolved partial and exit the loop.
              break;
            }
          } catch (err) {
            console.error(err);
          }
        }
      }
      // Return the path with the namespace resolved OR the regex'd partial.
      return namespaceResolvedPartial || partial;
    } catch (err) {
      console.error(
        'Error occured when trying to find partial name in: ' + partialString
      );
      return null;
    }
  },

  patternMatcher(pattern, regex) {
    return null;
  },
};

module.exports = engine_twig_php;
<commit_msg>fix(engine_twig_php): Allow additional flexibility with twig namespaces.<commit_after>'use strict';

/*
 * Twig PHP pattern engine for patternlab-node
 *
 * Evan Lovely
 * Licensed under the MIT license.
 *
 * Many thanks to Brad Frost and Dave Olsen for inspiration, encouragement, and advice.
 *
 */

/*
 * ENGINE SUPPORT LEVEL: Experimental
 */

const TwigRenderer = require('@basalt/twig-renderer');
const fs = require('fs-extra');
const path = require('path');

let twigRenderer;
let patternLabConfig = {};

const engine_twig_php = {
  engine: TwigRenderer,
  engineName: 'twig-php',
  engineFileExtension: '.twig',
  expandPartials: false,
  findPartialsRE: /{%\s*(?:extends|include|embed)\s+('[^']+'|"[^"]+").*?(with|%}|\s*%})/g,
  findPartialKeyRE: /"((?:\\.|[^"\\])*)"|'((?:\\.|[^"\\])*)'/,
  namespaces: [],

  /**
   * Accept a Pattern Lab config object from the core and put it in
   * this module's closure scope so we can configure engine behavior.
   *
   * @param {object} config - the global config object from core
   */
  usePatternLabConfig: function(config) {
    patternLabConfig = config;

    if (!config.engines.twig) {
      console.error('Missing "twig" in Pattern Lab config file; exiting...');
      process.exit(1);
    }

    const { namespaces, alterTwigEnv, relativeFrom } = config.engines.twig;

    // Schema on config object being passed in:
    // https://github.com/basaltinc/twig-renderer/blob/master/config.schema.json
    twigRenderer = new TwigRenderer({
      src: {
        roots: [config.paths.source.root, config.paths.source.patterns],
        namespaces,
      },
      relativeFrom,
      alterTwigEnv,
    });

    // Preserve the namespaces (after recursively adding nested folders) from the config so we can use them later to evaluate partials.
    this.namespaces = twigRenderer.config.src.namespaces;
  },

  renderPattern(pattern, data) {
    return new Promise((resolve, reject) => {
      // If this is a pseudo pattern the relPath will be incorrect.
      // i.e. /path/to/pattern.json
      // Twig can't render that file so we need to use the base patterns
      // relPath instead.
      const relPath = pattern.isPseudoPattern
        ? pattern.basePattern.relPath
        : pattern.relPath;

      const patternPath = path.isAbsolute(relPath)
        ? path.relative(patternLabConfig.paths.source.root, relPath)
        : relPath;
      let details = '';
      if (patternLabConfig.logLevel === 'debug') {
        details = `
${JSON.stringify(
          { pattern, data },
          null,
          '  '
        )}
`;
      }

      twigRenderer
        .render(patternPath, data)
        .then(results => {
          if (results.ok) {
            resolve(results.html + details);
          } else {
            reject(results.message);
          }
        })
        .catch(error => {
          reject(error);
        });
    });
  },

  /**
   * Checks to see if the _meta directory has engine-specific head and foot files,
   * spawning them if not found.
   *
   * @param {object} config - the global config object from core, since we won't
   * assume it's already present
   */
  spawnMeta(config) {
    const { paths } = config;
    ['_00-head.twig', '_01-foot.twig'].forEach(fileName => {
      const metaFilePath = path.resolve(paths.source.meta, fileName);
      try {
        fs.statSync(metaFilePath);
      } catch (err) {
        //not a file, so spawn it from the included file
        const metaFileContent = fs.readFileSync(
          path.resolve(__dirname, '..', '_meta/', fileName),
          'utf8'
        );
        fs.outputFileSync(metaFilePath, metaFileContent);
      }
    });
  },

  // Below exists several functions that core uses to build lineage through RegEx
  // @todo Add all functions that get called even if disabled to ease implementing engine further
  // Currently all of them return `null` as I'm not totally sure there absence will be ok. Additionally, future improvements may be implemented in this functions.

  // Find and return any {% extends|include|embed 'template-name' %} within pattern.
  // The regex should match the following examples:
  // {%
  //   include '@molecules/teaser-card/teaser-card.twig' with {
  //     teaser_card: card
  //   } only
  // %}
  // OR
  // {% include '@molecules/teaser-card/teaser-card.twig' %}
  // OR
  // {%
  //   include '@molecules/teaser-card/teaser-card.twig'
  // %}
  findPartials: function(pattern) {
    const matches = pattern.template.match(this.findPartialsRE);
    const filteredMatches =
      matches &&
      matches.filter(match => {
        // Filter out programmatically created includes.
        // i.e. {% include '@namespace/icons/assets/' ~ name ~ '.svg' %}
        return match.indexOf('~') === -1;
      });
    return filteredMatches;
  },

  findPartialsWithStyleModifiers(pattern) {
    return null;
  },

  findPartialsWithPatternParameters(pattern) {
    return null;
  },

  findListItems(pattern) {
    return null;
  },

  findPartial_new(partialString) {
    return null;
  },

  // Given a pattern, and a partial string, tease out the "pattern key" and
  // return it.
  findPartial: function(partialString) {
    try {
      let partial = partialString.match(this.findPartialKeyRE)[0];
      partial = partial.replace(/"/g, '');
      partial = partial.replace(/'/g, '');

      // Check if namespaces is not empty.
      const selectedNamespace = this.namespaces.filter(namespace => {
        // Check to see if this partial contains within the namespace id.
        return partial.indexOf(`@${namespace.id}`) !== -1;
      });

      let namespaceResolvedPartial = '';

      if (selectedNamespace.length > 0) {
        // Loop through all namespaces and try to resolve the namespace to a file path.
        for (
          let index = 0;
          index < selectedNamespace[0].paths.length;
          index++
        ) {
          const patternPath = path.isAbsolute(selectedNamespace[0].paths[index])
            ? path.relative(
                patternLabConfig.paths.source.root,
                selectedNamespace[0].paths[index]
              )
            : selectedNamespace[0].paths[index];

          // Replace the name space with the actual path.
          // i.e. @atoms -> source/_patterns/00-atoms
          const tempPartial = path.join(
            process.cwd(),
            partial.replace(`@${selectedNamespace[0].id}`, patternPath)
          );

          try {
            // Check to see if the file actually exists.
            if (fs.existsSync(tempPartial)) {
              // get the path to the top-level folder of this pattern
              // ex. /Users/bradfrost/sites/pattern-lab/packages/edition-twig/source/_patterns/00-atoms
              const fullFolderPath = `${
                tempPartial.split(selectedNamespace[0].paths[index])[0]
              }${selectedNamespace[0].paths[index]}`;

              // then tease out the folder name itself (including the # prefix)
              // ex. 00-atoms
              const folderName = fullFolderPath.substring(
                fullFolderPath.lastIndexOf('/') + 1,
                fullFolderPath.length
              );

              // finally, return the Twig path we created from the full file path
              // ex. 00-atoms/05-buttons/button.twig
              const fullIncludePath = tempPartial.replace(
                tempPartial.split(
                  `${folderName}${tempPartial.split(folderName)[1]}`
                )[0],
                ''
              );

              namespaceResolvedPartial = fullIncludePath;

              // After it matches one time, set the resolved partial and exit the loop.
              break;
            }
          } catch (err) {
            console.error(err);
          }
        }
      }
      // Return the path with the namespace resolved OR the regex'd partial.
      return namespaceResolvedPartial || partial;
    } catch (err) {
      console.error(
        'Error occured when trying to find partial name in: ' + partialString
      );
      return null;
    }
  },

  patternMatcher(pattern, regex) {
    return null;
  },
};

module.exports = engine_twig_php;
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Owner: felix@famo.us
 * @license MPL 2.0
 * @copyright Famous Industries, Inc. 2014
 */

define(function(require, exports, module) {
    var Modifier = require('../core/Modifier');
    var RenderNode = require('../core/RenderNode');
    var Transform = require('../core/Transform');
    var Transitionable = require('../transitions/Transitionable');
    var View = require('../core/View');

    /**
     * A dynamic view that can show or hide different renderables with transitions.
     * @class RenderController
     * @constructor
     * @param {Options} [options] An object of configurable options.
     * @param {Transition} [inTransition=true] The transition in charge of showing a renderable.
     * @param {Transition} [outTransition=true]  The transition in charge of removing your previous renderable when
     * you show a new one, or hiding your current renderable.
     * @param {Boolean} [overlap=true] When showing a new renderable, overlap determines if the
      out transition of the old one executes concurrently with the in transition of the new one,
       or synchronously beforehand.
     */
    function RenderController(options) {
        View.apply(this, arguments);

        this._showing = -1;
        this._outgoingRenderables = [];
        this._nextRenderable = null;

        this._renderables = [];
        this._nodes = [];
        this._modifiers = [];
        this._states = [];

        this.inTransformMap = RenderController.DefaultMap.transform;
        this.inOpacityMap = RenderController.DefaultMap.opacity;
        this.inOriginMap = RenderController.DefaultMap.origin;
        this.inAlignMap = RenderController.DefaultMap.align;
        this.outTransformMap = RenderController.DefaultMap.transform;
        this.outOpacityMap = RenderController.DefaultMap.opacity;
        this.outOriginMap = RenderController.DefaultMap.origin;
        this.outAlignMap = RenderController.DefaultMap.align;

        this._output = [];
    }
    RenderController.prototype = Object.create(View.prototype);
    RenderController.prototype.constructor = RenderController;

    RenderController.DEFAULT_OPTIONS = {
        inTransition: true,
        outTransition: true,
        overlap: true
    };

    RenderController.DefaultMap = {
        transform: function() {
            return Transform.identity;
        },
        opacity: function(progress) {
            return progress;
        },
        origin: null,
        align: null
    };

    function _mappedState(map, state) {
        return map(state.get());
    }

    /**
     * As your RenderController shows a new renderable, it executes a transition in. This transition in
     *  will affect a default interior state and modify it as you bring renderables in and out. However, if you want to control
     *  the transform, opacity, and origin state yourself, you may call certain methods (such as inTransformFrom) to obtain state from an outside source,
     *  that may either be a function or a Famous transitionable. inTransformFrom sets the accessor for the state of
     *  the transform used in transitioning in renderables.
     *
     * @method inTransformFrom
     * @param {Function|Transitionable} transform  A function that returns a transform from outside closure, or a
     * a transitionable that manages a full transform (a sixteen value array).
     * @chainable
     */
    RenderController.prototype.inTransformFrom = function inTransformFrom(transform) {
        if (transform instanceof Function) this.inTransformMap = transform;
        else if (transform && transform.get) this.inTransformMap = transform.get.bind(transform);
        else throw new Error('inTransformFrom takes only function or getter object');
        //TODO: tween transition
        return this;
    };

    /**
     * inOpacityFrom sets the accessor for the state of the opacity used in transitioning in renderables.
     * @method inOpacityFrom
     * @param {Function|Transitionable} opacity  A function that returns an opacity from outside closure, or a
     * a transitionable that manages opacity (a number between zero and one).
     * @chainable
     */
    RenderController.prototype.inOpacityFrom = function inOpacityFrom(opacity) {
        if (opacity instanceof Function) this.inOpacityMap = opacity;
        else if (opacity && opacity.get) this.inOpacityMap = opacity.get.bind(opacity);
        else throw new Error('inOpacityFrom takes only function or getter object');
        //TODO: tween opacity
        return this;
    };

    /**
     * inOriginFrom sets the accessor for the state of the origin used in transitioning in renderables.
     * @method inOriginFrom
     * @param {Function|Transitionable} origin A function that returns an origin from outside closure, or a
     * a transitionable that manages origin (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.inOriginFrom = function inOriginFrom(origin) {
        if (origin instanceof Function) this.inOriginMap = origin;
        else if (origin && origin.get) this.inOriginMap = origin.get.bind(origin);
        else throw new Error('inOriginFrom takes only function or getter object');
        //TODO: tween origin
        return this;
    };

    /**
     * inOriginFrom sets the accessor for the state of the align used in transitioning in renderables.
     * @method inAlignFrom
     * @param {Function|Transitionable} align A function that returns an align from outside closure, or a
     * a transitionable that manages align (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.inAlignFrom = function inAlignFrom(align) {
        if (align instanceof Function) this.inAlignMap = align;
        else if (align && align.get) this.inAlignMap = align.get.bind(align);
        else throw new Error('inAlignFrom takes only function or getter object');
        //TODO: tween align
        return this;
    };

    /**
     * outTransformFrom sets the accessor for the state of the transform used in transitioning out renderables.
     * @method show
     * @param {Function|Transitionable} transform  A function that returns a transform from outside closure, or a
     * a transitionable that manages a full transform (a sixteen value array).
     * @chainable
     */
    RenderController.prototype.outTransformFrom = function outTransformFrom(transform) {
        if (transform instanceof Function) this.outTransformMap = transform;
        else if (transform && transform.get) this.outTransformMap = transform.get.bind(transform);
        else throw new Error('inTransformFrom takes only function or getter object');
        //TODO: tween transition
        return this;
    };

    /**
     * outOpacityFrom sets the accessor for the state of the opacity used in transitioning out renderables.
     * @method inOpacityFrom
     * @param {Function|Transitionable} opacity  A function that returns an opacity from outside closure, or a
     * a transitionable that manages opacity (a number between zero and one).
     * @chainable
     */
    RenderController.prototype.outOpacityFrom = function outOpacityFrom(opacity) {
        if (opacity instanceof Function) this.outOpacityMap = opacity;
        else if (opacity && opacity.get) this.outOpacityMap = opacity.get.bind(opacity);
        else throw new Error('inOpacityFrom takes only function or getter object');
        //TODO: tween opacity
        return this;
    };

    /**
     * outOriginFrom sets the accessor for the state of the origin used in transitioning out renderables.
     * @method inOriginFrom
     * @param {Function|Transitionable} origin A function that returns an origin from outside closure, or a
     * a transitionable that manages origin (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.outOriginFrom = function outOriginFrom(origin) {
        if (origin instanceof Function) this.outOriginMap = origin;
        else if (origin && origin.get) this.outOriginMap = origin.get.bind(origin);
        else throw new Error('outOriginFrom takes only function or getter object');
        //TODO: tween origin
        return this;
    };

    /**
     * outAlignFrom sets the accessor for the state of the align used in transitioning out renderables.
     * @method inOriginFrom
     * @param {Function|Transitionable} align A function that returns an align from outside closure, or a
     * a transitionable that manages align (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.outAlignFrom = function outAlignFrom(align) {
        if (align instanceof Function) this.outAlignMap = align;
        else if (align && align.get) this.outAlignMap = align.get.bind(align);
        else throw new Error('outAlignFrom takes only function or getter object');
        //TODO: tween align
        return this;
    };

    /**
     * Show displays the targeted renderable with a transition and an optional callback to
     * execute afterwards.
     * @method show
     * @param {Object} renderable The renderable you want to show.
     * @param {Transition} [transition] Overwrites the default transition in to display the
     * passed-in renderable.
     * @param {function} [callback] Executes after transitioning in the renderable.
     * @chainable
     */
    RenderController.prototype.show = function show(renderable, transition, callback) {
        if (!renderable) {
            return this.hide(callback);
        }

        if (transition instanceof Function) {
            callback = transition;
            transition = null;
        }

        if (this._showing >= 0) {
            if (this.options.overlap) this.hide();
            else {
                if (this._nextRenderable) {
                    this._nextRenderable = renderable;
                }
                else {
                    this._nextRenderable = renderable;
                    this.hide(function() {
                        if (this._nextRenderable === renderable) this.show(this._nextRenderable, callback);
                        this._nextRenderable = null;
                    });
                }
                return undefined;
            }
        }

        var state = null;

        // check to see if we should restore
        var renderableIndex = this._renderables.indexOf(renderable);
        if (renderableIndex >= 0) {
            this._showing = renderableIndex;
            state = this._states[renderableIndex];
            state.halt();

            var outgoingIndex = this._outgoingRenderables.indexOf(renderable);
            if (outgoingIndex >= 0) this._outgoingRenderables.splice(outgoingIndex, 1);
        }
        else {
            state = new Transitionable(0);

            var modifier = new Modifier({
                transform: this.inTransformMap ? _mappedState.bind(this, this.inTransformMap, state) : null,
                opacity: this.inOpacityMap ? _mappedState.bind(this, this.inOpacityMap, state) : null,
                origin: this.inOriginMap ? _mappedState.bind(this, this.inOriginMap, state) : null,
                align: this.inAlignMap ? _mappedState.bind(this, this.inAlignMap, state) : null
            });

            var node = new RenderNode();
            node.add(modifier).add(renderable);

            this._showing = this._nodes.length;
            this._nodes.push(node);
            this._modifiers.push(modifier);
            this._states.push(state);
            this._renderables.push(renderable);
        }

        if (!transition) transition = this.options.inTransition;
        state.set(1, transition, callback);
    };

    /**
     * Hide hides the currently displayed renderable with an out transition.
     * @method hide
     * @param {Transition} [transition] Overwrites the default transition in to hide the
     * currently controlled renderable.
     * @param {function} [callback] Executes after transitioning out the renderable.
     * @chainable
     */
    RenderController.prototype.hide = function hide(transition, callback) {
        if (this._showing < 0) return;
        var index = this._showing;
        this._showing = -1;

        if (transition instanceof Function) {
            callback = transition;
            transition = undefined;
        }

        var node = this._nodes[index];
        var modifier = this._modifiers[index];
        var state = this._states[index];
        var renderable = this._renderables[index];

        modifier.transformFrom(this.outTransformMap ? _mappedState.bind(this, this.outTransformMap, state) : null);
        modifier.opacityFrom(this.outOpacityMap ? _mappedState.bind(this, this.outOpacityMap, state) : null);
        modifier.originFrom(this.outOriginMap ? _mappedState.bind(this, this.outOriginMap, state) : null);
        modifier.alignFrom(this.outAlignMap ? _mappedState.bind(this, this.outAlignMap, state) : null);

        if (this._outgoingRenderables.indexOf(renderable) < 0) this._outgoingRenderables.push(renderable);

        if (!transition) transition = this.options.outTransition;
        state.halt();
        state.set(0, transition, function(node, modifier, state, renderable) {
            if (this._outgoingRenderables.indexOf(renderable) >= 0) {
                var index = this._nodes.indexOf(node);
                this._nodes.splice(index, 1);
                this._modifiers.splice(index, 1);
                this._states.splice(index, 1);
                this._renderables.splice(index, 1);
                this._outgoingRenderables.splice(this._outgoingRenderables.indexOf(renderable), 1);

                if (this._showing >= index) this._showing--;
            }
            if (callback) callback.call(this);
        }.bind(this, node, modifier, state, renderable));
    };

    /**
     * Generate a render spec from the contents of this component.
     *
     * @private
     * @method render
     * @return {number} Render spec for this component
     */
    RenderController.prototype.render = function render() {
        var result = this._output;
        if (result.length > this._nodes.length) result.splice(this._nodes.length);
        for (var i = 0; i < this._nodes.length; i++) {
            result[i] = this._nodes[i].render();
        }
        return result;
    };

    module.exports = RenderController;
});
<commit_msg>fixed doc errors just created<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Owner: felix@famo.us
 * @license MPL 2.0
 * @copyright Famous Industries, Inc. 2014
 */

define(function(require, exports, module) {
    var Modifier = require('../core/Modifier');
    var RenderNode = require('../core/RenderNode');
    var Transform = require('../core/Transform');
    var Transitionable = require('../transitions/Transitionable');
    var View = require('../core/View');

    /**
     * A dynamic view that can show or hide different renderables with transitions.
     * @class RenderController
     * @constructor
     * @param {Options} [options] An object of configurable options.
     * @param {Transition} [inTransition=true] The transition in charge of showing a renderable.
     * @param {Transition} [outTransition=true]  The transition in charge of removing your previous renderable when
     * you show a new one, or hiding your current renderable.
     * @param {Boolean} [overlap=true] When showing a new renderable, overlap determines if the
      out transition of the old one executes concurrently with the in transition of the new one,
       or synchronously beforehand.
     */
    function RenderController(options) {
        View.apply(this, arguments);

        this._showing = -1;
        this._outgoingRenderables = [];
        this._nextRenderable = null;

        this._renderables = [];
        this._nodes = [];
        this._modifiers = [];
        this._states = [];

        this.inTransformMap = RenderController.DefaultMap.transform;
        this.inOpacityMap = RenderController.DefaultMap.opacity;
        this.inOriginMap = RenderController.DefaultMap.origin;
        this.inAlignMap = RenderController.DefaultMap.align;
        this.outTransformMap = RenderController.DefaultMap.transform;
        this.outOpacityMap = RenderController.DefaultMap.opacity;
        this.outOriginMap = RenderController.DefaultMap.origin;
        this.outAlignMap = RenderController.DefaultMap.align;

        this._output = [];
    }
    RenderController.prototype = Object.create(View.prototype);
    RenderController.prototype.constructor = RenderController;

    RenderController.DEFAULT_OPTIONS = {
        inTransition: true,
        outTransition: true,
        overlap: true
    };

    RenderController.DefaultMap = {
        transform: function() {
            return Transform.identity;
        },
        opacity: function(progress) {
            return progress;
        },
        origin: null,
        align: null
    };

    function _mappedState(map, state) {
        return map(state.get());
    }

    /**
     * As your RenderController shows a new renderable, it executes a transition in. This transition in
     *  will affect a default interior state and modify it as you bring renderables in and out. However, if you want to control
     *  the transform, opacity, and origin state yourself, you may call certain methods (such as inTransformFrom) to obtain state from an outside source,
     *  that may either be a function or a Famous transitionable. inTransformFrom sets the accessor for the state of
     *  the transform used in transitioning in renderables.
     *
     * @method inTransformFrom
     * @param {Function|Transitionable} transform  A function that returns a transform from outside closure, or a
     * a transitionable that manages a full transform (a sixteen value array).
     * @chainable
     */
    RenderController.prototype.inTransformFrom = function inTransformFrom(transform) {
        if (transform instanceof Function) this.inTransformMap = transform;
        else if (transform && transform.get) this.inTransformMap = transform.get.bind(transform);
        else throw new Error('inTransformFrom takes only function or getter object');
        //TODO: tween transition
        return this;
    };

    /**
     * inOpacityFrom sets the accessor for the state of the opacity used in transitioning in renderables.
     * @method inOpacityFrom
     * @param {Function|Transitionable} opacity  A function that returns an opacity from outside closure, or a
     * a transitionable that manages opacity (a number between zero and one).
     * @chainable
     */
    RenderController.prototype.inOpacityFrom = function inOpacityFrom(opacity) {
        if (opacity instanceof Function) this.inOpacityMap = opacity;
        else if (opacity && opacity.get) this.inOpacityMap = opacity.get.bind(opacity);
        else throw new Error('inOpacityFrom takes only function or getter object');
        //TODO: tween opacity
        return this;
    };

    /**
     * inOriginFrom sets the accessor for the state of the origin used in transitioning in renderables.
     * @method inOriginFrom
     * @param {Function|Transitionable} origin A function that returns an origin from outside closure, or a
     * a transitionable that manages origin (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.inOriginFrom = function inOriginFrom(origin) {
        if (origin instanceof Function) this.inOriginMap = origin;
        else if (origin && origin.get) this.inOriginMap = origin.get.bind(origin);
        else throw new Error('inOriginFrom takes only function or getter object');
        //TODO: tween origin
        return this;
    };

    /**
     * inAlignFrom sets the accessor for the state of the align used in transitioning in renderables.
     * @method inAlignFrom
     * @param {Function|Transitionable} align A function that returns an align from outside closure, or a
     * a transitionable that manages align (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.inAlignFrom = function inAlignFrom(align) {
        if (align instanceof Function) this.inAlignMap = align;
        else if (align && align.get) this.inAlignMap = align.get.bind(align);
        else throw new Error('inAlignFrom takes only function or getter object');
        //TODO: tween align
        return this;
    };

    /**
     * outTransformFrom sets the accessor for the state of the transform used in transitioning out renderables.
     * @method outTransformFrom
     * @param {Function|Transitionable} transform  A function that returns a transform from outside closure, or a
     * a transitionable that manages a full transform (a sixteen value array).
     * @chainable
     */
    RenderController.prototype.outTransformFrom = function outTransformFrom(transform) {
        if (transform instanceof Function) this.outTransformMap = transform;
        else if (transform && transform.get) this.outTransformMap = transform.get.bind(transform);
        else throw new Error('outTransformFrom takes only function or getter object');
        //TODO: tween transition
        return this;
    };

    /**
     * outOpacityFrom sets the accessor for the state of the opacity used in transitioning out renderables.
     * @method outOpacityFrom
     * @param {Function|Transitionable} opacity  A function that returns an opacity from outside closure, or a
     * a transitionable that manages opacity (a number between zero and one).
     * @chainable
     */
    RenderController.prototype.outOpacityFrom = function outOpacityFrom(opacity) {
        if (opacity instanceof Function) this.outOpacityMap = opacity;
        else if (opacity && opacity.get) this.outOpacityMap = opacity.get.bind(opacity);
        else throw new Error('outOpacityFrom takes only function or getter object');
        //TODO: tween opacity
        return this;
    };

    /**
     * outOriginFrom sets the accessor for the state of the origin used in transitioning out renderables.
     * @method outOriginFrom
     * @param {Function|Transitionable} origin A function that returns an origin from outside closure, or a
     * a transitionable that manages origin (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.outOriginFrom = function outOriginFrom(origin) {
        if (origin instanceof Function) this.outOriginMap = origin;
        else if (origin && origin.get) this.outOriginMap = origin.get.bind(origin);
        else throw new Error('outOriginFrom takes only function or getter object');
        //TODO: tween origin
        return this;
    };

    /**
     * outAlignFrom sets the accessor for the state of the align used in transitioning out renderables.
     * @method outAlignFrom
     * @param {Function|Transitionable} align A function that returns an align from outside closure, or a
     * a transitionable that manages align (a two value array of numbers between zero and one).
     * @chainable
     */
    RenderController.prototype.outAlignFrom = function outAlignFrom(align) {
        if (align instanceof Function) this.outAlignMap = align;
        else if (align && align.get) this.outAlignMap = align.get.bind(align);
        else throw new Error('outAlignFrom takes only function or getter object');
        //TODO: tween align
        return this;
    };

    /**
     * Show displays the targeted renderable with a transition and an optional callback to
     * execute afterwards.
     * @method show
     * @param {Object} renderable The renderable you want to show.
     * @param {Transition} [transition] Overwrites the default transition in to display the
     * passed-in renderable.
     * @param {function} [callback] Executes after transitioning in the renderable.
     * @chainable
     */
    RenderController.prototype.show = function show(renderable, transition, callback) {
        if (!renderable) {
            return this.hide(callback);
        }

        if (transition instanceof Function) {
            callback = transition;
            transition = null;
        }

        if (this._showing >= 0) {
            if (this.options.overlap) this.hide();
            else {
                if (this._nextRenderable) {
                    this._nextRenderable = renderable;
                }
                else {
                    this._nextRenderable = renderable;
                    this.hide(function() {
                        if (this._nextRenderable === renderable) this.show(this._nextRenderable, callback);
                        this._nextRenderable = null;
                    });
                }
                return undefined;
            }
        }

        var state = null;

        // check to see if we should restore
        var renderableIndex = this._renderables.indexOf(renderable);
        if (renderableIndex >= 0) {
            this._showing = renderableIndex;
            state = this._states[renderableIndex];
            state.halt();

            var outgoingIndex = this._outgoingRenderables.indexOf(renderable);
            if (outgoingIndex >= 0) this._outgoingRenderables.splice(outgoingIndex, 1);
        }
        else {
            state = new Transitionable(0);

            var modifier = new Modifier({
                transform: this.inTransformMap ? _mappedState.bind(this, this.inTransformMap, state) : null,
                opacity: this.inOpacityMap ? _mappedState.bind(this, this.inOpacityMap, state) : null,
                origin: this.inOriginMap ? _mappedState.bind(this, this.inOriginMap, state) : null,
                align: this.inAlignMap ? _mappedState.bind(this, this.inAlignMap, state) : null
            });

            var node = new RenderNode();
            node.add(modifier).add(renderable);

            this._showing = this._nodes.length;
            this._nodes.push(node);
            this._modifiers.push(modifier);
            this._states.push(state);
            this._renderables.push(renderable);
        }

        if (!transition) transition = this.options.inTransition;
        state.set(1, transition, callback);
    };

    /**
     * Hide hides the currently displayed renderable with an out transition.
     * @method hide
     * @param {Transition} [transition] Overwrites the default transition in to hide the
     * currently controlled renderable.
     * @param {function} [callback] Executes after transitioning out the renderable.
     * @chainable
     */
    RenderController.prototype.hide = function hide(transition, callback) {
        if (this._showing < 0) return;
        var index = this._showing;
        this._showing = -1;

        if (transition instanceof Function) {
            callback = transition;
            transition = undefined;
        }

        var node = this._nodes[index];
        var modifier = this._modifiers[index];
        var state = this._states[index];
        var renderable = this._renderables[index];

        modifier.transformFrom(this.outTransformMap ? _mappedState.bind(this, this.outTransformMap, state) : null);
        modifier.opacityFrom(this.outOpacityMap ? _mappedState.bind(this, this.outOpacityMap, state) : null);
        modifier.originFrom(this.outOriginMap ? _mappedState.bind(this, this.outOriginMap, state) : null);
        modifier.alignFrom(this.outAlignMap ? _mappedState.bind(this, this.outAlignMap, state) : null);

        if (this._outgoingRenderables.indexOf(renderable) < 0) this._outgoingRenderables.push(renderable);

        if (!transition) transition = this.options.outTransition;
        state.halt();
        state.set(0, transition, function(node, modifier, state, renderable) {
            if (this._outgoingRenderables.indexOf(renderable) >= 0) {
                var index = this._nodes.indexOf(node);
                this._nodes.splice(index, 1);
                this._modifiers.splice(index, 1);
                this._states.splice(index, 1);
                this._renderables.splice(index, 1);
                this._outgoingRenderables.splice(this._outgoingRenderables.indexOf(renderable), 1);

                if (this._showing >= index) this._showing--;
            }
            if (callback) callback.call(this);
        }.bind(this, node, modifier, state, renderable));
    };

    /**
     * Generate a render spec from the contents of this component.
     *
     * @private
     * @method render
     * @return {number} Render spec for this component
     */
    RenderController.prototype.render = function render() {
        var result = this._output;
        if (result.length > this._nodes.length) result.splice(this._nodes.length);
        for (var i = 0; i < this._nodes.length; i++) {
            result[i] = this._nodes[i].render();
        }
        return result;
    };

    module.exports = RenderController;
});
<|endoftext|>