Skip to content

Commit 69c29a0

Browse files
authored
Merge pull request #471 from rvsia/muiDualListSelect
Add MUI dual-list-select component
2 parents d705b19 + 35205a5 commit 69c29a0

File tree

13 files changed

+1603
-266
lines changed

13 files changed

+1603
-266
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useReducer } from 'react';
2+
import PropTypes from 'prop-types';
3+
import isEqual from 'lodash/isEqual';
4+
5+
import { useFieldApi } from '@data-driven-forms/react-form-renderer';
6+
7+
import reducer, { initialState } from './reducer';
8+
9+
const getOptionsGroup = (value, lastClicked, options) => {
10+
const lastIndex = options.map(({ value }) => value.toString()).indexOf(lastClicked.toString());
11+
const currentIndex = options.map(({ value }) => value.toString()).indexOf(value);
12+
const startIndex = Math.min(lastIndex, currentIndex);
13+
const endIndex = Math.max(lastIndex, currentIndex) + 1;
14+
return [...options.slice(startIndex, endIndex).map(({ value }) => value.toString())];
15+
};
16+
17+
const handleOptionClick = (event, value, options, isRight, dispatch, state) => {
18+
const selectedKey = isRight ? 'selectedLeftValues' : 'selectedRightValues';
19+
const lastKey = isRight ? 'lastLeftClicked' : 'lastRightClicked';
20+
if (event.shiftKey && state[lastKey]) {
21+
dispatch({ type: 'setSelectedValue', value, values: getOptionsGroup(value, state[lastKey], options), isRight });
22+
} else if (event.ctrlKey && state[lastKey]) {
23+
const selectedValues = state[selectedKey].includes(value) ? state[selectedKey].filter((item) => item !== value) : [...state[selectedKey], value];
24+
25+
dispatch({ type: 'setSelectedValue', value, values: selectedValues, isRight });
26+
} else {
27+
dispatch({ type: 'setSelectedValue', value, values: [value], isRight });
28+
}
29+
};
30+
31+
const DualListSelectCommon = (props) => {
32+
const [state, dispatch] = useReducer(reducer, initialState);
33+
34+
const { DualListSelect, ...rest } = useFieldApi({
35+
...props,
36+
isEqual: (current, initial) => isEqual([...(current || [])].sort(), [...(initial || [])].sort())
37+
});
38+
39+
const leftValues = rest.options
40+
.filter((option) => !rest.input.value.includes(option.value) && option.label.includes(state.filterOptions))
41+
.sort((a, b) => (state.sortLeftDesc ? a.label.localeCompare(b.label) : b.label.localeCompare(a.label)));
42+
const rightValues = rest.options
43+
.filter((option) => rest.input.value.includes(option.value) && option.label.includes(state.filterValue))
44+
.sort((a, b) => (state.sortRightDesc ? a.label.localeCompare(b.label) : b.label.localeCompare(a.label)));
45+
46+
const handleOptionsClick = (event, value) => handleOptionClick(event, value, leftValues, true, dispatch, state);
47+
48+
const handleValuesClick = (event, value) => handleOptionClick(event, value, rightValues, false, dispatch, state);
49+
50+
const handleMoveRight = () => {
51+
rest.input.onChange([...rest.input.value, ...state.selectedLeftValues]);
52+
dispatch({ type: 'clearLeftOptions' });
53+
};
54+
55+
const handleMoveLeft = () => {
56+
rest.input.onChange(rest.input.value.filter((value) => !state.selectedRightValues.includes(value)));
57+
dispatch({ type: 'clearRightValues' });
58+
};
59+
60+
const sortOptions = () => dispatch({ type: 'sortOptions' });
61+
62+
const sortValues = () => dispatch({ type: 'sortValue' });
63+
64+
const filterOptions = (value) => dispatch({ type: 'setFilterOptions', value });
65+
66+
const filterValues = (value) => dispatch({ type: 'setFilterValue', value });
67+
68+
const handleClearLeftValues = () => {
69+
dispatch({ type: 'clearLeftValues' });
70+
rest.input.onChange([...rest.input.value, ...leftValues.map(({ value }) => value)]);
71+
};
72+
73+
const handleClearRightValues = () => {
74+
dispatch({ type: 'clearRightValue' });
75+
rest.input.onChange([...rest.input.value.filter((val) => !rightValues.find(({ value }) => val === value))]);
76+
};
77+
78+
return (
79+
<DualListSelect
80+
{...rest}
81+
leftValues={leftValues}
82+
rightValues={rightValues}
83+
handleOptionsClick={handleOptionsClick}
84+
handleValuesClick={handleValuesClick}
85+
handleMoveRight={handleMoveRight}
86+
handleMoveLeft={handleMoveLeft}
87+
sortOptions={sortOptions}
88+
sortValues={sortValues}
89+
filterOptions={filterOptions}
90+
filterValues={filterValues}
91+
handleClearLeftValues={handleClearLeftValues}
92+
handleClearRightValues={handleClearRightValues}
93+
state={state}
94+
/>
95+
);
96+
};
97+
98+
DualListSelectCommon.propTypes = {
99+
DualListSelect: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
100+
};
101+
102+
export default DualListSelectCommon;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export const initialState = {
2+
lastLeftClicked: undefined,
3+
selectedLeftValues: [],
4+
lastRightClicked: undefined,
5+
selectedRightValues: [],
6+
sortLeftDesc: true,
7+
sortRightDesc: true,
8+
filterOptions: '',
9+
filterValue: ''
10+
};
11+
12+
const reducer = (state, { type, value, values, isRight }) => {
13+
switch (type) {
14+
case 'setSelectedValue':
15+
return {
16+
...state,
17+
...(isRight ? { selectedLeftValues: values } : { selectedRightValues: values }),
18+
...(isRight ? { lastLeftClicked: value } : { lastRightClicked: value })
19+
};
20+
case 'setFilterValue':
21+
return {
22+
...state,
23+
filterValue: value
24+
};
25+
case 'setFilterOptions':
26+
return {
27+
...state,
28+
filterOptions: value
29+
};
30+
case 'sortValue':
31+
return {
32+
...state,
33+
sortRightDesc: !state.sortRightDesc
34+
};
35+
case 'sortOptions':
36+
return {
37+
...state,
38+
sortLeftDesc: !state.sortLeftDesc
39+
};
40+
case 'clearRightValues':
41+
return {
42+
...state,
43+
selectedRightValues: []
44+
};
45+
case 'clearLeftOptions':
46+
return {
47+
...state,
48+
selectedLeftValues: []
49+
};
50+
default:
51+
return state;
52+
}
53+
};
54+
55+
export default reducer;

packages/mui-component-mapper/src/files/component-mapper.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Select from './select';
1212
import Radio from './radio';
1313
import Wizard from './wizard';
1414
import FieldArray from './field-array';
15+
import DualListSelect from './dual-list-select';
1516

1617
export const components = {
1718
TextField,
@@ -24,7 +25,8 @@ export const components = {
2425
TimePicker,
2526
PlainText,
2627
SubForm,
27-
Wizard
28+
Wizard,
29+
DualListSelect
2830
};
2931

3032
const componentMapper = {
@@ -40,7 +42,8 @@ const componentMapper = {
4042
[componentTypes.SWITCH]: Switch,
4143
[componentTypes.PLAIN_TEXT]: PlainText,
4244
[componentTypes.WIZARD]: Wizard,
43-
[componentTypes.FIELD_ARRAY]: FieldArray
45+
[componentTypes.FIELD_ARRAY]: FieldArray,
46+
[componentTypes.DUAL_LIST_SELECT]: DualListSelect
4447
};
4548

4649
export default componentMapper;

0 commit comments

Comments
 (0)