UNPKG

redux-search-filter

Version:

[![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![Test coverage][codecov-image]][codecov-url] [![npm download][download-image]][download-url]

617 lines (563 loc) 15.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var react = require('react'); var reactRedux = require('react-redux'); var PropTypes = _interopDefault(require('prop-types')); var immutable = require('immutable'); var lodashProperty = _interopDefault(require('lodash-es/property')); var min = _interopDefault(require('ml-array-min')); var max = _interopDefault(require('ml-array-max')); var reselect = require('reselect'); const prefix = '@@redux-search-filter/'; const RESET = `${prefix}RESET`; const UPDATE_FILTER = `${prefix}UPDATE_FILTER`; const SET_OPERATOR = `${prefix}SET_OPERATOR`; const SET_NEGATED = `${prefix}SET_NEGATED`; var actionTypes = /*#__PURE__*/Object.freeze({ prefix: prefix, RESET: RESET, UPDATE_FILTER: UPDATE_FILTER, SET_OPERATOR: SET_OPERATOR, SET_NEGATED: SET_NEGATED }); function updateFilter(name, filterName, prop, kind, value) { if (value && value.target) { // convert react event to a value value = value.target.value; } if (!value) value = null; return getAction(UPDATE_FILTER, name, filterName, prop, kind, value); } function setNegated(name, filterName, prop, kind, value) { if (value && value.target) { value = value.target.checked; } value = !!value; return getAction(SET_NEGATED, name, filterName, prop, kind, value); } function setOperator(name, filterName, prop, kind, value) { if (value && value.target) { value = value.target.value; } return getAction(SET_OPERATOR, name, filterName, prop, kind, value); } const reset = (name) => ({ type: RESET, meta: { name } }); function getAction(type, name, filterName, prop, kind, value) { return { type, meta: { name, filterName, prop, kind }, payload: value }; } class SearchFilter { constructor(options) { if (typeof options.name !== 'string') { throw new TypeError('filter name must be a string'); } if (typeof options.getData !== 'function') { throw new TypeError('getData must be a function'); } this.name = options.name; this.getData = options.getData; } } function searchFilter(options) { const searchFilterInstance = new SearchFilter(options); return function (UserComponent) { class SearchFilterComponent extends react.Component { render() { const props = Object.assign({}, this.props); return react.createElement(UserComponent, props); } getChildContext() { return { searchFilter: searchFilterInstance }; } } SearchFilterComponent.childContextTypes = { searchFilter: PropTypes.object }; const connector = reactRedux.connect( (state) => { return { data: searchFilterInstance.getData(state) }; }, { reset: () => reset(searchFilterInstance.name) } ); return connector(SearchFilterComponent); }; } const value = 'value'; const multiple = 'multiple'; const range = 'range'; const AND = 'AND'; const OR = 'OR'; const defaultMultipleFilter = { value: [], negated: false, operator: AND }; const defaultValueFilter = { value: [], negated: false, operator: OR }; function searchFilterReducer(state = new immutable.Map(), action) { let name, filters, filterName, filterProp; if (action.meta && action.meta.name) { name = action.meta.name; filters = state.get(name) || new immutable.Map(); filterProp = action.meta.prop; filterName = action.meta.filterName || filterProp; } switch (action.type) { case RESET: { return state.set(name, filters.clear()); } case UPDATE_FILTER: { if (action.payload === null) { return state.set(name, filters.delete(filterName)); } else { return state.set( name, filters.set( filterName, formatValue( action.meta.kind, filterProp, filters.get(filterName), action.payload ) ) ); } } case SET_NEGATED: { const oldValue = filters.get(filterName); if (oldValue && oldValue.negated === action.payload) { return state; } else { return state.set( name, filters.set( filterName, setNegated$1(action.meta.kind, filterProp, oldValue, action.payload) ) ); } } case SET_OPERATOR: { const oldValue = filters.get(filterName); if (oldValue && oldValue.operator === action.payload) { return state; } else { return state.set( name, filters.set( filterName, setOperator$1(action.meta.kind, filterProp, oldValue, action.payload) ) ); } } default: return state; } } function setNegated$1(kind, prop, filter, payload) { const result = { prop }; if (typeof payload !== 'boolean') { throw new TypeError( `SET_NEGATED expects a boolean value. Received ${typeof payload}` ); } switch (kind) { case value: case multiple: { return Object.assign(result, getDefaultFilter(kind), filter, { negated: payload }); } default: throw unexpectedKind(kind); } } function setOperator$1(kind, prop, filter, payload) { const result = { prop }; if (payload !== AND && payload !== OR) { throw new RangeError(`wrong operator: ${payload}`); } switch (kind) { case value: case multiple: { return Object.assign(result, getDefaultFilter(kind), filter, { operator: payload }); } default: throw unexpectedKind(kind); } } function formatValue(kind, prop, filter, payload) { const result = { prop }; switch (kind) { case value: case multiple: return Object.assign(result, getDefaultFilter(kind), filter, { value: Array.isArray(payload) ? payload : [payload] }); case range: { if ( typeof payload !== 'object' || payload === null || typeof payload.min !== 'number' || typeof payload.max !== 'number' ) { throw new TypeError('range value must have a min and a max'); } if (payload.min >= payload.max) { throw new RangeError('range min must be smaller than range max'); } return Object.assign(result, payload); } default: throw unexpectedKind(kind); } } function getDefaultFilter(kind) { switch (kind) { case multiple: return defaultMultipleFilter; case value: return defaultValueFilter; default: throw unexpectedKind(kind); } } function unexpectedKind(kind) { return new Error(`unexpected kind: ${kind}`); } const MULTIPLE_EMPTY = 'MULTIPLE_EMPTY'; function filterData(data, filters) { filters = Array.from(filters); filters = filters.map(([name, filterOptions]) => [ name, filterOptions, lodashProperty(filterOptions.prop) ]); return data.filter((item) => { main: for (const [, filterOptions, filterProp] of filters) { const filterValue = filterOptions.value; let operator = filterOptions.operator; let negated = filterOptions.negated; const itemValue = filterProp(item); if (filterValue) { if (filterValue.length > 0) { if (Array.isArray(itemValue)) { // kind: multiple operator = operator || AND; if (operator === AND) { for (const value of filterValue) { if ( itemValue.includes(value) || (value === MULTIPLE_EMPTY && itemValue.length === 0) ) { if (negated) { return false; } } else { if (!negated) { return false; } } } } else { for (const value of filterValue) { if ( itemValue.includes(value) || (value === MULTIPLE_EMPTY && itemValue.length === 0) ) { if (!negated) { continue main; } } else { if (negated) { continue main; } } } return false; } } else { // kind: value operator = operator || OR; const isIncluded = filterValue.includes(filterProp(item)); if (operator === AND) { if (negated) { if (isIncluded) { return false; } } else { if (filterValue.length > 1) return false; if (!isIncluded) { return false; } } } else { if (negated) { if (filterValue.length > 1) { continue; } if (isIncluded) { return false; } } else { if (!isIncluded) { return false; } } } } } } else { // kind: range const value = filterProp(item); if ( typeof value !== 'number' || value < filterOptions.min || value > filterOptions.max || Number.isNaN(value) ) { return false; } } } return true; }); } function filterOneSelector(data, name, propFunc, kind, filters) { switch (kind) { case value: return filterValue(data, name, propFunc, filters); case multiple: return filterMultiple(data, name, propFunc, filters); case range: return filterRange(data, name, propFunc, filters); default: throw new Error(`invalid filter kind: ${kind}`); } } function filterValue(data, name, propFunc, filters) { data = filterDataFor(data, name, filters); return makeArray(countBy(data, propFunc)); } function filterMultiple(data, name, propFunc, filters) { data = filterDataFor(data, name, filters); const filter = filters && filters.get(name); if (filter && filter.negated) { return makeArray(countMultipleNegated(data, propFunc)); } else { return makeArray(countMultiple(data, propFunc)); } } function filterRange(data, name, propFunc, filters) { data = filterDataFor(data, name, filters); data = data.map((item) => propFunc(item)).filter(isNumber); if (data.length === 0) { return { min: 0, max: 0 }; } return { min: min(data), max: max(data) }; } function filterDataFor(data, name, filters) { if (!filters) return data; filters = filters.filter((item, key) => { return key !== name; }); if (filters.size > 0) { data = filterData(data, filters); } return data; } function makeArray(counts) { const result = []; for (const [value$$1, count] of counts) { result.push({ value: value$$1, count }); } result.sort((a, b) => b.count - a.count); return result; } function countMultiple(data, propFunc) { const counts = new Map(); for (const item of data) { const value$$1 = propFunc(item); if (!value$$1 || !value$$1[Symbol.iterator]) { continue; } if (value$$1.length === 0) { if (!counts.has(MULTIPLE_EMPTY)) { counts.set(MULTIPLE_EMPTY, 1); } else { counts.set(MULTIPLE_EMPTY, counts.get(MULTIPLE_EMPTY) + 1); } } for (const el of value$$1) { if (!counts.has(el)) { counts.set(el, 1); } else { counts.set(el, counts.get(el) + 1); } } } return counts; } function countMultipleNegated(data, propFunc) { const counts = countMultiple(data, propFunc); for (const [key, value$$1] of counts) { counts.set(key, data.length - value$$1); } return counts; } function countBy(array, accessor) { const counts = new Map(); for (const element of array) { const value$$1 = accessor(element); if (!counts.has(value$$1)) { counts.set(value$$1, 1); } else { counts.set(value$$1, counts.get(value$$1) + 1); } } return counts; } function isNumber(x) { return typeof x === 'number'; } const specialProps = new Set(['component', 'data', 'filterUpdate', 'filters']); class ConnectedFilter extends react.Component { constructor(props) { super(props); this.setupSelector(); } setupSelector() { const data = (props) => props.data; const name = (props) => props.name; const propFunc = (props) => lodashProperty(props.prop); const kind = (props) => props.kind; const filters = (props) => props.filters; this.selector = reselect.createSelector( data, name, propFunc, kind, filters, filterOneSelector ); } render() { const currentFilter = this.props.filters ? this.props.filters.get(this.props.name) : null; const newProps = { onChange: this.props.filterUpdate }; for (var propName in this.props) { if (!specialProps.has(propName)) { newProps[propName] = this.props[propName]; } } if (this.props.kind === range) { newProps.range = this.selector(this.props); newProps.value = currentFilter; } else { newProps.options = this.selector(this.props); newProps.filter = currentFilter; } return react.createElement(this.props.component, newProps); } } var ConnectedFilter$1 = reactRedux.connect( (state, props) => { const searchFilterName = props.searchFilter.name; return { data: props.searchFilter.getData(state), filters: state.searchFilter.get(searchFilterName) }; }, (dispatch, props) => { const name = props.searchFilter.name; return { filterUpdate: (value$$1) => dispatch(updateFilter(name, props.name, props.prop, props.kind, value$$1)), setOperator: (value$$1) => dispatch(setOperator(name, props.name, props.prop, props.kind, value$$1)), setNegated: (value$$1) => dispatch(setNegated(name, props.name, props.prop, props.kind, value$$1)) }; } )(ConnectedFilter); class Filter extends react.Component { render() { const props = Object.assign({}, this.props, { searchFilter: this.context.searchFilter }); if (props.name === undefined) { props.name = props.prop; } return react.createElement(ConnectedFilter$1, props); } } Filter.contextTypes = { searchFilter: PropTypes.object }; Filter.propTypes = { prop: PropTypes.string.isRequired, kind: PropTypes.string.isRequired, component: PropTypes.func.isRequired, name: PropTypes.string }; function getFilteredData(name, getData) { const getFilter = (state) => state.searchFilter.get(name); return reselect.createSelector(getData, getFilter, (data, filter) => { let filteredData = data; if (filter !== undefined && filter.size > 0) { filteredData = filterData(data, filter); } return filteredData; }); } const reset$1 = reset; const updateFilter$1 = updateFilter; const setOperator$2 = setOperator; const setNegated$2 = setNegated; exports.actionTypes = actionTypes; exports.reset = reset$1; exports.updateFilter = updateFilter$1; exports.setOperator = setOperator$2; exports.setNegated = setNegated$2; exports.searchFilter = searchFilter; exports.reducer = searchFilterReducer; exports.Filter = Filter; exports.getFilteredData = getFilteredData; exports.MULTIPLE_EMPTY = MULTIPLE_EMPTY;