1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-26 07:22:34 +00:00
Files
seahub/frontend/src/components/search/search.js

472 lines
14 KiB
JavaScript
Raw Normal View History

import React, { Component, Fragment } from 'react';
2018-10-16 18:19:51 +08:00
import PropTypes from 'prop-types';
2023-10-08 17:20:38 +08:00
import isHotkey from 'is-hotkey';
import MediaQuery from 'react-responsive';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot } from '../../utils/constants';
2018-09-04 17:16:50 +08:00
import SearchResultItem from './search-result-item';
2020-04-01 11:35:40 +08:00
import { Utils } from '../../utils/utils';
import { isMac } from '../../utils/extra-attributes';
2020-04-01 11:35:40 +08:00
import toaster from '../toast';
2023-11-22 17:31:08 +08:00
import { SEARCH_DELAY_TIME, getValueLength } from './constant';
2018-10-16 18:19:51 +08:00
const propTypes = {
2018-12-07 10:36:59 +08:00
repoID: PropTypes.string,
2018-12-13 20:42:51 +08:00
placeholder: PropTypes.string,
2018-10-16 18:19:51 +08:00
onSearchedClick: PropTypes.func.isRequired,
isPublic: PropTypes.bool,
2018-10-16 18:19:51 +08:00
};
2023-10-08 17:20:38 +08:00
const PER_PAGE = 10;
const controlKey = isMac() ? '⌘' : 'Ctrl';
2023-10-08 17:20:38 +08:00
2018-08-06 18:29:12 +08:00
class Search extends Component {
2018-08-17 12:23:55 +08:00
constructor(props) {
super(props);
this.baseSearchPageURL = `${siteRoot}search/`;
2018-08-17 12:23:55 +08:00
this.state = {
width: 'default',
value: '',
resultItems: [],
2023-10-11 15:58:06 +08:00
highlightIndex: 0,
2023-10-08 17:20:38 +08:00
page: 0,
isLoading: false,
hasMore: true,
2018-08-17 12:23:55 +08:00
isMaskShow: false,
isResultShow: false,
isResultGetted: false,
isCloseShow: false,
isSearchInputShow: false, // for mobile
searchPageUrl: this.baseSearchPageURL,
2018-08-17 12:23:55 +08:00
};
this.inputValue = '';
this.highlightRef = null;
this.source = null; // used to cancel request;
2023-10-08 17:20:38 +08:00
this.inputRef = React.createRef();
this.searchContainer = React.createRef();
2023-10-08 17:20:38 +08:00
this.searchResultListRef = React.createRef();
this.timer = null;
2023-11-08 20:52:59 +08:00
this.isChineseInput = false;
2023-10-08 17:20:38 +08:00
}
componentDidMount() {
document.addEventListener('keydown', this.onDocumentKeydown);
2023-11-08 20:52:59 +08:00
document.addEventListener('compositionstart', this.onCompositionStart);
document.addEventListener('compositionend', this.onCompositionEnd);
2023-10-08 17:20:38 +08:00
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onDocumentKeydown);
2023-11-08 20:52:59 +08:00
document.removeEventListener('compositionstart', this.onCompositionStart);
document.removeEventListener('compositionend', this.onCompositionEnd);
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
2023-11-08 20:52:59 +08:00
this.isChineseInput = false;
2018-08-17 12:23:55 +08:00
}
2023-11-08 20:52:59 +08:00
onCompositionStart = () => {
this.isChineseInput = true;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
2023-11-08 20:52:59 +08:00
};
onCompositionEnd = () => {
this.isChineseInput = false;
// chromecompositionstart -> onChange -> compositionend
// not chromecompositionstart -> compositionend -> onChange
// The onChange event will setState and change input value, then setTimeout to initiate the search
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.timer = setTimeout(() => {
this.onSearch();
}, SEARCH_DELAY_TIME);
2023-11-08 20:52:59 +08:00
};
2023-10-08 17:20:38 +08:00
onDocumentKeydown = (e) => {
if (isHotkey('mod+f')(e)) {
e.preventDefault();
this.onFocusHandler();
if (this.inputRef && this.inputRef.current) {
this.inputRef.current.focus();
}
} else if (isHotkey('esc', e)) {
2023-10-08 17:20:38 +08:00
e.preventDefault();
this.inputRef && this.inputRef.current && this.inputRef.current.blur();
2023-10-08 17:20:38 +08:00
this.resetToDefault();
} else if (isHotkey('enter', e)) {
this.onEnter(e);
} else if (isHotkey('up', e)) {
this.onUp(e);
} else if (isHotkey('down', e)) {
this.onDown(e);
2023-10-11 15:58:06 +08:00
}
2023-10-08 17:20:38 +08:00
};
2018-08-17 12:23:55 +08:00
onFocusHandler = () => {
this.setState({ width: '570px', isMaskShow: true, isCloseShow: true });
};
2018-08-17 12:23:55 +08:00
onCloseHandler = () => {
this.resetToDefault();
};
2018-08-17 12:23:55 +08:00
onUp = (e) => {
e.preventDefault();
e.stopPropagation();
const { highlightIndex } = this.state;
if (highlightIndex > 0) {
this.setState({ highlightIndex: highlightIndex - 1 }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
if (top - height < 0) {
this.searchContainer.current.scrollTop -= height;
}
}
});
}
};
onDown = (e) => {
e.preventDefault();
e.stopPropagation();
const { highlightIndex, resultItems } = this.state;
if (highlightIndex < resultItems.length - 1) {
this.setState({ highlightIndex: highlightIndex + 1 }, () => {
if (this.highlightRef) {
const { top, height } = this.highlightRef.getBoundingClientRect();
const outerHeight = 300;
if (top > outerHeight) {
this.searchContainer.current.scrollTop += height;
}
}
});
}
};
onEnter = (e) => {
e.preventDefault();
let item = this.state.resultItems[this.state.highlightIndex];
if (item) {
if (document.activeElement) {
document.activeElement.blur();
}
this.onItemClickHandler(item);
}
};
onItemClickHandler = (item) => {
2018-08-17 12:23:55 +08:00
this.resetToDefault();
this.props.onSearchedClick(item);
};
2018-08-17 12:23:55 +08:00
onChangeHandler = (event) => {
const newValue = event.target.value;
this.setState({ value: newValue }, () => {
if (this.inputValue === newValue.trim()) return;
this.inputValue = newValue.trim();
2023-11-08 20:52:59 +08:00
if (!this.isChineseInput) {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.timer = setTimeout(() => {
this.onSearch();
}, SEARCH_DELAY_TIME);
2023-11-08 20:52:59 +08:00
}
});
};
2018-08-17 12:23:55 +08:00
onKeydownHandler = (event) => {
if (isHotkey('enter', event)) {
this.onSearch();
}
};
onSearch = () => {
const { value } = this.state;
const { repoID } = this.props;
2023-11-24 10:54:39 +08:00
if (this.inputValue === '' || getValueLength(this.inputValue) < 3) {
2018-08-17 12:23:55 +08:00
this.setState({
2023-10-11 15:58:06 +08:00
highlightIndex: 0,
2023-10-08 17:20:38 +08:00
resultItems: [],
2018-08-17 12:23:55 +08:00
isResultShow: false,
isResultGetted: false
});
return;
2018-08-17 12:23:55 +08:00
}
const queryData = {
q: value,
search_repo: repoID ? repoID : 'all',
2018-12-13 20:42:51 +08:00
search_ftypes: 'all',
};
this.getSearchResult(queryData);
};
2018-08-17 12:23:55 +08:00
getSearchResult = (queryData) => {
2023-10-08 17:20:38 +08:00
if (this.source) {
this.source.cancel('prev request is cancelled');
2018-08-17 12:23:55 +08:00
}
this.setState({
isResultShow: true,
2023-10-08 17:20:38 +08:00
isResultGetted: false,
resultItems: [],
2023-10-11 15:58:06 +08:00
highlightIndex: 0,
});
this.source = seafileAPI.getSource();
2023-10-08 17:20:38 +08:00
this.sendRequest(queryData, this.source.token, 1);
};
2018-08-17 12:23:55 +08:00
2023-10-08 17:20:38 +08:00
sendRequest = (queryData, cancelToken, page) => {
let isPublic = this.props.isPublic;
2023-10-08 17:20:38 +08:00
this.queryData = queryData;
if (isPublic) {
2023-10-08 17:20:38 +08:00
seafileAPI.searchFilesInPublishedRepo(queryData.search_repo, queryData.q, page, PER_PAGE).then(res => {
this.source = null;
if (res.data.total > 0) {
this.setState({
2023-11-15 17:46:00 +08:00
resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)],
2023-10-08 17:20:38 +08:00
isResultGetted: true,
page: page + 1,
isLoading: false,
hasMore: res.data.has_more,
});
} else {
this.setState({
2023-10-11 15:58:06 +08:00
highlightIndex: 0,
resultItems: [],
2023-10-08 17:20:38 +08:00
isLoading: false,
isResultGetted: true,
hasMore: res.data.has_more,
});
}
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
2023-10-08 17:20:38 +08:00
this.setState({ isLoading: false });
});
} else {
this.updateSearchPageURL(queryData);
2023-10-08 17:20:38 +08:00
queryData['per_page'] = PER_PAGE;
queryData['page'] = page;
seafileAPI.searchFiles(queryData, cancelToken).then(res => {
this.source = null;
if (res.data.total > 0) {
this.setState({
resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)],
isResultGetted: true,
isLoading: false,
page: page + 1,
hasMore: res.data.has_more,
});
return;
}
this.setState({
highlightIndex: 0,
resultItems: [],
isLoading: false,
isResultGetted: true,
hasMore: res.data.has_more,
});
}).catch(error => {
/* eslint-disable */
console.log(error);
this.setState({ isLoading: false });
});
}
};
2023-10-08 17:20:38 +08:00
onResultListScroll = (e) => {
// Load less than 100 results
if (!this.state.hasMore || this.state.isLoading || this.state.resultItems.length > 100) {
return;
}
const listPadding = 20;
if (e.target.scrollTop + e.target.clientHeight + listPadding > this.searchResultListRef.current.clientHeight - 10) {
this.setState({isLoading: true}, () => {
this.source = seafileAPI.getSource();
this.sendRequest(this.queryData, this.source.token, this.state.page);
});
}
};
2018-08-17 12:23:55 +08:00
updateSearchPageURL(queryData) {
let params = '';
for (let key in queryData) {
params += key + '=' + encodeURIComponent(queryData[key]) + '&';
}
this.setState({searchPageUrl: `${this.baseSearchPageURL}?${params.substring(0, params.length - 1)}`});
}
2018-08-17 12:23:55 +08:00
formatResultItems(data) {
let items = [];
2023-10-08 17:20:38 +08:00
for (let i = 0; i < data.length; i++) {
2018-08-17 12:23:55 +08:00
items[i] = {};
items[i]['index'] = [i];
items[i]['name'] = data[i].name;
items[i]['path'] = data[i].fullpath;
items[i]['repo_id'] = data[i].repo_id;
2018-12-13 20:42:51 +08:00
items[i]['repo_name'] = data[i].repo_name;
items[i]['is_dir'] = data[i].is_dir;
2018-08-17 12:23:55 +08:00
items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1);
items[i]['content'] = data[i].content_highlight;
items[i]['thumbnail_url'] = data[i].thumbnail_url;
2018-08-17 12:23:55 +08:00
}
return items;
}
resetToDefault() {
this.inputValue = '';
2018-08-17 12:23:55 +08:00
this.setState({
width: '',
value: '',
isMaskShow: false,
isCloseShow: false,
isResultShow: false,
isResultGetted: false,
resultItems: [],
2023-10-11 15:58:06 +08:00
highlightIndex: 0,
isSearchInputShow: false,
});
2018-08-17 12:23:55 +08:00
}
renderSearchResult() {
2023-11-15 14:27:17 +08:00
const { resultItems, highlightIndex, width } = this.state;
if (!width || width === 'default') return null;
if (!this.state.isResultShow) return null;
2023-11-22 17:31:08 +08:00
if (!this.state.isResultGetted || getValueLength(this.inputValue) < 3) {
2018-08-17 12:23:55 +08:00
return (
<span className="loading-icon loading-tip"></span>
);
2018-08-17 12:23:55 +08:00
}
2023-10-08 17:20:38 +08:00
if (!resultItems.length) {
2018-08-17 12:23:55 +08:00
return (
2019-03-09 12:06:11 +08:00
<div className="search-result-none">{gettext('No results matching.')}</div>
);
2018-08-17 12:23:55 +08:00
}
const results = (
2023-10-08 17:20:38 +08:00
<ul className="search-result-list" ref={this.searchResultListRef}>
{resultItems.map((item, index) => {
const isHighlight = index === highlightIndex;
2018-08-17 12:23:55 +08:00
return (
<SearchResultItem
key={index}
2018-08-17 12:23:55 +08:00
item={item}
2023-10-08 17:20:38 +08:00
onItemClickHandler={this.onItemClickHandler}
isHighlight={isHighlight}
setRef={isHighlight ? (ref) => {this.highlightRef = ref;} : () => {}}
2018-08-17 12:23:55 +08:00
/>
);
2018-08-17 12:23:55 +08:00
})}
</ul>
);
return (
<>
<MediaQuery query="(min-width: 768px)">
<div className="search-result-list-container">{results}</div>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
{results}
</MediaQuery>
</>
);
2018-08-17 12:23:55 +08:00
}
onSearchToggle = () => {
this.setState({
isSearchInputShow: !this.state.isSearchInputShow,
isMaskShow: !this.state.isMaskShow,
});
};
2018-08-06 18:29:12 +08:00
render() {
2018-08-17 12:23:55 +08:00
let width = this.state.width !== 'default' ? this.state.width : '';
let style = {'width': width};
const { isMaskShow } = this.state;
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`;
2018-08-06 18:29:12 +08:00
return (
<Fragment>
<MediaQuery query="(min-width: 768px)">
<div className="search">
<div className={`search-mask ${isMaskShow ? 'show' : 'hide'}`} onClick={this.onCloseHandler}></div>
<div className={`search-container ${isMaskShow ? 'show' : ''}`}>
<div className={`input-icon ${isMaskShow ? 'mb-1' : ''}`}>
<i className="search-icon-left input-icon-addon fas fa-search"></i>
<input
type="text"
className="form-control search-input"
name="query"
placeholder={placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
onChange={this.onChangeHandler}
autoComplete="off"
2023-10-08 17:20:38 +08:00
ref={this.inputRef}
onKeyDown={this.onKeydownHandler}
/>
{this.state.isCloseShow &&
2023-10-08 17:20:38 +08:00
<button type="button" className="search-icon-right input-icon-addon fas fa-times border-0 bg-transparent mr-4" onClick={this.onCloseHandler} aria-label={gettext('Close')}></button>
2019-04-24 11:10:59 +08:00
}
</div>
<div
className="search-result-container dropdown-search-result-container"
onScroll={this.onResultListScroll}
ref={this.searchContainer}
>
{this.renderSearchResult()}
</div>
</div>
2018-08-17 12:23:55 +08:00
</div>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
<div className="search-icon-container">
<i className="search-icon fas fa-search" onClick={this.onSearchToggle}></i>
2018-08-17 12:23:55 +08:00
</div>
{this.state.isSearchInputShow &&
<div className="search">
<div className={`search-mask ${isMaskShow ? '' : 'hide'}`} onClick={this.onCloseHandler}></div>
<div className="search-container">
<div className="input-icon">
<i className="search-icon-left input-icon-addon fas fa-search"></i>
<input
type="text"
className="form-control search-input"
name="query"
placeholder={placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
onChange={this.onChangeHandler}
autoComplete="off"
/>
{this.state.isCloseShow &&
<button type="button" className="search-icon-right input-icon-addon fas fa-times border-0 bg-transparent" onClick={this.onCloseHandler} aria-label={gettext('Close')}></button>
2019-04-24 11:10:59 +08:00
}
</div>
2023-10-08 17:20:38 +08:00
<div className="search-result-container dropdown-search-result-container" onScroll={this.onResultListScroll}>
{this.renderSearchResult()}
</div>
</div>
</div>
}
</MediaQuery>
</Fragment>
);
2018-08-06 18:29:12 +08:00
}
}
2018-10-16 18:19:51 +08:00
Search.propTypes = propTypes;
2018-08-06 18:29:12 +08:00
export default Search;