<!DOCTYPE html>
<html>

  <head>
    <script data-require="react@*" data-semver="0.12.2" src="http://fb.me/JSXTransformer-0.12.2.js"></script>
    <script data-require="react@*" data-semver="0.12.2" src="http://fb.me/react-0.12.2.js"></script>
    <script data-require="react@*" data-semver="0.12.2" src="http://fb.me/react-with-addons-0.12.2.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>

  <body>
    <div id="main"></div>

    <script src="Dispatcher.js"></script>
    <script src="filterSetRadio.js" type="text/jsx"></script>
    <script src="filterSetCheckbox.js" type="text/jsx"></script>
    <script src="tab.js" type="text/jsx"></script>
    <script src="tabSet.js" type="text/jsx"></script>
    <script src="resultList.js" type="text/jsx"></script>
    <script src="app.js" type="text/jsx"></script>
  </body>

</html>
/* Styles go here */

body {
  font-family: sans-serif;
  font-size: 12px;
}

.filters {
  float: left;
  padding: 10px;
  box-sizing: border-box;
  width: 30%;
}

.results {
  float: right;
  padding: 10px;
  box-sizing: border-box;
  width: 65%;
}

.select-all span.button {
  display: inline-block;
  padding: 0 5px;
  border: 1px solid #000;
  border-radius: 5px;
}

.select-all span.button.active {
  background: green;
}

.select-all span.state {
  padding: 0 5px;
  border: 0;
}

.tabset nav {
  display: block;
}

.tabset nav ul,
ul.filterset {
  list-style: none;
  margin: 0;
  padding: 0;
}

.tabset nav li {
  float: left;
  display: inline-block;
  padding: 10px;
  cursor: pointer;
}

.tabset nav li {
  background: #ddd;
  margin-left: 1px;
}

.tabset nav li:first-child {
  margin-left: 0;
}

.tabset nav li.active {
  background: #eee;
}

.tab {
  display: none;
  clear: both;
  padding: 10px;
  background: #efefef;
}

.tab.active {
  display:block;
}
'use strict';

var Store = {};

/**
 * Utility responsible for triggering the special/custom events from within the components.
 * With this utility you are able to register, unregister and trigger custom event handlers.
 *
 * @class Dispatcher
 * @public
 * @version 1.0
 * @author Ricardo Machado <rmachado@travix.com>
 * @example
 *     var fnHandler = function( data, store ){
 *       console.log('Storing my data in the store');
 *       store.myData = data;
 *     };
 *     var myObject = {
 *       foo: bar
 *     };
 *
 *     Dispatcher.register('myCustomEvent', fnHandler);
 *     Dispatcher.unregister('myCustomEvent', fnHandler);
 *     Dispatcher.trigger('myCustomEvent', myObject);
 */
var Dispatcher = {

  /**
   * @property {Object} _eventHandlers Private property to track the event handlers registered
   * @private
   */
  _eventHandlers: {},

  /**
   * Registers a handler for a given event name
   *
   * @method register
   * @param {String} eventName Name of the event that will trigger the function
   * @param {Function} handlerFn Function to execute when triggering the event
   * @return {void}
   * @public
   */
  register: function( eventName, handlerFn ){
    if( (typeof(eventName) === 'string') && (typeof(handlerFn) === 'function') && ('call' in handlerFn) ){
      if( !(eventName in this._eventHandlers) ) {
        this._eventHandlers[eventName] = [];
      }

      this._eventHandlers[eventName].push( handlerFn );
    }
  },

  /**
   * Un-registers a handler for a given event name
   *
   * @method unregister
   * @param {String} eventName Name of the event used to register the function
   * @param {Function} handlerFn Same function used on the register
   * @return {void}
   * @public
   */
  unregister: function( eventName, handlerFn ){
    if( (typeof(eventName) === 'string') && (typeof(handlerFn) === 'function') && ('call' in handlerFn) ){
      if( (eventName in this._eventHandlers) ) {
        var index;
        while( (index = this._eventHandlers[eventName].indexOf(handlerFn)) !== -1 ){
          this._eventHandlers[eventName].splice(index,1);
        }

        if( !this._eventHandlers[eventName].length ){
          delete this._eventHandlers[eventName];
        }
      }
    }
  },

  /**
   * Triggers the functions registered for a specific event.
   *
   * @method dispatch
   * @param {String} eventName Name of the event that you want to run the functions from.
   * @param {Object|undefined} [err] Optional. In case there's an error, this parameter contains the error object.
   * @param {Object|undefined} [data] Optional. Data related to the event.
   * @return {void}
   * @public
   */
  dispatch: function( eventName, err, data ){
    if( (typeof(eventName) === 'string') && (eventName in this._eventHandlers) ){
      this._eventHandlers[eventName].forEach( function(fn){
        fn( err, data, Store );
      });
    }
  }
};
/** @jsx React.createElement */

