check-list.jsx (4534B)
1 /* 2 GoToSocial 3 Copyright (C) GoToSocial Authors admin@gotosocial.org 4 SPDX-License-Identifier: AGPL-3.0-or-later 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 "use strict"; 21 22 const React = require("react"); 23 const syncpipe = require("syncpipe"); 24 const { createSlice } = require("@reduxjs/toolkit"); 25 const { enableMapSet } = require("immer"); 26 27 enableMapSet(); // for use in reducers 28 29 const { reducer, actions } = createSlice({ 30 name: "checklist", 31 initialState: {}, // not handled by slice itself 32 reducers: { 33 updateAll: (state, { payload: checked }) => { 34 const selectedEntries = new Set(); 35 return { 36 entries: syncpipe(state.entries, [ 37 (_) => Object.values(_), 38 (_) => _.map((entry) => { 39 if (checked) { 40 selectedEntries.add(entry.key); 41 } 42 return [entry.key, { 43 ...entry, 44 checked 45 }]; 46 }), 47 (_) => Object.fromEntries(_) 48 ]), 49 selectedEntries 50 }; 51 }, 52 update: (state, { payload: { key, value } }) => { 53 if (value.checked !== undefined) { 54 if (value.checked === true) { 55 state.selectedEntries.add(key); 56 } else { 57 state.selectedEntries.delete(key); 58 } 59 } 60 61 state.entries[key] = { 62 ...state.entries[key], 63 ...value 64 }; 65 }, 66 updateMultiple: (state, { payload }) => { 67 payload.forEach(([key, value]) => { 68 if (value.checked !== undefined) { 69 if (value.checked === true) { 70 state.selectedEntries.add(key); 71 } else { 72 state.selectedEntries.delete(key); 73 } 74 } 75 76 state.entries[key] = { 77 ...state.entries[key], 78 ...value 79 }; 80 }); 81 } 82 } 83 }); 84 85 function initialState({ entries, uniqueKey, initialValue }) { 86 const selectedEntries = new Set(); 87 return { 88 entries: syncpipe(entries, [ 89 (_) => _.map((entry) => { 90 let key = entry[uniqueKey]; 91 let checked = entry.checked ?? initialValue; 92 93 if (checked) { 94 selectedEntries.add(key); 95 } else { 96 selectedEntries.delete(key); 97 } 98 99 return [ 100 key, 101 { 102 ...entry, 103 key, 104 checked 105 } 106 ]; 107 }), 108 (_) => Object.fromEntries(_) 109 ]), 110 selectedEntries 111 }; 112 } 113 114 module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", initialValue = false }) { 115 const [state, dispatch] = React.useReducer(reducer, null, 116 () => initialState({ entries, uniqueKey, initialValue }) // initial state 117 ); 118 119 const toggleAllRef = React.useRef(null); 120 121 React.useEffect(() => { 122 if (toggleAllRef.current != null) { 123 let some = state.selectedEntries.size > 0; 124 let all = false; 125 if (some) { 126 all = state.selectedEntries.size == Object.values(state.entries).length; 127 } 128 toggleAllRef.current.checked = all; 129 toggleAllRef.current.indeterminate = some && !all; 130 } 131 // only needs to update when state.selectedEntries changes, not state.entries 132 // eslint-disable-next-line react-hooks/exhaustive-deps 133 }, [state.selectedEntries]); 134 135 const reset = React.useCallback( 136 () => dispatch(actions.updateAll(initialValue)), 137 [initialValue] 138 ); 139 140 const onChange = React.useCallback( 141 (key, value) => dispatch(actions.update({ key, value })), 142 [] 143 ); 144 145 const updateMultiple = React.useCallback( 146 (entries) => dispatch(actions.updateMultiple(entries)), 147 [] 148 ); 149 150 return React.useMemo(() => { 151 function toggleAll(e) { 152 let checked = e.target.checked; 153 if (e.target.indeterminate) { 154 checked = false; 155 } 156 dispatch(actions.updateAll(checked)); 157 } 158 159 function selectedValues() { 160 return Array.from((state.selectedEntries)).map((key) => ({ 161 ...state.entries[key] // returned as new object, because reducer state is immutable 162 })); 163 } 164 165 return Object.assign([ 166 state, 167 reset, 168 { name } 169 ], { 170 name, 171 value: state.entries, 172 onChange, 173 selectedValues, 174 reset, 175 someSelected: state.selectedEntries.size > 0, 176 updateMultiple, 177 toggleAll: { 178 ref: toggleAllRef, 179 onChange: toggleAll 180 } 181 }); 182 }, [state, reset, name, onChange, updateMultiple]); 183 };