Просмотр исходного кода

WIP: Search under the tree

Remaining TODO:

- Handling form clear button
- Handling affix
Sotaro KARASAWA 9 лет назад
Родитель
Сommit
34de0b7ead

+ 1 - 1
lib/views/layout/layout.html

@@ -52,7 +52,7 @@
       {% block title %}{{ config.crowi['app:title'] }}{% endblock %}
     </a>
   {% if searchConfigured() %}
-  <div class="navbar-form navbar-left search-top" role="search" id="search-top">
+  <div class="navbar-form navbar-left search-top visible-lg visible-md" role="search" id="search-top">
   </div>
   {% endif %}
   </div>

+ 11 - 6
lib/views/page_list.html

@@ -16,12 +16,15 @@
     <h1 class="title">
       <span class="" id="revision-path">{{ path|insertSpaceToEachSlashes }}</span>
       {% if searchConfigured() && path != '/' %}
-      <div class="form-group form-group-sm has-feedback search-input-group" data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索">
-        <label class="control-label sr-only" for="inputSuccess5">Search</label>
-        <input
-        type="text" class="search-listpage-input form-control" data-path="{{ path }}" id="page-list-search-form" >
-        <i class="form-control-feedback search-listpage-icon fa fa-search"></i>
-      </div>
+      <form class="input-group search-input-group hidden-xs hidden-sm" data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索" id="search-listpage-form">
+        <input type="text" class="search-listpage-input form-control" data-path="{{ path }}" id="search-listpage-input">
+        <span class="input-group-btn search-listpage-submit-group">
+          <button type="submit" class="btn btn-default" id="search-listpage-submit">
+            <i class="fa fa-search"></i>
+          </button>
+        </span>
+        <a class="search-listpage-clear" id="search-listpage-clear"><i class="fa fa-times-circle"></i></a>
+      </form>
       {% endif %}
     </h1>
   </header>
@@ -38,8 +41,10 @@
  # but now the header and page list content is rendered separately by the server,
  # so now bind the values through the hidden fields.
  #}
+{% if searchConfigured() && path != '/' %}
 <div id="page-list-search">
 </div>
+{% endif %}
 
 <div class="page-list content-main {% if req.body.pageForm %}on-edit{% endif %}"
   id="content-main"

+ 27 - 5
resource/css/_search.scss

@@ -2,24 +2,46 @@
   font-size: 16px;
   color: #999;
 }
+.search-listpage-clear {
+  display: none;
+  position: absolute;
+  font-size: 0.6em;
+  width: 22px;
+  height: 22px;
+  color: #ccc;
+  padding: 5px 5px 5px 42px;
+}
 
 .search-input-group {
   display: inline-block;
   margin-bottom: 0;
   width: 200px;
   vertical-align: bottom;
-}
 
-.search-listpage-input {
+  .search-listpage-submit-group {
+    position: absolute;
+    width: 30px;
+    height: 30px;
+    padding-left: 197px;
+    border-radius: 0;
+
+    > .btn {
+      padding: 6px 12px 7px;
+    }
+  }
 }
 
