import React from "react" import { InputLabel, FormGroup, TextField, Grid, Select, MenuItem, } from "@material-ui/core" export const getRangeFilterOptions = fieldLabel => { return { filter: true, filterType: "custom", filterOptions: { names: [], logic(value, filters) { /** * Necessary for clearing non number values * and parsing to number (e.g. localized restricted price "(R) 1,000") */ const clearedValue = typeof value === "string" ? parseInt(value.replace(/[\D]/gi, ""), 10) : value if (filters[0] && filters[1]) { return clearedValue < filters[0] || clearedValue > filters[1] } else if (filters[0]) { return clearedValue < filters[0] } else if (filters[1]) { return clearedValue > filters[1] } return false }, display: (filters, onChange, filterIndex, column) => ( <RangeFilter filters={filters} onChange={onChange} filterIndex={filterIndex} column={column} fieldLabel={fieldLabel} /> ), }, customFilterListOptions: { render: filterVal => { if (filterVal[0] && filterVal[1]) { return `Min ${fieldLabel}: ${filterVal[0]}, Max ${fieldLabel}: ${filterVal[1]}` } else if (filterVal[0]) { return `Min ${fieldLabel}: ${filterVal[0]}` } else if (filterVal[1]) { return `Max ${fieldLabel}: ${filterVal[1]}` } return [] }, update: (filterList, filterPos, index) => { if (filterPos === 0) { filterList[index].splice(filterPos, 1, "") } else if (filterPos === 1) { filterList[index].splice(filterPos, 1) } else if (filterPos === -1) { filterList[index] = [] } return filterList }, }, } } const PriceFilterOperator = Object.freeze({ GT: 0, GTE: 1, LT: 2, LTE: 3, toString: val => { switch (val) { case PriceFilterOperator.GT: return ">" case PriceFilterOperator.GTE: return ">=" case PriceFilterOperator.LT: return "<" case PriceFilterOperator.LTE: return "<=" default: throw Error(`Invalid price filter operator value: ${val}`) } }, }) function debounce(fn, delay) { var timer = null return function() { var context = this, args = arguments clearTimeout(timer) timer = setTimeout(function() { fn.apply(context, args) }, delay) } } class RangeFilter extends React.Component { state = { minVal: this.props.filters[this.props.filterIndex][0] || "", maxVal: this.props.filters[this.props.filterIndex][1] || "", } constructor(props) { super(props) props.filters[props.filterIndex] = ["", ""] } debouncedChange = debounce(this.props.onChange, 400) static getDerivedStateFromProps(props, state) { let { filters, filterIndex } = props let [minVal, maxVal] = filters[filterIndex] if ( (minVal === null || typeof minVal === "undefined") && (maxVal === null || typeof maxVal === "undefined") ) { filters[filterIndex] = ["", ""] return { minVal: "", maxVal: "" } } else return null } render() { let { filterIndex, column, fieldLabel } = this.props return ( <Grid xs={12}> <InputLabel shrink>{fieldLabel}</InputLabel> <FormGroup row style={{ justifyContent: "space-between", marginTop: 16 }}> <TextField style={{ flex: "0 1 45%", }} type="number" placeholder="Min" value={this.state.minVal} onChange={evt => { this.setState({ minVal: evt.target.value }) this.debouncedChange( [evt.target.value, this.state.maxVal], filterIndex, column ) }} /> <TextField style={{ flex: "0 1 45%", }} type="number" placeholder="Max" value={this.state.maxVal} onChange={evt => { this.setState({ maxVal: evt.target.value }) this.debouncedChange( [this.state.minVal, evt.target.value], filterIndex, column ) }} /> </FormGroup> </Grid> ) } } class PriceFilter extends React.Component { state = { operator: this.props.filters[this.props.filterIndex][0] || PriceFilterOperator.GTE, amount: this.props.filters[this.props.filterIndex][1] || "", } constructor(props) { super(props) props.filters[props.filterIndex] = [PriceFilterOperator.GTE, ""] } debouncedChange = debounce(this.props.onChange, 400) static getDerivedStateFromProps(props, state) { let { filters, filterIndex } = props let [operator, amount] = filters[filterIndex] //set initial values if ( (operator === null || typeof operator === "undefined") && (amount === null || typeof amount === "undefined") ) { //need to set value in props as well, otherwise this will be triggered again after //a reset, which causes the state-based updating on the amount field to fail the first time filters[filterIndex] = [PriceFilterOperator.GTE, ""] return { operator: PriceFilterOperator.GTE, amount: "" } } else return null } render() { let { filterIndex, column } = this.props return ( <Grid xs={12}> <InputLabel shrink>Price</InputLabel> <FormGroup row style={{ justifyContent: "space-between", marginTop: 16 }}> <Select style={{ flex: "0 1 20%", textAlign: "center", }} value={this.state.operator} onChange={evt => { this.setState({ operator: evt.target.value }) this.debouncedChange( [evt.target.value, this.state.amount], filterIndex, column ) }} > <MenuItem value={PriceFilterOperator.GT}>></MenuItem> <MenuItem value={PriceFilterOperator.GTE}>>=</MenuItem> <MenuItem value={PriceFilterOperator.LT}><</MenuItem> <MenuItem value={PriceFilterOperator.LTE}><=</MenuItem> </Select> <TextField style={{ flex: "0 1 75%" }} type="number" value={this.state.amount} onChange={evt => { this.setState({ amount: evt.target.value }) this.debouncedChange( [this.state.operator, evt.target.value], filterIndex, column ) }} /> </FormGroup> </Grid> ) } } export const PRICE_FILTER_OPTIONS = { filter: true, filterType: "custom", customFilterListOptions: { render: filterVal => filterVal[1] === "" ? [] : `${PriceFilterOperator.toString(filterVal[0])} ${filterVal[1]}`, }, filterOptions: { display: (filters, onChange, filterIndex, column) => { return ( <PriceFilter filters={filters} onChange={onChange} filterIndex={filterIndex} column={column} /> ) }, logic: (price, filter) => { price = Number(price.replace(/\D/g, "")) let [operator, amount] = filter let include = true //skip filtering if user hasn't actually entered anything if (amount !== null && typeof amount !== "undefined") { switch (operator) { case PriceFilterOperator.GT: include = price > amount break case PriceFilterOperator.GTE: include = price >= amount break case PriceFilterOperator.LT: include = price < amount break case PriceFilterOperator.LTE: include = price <= amount break } } //logic wants to know what we DONT want to show return !include }, }, }