UNPKG

25.9 kBJavaScriptView Raw
1import { __rest } from "tslib";
2import * as React from 'react';
3import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
4import { css } from '@patternfly/react-styles';
5import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon';
6import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
7import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon';
8import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
9import { DualListSelectorPane } from './DualListSelectorPane';
10import { getUniqueId } from '../../helpers';
11import { flattenTree, flattenTreeWithFolders, filterFolders, filterTreeItems, filterTreeItemsWithoutFolders, filterRestTreeItems } from './treeUtils';
12import { DualListSelectorControlsWrapper } from './DualListSelectorControlsWrapper';
13import { DualListSelectorControl } from './DualListSelectorControl';
14import { DualListSelectorContext } from './DualListSelectorContext';
15export class DualListSelector extends React.Component {
16 constructor(props) {
17 super(props);
18 this.addAllButtonRef = React.createRef();
19 this.addSelectedButtonRef = React.createRef();
20 this.removeSelectedButtonRef = React.createRef();
21 this.removeAllButtonRef = React.createRef();
22 this.onFilterUpdate = (newFilteredOptions, paneType, isSearchReset) => {
23 const { isTree } = this.props;
24 if (paneType === 'available') {
25 if (isSearchReset) {
26 this.setState({
27 availableFilteredOptions: null,
28 availableTreeFilteredOptions: null
29 });
30 return;
31 }
32 if (isTree) {
33 this.setState({
34 availableTreeFilteredOptions: flattenTreeWithFolders(newFilteredOptions)
35 });
36 }
37 else {
38 this.setState({
39 availableFilteredOptions: newFilteredOptions
40 });
41 }
42 }
43 else if (paneType === 'chosen') {
44 if (isSearchReset) {
45 this.setState({
46 chosenFilteredOptions: null,
47 chosenTreeFilteredOptions: null
48 });
49 return;
50 }
51 if (isTree) {
52 this.setState({
53 chosenTreeFilteredOptions: flattenTreeWithFolders(newFilteredOptions)
54 });
55 }
56 else {
57 this.setState({
58 chosenFilteredOptions: newFilteredOptions
59 });
60 }
61 }
62 };
63 this.addAllVisible = () => {
64 this.setState(prevState => {
65 const itemsToRemove = [];
66 const newAvailable = [];
67 const movedOptions = prevState.availableFilteredOptions || prevState.availableOptions;
68 prevState.availableOptions.forEach(value => {
69 if (movedOptions.indexOf(value) !== -1) {
70 itemsToRemove.push(value);
71 }
72 else {
73 newAvailable.push(value);
74 }
75 });
76 const newChosen = [...prevState.chosenOptions, ...itemsToRemove];
77 this.props.addAll && this.props.addAll(newAvailable, newChosen);
78 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
79 return {
80 chosenOptions: newChosen,
81 availableOptions: newAvailable,
82 chosenOptionsSelected: [],
83 availableOptionsSelected: []
84 };
85 });
86 };
87 this.addAllTreeVisible = () => {
88 this.setState(prevState => {
89 const movedOptions = prevState.availableTreeFilteredOptions ||
90 flattenTreeWithFolders(prevState.availableOptions);
91 const newAvailable = prevState.availableOptions
92 .map(opt => Object.assign({}, opt))
93 .filter(item => filterRestTreeItems(item, movedOptions));
94 const currChosen = flattenTree(prevState.chosenOptions);
95 const nextChosenOptions = currChosen.concat(movedOptions);
96 const newChosen = this.createMergedCopy()
97 .map(opt => Object.assign({}, opt))
98 .filter(item => filterTreeItemsWithoutFolders(item, nextChosenOptions));
99 this.props.addAll && this.props.addAll(newAvailable, newChosen);
100 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
101 return {
102 chosenOptions: newChosen,
103 chosenFilteredOptions: newChosen,
104 availableOptions: newAvailable,
105 availableFilteredOptions: newAvailable,
106 availableTreeOptionsChecked: [],
107 chosenTreeOptionsChecked: []
108 };
109 });
110 };
111 this.addSelected = () => {
112 this.setState(prevState => {
113 const itemsToRemove = [];
114 const newAvailable = [];
115 prevState.availableOptions.forEach((value, index) => {
116 if (prevState.availableOptionsSelected.indexOf(index) !== -1) {
117 itemsToRemove.push(value);
118 }
119 else {
120 newAvailable.push(value);
121 }
122 });
123 const newChosen = [...prevState.chosenOptions, ...itemsToRemove];
124 this.props.addSelected && this.props.addSelected(newAvailable, newChosen);
125 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
126 return {
127 chosenOptionsSelected: [],
128 availableOptionsSelected: [],
129 chosenOptions: newChosen,
130 availableOptions: newAvailable
131 };
132 });
133 };
134 this.addTreeSelected = () => {
135 this.setState(prevState => {
136 // Remove selected available nodes from current available nodes
137 const newAvailable = prevState.availableOptions
138 .map(opt => Object.assign({}, opt))
139 .filter(item => filterRestTreeItems(item, prevState.availableTreeOptionsChecked));
140 // Get next chosen options from current + new nodes and remap from base
141 const currChosen = flattenTree(prevState.chosenOptions);
142 const nextChosenOptions = currChosen.concat(prevState.availableTreeOptionsChecked);
143 const newChosen = this.createMergedCopy()
144 .map(opt => Object.assign({}, opt))
145 .filter(item => filterTreeItemsWithoutFolders(item, nextChosenOptions));
146 this.props.addSelected && this.props.addSelected(newAvailable, newChosen);
147 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
148 return {
149 availableTreeOptionsChecked: [],
150 chosenTreeOptionsChecked: [],
151 availableOptions: newAvailable,
152 chosenOptions: newChosen
153 };
154 });
155 };
156 this.removeAllVisible = () => {
157 this.setState(prevState => {
158 const itemsToRemove = [];
159 const newChosen = [];
160 const movedOptions = prevState.chosenFilteredOptions || prevState.chosenOptions;
161 prevState.chosenOptions.forEach(value => {
162 if (movedOptions.indexOf(value) !== -1) {
163 itemsToRemove.push(value);
164 }
165 else {
166 newChosen.push(value);
167 }
168 });
169 const newAvailable = [...prevState.availableOptions, ...itemsToRemove];
170 this.props.removeAll && this.props.removeAll(newAvailable, newChosen);
171 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
172 return {
173 chosenOptions: newChosen,
174 availableOptions: newAvailable,
175 chosenOptionsSelected: [],
176 availableOptionsSelected: []
177 };
178 });
179 };
180 this.removeAllTreeVisible = () => {
181 this.setState(prevState => {
182 const movedOptions = prevState.chosenTreeFilteredOptions ||
183 flattenTreeWithFolders(prevState.chosenOptions);
184 const newChosen = prevState.chosenOptions
185 .map(opt => Object.assign({}, opt))
186 .filter(item => filterRestTreeItems(item, movedOptions));
187 const currAvailable = flattenTree(prevState.availableOptions);
188 const nextAvailableOptions = currAvailable.concat(movedOptions);
189 const newAvailable = this.createMergedCopy()
190 .map(opt => Object.assign({}, opt))
191 .filter(item => filterTreeItemsWithoutFolders(item, nextAvailableOptions));
192 this.props.removeAll && this.props.removeAll(newAvailable, newChosen);
193 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
194 return {
195 chosenOptions: newChosen,
196 availableOptions: newAvailable,
197 availableTreeOptionsChecked: [],
198 chosenTreeOptionsChecked: []
199 };
200 });
201 };
202 this.removeSelected = () => {
203 this.setState(prevState => {
204 const itemsToRemove = [];
205 const newChosen = [];
206 prevState.chosenOptions.forEach((value, index) => {
207 if (prevState.chosenOptionsSelected.indexOf(index) !== -1) {
208 itemsToRemove.push(value);
209 }
210 else {
211 newChosen.push(value);
212 }
213 });
214 const newAvailable = [...prevState.availableOptions, ...itemsToRemove];
215 this.props.removeSelected && this.props.removeSelected(newAvailable, newChosen);
216 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
217 return {
218 chosenOptionsSelected: [],
219 availableOptionsSelected: [],
220 chosenOptions: newChosen,
221 availableOptions: newAvailable
222 };
223 });
224 };
225 this.removeTreeSelected = () => {
226 this.setState(prevState => {
227 // Remove selected chosen nodes from current chosen nodes
228 const newChosen = prevState.chosenOptions
229 .map(opt => Object.assign({}, opt))
230 .filter(item => filterRestTreeItems(item, prevState.chosenTreeOptionsChecked));
231 // Get next chosen options from current and remap from base
232 const currAvailable = flattenTree(prevState.availableOptions);
233 const nextAvailableOptions = currAvailable.concat(prevState.chosenTreeOptionsChecked);
234 const newAvailable = this.createMergedCopy()
235 .map(opt => Object.assign({}, opt))
236 .filter(item => filterTreeItemsWithoutFolders(item, nextAvailableOptions));
237 this.props.removeSelected && this.props.removeSelected(newAvailable, newChosen);
238 this.props.onListChange && this.props.onListChange(newAvailable, newChosen);
239 return {
240 availableTreeOptionsChecked: [],
241 chosenTreeOptionsChecked: [],
242 availableOptions: newAvailable,
243 chosenOptions: newChosen
244 };
245 });
246 };
247 this.onOptionSelect = (e, index, isChosen,
248 /* eslint-disable @typescript-eslint/no-unused-vars */
249 id, itemData, parentData
250 /* eslint-enable @typescript-eslint/no-unused-vars */
251 ) => {
252 this.setState(prevState => {
253 const originalArray = isChosen ? prevState.chosenOptionsSelected : prevState.availableOptionsSelected;
254 let updatedArray = null;
255 if (originalArray.indexOf(index) !== -1) {
256 updatedArray = originalArray.filter(value => value !== index);
257 }
258 else {
259 updatedArray = [...originalArray, index];
260 }
261 return {
262 chosenOptionsSelected: isChosen ? updatedArray : prevState.chosenOptionsSelected,
263 availableOptionsSelected: isChosen ? prevState.availableOptionsSelected : updatedArray
264 };
265 });
266 this.props.onOptionSelect && this.props.onOptionSelect(e, index, isChosen, id, itemData, parentData);
267 };
268 this.isChecked = (treeItem, isChosen) => isChosen
269 ? this.state.chosenTreeOptionsChecked.includes(treeItem.id)
270 : this.state.availableTreeOptionsChecked.includes(treeItem.id);
271 this.areAllDescendantsChecked = (treeItem, isChosen) => treeItem.children
272 ? treeItem.children.every(child => this.areAllDescendantsChecked(child, isChosen))
273 : this.isChecked(treeItem, isChosen);
274 this.areSomeDescendantsChecked = (treeItem, isChosen) => treeItem.children
275 ? treeItem.children.some(child => this.areSomeDescendantsChecked(child, isChosen))
276 : this.isChecked(treeItem, isChosen);
277 this.mapChecked = (item, isChosen) => {
278 const hasCheck = this.areAllDescendantsChecked(item, isChosen);
279 item.isChecked = false;
280 if (hasCheck) {
281 item.isChecked = true;
282 }
283 else {
284 const hasPartialCheck = this.areSomeDescendantsChecked(item, isChosen);
285 if (hasPartialCheck) {
286 item.isChecked = null;
287 }
288 }
289 if (item.children) {
290 return Object.assign(Object.assign({}, item), { children: item.children.map(child => this.mapChecked(child, isChosen)) });
291 }
292 return item;
293 };
294 this.onTreeOptionCheck = (evt, isChecked, itemData, isChosen) => {
295 const { availableOptions, availableTreeFilteredOptions, chosenOptions, chosenTreeFilteredOptions } = this.state;
296 let panelOptions;
297 if (isChosen) {
298 if (chosenTreeFilteredOptions) {
299 panelOptions = chosenOptions
300 .map(opt => Object.assign({}, opt))
301 .filter(item => filterTreeItemsWithoutFolders(item, chosenTreeFilteredOptions));
302 }
303 else {
304 panelOptions = chosenOptions;
305 }
306 }
307 else {
308 if (availableTreeFilteredOptions) {
309 panelOptions = availableOptions
310 .map(opt => Object.assign({}, opt))
311 .filter(item => filterTreeItemsWithoutFolders(item, availableTreeFilteredOptions));
312 }
313 else {
314 panelOptions = availableOptions;
315 }
316 }
317 const checkedOptionTree = panelOptions
318 .map(opt => Object.assign({}, opt))
319 .filter(item => filterTreeItems(item, [itemData.id]));
320 const flatTree = flattenTreeWithFolders(checkedOptionTree);
321 const prevChecked = isChosen ? this.state.chosenTreeOptionsChecked : this.state.availableTreeOptionsChecked;
322 let updatedChecked = [];
323 if (isChecked) {
324 updatedChecked = prevChecked.concat(flatTree.filter(id => !prevChecked.includes(id)));
325 }
326 else {
327 updatedChecked = prevChecked.filter(id => !flatTree.includes(id));
328 }
329 this.setState(prevState => ({
330 availableTreeOptionsChecked: isChosen ? prevState.availableTreeOptionsChecked : updatedChecked,
331 chosenTreeOptionsChecked: isChosen ? updatedChecked : prevState.chosenTreeOptionsChecked
332 }), () => {
333 this.props.onOptionCheck && this.props.onOptionCheck(evt, isChecked, itemData.id, updatedChecked);
334 });
335 };
336 this.state = {
337 availableOptions: [...this.props.availableOptions],
338 availableOptionsSelected: [],
339 availableFilteredOptions: null,
340 availableTreeFilteredOptions: null,
341 chosenOptions: [...this.props.chosenOptions],
342 chosenOptionsSelected: [],
343 chosenFilteredOptions: null,
344 chosenTreeFilteredOptions: null,
345 availableTreeOptionsChecked: [],
346 chosenTreeOptionsChecked: []
347 };
348 }
349 // If the DualListSelector uses trees, concat the two initial arrays and merge duplicate folder IDs
350 createMergedCopy() {
351 const copyOfAvailable = JSON.parse(JSON.stringify(this.props.availableOptions));
352 const copyOfChosen = JSON.parse(JSON.stringify(this.props.chosenOptions));
353 return this.props.isTree
354 ? Object.values(copyOfAvailable
355 .concat(copyOfChosen)
356 .reduce((mapObj, item) => {
357 const key = item.id;
358 if (mapObj[key]) {
359 // If map already has an item ID, add the dupe ID's children to the existing map
360 mapObj[key].children.push(...item.children);
361 }
362 else {
363 // Else clone the item data
364 mapObj[key] = Object.assign({}, item);
365 }
366 return mapObj;
367 }, {}))
368 : null;
369 }
370 componentDidUpdate() {
371 if (JSON.stringify(this.props.availableOptions) !== JSON.stringify(this.state.availableOptions) ||
372 JSON.stringify(this.props.chosenOptions) !== JSON.stringify(this.state.chosenOptions)) {
373 this.setState({
374 availableOptions: [...this.props.availableOptions],
375 chosenOptions: [...this.props.chosenOptions]
376 });
377 }
378 }
379 render() {
380 const _a = this.props, { availableOptionsTitle, availableOptionsActions, availableOptionsSearchAriaLabel, className, children, chosenOptionsTitle, chosenOptionsActions, chosenOptionsSearchAriaLabel, filterOption, isSearchable, chosenOptionsStatus, availableOptionsStatus, controlsAriaLabel, addAllAriaLabel, addSelectedAriaLabel, removeSelectedAriaLabel, removeAllAriaLabel,
381 /* eslint-disable @typescript-eslint/no-unused-vars */
382 availableOptions: consumerPassedAvailableOptions, chosenOptions: consumerPassedChosenOptions, removeSelected, addAll, removeAll, addSelected, onListChange, onAvailableOptionsSearchInputChanged, onChosenOptionsSearchInputChanged, onOptionSelect, onOptionCheck, id, isTree, isDisabled, addAllTooltip, addAllTooltipProps, addSelectedTooltip, addSelectedTooltipProps, removeAllTooltip, removeAllTooltipProps, removeSelectedTooltip, removeSelectedTooltipProps } = _a, props = __rest(_a, ["availableOptionsTitle", "availableOptionsActions", "availableOptionsSearchAriaLabel", "className", "children", "chosenOptionsTitle", "chosenOptionsActions", "chosenOptionsSearchAriaLabel", "filterOption", "isSearchable", "chosenOptionsStatus", "availableOptionsStatus", "controlsAriaLabel", "addAllAriaLabel", "addSelectedAriaLabel", "removeSelectedAriaLabel", "removeAllAriaLabel", "availableOptions", "chosenOptions", "removeSelected", "addAll", "removeAll", "addSelected", "onListChange", "onAvailableOptionsSearchInputChanged", "onChosenOptionsSearchInputChanged", "onOptionSelect", "onOptionCheck", "id", "isTree", "isDisabled", "addAllTooltip", "addAllTooltipProps", "addSelectedTooltip", "addSelectedTooltipProps", "removeAllTooltip", "removeAllTooltipProps", "removeSelectedTooltip", "removeSelectedTooltipProps"]);
383 const { availableOptions, chosenOptions, chosenOptionsSelected, availableOptionsSelected, chosenTreeOptionsChecked, availableTreeOptionsChecked } = this.state;
384 const availableOptionsStatusToDisplay = availableOptionsStatus ||
385 (isTree
386 ? `${filterFolders(availableOptions, availableTreeOptionsChecked).length} of ${flattenTree(availableOptions).length} items selected`
387 : `${availableOptionsSelected.length} of ${availableOptions.length} items selected`);
388 const chosenOptionsStatusToDisplay = chosenOptionsStatus ||
389 (isTree
390 ? `${filterFolders(chosenOptions, chosenTreeOptionsChecked).length} of ${flattenTree(chosenOptions).length} items selected`
391 : `${chosenOptionsSelected.length} of ${chosenOptions.length} items selected`);
392 const available = isTree
393 ? availableOptions.map(item => this.mapChecked(item, false))
394 : availableOptions;
395 const chosen = isTree
396 ? chosenOptions.map(item => this.mapChecked(item, true))
397 : chosenOptions;
398 return (React.createElement(DualListSelectorContext.Provider, { value: { isTree } },
399 React.createElement("div", Object.assign({ className: css(styles.dualListSelector, className), id: id }, props), children === '' ? (React.createElement(React.Fragment, null,
400 React.createElement(DualListSelectorPane, { isSearchable: isSearchable, onFilterUpdate: this.onFilterUpdate, searchInputAriaLabel: availableOptionsSearchAriaLabel, filterOption: filterOption, onSearchInputChanged: onAvailableOptionsSearchInputChanged, status: availableOptionsStatusToDisplay, title: availableOptionsTitle, options: available, selectedOptions: isTree ? availableTreeOptionsChecked : availableOptionsSelected, onOptionSelect: this.onOptionSelect, onOptionCheck: (e, isChecked, itemData) => this.onTreeOptionCheck(e, isChecked, itemData, false), actions: availableOptionsActions, id: `${id}-available-pane`, isDisabled: isDisabled }),
401 React.createElement(DualListSelectorControlsWrapper, { "aria-label": controlsAriaLabel },
402 React.createElement(DualListSelectorControl, { isDisabled: (isTree ? availableTreeOptionsChecked.length === 0 : availableOptionsSelected.length === 0) ||
403 isDisabled, onClick: isTree ? this.addTreeSelected : this.addSelected, ref: this.addSelectedButtonRef, "aria-label": addSelectedAriaLabel, tooltipContent: addSelectedTooltip, tooltipProps: addSelectedTooltipProps },
404 React.createElement(AngleRightIcon, null)),
405 React.createElement(DualListSelectorControl, { isDisabled: availableOptions.length === 0 || isDisabled, onClick: isTree ? this.addAllTreeVisible : this.addAllVisible, ref: this.addAllButtonRef, "aria-label": addAllAriaLabel, tooltipContent: addAllTooltip, tooltipProps: addAllTooltipProps },
406 React.createElement(AngleDoubleRightIcon, null)),
407 React.createElement(DualListSelectorControl, { isDisabled: chosenOptions.length === 0 || isDisabled, onClick: isTree ? this.removeAllTreeVisible : this.removeAllVisible, "aria-label": removeAllAriaLabel, ref: this.removeAllButtonRef, tooltipContent: removeAllTooltip, tooltipProps: removeAllTooltipProps },
408 React.createElement(AngleDoubleLeftIcon, null)),
409 React.createElement(DualListSelectorControl, { onClick: isTree ? this.removeTreeSelected : this.removeSelected, isDisabled: (isTree ? chosenTreeOptionsChecked.length === 0 : chosenOptionsSelected.length === 0) || isDisabled, ref: this.removeSelectedButtonRef, "aria-label": removeSelectedAriaLabel, tooltipContent: removeSelectedTooltip, tooltipProps: removeSelectedTooltipProps },
410 React.createElement(AngleLeftIcon, null))),
411 React.createElement(DualListSelectorPane, { isChosen: true, isSearchable: isSearchable, onFilterUpdate: this.onFilterUpdate, searchInputAriaLabel: chosenOptionsSearchAriaLabel, filterOption: filterOption, onSearchInputChanged: onChosenOptionsSearchInputChanged, title: chosenOptionsTitle, status: chosenOptionsStatusToDisplay, options: chosen, selectedOptions: isTree ? chosenTreeOptionsChecked : chosenOptionsSelected, onOptionSelect: this.onOptionSelect, onOptionCheck: (e, isChecked, itemData) => this.onTreeOptionCheck(e, isChecked, itemData, true), actions: chosenOptionsActions, id: `${id}-chosen-pane`, isDisabled: isDisabled }))) : (children))));
412 }
413}
414DualListSelector.displayName = 'DualListSelector';
415DualListSelector.defaultProps = {
416 children: '',
417 availableOptions: [],
418 availableOptionsTitle: 'Available options',
419 availableOptionsSearchAriaLabel: 'Available search input',
420 chosenOptions: [],
421 chosenOptionsTitle: 'Chosen options',
422 chosenOptionsSearchAriaLabel: 'Chosen search input',
423 id: getUniqueId('dual-list-selector'),
424 controlsAriaLabel: 'Selector controls',
425 addAllAriaLabel: 'Add all',
426 addSelectedAriaLabel: 'Add selected',
427 removeSelectedAriaLabel: 'Remove selected',
428 removeAllAriaLabel: 'Remove all',
429 isTree: false,
430 isDisabled: false
431};
432//# sourceMappingURL=DualListSelector.js.map
\No newline at end of file