Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / items / useItemsSelection.ts
blobe620c4fb84550c3846cf36abc65859686d5b7656
1 import type { ChangeEvent, DependencyList } from 'react';
2 import { useEffect, useMemo, useState } from 'react';
4 import useHandler from '@proton/components/hooks/useHandler';
5 import unique from '@proton/utils/unique';
7 interface Props {
8     activeID?: string;
9     allIDs: string[];
10     resetDependencies?: DependencyList;
11     onCheck?: (checked: boolean) => void;
12     rowMode?: boolean;
15 /**
16  * Implement the selection logic shared between mail and contacts
17  * You have an active id which represents the selection if there is no checked items
18  * As soon as you have checked items, it replaces the active item
19  * Items can be any object, we only deal with IDs
20  * @param activeID The current active item
21  * @param allIDs The complete list of ids in the list
22  * @param resetDependencies React dependencies to reset selection if there is a change
23  * @param onCheck Optional action to be triggered when interacting with element checkboxes
24  * @param rowMode Used only for mail since we keep checkedMap state in row mode
25  * @returns all helpers useful to check one, a range or all items
26  */
27 const useItemsSelection = ({ activeID, allIDs, rowMode, resetDependencies, onCheck }: Props) => {
28     // We are managing checked IDs through a Map and not an array for performance issues.
29     const [checkedMap, setCheckedMap] = useState<{ [ID: string]: boolean }>({});
31     // Last item check to deal with range selection
32     const [lastChecked, setLastChecked] = useState<string>('');
34     const isChecked = (ID: string) => !!checkedMap[ID];
36     useEffect(() => setCheckedMap({}), resetDependencies || []);
38     const checkedIDs = useMemo(() => {
39         return Object.keys(checkedMap).filter((ID) => checkedMap[ID]);
40     }, [checkedMap]);
42     const selectedIDs = useMemo(() => {
43         // In row mode, activeID has priority over checkedIDs
44         if (activeID && rowMode) {
45             return [activeID];
46         }
47         if (checkedIDs.length) {
48             return checkedIDs;
49         }
50         if (activeID) {
51             return [activeID];
52         }
53         return [];
54     }, [checkedIDs, activeID]);
56     /**
57      * Put *IDs* to *checked* state
58      * Uncheck others if *replace* is true
59      */
60     const handleCheck = useHandler((IDs: string[], checked: boolean, replace: boolean) => {
61         // Run onCheck function when interacting with a checkbox
62         onCheck?.(checked);
63         // Items can be checked and included in a new selection (select all/select range).
64         // In that case they will be duplicated in the array, which could break the length we expect in the 2nd condition
65         const uniqueIDs = unique(IDs);
66         if (uniqueIDs.length === 0) {
67             setCheckedMap({});
68         } else if (uniqueIDs.length === allIDs.length) {
69             setCheckedMap(
70                 allIDs.reduce<{ [ID: string]: boolean }>((acc, ID) => {
71                     acc[ID] = checked;
72                     return acc;
73                 }, {})
74             );
75         } else {
76             setCheckedMap(
77                 allIDs.reduce<{ [ID: string]: boolean }>((acc, ID) => {
78                     const wasChecked = isChecked(ID);
79                     const toCheck = IDs.includes(ID);
80                     let value: boolean;
81                     if (toCheck) {
82                         value = checked;
83                     } else if (replace) {
84                         value = !checked;
85                     } else {
86                         value = wasChecked;
87                     }
88                     acc[ID] = value;
89                     return acc;
90                 }, {})
91             );
92         }
94         setLastChecked('');
95     });
97     /**
98      * Check or uncheck all items
99      */
100     const handleCheckAll = useHandler((check: boolean) =>
101         check ? handleCheck(allIDs, true, true) : handleCheck([], true, true)
102     );
104     /**
105      * Just check the given id, nothing more
106      */
107     const handleCheckOnlyOne = useHandler((id: string) => {
108         handleCheck([id], !isChecked(id), false);
109         setLastChecked(id);
110     });
112     /**
113      * Check all items from the last checked to the given id
114      */
115     const handleCheckRange = useHandler((id: string) => {
116         const ids = [id];
118         if (lastChecked) {
119             const start = allIDs.findIndex((ID) => ID === id);
120             const end = allIDs.findIndex((ID) => ID === lastChecked);
122             ids.push(...allIDs.slice(Math.min(start, end), Math.max(start, end) + 1));
123         }
125         handleCheck(ids, !isChecked(id), false);
126         setLastChecked(id);
127     });
129     /**
130      * Check only one or check range depending on the shift key value in the event
131      */
132     const handleCheckOne = useHandler((event: ChangeEvent, id: string) => {
133         const { shiftKey } = event.nativeEvent as any;
135         if (shiftKey) {
136             handleCheckRange(id);
137         } else {
138             handleCheckOnlyOne(id);
139         }
140     });
142     // Automatically uncheck an id which is not anymore in the list (Happens frequently when using search)
143     useEffect(() => {
144         const filteredCheckedIDs = checkedIDs.filter((id) => allIDs.includes(id));
146         if (filteredCheckedIDs.length !== checkedIDs.length) {
147             handleCheck(filteredCheckedIDs, true, true);
148         }
149     }, [allIDs]);
151     return {
152         checkedIDs,
153         selectedIDs,
154         handleCheck,
155         handleCheckAll,
156         handleCheckOne,
157         handleCheckOnlyOne,
158         handleCheckRange,
159     };
162 export default useItemsSelection;