var App = React.createClass({
  getDefaultProps: function() {
  },
  filters: [[{
      label: 'blue',
      criteria: {
        value: 'blue'
      }
    }, {
      label: 'Red',
      criteria: {
        value: 'red'
      }
    }, {
      label: 'Green',
      criteria: {
        value: 'green'
      }
    }, {
      label: 'Purple',
      criteria: {
        value: 'purple'
      }
    }],

    [{
      label: 'S',
      criteria: {
        value: 's'
      }
    }, {
      label: 'M',
      criteria: {
        value: 'm'
      }
    }, {
      label: 'L',
      criteria: {
        value: 'l'
      }
    }, {
      label: 'XL',
      criteria: {
        value: 'xl'
      }
    }],

    [{
      label: 'All',
      criteria: {
        value: '-1',
        comparator: 'greaterThan'
      }
    }, {
      label: '>10',
      criteria: {
        value: '10',
        comparator: 'greaterThan'
      }
    }, {
      label: '>50',
      criteria: {
        value: '50',
        comparator: 'greaterThan'
      }
    }, {
      label: '<50',
      criteria: {
        value: '50',
        comparator: 'lessThan'
      }
    }, {
      label: '<25',
      criteria: {
        value: '25',
        comparator: 'lessThan'
      }
    }]],

  results: [{
    color: 'red',
    size: 'm',
    age: 10
  }, {
    color: 'blue',
    size: 's',
    age: 18
  }, {
    color: 'green',
    size: 'l',
    age: 40
  }, {
    color: 'yellow',
    size: 'xl',
    age: 23
  }, {
    color: 'purple',
    size: 's',
    age: 64
  }],

  getInitialState: function() {
    return {
      filterCriteria: {},
      results: this.results
    };
  },

  componentDidMount: function () {
    Dispatcher.register('filter1:change', this.handleFilterChange);
    Dispatcher.register('filter2:change', this.handleFilterChange);
    Dispatcher.register('filter3:change', this.handleFilterChange);
  },

  componentWillUnmount: function() {
    Dispatcher.unregister('filter1:change', this.handleFilterChange);
    Dispatcher.unregister('filter2:change', this.handleFilterChange);
    Dispatcher.unregister('filter3:change', this.handleFilterChange);
  },

  handleFilterChange: function(filterKey, filters) {
    var filterCriteria = this.state.filterCriteria;
    filterCriteria[filterKey] = filters;

    // Filter results, AND comparator per filter set, OR comparator for filter item
    var filteredResults = this.results.filter(function(item, index) {
      var drop = true;
      for (var key in item) {
        if (filterCriteria.hasOwnProperty(key) && filterCriteria[key].length > 0) {
          var keep = false;
          for (var i in filterCriteria[key]) {
            switch (filterCriteria[key][i].criteria.comparator) {
              case 'greaterThan':
                keep = keep || item[key] > parseInt(filterCriteria[key][i].criteria.value);
                break;
              case 'lessThan':
                keep = keep || item[key] < parseInt(filterCriteria[key][i].criteria.value);
                break;
              default:
                keep = keep || item[key] === filterCriteria[key][i].criteria.value;
                break;
            }
          }
          drop = drop && keep;
        }
      }
      return drop;
    });

    this.setState({
      filterCriteria: filterCriteria,
      results: filteredResults
    });
  },
  
  handleReset: function() {
    
    Dispatcher.dispatch('filter1:reset');
    Dispatcher.dispatch('filter2:reset');
    Dispatcher.dispatch('filter3:reset');
    
    this.setState({
      filterCriteria: [],
      results: this.results
    });
  },

  render: function() {
    return (
      <div>
        <div className="filters">
          <TabSet>
            <Tab title="Tab 1">
              <h2>Tab 1</h2>
              <h3>Filter by color</h3>
              <FilterSetCheckbox title="Filter set 1"
                name="color"
                eventName="filter1"
                filters={this.filters[0]} />
              <h3>Filter by size</h3>
              <FilterSetCheckbox title="Filter set 2"
                name="size"
                eventName="filter2"
                filters={this.filters[1]} />
            </Tab>
            <Tab title="Tab 2">
              <h2>Tab 2</h2>
              <h3>Filter by age</h3>
              <FilterSetRadio title="Filter set 3"
                eventName="filter3"
                name="age"
                filters={this.filters[2]}
                defaultChecked="All" />
            </Tab>
          </TabSet>
        </div>
        <div className="results">
          <button type="button" onClick={this.handleReset}>Reset all filter</button>
          <ResultList list={this.state.results} />
        </div>
      </div>
    );
  }
});

