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
JavaScript
;
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;