]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/common/ChannelSelect.js
compact keysanity tracker
[alttp.git] / resources / js / components / common / ChannelSelect.js
1 import axios from 'axios';
2 import PropTypes from 'prop-types';
3 import React, { useCallback, useEffect, useRef, useState } from 'react';
4 import { Alert, Button, Form, ListGroup } from 'react-bootstrap';
5 import { useTranslation } from 'react-i18next';
6
7 import Icon from './Icon';
8 import debounce from '../../helpers/debounce';
9
10 const ChannelSelect = ({
11         autoSelect,
12         joinable,
13         manageable,
14         onChange,
15         readOnly,
16         value,
17 }) => {
18         const [resolved, setResolved] = useState(null);
19         const [results, setResults] = useState([]);
20         const [search, setSearch] = useState('');
21         const [showResults, setShowResults] = useState(false);
22
23         const ref = useRef(null);
24         const { t } = useTranslation();
25
26         useEffect(() => {
27                 const handleEventOutside = e => {
28                         if (ref.current && !ref.current.contains(e.target)) {
29                                 setShowResults(false);
30                         }
31                 };
32                 document.addEventListener('click', handleEventOutside, true);
33                 document.addEventListener('focus', handleEventOutside, true);
34                 return () => {
35                         document.removeEventListener('click', handleEventOutside, true);
36                         document.removeEventListener('focus', handleEventOutside, true);
37                 };
38         }, []);
39
40         let ctrl = null;
41         const fetch = useCallback(debounce(async phrase => {
42                 if (ctrl) {
43                         ctrl.abort();
44                 }
45                 ctrl = new AbortController();
46                 try {
47                         const response = await axios.get(`/api/channels`, {
48                                 params: {
49                                         joinable: joinable ? 1 : 0,
50                                         manageable: manageable ? 1 : 0,
51                                         phrase,
52                                 },
53                                 signal: ctrl.signal,
54                         });
55                         ctrl = null;
56                         setResults(response.data);
57                         if (autoSelect && !phrase && response.data.length === 1) {
58                                 onChange({
59                                         channel: response.data[0],
60                                         target: { value: response.data[0].id },
61                                 });
62                         }
63                 } catch (e) {
64                         ctrl = null;
65                         console.error(e);
66                 }
67         }, 300), [autoSelect, joinable, manageable]);
68
69         useEffect(() => {
70                 fetch(search);
71         }, [search]);
72
73         useEffect(() => {
74                 if (value) {
75                         axios
76                                 .get(`/api/channels/${value}`)
77                         .then(response => {
78                                 setResolved(response.data);
79                         });
80                 } else {
81                         setResolved(null);
82                 }
83         }, [value]);
84
85         if (value) {
86                 return <div className="d-flex align-items-center justify-content-between">
87                         <span>{resolved ? resolved.title : value}</span>
88                         {!readOnly ?
89                                 <Button
90                                         className="ms-2"
91                                         onClick={() => onChange({ channel: null, target: { value: '' }})}
92                                         title={t('button.unset')}
93                                         variant="outline-danger"
94                                 >
95                                         <Icon.REMOVE title="" />
96                                 </Button>
97                         : null}
98                 </div>;
99         }
100         return <div className={`channel-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
101                 <Form.Control
102                         className="search-input"
103                         name={Math.random().toString(20).substr(2, 10)}
104                         onChange={e => setSearch(e.target.value)}
105                         onFocus={() => setShowResults(true)}
106                         readOnly={readOnly}
107                         type="search"
108                         value={search}
109                 />
110                 <div className="search-results-holder">
111                         {results.length ?
112                                 <ListGroup className="search-results">
113                                         {results.map(result =>
114                                                 <ListGroup.Item
115                                                         action
116                                                         key={result.id}
117                                                         onClick={() => onChange({
118                                                                 channel: result,
119                                                                 target: { value: result.id },
120                                                         })}
121                                                 >
122                                                         {result.title}
123                                                 </ListGroup.Item>
124                                         )}
125                                 </ListGroup>
126                         :
127                                 <Alert className="search-results" variant="info">
128                                         {t('search.noResults')}
129                                 </Alert>
130                         }
131                 </div>
132         </div>;
133 };
134
135 ChannelSelect.propTypes = {
136         autoSelect: PropTypes.bool,
137         joinable: PropTypes.bool,
138         manageable: PropTypes.bool,
139         onChange: PropTypes.func,
140         readOnly: PropTypes.bool,
141         value: PropTypes.oneOfType([
142                 PropTypes.number,
143                 PropTypes.string,
144         ]),
145 };
146
147 export default ChannelSelect;