|
|
@@ -1,274 +0,0 @@
|
|
|
-import React from 'react';
|
|
|
-import PropTypes from 'prop-types';
|
|
|
-
|
|
|
-import { noop } from 'lodash/noop';
|
|
|
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
|
|
|
-
|
|
|
-import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
|
|
|
-
|
|
|
-import AppContainer from '~/client/services/AppContainer';
|
|
|
-import { withUnstatedContainers } from './UnstatedUtils';
|
|
|
-
|
|
|
-class SearchTypeahead extends React.Component {
|
|
|
-
|
|
|
- constructor(props) {
|
|
|
-
|
|
|
- super(props);
|
|
|
-
|
|
|
- this.state = {
|
|
|
- input: this.props.keywordOnInit,
|
|
|
- pages: [],
|
|
|
- isLoading: false,
|
|
|
- searchError: null,
|
|
|
- };
|
|
|
-
|
|
|
- this.restoreInitialData = this.restoreInitialData.bind(this);
|
|
|
- this.clearKeyword = this.clearKeyword.bind(this);
|
|
|
- this.changeKeyword = this.changeKeyword.bind(this);
|
|
|
- this.search = this.search.bind(this);
|
|
|
- this.onInputChange = this.onInputChange.bind(this);
|
|
|
- this.onKeyDown = this.onKeyDown.bind(this);
|
|
|
- this.dispatchSubmit = this.dispatchSubmit.bind(this);
|
|
|
- this.getEmptyLabel = this.getEmptyLabel.bind(this);
|
|
|
- this.getResetFormButton = this.getResetFormButton.bind(this);
|
|
|
- this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
|
|
|
- this.getTypeahead = this.getTypeahead.bind(this);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Get instance of AsyncTypeahead
|
|
|
- */
|
|
|
- getTypeahead() {
|
|
|
- return this.typeahead ? this.typeahead.getInstance() : null;
|
|
|
- }
|
|
|
-
|
|
|
- componentDidMount() {
|
|
|
- }
|
|
|
-
|
|
|
- componentWillUnmount() {
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Initialize keywordyword
|
|
|
- */
|
|
|
- restoreInitialData() {
|
|
|
- this.changeKeyword(this.props.keywordOnInit);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * clear keyword
|
|
|
- */
|
|
|
- clearKeyword(text) {
|
|
|
- this.changeKeyword('');
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * change keyword
|
|
|
- */
|
|
|
- changeKeyword(text) {
|
|
|
- // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
|
|
|
- const instance = this.typeahead.getInstance();
|
|
|
- instance.clear();
|
|
|
- instance.setState({ text });
|
|
|
- }
|
|
|
-
|
|
|
- search(keyword) {
|
|
|
-
|
|
|
- if (keyword === '') {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.setState({ isLoading: true });
|
|
|
-
|
|
|
- this.props.appContainer.apiGet('/search', { q: keyword })
|
|
|
- .then((res) => { this.onSearchSuccess(res) })
|
|
|
- .catch((err) => { this.onSearchError(err) });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Callback function which is occured when search is exit successfully
|
|
|
- * @param {*} pages
|
|
|
- */
|
|
|
- onSearchSuccess(res) {
|
|
|
- this.setState({
|
|
|
- isLoading: false,
|
|
|
- pages: res.data,
|
|
|
- });
|
|
|
- if (this.props.onSearchSuccess != null) {
|
|
|
- this.props.onSearchSuccess(res);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Callback function which is occured when search is exit abnormaly
|
|
|
- * @param {*} err
|
|
|
- */
|
|
|
- onSearchError(err) {
|
|
|
- this.setState({
|
|
|
- isLoading: false,
|
|
|
- searchError: err,
|
|
|
- });
|
|
|
- if (this.props.onSearchError != null) {
|
|
|
- this.props.onSearchError(err);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- onInputChange(text) {
|
|
|
- this.setState({ input: text });
|
|
|
- this.props.onInputChange(text);
|
|
|
- if (text === '') {
|
|
|
- this.setState({ pages: [] });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- onKeyDown(event) {
|
|
|
- if (event.keyCode === 13) {
|
|
|
- this.dispatchSubmit();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- dispatchSubmit() {
|
|
|
- if (this.props.onSubmit != null) {
|
|
|
- this.props.onSubmit(this.state.input);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- getEmptyLabel() {
|
|
|
- const { emptyLabel, helpElement } = this.props;
|
|
|
- const { input } = this.state;
|
|
|
-
|
|
|
- // show help element if empty
|
|
|
- if (input.length === 0) {
|
|
|
- return helpElement;
|
|
|
- }
|
|
|
-
|
|
|
- // use props.emptyLabel as is if defined
|
|
|
- if (emptyLabel !== undefined) {
|
|
|
- return this.props.emptyLabel;
|
|
|
- }
|
|
|
-
|
|
|
- let emptyLabelExceptError = 'No matches found on title...';
|
|
|
- if (this.props.emptyLabelExceptError !== undefined) {
|
|
|
- emptyLabelExceptError = this.props.emptyLabelExceptError;
|
|
|
- }
|
|
|
-
|
|
|
- return (this.state.searchError !== null)
|
|
|
- ? 'Error on searching.'
|
|
|
- : emptyLabelExceptError;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Get restore form button to initialize button
|
|
|
- */
|
|
|
- getResetFormButton() {
|
|
|
- const isClearBtn = this.props.behaviorOfResetBtn === 'clear';
|
|
|
- const initialKeyword = isClearBtn ? '' : this.props.keywordOnInit;
|
|
|
- const isHidden = this.state.input === initialKeyword;
|
|
|
- const resetForm = isClearBtn ? this.clearKeyword : this.restoreInitialData;
|
|
|
-
|
|
|
- return isHidden ? (
|
|
|
- <span />
|
|
|
- ) : (
|
|
|
- <button type="button" className="btn btn-link search-clear" onMouseDown={resetForm}>
|
|
|
- <i className="icon-close" />
|
|
|
- </button>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- renderMenuItemChildren(option, props, index) {
|
|
|
- const page = option.pageData;
|
|
|
- return (
|
|
|
- <span>
|
|
|
- <UserPicture user={page.lastUpdateUser} size="sm" noLink />
|
|
|
- <span className="ml-1 text-break text-wrap"><PagePathLabel path={page.path} /></span>
|
|
|
- <PageListMeta page={page} />
|
|
|
- </span>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- render() {
|
|
|
- const defaultSelected = (this.props.keywordOnInit !== '')
|
|
|
- ? [{ path: this.props.keywordOnInit }]
|
|
|
- : [];
|
|
|
- const inputProps = { autoComplete: 'off' };
|
|
|
- if (this.props.inputName != null) {
|
|
|
- inputProps.name = this.props.inputName;
|
|
|
- }
|
|
|
-
|
|
|
- const resetFormButton = this.getResetFormButton();
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="search-typeahead">
|
|
|
- <AsyncTypeahead
|
|
|
- {...this.props}
|
|
|
- id="search-typeahead-asynctypeahead"
|
|
|
- ref={(c) => { this.typeahead = c }}
|
|
|
- inputProps={inputProps}
|
|
|
- isLoading={this.state.isLoading}
|
|
|
- labelKey={data => data?.pageData?.path || this.props.keywordOnInit || ''} // https://github.com/ericgio/react-bootstrap-typeahead/blob/master/docs/Rendering.md#labelkey-stringfunction
|
|
|
- minLength={0}
|
|
|
- options={this.state.pages} // Search result (Some page names)
|
|
|
- promptText={this.props.helpElement}
|
|
|
- emptyLabel={this.getEmptyLabel()}
|
|
|
- align="left"
|
|
|
- submitFormOnEnter
|
|
|
- onSearch={this.search}
|
|
|
- onInputChange={this.onInputChange}
|
|
|
- onKeyDown={this.onKeyDown}
|
|
|
- renderMenuItemChildren={this.renderMenuItemChildren}
|
|
|
- caseSensitive={false}
|
|
|
- defaultSelected={defaultSelected}
|
|
|
- autoFocus={this.props.autoFocus}
|
|
|
- onBlur={this.props.onBlur}
|
|
|
- onFocus={this.props.onFocus}
|
|
|
- />
|
|
|
- {resetFormButton}
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Wrapper component for using unstated
|
|
|
- */
|
|
|
-const SearchTypeaheadWrapper = withUnstatedContainers(SearchTypeahead, [AppContainer]);
|
|
|
-
|
|
|
-/**
|
|
|
- * Properties
|
|
|
- */
|
|
|
-SearchTypeahead.propTypes = {
|
|
|
- appContainer: PropTypes.instanceOf(AppContainer).isRequired,
|
|
|
-
|
|
|
- onSearchSuccess: PropTypes.func,
|
|
|
- onSearchError: PropTypes.func,
|
|
|
- onChange: PropTypes.func,
|
|
|
- onBlur: PropTypes.func,
|
|
|
- onFocus: PropTypes.func,
|
|
|
- onSubmit: PropTypes.func,
|
|
|
- onInputChange: PropTypes.func,
|
|
|
- inputName: PropTypes.string,
|
|
|
- emptyLabel: PropTypes.string,
|
|
|
- emptyLabelExceptError: PropTypes.string,
|
|
|
- placeholder: PropTypes.string,
|
|
|
- keywordOnInit: PropTypes.string,
|
|
|
- helpElement: PropTypes.object,
|
|
|
- autoFocus: PropTypes.bool,
|
|
|
- behaviorOfResetBtn: PropTypes.oneOf(['restore', 'clear']),
|
|
|
-};
|
|
|
-
|
|
|
-/**
|
|
|
- * Properties
|
|
|
- */
|
|
|
-SearchTypeahead.defaultProps = {
|
|
|
- onSearchSuccess: noop,
|
|
|
- onSearchError: noop,
|
|
|
- onChange: noop,
|
|
|
- placeholder: '',
|
|
|
- keywordOnInit: '',
|
|
|
- behaviorOfResetBtn: 'restore',
|
|
|
- autoFocus: false,
|
|
|
- onInputChange: () => {},
|
|
|
-};
|
|
|
-
|
|
|
-export default SearchTypeaheadWrapper;
|