1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-08 18:30:53 +00:00

[search] redesigned 'show more' for search in wiki (#4383)

* [search] redesigned 'show more' for search in wiki

* [search] modification

* [search] added wiki-search.js

* [wiki] search: modified placeholder text; removed 'arrow'
This commit is contained in:
llj
2019-12-31 16:58:45 +08:00
committed by Daniel Pan
parent fbac92ec68
commit 1c76000843
5 changed files with 348 additions and 59 deletions

View File

@@ -4,13 +4,9 @@ import MediaQuery from 'react-responsive';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot, username } from '../../utils/constants'; import { gettext, siteRoot, username } from '../../utils/constants';
import SearchResultItem from './search-result-item'; import SearchResultItem from './search-result-item';
import editorUtilities from '../../utils/editor-utilities';
import More from '../more'; import More from '../more';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
const propTypes = { const propTypes = {
isPublic: PropTypes.bool,
repoID: PropTypes.string, repoID: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
onSearchedClick: PropTypes.func.isRequired, onSearchedClick: PropTypes.func.isRequired,
@@ -24,6 +20,7 @@ class Search extends Component {
width: 'default', width: 'default',
value: '', value: '',
resultItems: [], resultItems: [],
total: 0,
isMaskShow: false, isMaskShow: false,
isResultShow: false, isResultShow: false,
isResultGetted: false, isResultGetted: false,
@@ -91,58 +88,35 @@ class Search extends Component {
isResultGetted: false isResultGetted: false
}); });
this.source = editorUtilities.getSource(); this.source = seafileAPI.getSource();
this.sendRequest(queryData, this.source.token); this.sendRequest(queryData, this.source.token);
} }
sendRequest(queryData, cancelToken) { sendRequest(queryData, cancelToken) {
var _this = this; var _this = this;
let isPublic = this.props.isPublic;
if (isPublic) { seafileAPI.searchFiles(queryData, cancelToken).then(res => {
seafileAPI.searchFilesInPublishedRepo(queryData.q, queryData.search_repo).then(res => { if (!res.data.total) {
if (!res.data.total) {
_this.setState({
resultItems: [],
isResultGetted: true
});
_this.source = null;
return;
}
let items = _this.formatResultItems(res.data.results);
_this.setState({ _this.setState({
resultItems: items, resultItems: [],
isResultGetted: true isResultGetted: true
}); });
_this.source = null; _this.source = null;
}).catch(error => { return;
let errMessage = Utils.getErrorMsg(error); }
toaster.danger(errMessage);
});
} else {
editorUtilities.searchFiles(queryData,cancelToken).then(res => {
if (!res.data.total) {
_this.setState({
resultItems: [],
isResultGetted: true
});
_this.source = null;
return;
}
let items = _this.formatResultItems(res.data.results); let items = _this.formatResultItems(res.data.results);
_this.setState({ _this.setState({
resultItems: items, total: res.data.total,
isResultGetted: true resultItems: items,
}); isResultGetted: true
_this.source = null; });
}).catch(res => { _this.source = null;
/* eslint-disable */ }).catch(res => {
/* eslint-disable */
console.log(res); console.log(res);
/* eslint-enable */ /* eslint-enable */
}); });
}
} }
cancelRequest() { cancelRequest() {
@@ -209,6 +183,7 @@ class Search extends Component {
for (let key in queryData) { for (let key in queryData) {
params += key + '=' + queryData[key] + '&'; params += key + '=' + queryData[key] + '&';
} }
window.location = siteRoot + 'search/?' + params.slice(0, params.length - 1); window.location = siteRoot + 'search/?' + params.slice(0, params.length - 1);
} }
@@ -227,19 +202,20 @@ class Search extends Component {
<div className="search-result-none">{gettext('No results matching.')}</div> <div className="search-result-none">{gettext('No results matching.')}</div>
); );
} }
let isShowMore = this.state.resultItems.length >= 5 ? true : false; const { resultItems, total } = this.state;
const isShowMore = total > resultItems.length;
return ( return (
<ul className="search-result-list"> <ul className="search-result-list">
{this.state.resultItems.map(item => { {this.state.resultItems.map((item, index) => {
return ( return (
<SearchResultItem <SearchResultItem
key={item.index} key={index}
item={item} item={item}
onItemClickHandler={_this.onItemClickHandler} onItemClickHandler={_this.onItemClickHandler}
/> />
); );
})} })}
{isShowMore && <More onShowMore={this.onShowMore}/>} {isShowMore && <More onShowMore={this.onShowMore} />}
</ul> </ul>
); );
} }
@@ -283,7 +259,7 @@ class Search extends Component {
} }
{this.state.isCloseShow && <i className='search-icon-right input-icon-addon fas fa-times' onClick={this.onCloseHandler}></i>} {this.state.isCloseShow && <i className='search-icon-right input-icon-addon fas fa-times' onClick={this.onCloseHandler}></i>}
</div> </div>
<div className="search-result-container"> <div className="search-result-container dropdown-search-result-container">
{this.renderSearchResult()} {this.renderSearchResult()}
</div> </div>
</div> </div>
@@ -316,7 +292,7 @@ class Search extends Component {
} }
{this.state.isCloseShow && <i className='search-icon-right input-icon-addon fas fa-times' onClick={this.onCloseHandler}></i>} {this.state.isCloseShow && <i className='search-icon-right input-icon-addon fas fa-times' onClick={this.onCloseHandler}></i>}
</div> </div>
<div className="search-result-container"> <div className="search-result-container dropdown-search-result-container">
{this.renderSearchResult()} {this.renderSearchResult()}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,304 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot, username } from '../../utils/constants';
import SearchResultItem from './search-result-item';
import More from '../more';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
const propTypes = {
repoID: PropTypes.string,
placeholder: PropTypes.string,
onSearchedClick: PropTypes.func.isRequired
};
class Search extends Component {
constructor(props) {
super(props);
this.state = {
width: 'default',
value: '',
resultItems: [],
page: 1,
perPage: 5,
total: 0,
isMaskShow: false,
isResultShow: false,
isResultGetted: false,
isCloseShow: false,
isSearchInputShow: false, // for mobile
};
this.inputValue = '';
this.source = null; // used to cancel request;
}
onFocusHandler = () => {
this.setState({
width: '30rem',
isMaskShow: true,
isCloseShow: true
});
}
onCloseHandler = () => {
this.resetToDefault();
}
onItemClickHandler = (item) => {
this.resetToDefault();
this.props.onSearchedClick(item);
}
onChangeHandler = (event) => {
let _this = this;
this.setState({value: event.target.value});
let newValue = event.target.value;
if (this.inputValue === newValue.trim()) {
return false;
}
this.inputValue = newValue.trim();
if (this.inputValue === '' || _this.getValueLength(this.inputValue) < 3) {
this.setState({
isResultShow: false,
isResultGetted: false
});
return false;
}
let repoID = this.props.repoID;
let queryData = {
q: newValue,
search_repo: repoID ? repoID : 'all',
search_ftypes: 'all',
};
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(_this.getSearchResult(queryData), 500);
}
getSearchResult(queryData) {
if(this.source){
this.cancelRequest();
}
this.setState({
isResultShow: true,
isResultGetted: false
});
this.source = seafileAPI.getSource();
this.sendRequest(queryData, this.source.token);
}
searchWiki(search_repo, q, page, perPage) {
var _this = this;
seafileAPI.searchFilesInPublishedRepo(search_repo, q, page, perPage).then(res => {
if (!res.data.total) {
_this.setState({
resultItems: [],
isResultGetted: true
});
_this.source = null;
return;
}
const items = _this.formatResultItems(res.data.results);
_this.setState({
total: res.data.total,
resultItems: page == 1 ? items : this.state.resultItems.concat(items),
isResultGetted: true
});
_this.source = null;
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
sendRequest(queryData) {
// 'page=1' for this first request
this.setState({page: 1}, () => {
const { search_repo, q } = queryData;
const { page, perPage } = this.state;
this.searchWiki(search_repo, q, page, perPage);
});
}
cancelRequest() {
this.source.cancel('prev request is cancelled');
}
getValueLength(str) {
var i = 0, code, len = 0;
for (; i < str.length; i++) {
code = str.charCodeAt(i);
if (code == 10) { //solve enter problem
len += 2;
} else if (code < 0x007f) {
len += 1;
} else if (code >= 0x0080 && code <= 0x07ff) {
len += 2;
} else if (code >= 0x0800 && code <= 0xffff) {
len += 3;
}
}
return len;
}
formatResultItems(data) {
let items = [];
let length = data.length > 5 ? 5 : data.length;
for (let i = 0; i < length; i++) {
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;
items[i]['repo_name'] = data[i].repo_name;
items[i]['is_dir'] = data[i].is_dir;
items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1);
items[i]['content'] = data[i].content_highlight;
}
return items;
}
resetToDefault() {
this.inputValue = null;
this.setState({
width: '',
value: '',
isMaskShow: false,
isCloseShow: false,
isResultShow: false,
isResultGetted: false,
resultItems: [],
isSearchInputShow: false,
});
}
onShowMore = () => {
let repoID = this.props.repoID;
let newValue = this.state.value;
this.setState({
page: this.state.page + 1
}, () => {
const { page, perPage } = this.state;
this.searchWiki(repoID, newValue, page, perPage);
});
}
renderSearchResult() {
var _this = this;
if (!this.state.isResultShow) {
return;
}
if (!this.state.isResultGetted || this.getValueLength(this.inputValue) < 3) {
return (
<span className="loading-icon loading-tip"></span>
);
}
if (!this.state.resultItems.length) {
return (
<div className="search-result-none">{gettext('No results matching.')}</div>
);
}
const { resultItems, total } = this.state;
const isShowMore = total > resultItems.length;
return (
<ul className="search-result-list">
{this.state.resultItems.map((item, index) => {
return (
<SearchResultItem
key={index}
item={item}
onItemClickHandler={_this.onItemClickHandler}
/>
);
})}
{isShowMore && <More onShowMore={this.onShowMore} />}
</ul>
);
}
onSearchToggle = () => {
this.setState({
isSearchInputShow: !this.state.isSearchInputShow,
isMaskShow: !this.state.isMaskShow,
});
}
render() {
let width = this.state.width !== 'default' ? this.state.width : '';
let style = {'width': width};
return (
<Fragment>
<MediaQuery query="(min-width: 768px)">
<div className="search">
<div className={`search-mask ${this.state.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={this.props.placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
onChange={this.onChangeHandler}
autoComplete="off"
/>
{this.state.isCloseShow && <i className='search-icon-right input-icon-addon fas fa-times' onClick={this.onCloseHandler}></i>}
</div>
<div className="search-result-container dropdown-search-result-container">
{this.renderSearchResult()}
</div>
</div>
</div>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
<div className="search-icon-container">
<i className="search-icon fas fa-search" onClick={this.onSearchToggle}></i>
</div>
{this.state.isSearchInputShow &&
<div className="search">
<div className={`search-mask ${this.state.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={this.props.placeholder}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
onChange={this.onChangeHandler}
autoComplete="off"
/>
{this.state.isCloseShow && <i className='search-icon-right input-icon-addon fas fa-times' onClick={this.onCloseHandler}></i>}
</div>
<div className="search-result-container dropdown-search-result-container">
{this.renderSearchResult()}
</div>
</div>
</div>
}
</MediaQuery>
</Fragment>
);
}
}
Search.propTypes = propTypes;
export default Search;

View File

@@ -23,7 +23,7 @@ class CommonToolbar extends React.Component {
onSearchedClick={this.props.onSearchedClick} onSearchedClick={this.props.onSearchedClick}
/> />
)} )}
<Notification /> <Notification />
<Account /> <Account />
</div> </div>
); );
@@ -32,4 +32,4 @@ class CommonToolbar extends React.Component {
CommonToolbar.propTypes = propTypes; CommonToolbar.propTypes = propTypes;
export default CommonToolbar; export default CommonToolbar;

View File

@@ -44,6 +44,10 @@
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1); box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
} }
.dropdown-search-result-container {
max-height: 300px;
overflow: auto;
}
.search-result-container .search-result-none { .search-result-container .search-result-none {
text-align: center; text-align: center;

View File

@@ -6,7 +6,9 @@ import WikiMarkdownViewer from '../../components/wiki-markdown-viewer';
import WikiDirListView from '../../components/wiki-dir-list-view/wiki-dir-list-view'; import WikiDirListView from '../../components/wiki-dir-list-view/wiki-dir-list-view';
import Loading from '../../components/loading'; import Loading from '../../components/loading';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import Search from '../../components/search/search'; import Search from '../../components/search/wiki-search';
import Notification from '../../components/common/notification';
import Account from '../../components/common/account';
const propTypes = { const propTypes = {
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
@@ -89,10 +91,9 @@ class MainPanel extends Component {
</div> </div>
<div className="common-toolbar"> <div className="common-toolbar">
<Search <Search
isPublic={true}
repoID={repoID} repoID={repoID}
onSearchedClick={this.props.onSearchedClick} onSearchedClick={this.props.onSearchedClick}
placeholder={gettext('Search files in this library')} placeholder={gettext('Search')}
/> />
</div> </div>
</Fragment> </Fragment>
@@ -107,11 +108,15 @@ class MainPanel extends Component {
<span className="fa fa-pencil-alt mobile-toolbar-icon" title={gettext('Edit')} onClick={this.onEditClick} style={{'font-size': '1.1rem'}}></span> <span className="fa fa-pencil-alt mobile-toolbar-icon" title={gettext('Edit')} onClick={this.onEditClick} style={{'font-size': '1.1rem'}}></span>
)} )}
</div> </div>
<CommonToolbar <div className="common-toolbar">
repoID={repoID} <Search
onSearchedClick={this.props.onSearchedClick} repoID={repoID}
searchPlaceholder={gettext('Search files in this library')} onSearchedClick={this.props.onSearchedClick}
/> placeholder={gettext('Search')}
/>
<Notification />
<Account />
</div>
</Fragment> </Fragment>
)} )}
</div> </div>