React.render(<App />, document.getElementById('main'));
/** @jsx React.createElement */

var TabSet = React.createClass({
  getDefaultProps: function() {
    return {};
  },
  
  getInitialState: function() {
    return {
      selectedTab: 0
    };
  },
  
  handleTabClick: function(index) {
    this.setState({
      selectedTab: index
    });
  },
  
  render: function() {
    var items = [];
    if(this.props.children && ('length' in this.props.children)) {
      items = this.props.children.filter(function(item) {
        return item.type.displayName;
      });
    } else if (this.props.children && (this.props.children.type.displayName === 'Tab')) {
      items = [ this.props.children ];
    }
    
    return (
      <div className="tabset">
        <nav>
          <ul>
            {items.map(function(item, index) {
              var className = (this.state.selectedTab === index) ? 'active' : '';
              return <li key={'tabLink_' + index}
                className={className}
                onClick={this.handleTabClick.bind(this, index)}>
                  {item.props.title}
                </li>;
            }.bind(this))}
          </ul>
        </nav>
        {items.map(function(item, index) {
          item.props.selected = (this.state.selectedTab === index);
          item.props.key='tab_' + index;
          return React.createElement(Tab, item.props);
        }.bind(this))}
      </div>
    );
  }
});
/** @jsx React.createElement */

var Tab = React.createClass({
  getDefaultProps: function() {
    return {
      selected: true
    };
  },
  
  componentWillReceiveProps: function(newProps) {
    this.setState({
      selected: newProps.selected
    });
  },
  
  getInitialState: function() {
    return {
      selected: this.props.selected
    };
  },
  
  render: function() {
    var className= this.state.selected ? 'active' : '';
    return (
      <div className={'tab ' + className}>
        {this.props.children}
      </div>
    );
  }
});
/** @jsx React.createElement */

var ResultList = React.createClass({
  getDefaultProps: function() {
    return {
      list: []
    };
  },
  
  render: function() {
    return (
      <ul>
        {this.props.list.map(function(item, index) {
          return (
            <li key={'list_item_' + index}>
              Color: {item.color}<br />
              Size: {item.size}<br />
              Age: {item.age}
            </li>
          );
        })}
      </ul>
    );
  }
});
/** @jsx React.createElement */

