mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-03 16:10:26 +00:00
wiki search (#2278)
This commit is contained in:
committed by
Daniel Pan
parent
c24d527a03
commit
0105e8e8ef
@@ -3,7 +3,6 @@ import Search from './search';
|
|||||||
import MarkdownViewer from './markdown-viewer';
|
import MarkdownViewer from './markdown-viewer';
|
||||||
import Account from './account';
|
import Account from './account';
|
||||||
import { repoID, serviceUrl, slug, siteRoot } from './constance';
|
import { repoID, serviceUrl, slug, siteRoot } from './constance';
|
||||||
import { processorGetAST } from '@seafile/seafile-editor/src/lib/seafile-markdown2html';
|
|
||||||
|
|
||||||
class MainPanel extends Component {
|
class MainPanel extends Component {
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ class MainPanel extends Component {
|
|||||||
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>Edit Page</a>
|
<a className="btn btn-secondary btn-topbar" onClick={this.onEditClick}>Edit Page</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="common-toolbar">
|
<div className="common-toolbar">
|
||||||
<Search />
|
<Search seafileAPI={this.props.seafileAPI} onSearchedClick={this.props.onSearchedClick}/>
|
||||||
<Account seafileAPI={this.props.seafileAPI} />
|
<Account seafileAPI={this.props.seafileAPI} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +55,7 @@ class MainPanel extends Component {
|
|||||||
latestContributor={this.props.latestContributor}
|
latestContributor={this.props.latestContributor}
|
||||||
lastModified = {this.props.lastModified}
|
lastModified = {this.props.lastModified}
|
||||||
onLinkClick={this.props.onLinkClick}
|
onLinkClick={this.props.onLinkClick}
|
||||||
|
isFileLoading={this.props.isFileLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
22
frontend/src/components/SearchResultItem.js
Normal file
22
frontend/src/components/SearchResultItem.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
class SearchResultItem extends React.Component {
|
||||||
|
|
||||||
|
onClickHandler = () => {
|
||||||
|
var item = this.props.item;
|
||||||
|
this.props.onItemClickHandler(item.link);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let item = this.props.item;
|
||||||
|
return (
|
||||||
|
<li className="search-result-item" onClick={this.onClickHandler}>
|
||||||
|
<span className="item-content item-name">{item.name}</span>
|
||||||
|
<span className="item-content item-link">{item.link_content}</span>
|
||||||
|
<div className="item-content item-text" dangerouslySetInnerHTML={{__html: item.content}}></div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchResultItem;
|
@@ -50,8 +50,7 @@ class MarkdownViewer extends React.Component {
|
|||||||
html: '',
|
html: '',
|
||||||
outlineTreeRoot: null,
|
outlineTreeRoot: null,
|
||||||
navItems: [],
|
navItems: [],
|
||||||
activeId: 0,
|
activeId: 0
|
||||||
isLoading: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToNode(node) {
|
scrollToNode(node) {
|
||||||
@@ -117,15 +116,9 @@ class MarkdownViewer extends React.Component {
|
|||||||
var currentId = navItems.length > 0 ? navItems[0].id : 0;
|
var currentId = navItems.length > 0 ? navItems[0].id : 0;
|
||||||
_this.setState({
|
_this.setState({
|
||||||
navItems: navItems,
|
navItems: navItems,
|
||||||
activeId: currentId,
|
activeId: currentId
|
||||||
isLoading: false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_this.setState({
|
|
||||||
isLoading: false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +144,7 @@ class MarkdownViewer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.isLoading) {
|
if (this.props.isFileLoading) {
|
||||||
return (
|
return (
|
||||||
<span className="loading-icon loading-tip"></span>
|
<span className="loading-icon loading-tip"></span>
|
||||||
)
|
)
|
||||||
|
@@ -1,17 +1,215 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { repoID, siteRoot } from './constance';
|
import { repoID } from './constance';
|
||||||
|
import SearchResultItem from './SearchResultItem';
|
||||||
|
|
||||||
class Search extends Component {
|
class Search extends Component {
|
||||||
render() {
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
width: 'default',
|
||||||
|
value: '',
|
||||||
|
resultItems: [],
|
||||||
|
isMaskShow: false,
|
||||||
|
isResultShow: false,
|
||||||
|
isResultGetted: false,
|
||||||
|
isCloseShow: false
|
||||||
|
};
|
||||||
|
this.inputValue = '';
|
||||||
|
this.source = null; // used to cancle request;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusHandler = () => {
|
||||||
|
this.setState({
|
||||||
|
width: '30rem',
|
||||||
|
isMaskShow: true,
|
||||||
|
isCloseShow: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseHandler = () => {
|
||||||
|
this.resetToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemClickHandler = (path) => {
|
||||||
|
this.resetToDefault();
|
||||||
|
this.props.onSearchedClick(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 queryData = {
|
||||||
|
q: newValue,
|
||||||
|
search_repo: repoID,
|
||||||
|
search_ftypes: 'custom',
|
||||||
|
ftype: 'Markdown',
|
||||||
|
input_fexts: 'md'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = this.props.seafileAPI.getSource();
|
||||||
|
this.sendRequest(queryData, this.source.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(queryData, cancelToken) {
|
||||||
|
var _this = this;
|
||||||
|
this.props.seafileAPI.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);
|
||||||
|
_this.setState({
|
||||||
|
resultItems: items,
|
||||||
|
isResultGetted: true
|
||||||
|
})
|
||||||
|
_this.source = null;
|
||||||
|
}).catch(res => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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]['link'] = data[i].fullpath;
|
||||||
|
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: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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">No results matching.</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<form id="top-search-form" method="get" action={siteRoot + 'search/'} className="hidden-sm-down search-form">
|
<ul className="search-result-list">
|
||||||
<input
|
{this.state.resultItems.map(item => {
|
||||||
type="text" className="search-input" name="q"
|
return (
|
||||||
placeholder="Search files in this wiki"
|
<SearchResultItem
|
||||||
/>
|
key={item.index}
|
||||||
<input type="hidden" name="search_repo" value={repoID} />
|
item={item}
|
||||||
<button type="submit" className="search-submit" aria-label="Submit"><span className="icon-search"></span></button>
|
onItemClickHandler={_this.onItemClickHandler}
|
||||||
</form>
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let width = this.state.width !== 'default' ? this.state.width : '';
|
||||||
|
let style = {'width': width};
|
||||||
|
return (
|
||||||
|
<div className="search">
|
||||||
|
<div className={`search-mask ${this.state.isMaskShow ? "" : "hide"}`} onClick={this.onCloseHandler}></div>
|
||||||
|
<div className="search-container">
|
||||||
|
<div className="search-input-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="search-input"
|
||||||
|
name="query"
|
||||||
|
placeholder="Search files in this wiki"
|
||||||
|
style={style}
|
||||||
|
value={this.state.value}
|
||||||
|
onFocus={this.onFocusHandler}
|
||||||
|
onChange={this.onChangeHandler}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<a className="search-icon icon-search"></a>
|
||||||
|
<a className={`search-icon sf2-icon-x3 ${this.state.isCloseShow ? "" : "hide"}`} onClick={this.onCloseHandler}></a>
|
||||||
|
</div>
|
||||||
|
<div className="search-result-container">
|
||||||
|
{this.renderSearchResult()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
97
frontend/src/css/search.css
Normal file
97
frontend/src/css/search.css
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.search-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 0 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content:center;
|
||||||
|
align-items:center;
|
||||||
|
color: #999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-search{
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf2-icon-x3{
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 2rem;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-container .search-result-none {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-container .search-result-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-container .search-result-item {
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border-left: 2px solid #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-container .search-result-item:hover {
|
||||||
|
border-left: 2px solid #eb8205;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item .item-content {
|
||||||
|
font-weight: normal;
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.search-result-item .item-name {
|
||||||
|
color: #eb8205 !important;
|
||||||
|
}
|
||||||
|
.search-result-item .item-link {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.search-result-item .item-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.search-result-item .item-text b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
@@ -182,4 +182,4 @@ img[src=""] {
|
|||||||
|
|
||||||
.wiki-md-viewer-rendered-content.article h1 {
|
.wiki-md-viewer-rendered-content.article h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
@@ -12,6 +12,7 @@ import './assets/css/fontawesome.css';
|
|||||||
import 'seafile-ui';
|
import 'seafile-ui';
|
||||||
import './css/side-panel.css';
|
import './css/side-panel.css';
|
||||||
import './css/wiki.css';
|
import './css/wiki.css';
|
||||||
|
import './css/search.css';
|
||||||
|
|
||||||
// init seafileAPI
|
// init seafileAPI
|
||||||
let seafileAPI = new SeafileAPI();
|
let seafileAPI = new SeafileAPI();
|
||||||
@@ -46,7 +47,8 @@ class Wiki extends Component {
|
|||||||
filePath: '',
|
filePath: '',
|
||||||
latestContributor: '',
|
latestContributor: '',
|
||||||
lastModified: '',
|
lastModified: '',
|
||||||
permission: ''
|
permission: '',
|
||||||
|
isFileLoading: false
|
||||||
};
|
};
|
||||||
window.onpopstate = this.onpopstate;
|
window.onpopstate = this.onpopstate;
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,12 @@ class Wiki extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSearchedClick = (path) => {
|
||||||
|
if (path) {
|
||||||
|
this.loadFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onFileClick = (e, node) => {
|
onFileClick = (e, node) => {
|
||||||
if (node.isMarkdown()) {
|
if (node.isMarkdown()) {
|
||||||
this.loadFile(node.path);
|
this.loadFile(node.path);
|
||||||
@@ -91,6 +99,9 @@ class Wiki extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadFile(filePath) {
|
loadFile(filePath) {
|
||||||
|
this.setState({
|
||||||
|
isFileLoading: true
|
||||||
|
})
|
||||||
seafileAPI.getWikiFileContent(slug, filePath)
|
seafileAPI.getWikiFileContent(slug, filePath)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -99,7 +110,8 @@ class Wiki extends Component {
|
|||||||
lastModified: moment.unix(res.data.last_modified).fromNow(),
|
lastModified: moment.unix(res.data.last_modified).fromNow(),
|
||||||
permission: res.data.permission,
|
permission: res.data.permission,
|
||||||
fileName: this.fileNameFromPath(filePath),
|
fileName: this.fileNameFromPath(filePath),
|
||||||
filePath: filePath
|
filePath: filePath,
|
||||||
|
isFileLoading: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -140,10 +152,12 @@ class Wiki extends Component {
|
|||||||
filePath={this.state.filePath}
|
filePath={this.state.filePath}
|
||||||
onLinkClick={this.onLinkClick}
|
onLinkClick={this.onLinkClick}
|
||||||
onMenuClick={this.onMenuClick}
|
onMenuClick={this.onMenuClick}
|
||||||
|
onSearchedClick={this.onSearchedClick}
|
||||||
latestContributor={this.state.latestContributor}
|
latestContributor={this.state.latestContributor}
|
||||||
lastModified={this.state.lastModified}
|
lastModified={this.state.lastModified}
|
||||||
seafileAPI={seafileAPI}
|
seafileAPI={seafileAPI}
|
||||||
permission={this.state.permission}
|
permission={this.state.permission}
|
||||||
|
isFileLoading={this.state.isFileLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
.sf2-icon-monitor:before { content:"\e007"; }
|
.sf2-icon-monitor:before { content:"\e007"; }
|
||||||
.sf2-icon-wrench:before { content:"\e001"; }
|
.sf2-icon-wrench:before { content:"\e001"; }
|
||||||
.sf2-icon-bell:before { content:"\e003"; }
|
.sf2-icon-bell:before { content:"\e003"; }
|
||||||
|
.sf2-icon-x3:before { content:"\e035"; }
|
||||||
|
|
||||||
|
|
||||||
/****** icon-xx ********/
|
/****** icon-xx ********/
|
||||||
|
Reference in New Issue
Block a user