import * as React from 'react'; import { Dispatch } from 'redux'; import { useState, useCallback, useEffect, useRef } from 'react'; import SearchInput from '../lib/SearchInput/SearchInput'; import Setting from '@joplin/lib/models/Setting'; import { stateUtils } from '@joplin/lib/reducer'; import BaseModel from '@joplin/lib/BaseModel'; import uuid from '@joplin/lib/uuid'; import { connect } from 'react-redux'; import Note from '@joplin/lib/models/Note'; import { AppState } from '../../app.reducer'; import { blur, focus } from '@joplin/lib/utils/focusHandler'; const debounce = require('debounce'); import styled from 'styled-components'; export const Root = styled.div` position: relative; display: flex; width: 100%; min-width: 30px; `; interface Props { inputRef?: React.MutableRefObject; notesParentType: string; dispatch?: Dispatch; selectedNoteId: string; isFocused?: boolean; globalQuery?: string; } function SearchBar(props: Props) { const [query, setQuery] = useState(''); const [searchStarted, setSearchStarted] = useState(false); const searchId = useRef(uuid.create()); useEffect(() => { function search(searchId: string, query: string, dispatch: Dispatch) { dispatch({ type: 'SEARCH_UPDATE', search: { id: searchId, title: query, query_pattern: query, query_folder_id: null, type_: BaseModel.TYPE_SEARCH, }, }); dispatch({ type: 'SEARCH_SELECT', id: searchId, }); } const debouncedSearch = debounce(search, 500); if (searchStarted) debouncedSearch(searchId.current, query, props.dispatch); return () => { debouncedSearch.clear(); }; // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [query, searchStarted]); const onExitSearch = useCallback(async (navigateAway = true) => { setQuery(''); setSearchStarted(false); if (navigateAway) { const note = props.selectedNoteId ? await Note.load(props.selectedNoteId) : null; if (note) { props.dispatch({ type: 'FOLDER_AND_NOTE_SELECT', folderId: note.parent_id, noteId: note.id, }); } else { const folderId = Setting.value('activeFolderId'); if (folderId) { props.dispatch({ type: 'FOLDER_SELECT', id: folderId, }); } } } // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [props.selectedNoteId]); function onChange(event: { value: string }) { if (event.value.length === 0) { // Revert to previous state if query string becomes empty void onExitSearch(); return; } setSearchStarted(true); setQuery(event.value); } function onFocus() { props.dispatch({ type: 'FOCUS_SET', field: 'globalSearch', }); } function onBlur() { // Do it after a delay so that the "Clear" button // can be clicked on (otherwise the field loses focus // and is resized before the click event has been processed) setTimeout(() => { props.dispatch({ type: 'FOCUS_CLEAR', field: 'globalSearch', }); }, 300); } const onKeyDown = useCallback((event: React.KeyboardEvent) => { if (event.key === 'Escape') { if (document.activeElement) blur('SearchBar::onKeyDown', document.activeElement as HTMLElement); void onExitSearch(); } }, [onExitSearch]); const onSearchButtonClick = useCallback(() => { if (props.isFocused || searchStarted) { void onExitSearch(); } else { setSearchStarted(true); focus('SearchBar::onSearchButtonClick', props.inputRef.current); props.dispatch({ type: 'FOCUS_SET', field: 'globalSearch', }); } // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [onExitSearch, props.isFocused, searchStarted]); useEffect(() => { if (props.notesParentType !== 'Search') { void onExitSearch(false); } }, [props.notesParentType, onExitSearch]); // When the searchbar is remounted, exit the search if it was previously open // or else other buttons stay hidden (e.g. when opening Layout Editor and closing it) // https://github.com/laurent22/joplin/issues/5953 useEffect(() => { if (props.notesParentType === 'Search' || props.isFocused) { if (props.isFocused) { props.dispatch({ type: 'FOCUS_CLEAR', field: 'globalSearch', }); } void onExitSearch(true); } // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, []); // if the globalQuery is not undefined and is not equal to the current value of query changes the query to global query. Else the current query remains the same. // used setQuery((previousQuery)=>{}) to prevent linter error asking to have [query] in the dependency array, since this useEffect would then run every time the query is changed useEffect(() => { if (props.globalQuery !== undefined) { setQuery((previousQuery) => { if (props.globalQuery !== previousQuery) { if (props.globalQuery.length > 0) { setSearchStarted(true); } return props.globalQuery; } return previousQuery; }); } }, [props.globalQuery]); return ( ); } interface OwnProps { windowId: string; } const mapStateToProps = (state: AppState, ownProps: OwnProps) => { const windowState = stateUtils.windowStateById(state, ownProps.windowId); let globalQuery = ''; if (windowState.notesParentType === 'Search' && windowState.selectedSearchId) { const activeSearch = state.searches.find((s: { id: string; query_pattern: string }) => s.id === windowState.selectedSearchId); if (activeSearch && activeSearch.query_pattern) { globalQuery = activeSearch.query_pattern; } } return { notesParentType: windowState.notesParentType, selectedNoteId: stateUtils.selectedNoteId(windowState), isFocused: state.focusedField === 'globalSearch', globalQuery: globalQuery, }; }; export default connect(mapStateToProps)(SearchBar);