Skip to content
Snippets Groups Projects
Select Git revision
  • 88924985f54b8b9b0b58bd98dd36aa392249aaa3
  • mui5-annotation-on-video-stable default
  • get_setter_canvasSizeInformations
  • fix-error-div-into-p
  • annotation-on-video-v2
  • detached
  • annotation-on-video-r17
  • mui5
  • mui5-react-18
  • jacob-test
  • annotation-on-video protected
  • master
  • test-antoinev1
  • 20-fetch-thumbnail-on-annotation
  • add-research-field
  • Save
  • add-plugin
  • 14-wip-no-seek-to
  • 14-bug-on-video-time-control
  • 9_wip_videotests
  • _upgrade_material_ui
  • latest-tetras-16
  • v3.3.0
  • v3.2.0
  • v3.1.1
  • v3.1.0
  • v3.0.0
  • v3.0.0-rc.7
  • v3.0.0-rc.6
  • v3.0.0-rc.5
  • v3.0.0-rc.4
  • v3.0.0-rc.3
  • v3.0.0-rc.2
  • v3.0.0-rc.1
  • v3.0.0-beta.10
  • v3.0.0-beta.9
  • v3.0.0-beta.8
  • v3.0.0-beta.7
  • v3.0.0-beta.6
  • v3.0.0-beta.5
  • v3.0.0-beta.3
41 results

index.js

Blame
  • SearchPanelControls.js 6.25 KiB
    import { Component } from 'react';
    import PropTypes from 'prop-types';
    import { styled } from '@mui/material/styles';
    import deburr from 'lodash/deburr';
    import debounce from 'lodash/debounce';
    import isObject from 'lodash/isObject';
    import Autocomplete from '@mui/material/Autocomplete';
    import CircularProgress from '@mui/material/CircularProgress';
    import TextField from '@mui/material/TextField';
    import InputAdornment from '@mui/material/InputAdornment';
    import SearchIcon from '@mui/icons-material/SearchSharp';
    import MiradorMenuButton from '../containers/MiradorMenuButton';
    import SearchPanelNavigation from '../containers/SearchPanelNavigation';
    
    const StyledForm = styled('form', { name: 'SearchPanelControls', slot: 'form' })(({ theme }) => ({
      paddingBottom: theme.spacing(1),
      paddingRight: theme.spacing(1.5),
      width: '100%',
    }));
    
    /** Sometimes an autocomplete match can be a simple string, other times an object
        with a `match` property, this function abstracts that away */
    const getMatch = (option) => (isObject(option) ? option.match : option);
    
    /** */
    export class SearchPanelControls extends Component {
      /** */
      constructor(props) {
        super(props);
    
        this.state = { search: props.query, suggestions: [] };
        this.handleChange = this.handleChange.bind(this);
        this.submitSearch = this.submitSearch.bind(this);
        this.getSuggestions = this.getSuggestions.bind(this);
        this.selectItem = this.selectItem.bind(this);
        this.fetchAutocomplete = debounce(this.fetchAutocomplete.bind(this), 500);
        this.receiveAutocomplete = this.receiveAutocomplete.bind(this);
      }
    
      /**
       * Update the query in the component state if the query has changed in the redux store
       */
      componentDidUpdate(prevProps) {
        const { query } = this.props;
        if (query !== prevProps.query) {
          // We are setting local state directly here ONLY when the query prop (from redux)
          // changed
          this.setState({ // eslint-disable-line react/no-did-update-set-state
            search: query,
          });
        }
      }
    
      /**
       * Cancel the debounce function when the component unmounts
       */
      componentWillUnmount() {
        this.fetchAutocomplete.cancel();
      }
    
      /** */
      handleChange(event, value, reason) {
        // For some reason the value gets reset to an empty value from the
        // useAutocomplete hook sometimes, we just ignore these cases
        if (reason === 'reset' && !value) {
          return;
        }
        this.setState({
          search: value,
          suggestions: [],
        });
    
        if (value) {
          this.fetchAutocomplete(value);
        }
      }
    
      /** */
      getSuggestions(value, { showEmpty = false } = {}) {
        const { suggestions } = this.state;
    
        const inputValue = deburr(value.trim()).toLowerCase();
        const inputLength = inputValue.length;
    
        return inputLength === 0 && !showEmpty
          ? []
          : suggestions;
      }
    
      /** */
      fetchAutocomplete(value) {
        const { autocompleteService } = this.props;
    
        if (!autocompleteService) return;
        if (!value) return;
    
        fetch(`${autocompleteService.id}?${new URLSearchParams({ q: value })}`)
          .then(response => response.json())
          .then(this.receiveAutocomplete);
      }
    
      /** */
      receiveAutocomplete(json) {
        this.setState({ suggestions: json.terms });
      }
    
      /** */
      submitSearch(event) {
        const {
          companionWindowId, fetchSearch, searchService, windowId,
        } = this.props;
        const { search } = this.state;
        event && event.preventDefault();
        if (!search) return;
        fetchSearch(windowId, companionWindowId, `${searchService.id}?${new URLSearchParams({ q: search })}`, search);
      }
    
      /** */
      selectItem(_event, selectedItem, _reason) {
        if (selectedItem && getMatch(selectedItem)) {
          this.setState({ search: getMatch(selectedItem) }, this.submitSearch);
        }
      }
    
      /** */
      render() {
        const {
          companionWindowId, searchIsFetching, t, windowId,
        } = this.props;
    
        const { search, suggestions } = this.state;
        const id = `search-${companionWindowId}`;
        return (
          <>
            <StyledForm
              aria-label={t('searchTitle')}
              onSubmit={this.submitSearch}
            >
              <Autocomplete
                id={id}
                inputValue={search}
                options={suggestions}
                getOptionLabel={getMatch}
                isOptionEqualToValue={(option, value) => (
                  deburr(getMatch(option).trim()).toLowerCase()
                  === deburr(getMatch(value).trim()).toLowerCase()
                )}
                noOptionsText=""
                onChange={this.selectItem}
                onInputChange={this.handleChange}
                freeSolo
                disableClearable
                renderInput={params => (
                  <TextField
                    {...params}
                    label={t('searchInputLabel')}
                    variant="standard"
                    InputProps={{
                      ...params.InputProps,
                      endAdornment: (
                        <InputAdornment sx={{ position: 'relative' }} position="end">
                          <MiradorMenuButton aria-label={t('searchSubmitAria')} type="submit">
                            <SearchIcon />
                          </MiradorMenuButton>
                          {Boolean(searchIsFetching) && (
                          <CircularProgress
                            sx={{
                              left: '50%',
                              marginLeft: '-25px',
                              marginTop: '-25px',
                              position: 'absolute',
                              top: '50%',
                            }}
                            size={50}
                          />
                          )}
                        </InputAdornment>
                      ),
                    }}
                  />
                )}
              />
            </StyledForm>
            <SearchPanelNavigation windowId={windowId} companionWindowId={companionWindowId} />
          </>
        );
      }
    }
    
    SearchPanelControls.propTypes = {
      autocompleteService: PropTypes.shape({
        id: PropTypes.string,
      }),
      companionWindowId: PropTypes.string.isRequired,
      fetchSearch: PropTypes.func.isRequired,
      query: PropTypes.string,
      searchIsFetching: PropTypes.bool.isRequired,
      searchService: PropTypes.shape({
        id: PropTypes.string,
      }).isRequired,
      t: PropTypes.func,
      windowId: PropTypes.string.isRequired,
    };
    
    SearchPanelControls.defaultProps = {
      autocompleteService: undefined,
      query: '',
      t: key => key,
    };