var FilterSetCheckbox = React.createClass({

  propTypes: {
    name: React.PropTypes.string,
    filters: React.PropTypes.array.isRequired,
    defaultAllSelected: React.PropTypes.bool,
    eventName: React.PropTypes.string,
    label1: React.PropTypes.string,
    label2: React.PropTypes.string

  },

  getDefaultProps: function() {
    return {
      name: 'filterSet',
      eventName: 'filterSet',
      defaultAllSelected: false,
      label1: 'Select All',
      label2: 'Unselect All'
    };
  },

  getInitialState: function() {
    return {
      filters: this.initFilters(this.props.defaultAllSelected)
    };
  },
  
  componentDidMount: function() {
    Dispatcher.register(this.props.eventName + ':reset', this.handleReset);
  },
  
  componentWillUnmount: function() {
    Dispatcher.unregister(this.props.eventName + ':reset', this.handleReset);
  },
  
  handleReset: function() {
    this.setState({
      filters: this.initFilters(false),
    });
  },

  initFilters: function(value) {
    return this.props.filters.map(function(item) {
      item.isChecked = value;
      return item;
    });
  },

  handleChange: function(index) {
    var filters = this.state.filters;
    filters[index].isChecked = !filters[index].isChecked;

    this.setState({
      filters: filters
    });

    this.dispatchChange();
  },

  handleSelectAll: function(value) {
    var filters = this.initFilters(value);

    this.setState({
      filters: filters
    });

    this.dispatchChange();
  },

  dispatchChange: function() {
    var filters = this.state.filters.filter(function(item) {
      return item.isChecked;
    });
    Dispatcher.dispatch(this.props.eventName + ':change', this.props.name, filters);
  },

  render: function() {
    var items = [];
    var selectedItems = 0;

    this.state.filters.map(function(item, index) {
      if (item.isChecked) {
        selectedItems++;
      }

      var key = this.props.name + '-' + index;
      items.push(
        <li key={key}>
          <input type="checkbox"
            id={key}
            checked={item.isChecked}
            onChange={this.handleChange.bind(this, index)} />
          <label htmlFor={key}>{item.label}</label>
        </li>
      );
    }.bind(this));

    var currentState = 'some';
    if (selectedItems <= 0) {
      currentState = 'none';
    } else if (selectedItems >= this.props.filters.length) {
      currentState = 'all';
    }

    var label1ClassName = (currentState === 'all') ? 'active' : '';
    var label1 = <span className={'button ' + label1ClassName} onClick={this.handleSelectAll.bind(this, true)}>{this.props.label1}</span>;
    var label2ClassName = (currentState === 'none') ? 'active' : '';
    var label2 = <span className={'button ' + label2ClassName} onClick={this.handleSelectAll.bind(this, false)}>{this.props.label2}</span>;

    return (
      <ul className="filterset">
        <li>
          <label className="select-all">
            <span className="state">{currentState}</span>
            {label1} {label2}
          </label>
        </li>
        {items}
      </ul>
    );
  }
});
/** @jsx React.createElement */

var FilterSetRadio = React.createClass({

  propTypes: {
    name: React.PropTypes.string,
    filters: React.PropTypes.array.isRequired,
    defaultChecked: React.PropTypes.string,
    eventName: React.PropTypes.string
  },

  getDefaultProps: function() {
    return {
      name: 'filterSet',
      eventName: 'filterSet',
      defaultChecked: ''
    };
  },
  
  getInitialState: function() {
    return {
      checked: this.props.defaultChecked
    };
  },
  
  componentDidMount: function() {
    Dispatcher.register(this.props.eventName + ':reset', this.handleReset);
  },
  
  componentWillUnmount: function() {
    Dispatcher.unregister(this.props.eventName + ':reset', this.handleReset);
  },
  
  handleReset: function() {
    this.setState({
      checked: this.props.defaultChecked
    });
  },

  handleChange: function(index) {
    this.setState({
      checked: this.props.filters[index].label
    });
    Dispatcher.dispatch(this.props.eventName + ':change', this.props.name, [this.props.filters[index]]);
  },

  render: function() {
    return (
      <ul className="filterset">
        {this.props.filters.map(function(item, index) {
          var key = this.props.name + '-' + index;
          return (
            <li key={key}>
              <input type="radio"
                id={key}
                name={this.props.name}
                value={item.label}
                checked={item.label === this.state.checked}
                onChange={this.handleChange.bind(this, index)} />
              <label htmlFor={key}>{item.label}</label>
            </li>
          );
        }.bind(this))}
      </ul>
    );
  }
});