您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

MultiSelectAutocomplete.js 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. // @flow
  2. import { MultiSelectStateless } from '@atlaskit/multi-select';
  3. import AKInlineDialog from '@atlaskit/inline-dialog';
  4. import _debounce from 'lodash/debounce';
  5. import React, { Component } from 'react';
  6. import InlineDialogFailure from './InlineDialogFailure';
  7. const logger = require('jitsi-meet-logger').getLogger(__filename);
  8. /**
  9. * The type of the React {@code Component} props of
  10. * {@link MultiSelectAutocomplete}.
  11. */
  12. type Props = {
  13. /**
  14. * The default value of the selected item.
  15. */
  16. defaultValue: Array<Object>,
  17. /**
  18. * Optional footer to show as a last element in the results.
  19. * Should be of type {content: <some content>}
  20. */
  21. footer: Object,
  22. /**
  23. * Indicates if the component is disabled.
  24. */
  25. isDisabled: boolean,
  26. /**
  27. * Text to display while a query is executing.
  28. */
  29. loadingMessage: string,
  30. /**
  31. * The text to show when no matches are found.
  32. */
  33. noMatchesFound: string,
  34. /**
  35. * The function called immediately before a selection has been actually
  36. * selected. Provides an opportunity to do any formatting.
  37. */
  38. onItemSelected: Function,
  39. /**
  40. * The function called when the selection changes.
  41. */
  42. onSelectionChange: Function,
  43. /**
  44. * The placeholder text of the input component.
  45. */
  46. placeholder: string,
  47. /**
  48. * The service providing the search.
  49. */
  50. resourceClient: { makeQuery: Function, parseResults: Function },
  51. /**
  52. * Indicates if the component should fit the container.
  53. */
  54. shouldFitContainer: boolean,
  55. /**
  56. * Indicates if we should focus.
  57. */
  58. shouldFocus: boolean
  59. };
  60. /**
  61. * The type of the React {@code Component} state of
  62. * {@link MultiSelectAutocomplete}.
  63. */
  64. type State = {
  65. /**
  66. * Indicates if the dropdown is open.
  67. */
  68. isOpen: boolean,
  69. /**
  70. * The text that filters the query result of the search.
  71. */
  72. filterValue: string,
  73. /**
  74. * Indicates if the component is currently loading results.
  75. */
  76. loading: boolean,
  77. /**
  78. * Indicates if there was an error.
  79. */
  80. error: boolean,
  81. /**
  82. * The list of result items.
  83. */
  84. items: Array<Object>,
  85. /**
  86. * The list of selected items.
  87. */
  88. selectedItems: Array<Object>
  89. };
  90. /**
  91. * A MultiSelect that is also auto-completing.
  92. */
  93. class MultiSelectAutocomplete extends Component<Props, State> {
  94. /**
  95. * Initializes a new {@code MultiSelectAutocomplete} instance.
  96. *
  97. * @param {Object} props - The read-only properties with which the new
  98. * instance is to be initialized.
  99. */
  100. constructor(props: Props) {
  101. super(props);
  102. const defaultValue = this.props.defaultValue || [];
  103. this.state = {
  104. isOpen: false,
  105. filterValue: '',
  106. loading: false,
  107. error: false,
  108. items: [],
  109. selectedItems: [ ...defaultValue ]
  110. };
  111. this._onFilterChange = this._onFilterChange.bind(this);
  112. this._onRetry = this._onRetry.bind(this);
  113. this._onSelectionChange = this._onSelectionChange.bind(this);
  114. this._sendQuery = _debounce(this._sendQuery.bind(this), 200);
  115. }
  116. /**
  117. * Sets the items to display as selected.
  118. *
  119. * @param {Array<Object>} selectedItems - The list of items to display as
  120. * having been selected.
  121. * @returns {void}
  122. */
  123. setSelectedItems(selectedItems: Array<Object> = []) {
  124. this.setState({ selectedItems });
  125. }
  126. /**
  127. * Renders the content of this component.
  128. *
  129. * @returns {ReactElement}
  130. */
  131. render() {
  132. const shouldFitContainer = this.props.shouldFitContainer || false;
  133. const shouldFocus = this.props.shouldFocus || false;
  134. const isDisabled = this.props.isDisabled || false;
  135. const placeholder = this.props.placeholder || '';
  136. const noMatchesFound = this.props.noMatchesFound || '';
  137. return (
  138. <div>
  139. <MultiSelectStateless
  140. filterValue = { this.state.filterValue }
  141. footer = { this.props.footer }
  142. icon = { null }
  143. isDisabled = { isDisabled }
  144. isLoading = { this.state.loading }
  145. isOpen = { this.state.isOpen }
  146. items = { this.state.items }
  147. loadingMessage = { this.props.loadingMessage }
  148. noMatchesFound = { noMatchesFound }
  149. onFilterChange = { this._onFilterChange }
  150. onRemoved = { this._onSelectionChange }
  151. onSelected = { this._onSelectionChange }
  152. placeholder = { placeholder }
  153. selectedItems = { this.state.selectedItems }
  154. shouldFitContainer = { shouldFitContainer }
  155. shouldFocus = { shouldFocus } />
  156. { this._renderError() }
  157. </div>
  158. );
  159. }
  160. _onFilterChange: (string) => void;
  161. /**
  162. * Sets the state and sends a query on filter change.
  163. *
  164. * @param {string} filterValue - The filter text value.
  165. * @private
  166. * @returns {void}
  167. */
  168. _onFilterChange(filterValue) {
  169. this.setState({
  170. // Clean the error if the filterValue is empty.
  171. error: this.state.error && Boolean(filterValue),
  172. filterValue,
  173. isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
  174. items: filterValue ? this.state.items : [],
  175. loading: Boolean(filterValue)
  176. });
  177. if (filterValue) {
  178. this._sendQuery(filterValue);
  179. }
  180. }
  181. _onRetry: () => void;
  182. /**
  183. * Retries the query on retry.
  184. *
  185. * @private
  186. * @returns {void}
  187. */
  188. _onRetry() {
  189. this._sendQuery(this.state.filterValue);
  190. }
  191. _onSelectionChange: (Object) => void;
  192. /**
  193. * Updates the selected items when a selection event occurs.
  194. *
  195. * @param {Object} item - The selected item.
  196. * @private
  197. * @returns {void}
  198. */
  199. _onSelectionChange(item) {
  200. const existing
  201. = this.state.selectedItems.find(k => k.value === item.value);
  202. let selectedItems = this.state.selectedItems;
  203. if (existing) {
  204. selectedItems = selectedItems.filter(k => k !== existing);
  205. } else {
  206. selectedItems.push(this.props.onItemSelected(item));
  207. }
  208. this.setState({
  209. isOpen: false,
  210. selectedItems
  211. });
  212. if (this.props.onSelectionChange) {
  213. this.props.onSelectionChange(selectedItems);
  214. }
  215. }
  216. /**
  217. * Renders the error UI.
  218. *
  219. * @returns {ReactElement|null}
  220. */
  221. _renderError() {
  222. if (!this.state.error) {
  223. return null;
  224. }
  225. const content = (
  226. <div className = 'autocomplete-error'>
  227. <InlineDialogFailure
  228. onRetry = { this._onRetry } />
  229. </div>
  230. );
  231. return (
  232. <AKInlineDialog
  233. content = { content }
  234. isOpen = { true } />
  235. );
  236. }
  237. _sendQuery: (string) => void;
  238. /**
  239. * Sends a query to the resourceClient.
  240. *
  241. * @param {string} filterValue - The string to use for the search.
  242. * @returns {void}
  243. */
  244. _sendQuery(filterValue) {
  245. if (!filterValue) {
  246. return;
  247. }
  248. this.setState({
  249. error: false
  250. });
  251. const resourceClient = this.props.resourceClient || {
  252. makeQuery: () => Promise.resolve([]),
  253. parseResults: results => results
  254. };
  255. resourceClient.makeQuery(filterValue)
  256. .then(results => {
  257. if (this.state.filterValue !== filterValue) {
  258. this.setState({
  259. error: false
  260. });
  261. return;
  262. }
  263. const itemGroups = [
  264. {
  265. items: resourceClient.parseResults(results)
  266. }
  267. ];
  268. this.setState({
  269. items: itemGroups,
  270. isOpen: true,
  271. loading: false,
  272. error: false
  273. });
  274. })
  275. .catch(error => {
  276. logger.error('MultiSelectAutocomplete error in query', error);
  277. this.setState({
  278. error: true,
  279. loading: false,
  280. isOpen: false
  281. });
  282. });
  283. }
  284. }
  285. export default MultiSelectAutocomplete;