+
 .search-top {
   .search-top-input-group {
     .search-top-input {
+      &:focus {
+        width: 400px;
+        transition: .3s ease;
+      }
     }
-    &:focus {
-      width: 400px;
-      transition: .3s ease;
+    .btn {
+      padding: 6px 12px 7px;
     }
   }
 }

+ 2 - 1
resource/js/components/PageList/Page.js

@@ -18,7 +18,7 @@ export default class Page extends React.Component {
         {this.props.children}
         <UserPicture user={page.revision.author} />
         <a className="page-list-link" href={link}>
-          <PagePath page={page} />
+          <PagePath page={page} excludePathString={this.props.excludePathString} />
         </a>
         <PageListMeta page={page} />
       </li>
@@ -34,5 +34,6 @@ Page.propTypes = {
 Page.defaultProps = {
   page: {},
   linkTo: '',
+  excludePathString: '',
 };
 

+ 4 - 2
resource/js/components/PageList/PagePath.js

@@ -26,8 +26,9 @@ export default class PagePath extends React.Component {
 
   render() {
     const page = this.props.page;
-    const shortPath = this.getShortPath(page.path);
-    const pathPrefix = page.path.replace(new RegExp(shortPath + '(/)?$'), '');
+    const pagePath = page.path.replace(this.props.excludePathString.replace(/^\//, ''), '');
+    const shortPath = this.getShortPath(pagePath);
+    const pathPrefix = pagePath.replace(new RegExp(shortPath + '(/)?$'), '');
 
     return (
       <span className="page-path">
@@ -43,4 +44,5 @@ PagePath.propTypes = {
 
 PagePath.defaultProps = {
   page: {},
+  excludePathString: '',
 };

+ 66 - 23
resource/js/components/PageListSearch.js

@@ -2,6 +2,7 @@
 
 import React from 'react';
 import axios from 'axios'
+import SearchResult from './SearchPage/SearchResult';
 
 export default class PageListSearch extends React.Component {
 
@@ -9,28 +10,50 @@ export default class PageListSearch extends React.Component {
     super(props);
 
     this.state = {
-      keyword: '',
+      location: location,
+      tree: $('#search-listpage-input').data('path'),
+      searchingKeyword: this.props.query.q || '',
+      searchedKeyword: '',
+      searchedPages: [],
+      searchResultMeta: {},
+      searchError: null,
     }
 
-    //this.changeURL = this.changeURL.bind(this);
+    this.changeURL = this.changeURL.bind(this);
     this.search = this.search.bind(this);
     this.handleChange = this.handleChange.bind(this);
-    this.ticker = null;
   }
 
   componentDidMount() {
-    // This is temporary data bind
-    this.ticker = setInterval(this.pageListObserver.bind(this), 1000);
-  }
+    const $pageListSearchForm = $('#search-listpage-input');
 
-  componentWillUnmount() {
-    clearInterval(this.ticker);
+    // search after page initialized
+    if (this.state.searchingKeyword !== '') {
+      const keyword = this.state.searchingKeyword;
+      $pageListSearchForm.val(keyword);
+      this.search({keyword});
+    }
+
+    // This is temporary data bind ... (out of component)
+    $('#search-listpage-form').on('submit', () => {
+      const keyword = $pageListSearchForm.val();
+      console.log('submit with', keyword);
+      if (keyword != this.state.searchingKeyword)  {
+        this.search({keyword});
+      }
+
+      return false;
+    });
+
+    $('#search-listpage-clear').on('click', () => {
+      $pageListSearchForm.val('');
+      this.search({keyword: ''});
+
+      return false;
+    });
   }
 
-  pageListObserver() {
-    let value = $('#page-list-search-form').val();
-    this.setState({keyword: value});
-    this.search();
+  componentWillUnmount() {
   }
 
   static getQueryByLocation(location) {
@@ -48,44 +71,58 @@ export default class PageListSearch extends React.Component {
   handleChange(event) {
     // this is not fired now because of force-data-bound by jQuery
     const keyword = event.target.value;
-    this.setState({keyword});
+    this.setState({searchedKeyword: keyword});
     console.log('Changed');
   }
 
+  stopSearching() {
+    $('#content-main').show();
+    $('#search-listpage-clear').hide();
+    $('.main-container').removeClass('aside-hidden');
+  }
+
+  startSearching() {
+    $('#content-main').hide();
+    $('#search-listpage-clear').show();
+    $('.main-container').addClass('aside-hidden');
+  }
+
   changeURL(keyword, refreshHash) {
+    const tree = this.state.tree;
     let hash = location.hash || '';
     // TODO 整理する
     if (refreshHash || this.state.searchedKeyword !== '') {
         hash = '';
     }
     if (window.history && window.history.pushState){
-      window.history.pushState('', `Search - ${keyword}`, `/_search?q=${keyword}${hash}`);
+      window.history.pushState('', `Search - ${keyword}`, `${tree}?q=${keyword}${hash}`);
     }
   }
 
-  search() {
-    const keyword = this.state.keyword;
-
-    console.log('Search with', keyword);
-    return true ;
+  search(data) {
+    const keyword = data.keyword || '';
+    const tree = this.state.tree;
+    console.log('search with', keyword, tree);
 
+    this.changeURL(keyword);
     if (keyword === '') {
+      this.stopSearching();
       this.setState({
         searchingKeyword: '',
         searchedPages: [],
+        searchResultMeta: {},
       });
 
       return true;
     }
 
+    this.startSearching();
     this.setState({
       searchingKeyword: keyword,
     });
-
-    axios.get('/_api/search', {params: {q: keyword}})
+    axios.get('/_api/search', {params: {q: keyword, tree: tree}})
     .then((res) => {
       if (res.data.ok) {
-        this.changeURL(keyword);
 
         this.setState({
           searchedKeyword: keyword,
@@ -108,10 +145,16 @@ export default class PageListSearch extends React.Component {
         <input
           type="hidden"
           name="q"
-          value={this.state.keyword}
+          value={this.state.searchingKeyword}
           onChange={this.handleChange}
           className="form-control"
           />
+        <SearchResult
+          tree={this.state.tree}
+          pages={this.state.searchedPages}
+          searchingKeyword={this.state.searchingKeyword}
+          searchResultMeta={this.state.searchResultMeta}
+          />
       </div>
     );
   }

+ 1 - 0
resource/js/components/SearchPage.js

@@ -59,6 +59,7 @@ export default class SearchPage extends React.Component {
       this.setState({
         searchingKeyword: '',
         searchedPages: [],
+        searchResultMeta: {},
       });
 
       return true;

+ 37 - 4
resource/js/components/SearchPage/SearchResult.js

@@ -6,12 +6,42 @@ import SearchResultList from './SearchResultList';
 // Search.SearchResult
 export default class SearchResult extends React.Component {
 
+  isNotSearchedYet() {
+    return !this.props.searchResultMeta.took;
+  }
+
+  isNotFound() {
+    return this.props.searchingKeyword !== '' && this.props.pages.length === 0;
+  }
+
   render() {
+    const excludePathString = this.props.tree;
+
+    if (this.isNotSearchedYet()) {
+      return <div />;
+    }
+
+    if (this.isNotFound()) {
+      let under = '';
+      if (this.props.tree !== '') {
+        under = ` under "${this.props.tree}"`;
+      }
+      return (
+        <div className="content-main">
+            <i className="fa fa-meh-o" /> No page found with "{this.props.searchingKeyword}"{under}
+        </div>
+      );
+
+    }
 
     const listView = this.props.pages.map((page) => {
       const pageId = "#" + page._id;
       return (
-        <Page page={page} linkTo={pageId} key={page._id}>
+        <Page page={page}
+          linkTo={pageId}
+          key={page._id}
+          excludePathString={excludePathString}
+          >
           <div className="page-list-option">
             <a href={page.path}><i className="fa fa-arrow-circle-right" /></a>
           </div>
@@ -24,7 +54,7 @@ export default class SearchResult extends React.Component {
     <span className="search-result-meta">Found: {this.props.searchResultMeta.total} pages with "{this.props.searchingKeyword}"</span>
     */
     return (
-      <div className="content-main" id="content-main">
+      <div className="content-main">
         <div className="search-result row" id="search-result">
           <div className="col-md-4 page-list search-result-list" id="search-result-list">
             <nav data-spy="affix"  data-offset-top="120">
@@ -47,11 +77,14 @@ export default class SearchResult extends React.Component {
 }
 
 SearchResult.propTypes = {
-  searchedPages: React.PropTypes.array.isRequired,
+  tree: React.PropTypes.string.isRequired,
+  pages: React.PropTypes.array.isRequired,
   searchingKeyword: React.PropTypes.string.isRequired,
+  searchResultMeta: React.PropTypes.object.isRequired,
 };
 SearchResult.defaultProps = {
-  searchedPages: [],
+  tree: '',
+  pages: [],
   searchingKeyword: '',
   searchResultMeta: {},
 };