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

Merge pull request #89 from crowi/feature-search

Search Feature
Sotaro KARASAWA 9 лет назад
Родитель
Сommit
633bd59478

+ 0 - 2
lib/views/page_list.html

@@ -15,7 +15,6 @@
     {% endif %}
     {% endif %}
     <h1 class="title">
     <h1 class="title">
       <span class="" id="revision-path">{{ path|insertSpaceToEachSlashes }}</span>
       <span class="" id="revision-path">{{ path|insertSpaceToEachSlashes }}</span>
-      {#
       {% if searchConfigured() && path != '/' %}
       {% if searchConfigured() && path != '/' %}
       <div class="form-group form-group-sm has-feedback search-input-group" data-toggle="tooltip" data-placement="bottom" title="{{ 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>
         <label class="control-label sr-only" for="inputSuccess5">Search</label>
@@ -25,7 +24,6 @@
         <i class="form-control-feedback search-listpage-icon fa fa-search"></i>
         <i class="form-control-feedback search-listpage-icon fa fa-search"></i>
       </div>
       </div>
       {% endif %}
       {% endif %}
-      #}
     </h1>
     </h1>
   </header>
   </header>
 </div>
 </div>

+ 19 - 0
resource/css/_search.scss

@@ -70,5 +70,24 @@
   }
   }
 
 
   .search-result-content {
   .search-result-content {
+    .search-result-meta {
+      margin-bottom: 16px;
+      font-weight: bold;
+    }
+    .search-result-page {
+      > h2 {
+        font-size: 20px;
+      }
+      &:first-child > h2 {
+        margin-top: 0;
+      }
+
+      .wiki {
+        border: solid 1px #ccc;
+        padding: 16px;
+        border-radius: 3px;
+        font-size: 13px;
+      }
+    }
   }
   }
 }
 }

+ 10 - 0
resource/css/_wiki.scss

@@ -130,6 +130,16 @@ div.body {
     }
     }
   }
   }
 
 
+  em.highlighted {
+    padding: 2px;
+    margin: 0 -2px;
+    font-weight: bold;
+    font-style: normal;
+    color: #333;
+    background-color: rgba(255,255,140,0.5);
+    border-radius: 3px;
+  }
+
   // {{{ table (copied from bootstrap .table
   // {{{ table (copied from bootstrap .table
   table {
   table {
     width: 100%;
     width: 100%;

+ 4 - 3
resource/js/app.js

@@ -1,8 +1,9 @@
 import React from 'react';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactDOM from 'react-dom';
 
 
-import SearchBox  from './components/Header/SearchBox';
-import SearchPage  from './components/Search/SearchPage';
+import HeaderSearchBox  from './components/HeaderSearchBox';
+import SearchPage  from './components/SearchPage';
+//import ListPageSearch  from './components/ListPageSearch';
 
 
 /*
 /*
 class Crowi extends React.Component {
 class Crowi extends React.Component {
@@ -21,7 +22,7 @@ class Crowi extends React.Component {
 */
 */
 
 
 var componentMappings = {
 var componentMappings = {
-  'search-top': <SearchBox />,
+  'search-top': <HeaderSearchBox />,
   'search-page': <SearchPage />,
   'search-page': <SearchPage />,
 };
 };
 
 

+ 19 - 3
resource/js/components/Header/SearchBox.js → resource/js/components/HeaderSearchBox.js

@@ -2,8 +2,8 @@
 
 
 import React from 'react';
 import React from 'react';
 
 
-import SearchForm from './SearchForm';
-import SearchSuggest from './SearchSuggest';
+import SearchForm from './HeaderSearchBox/SearchForm';
+import SearchSuggest from './HeaderSearchBox/SearchSuggest';
 import axios from 'axios'
 import axios from 'axios'
 
 
 export default class SearchBox extends React.Component {
 export default class SearchBox extends React.Component {
@@ -16,9 +16,15 @@ export default class SearchBox extends React.Component {
       searchedPages: [],
       searchedPages: [],
       searchError: null,
       searchError: null,
       searching: false,
       searching: false,
+      focused: false,
     }
     }
 
 
     this.search = this.search.bind(this);
     this.search = this.search.bind(this);
+    this.isShown = this.isShown.bind(this);
+  }
+
+  isShown(focused) {
+    this.setState({focused: !!focused});
   }
   }
 
 
   search(data) {
   search(data) {
@@ -44,6 +50,12 @@ export default class SearchBox extends React.Component {
           searchingKeyword: keyword,
           searchingKeyword: keyword,
           searchedPages: res.data.data,
           searchedPages: res.data.data,
           searching: false,
           searching: false,
+          searchError: null,
+        });
+      } else {
+        this.setState({
+          searchError: res,
+          searching: false,
         });
         });
       }
       }
       // TODO error
       // TODO error
@@ -59,12 +71,16 @@ export default class SearchBox extends React.Component {
   render() {
   render() {
     return (
     return (
       <div className="search-box">
       <div className="search-box">
-        <SearchForm onSearchFormChanged={this.search} />
+        <SearchForm
+          onSearchFormChanged={this.search}
+          isShown={this.isShown}
+          />
         <SearchSuggest
         <SearchSuggest
           searchingKeyword={this.state.searchingKeyword}
           searchingKeyword={this.state.searchingKeyword}
           searchedPages={this.state.searchedPages}
           searchedPages={this.state.searchedPages}
           searchError={this.state.searchError}
           searchError={this.state.searchError}
           searching={this.state.searching}
           searching={this.state.searching}
+          focused={this.state.focused}
           />
           />
       </div>
       </div>
     );
     );

+ 11 - 5
resource/js/components/Header/SearchForm.js → resource/js/components/HeaderSearchBox/SearchForm.js

@@ -12,7 +12,8 @@ export default class SearchForm extends React.Component {
     };
     };
 
 
     this.handleChange = this.handleChange.bind(this);
     this.handleChange = this.handleChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
+    this.handleFocus = this.handleFocus.bind(this);
+    this.handleBlur = this.handleBlur.bind(this);
     this.ticker = null;
     this.ticker = null;
   }
   }
 
 
@@ -35,9 +36,12 @@ export default class SearchForm extends React.Component {
     this.search();
     this.search();
   }
   }
 
 
-  handleSubmit(event) {
-    event.preventDefault();
-    this.search();
+  handleFocus(event) {
+    this.props.isShown(true);
+  }
+
+  handleBlur(event) {
+    //this.props.isShown(false);
   }
   }
 
 
   handleChange(event) {
   handleChange(event) {
@@ -50,7 +54,6 @@ export default class SearchForm extends React.Component {
       <form
       <form
         action="/_search"
         action="/_search"
         className="search-form form-group input-group search-top-input-group"
         className="search-form form-group input-group search-top-input-group"
-        onSubmit={this.handleSubmit}
       >
       >
         <input
         <input
           autocomplete="off"
           autocomplete="off"
@@ -59,6 +62,8 @@ export default class SearchForm extends React.Component {
           placeholder="Search ..."
           placeholder="Search ..."
           name="q"
           name="q"
           value={this.state.keyword}
           value={this.state.keyword}
+          onFocus={this.handleFocus}
+          onBlur={this.handleBlur}
           onChange={this.handleChange}
           onChange={this.handleChange}
         />
         />
         <span className="input-group-btn">
         <span className="input-group-btn">
@@ -73,6 +78,7 @@ export default class SearchForm extends React.Component {
 
 
 SearchForm.propTypes = {
 SearchForm.propTypes = {
   onSearchFormChanged: React.PropTypes.func.isRequired,
   onSearchFormChanged: React.PropTypes.func.isRequired,
+  isShown: React.PropTypes.func.isRequired,
   pollInterval: React.PropTypes.number,
   pollInterval: React.PropTypes.number,
 };
 };
 SearchForm.defaultProps = {
 SearchForm.defaultProps = {

+ 13 - 0
resource/js/components/Header/SearchSuggest.js → resource/js/components/HeaderSearchBox/SearchSuggest.js

@@ -5,6 +5,10 @@ import ListView from '../PageList/ListView';
 export default class SearchSuggest extends React.Component {
 export default class SearchSuggest extends React.Component {
 
 
   render() {
   render() {
+    if (!this.props.focused) {
+      return <div></div>;
+    }
+
     if (this.props.searching) {
     if (this.props.searching) {
       return (
       return (
         <div className="search-suggest" id="search-suggest">
         <div className="search-suggest" id="search-suggest">
@@ -13,6 +17,14 @@ export default class SearchSuggest extends React.Component {
       );
       );
     }
     }
 
 
+    if (this.props.searchError !== null) {
+      return (
+        <div className="search-suggest" id="search-suggest">
+          <i className="searcing fa fa-warning"></i> Error on searching.
+        </div>
+      );
+    }
+
     if (this.props.searchedPages.length < 1) {
     if (this.props.searchedPages.length < 1) {
       if (this.props.searchingKeyword !== '') {
       if (this.props.searchingKeyword !== '') {
         return (
         return (
@@ -44,4 +56,5 @@ SearchSuggest.defaultProps = {
   searchingKeyword: '',
   searchingKeyword: '',
   searchError: null,
   searchError: null,
   searching: false,
   searching: false,
+  focused: false,
 };
 };

+ 28 - 44
resource/js/components/Page/PageBody.js

@@ -16,62 +16,46 @@ export default class PageBody extends React.Component {
       body = this.props.page.revision.body;
       body = this.props.page.revision.body;
     }
     }
 
 
-
-    //var contentHtml = Crowi.unescape(contentText);
-    //// TODO 前処理系のプラグイン化
-    //contentHtml = this.preFormatMarkdown(contentHtml);
-    //contentHtml = this.expandImage(contentHtml);
-    //contentHtml = this.link(contentHtml);
-
-    //var $body = this.$revisionBody;
-    // Using async version of marked
-    //{}, function (err, content) {
-    //  if (err) {
-    //    throw err;
-    //  }
-    //  $body.html(content);
-    //});
-    //return body;
+    let parsed = '<b>...</b>';
     try {
     try {
-    marked.setOptions({
-      gfm: true,
-      highlight: (code, lang, callback) => {
-        let result, hl;
-        if (lang) {
-          try {
-            hl = hljs.highlight(lang, code);
-            result = hl.value;
-          } catch (e) {
+      // TODO
+      marked.setOptions({
+        gfm: true,
+        highlight: function (code, lang) {
+          let result, hl;
+          if (lang) {
+            try {
+              hl = hljs.highlight(lang, code);
+              result = hl.value;
+            } catch (e) {
+              result = code;
+            }
+          } else {
             result = code;
             result = code;
           }
           }
-        } else {
-          result = code;
-        }
-        return callback(null, result);
-      },
-      tables: true,
-      breaks: true,
-      pedantic: false,
-      sanitize: false,
-      smartLists: true,
-      smartypants: false,
-      langPrefix: 'lang-'
-    });
-    console.log('parsing', 'いくぜ');
-    const parsed = marked(body);
-    console.log('parsed', parsed);
-    } catch (e) { console.log(e); }
+          return result;
+        },
+        tables: true,
+        breaks: true,
+        pedantic: false,
+        sanitize: false,
+        smartLists: true,
+        smartypants: false,
+        langPrefix: 'lang-'
+      });
+      parsed = marked(body);
+    } catch (e) { console.log(e, e.stack); }
 
 
     return { __html: parsed };
     return { __html: parsed };
   }
   }
 
 
   render() {
   render() {
-    console.log('Render!');
+    const parsedBody = this.getMarkupHTML();
 
 
     return (
     return (
       <div
       <div
         className="content"
         className="content"
-        dangerouslySetInnerHTML={this.getMarkupHTML()}
+        dangerouslySetInnerHTML={parsedBody}
         />
         />
     );
     );
   }
   }

+ 64 - 0
resource/js/components/Page/PagePath.js

@@ -0,0 +1,64 @@
+import React from 'react';
+
+export default class PagePath extends React.Component {
+
+  // Original Crowi.linkPath
+  /*
+  Crowi.linkPath = function(revisionPath) {
+    var $revisionPath = revisionPath || '#revision-path';
+    var $title = $($revisionPath);
+    var pathData = $('#content-main').data('path');
+
+    if (!pathData) {
+      return ;
+    }
+
+    var realPath = pathData.trim();
+    if (realPath.substr(-1, 1) == '/') {
+      realPath = realPath.substr(0, realPath.length - 1);
+    }
+
+    var path = '';
+    var pathHtml = '';
+    var splittedPath = realPath.split(/\//);
+    splittedPath.shift();
+    splittedPath.forEach(function(sub) {
+      path += '/';
+      pathHtml += ' <a href="' + path + '">/</a> ';
+      if (sub) {
+        path += sub;
+        pathHtml += '<a href="' + path + '">' + sub + '</a>';
+      }
+    });
+    if (path.substr(-1, 1) != '/') {
+      path += '/';
+      pathHtml += ' <a href="' + path + '" class="last-path">/</a>';
+    }
+    $title.html(pathHtml);
+  };
+  */
+
+  linkPath(path) {
+    return path;
+  }
+
+  render() {
+    const page = this.props.page;
+    const shortPath = this.getShortPath(page.path);
+    const pathPrefix = page.path.replace(new RegExp(shortPath + '(/)?$'), '');
+
+    return (
+      <span className="page-path">
+        {pathPrefix}<strong>{shortPath}</strong>
+      </span>
+    );
+  }
+}
+
+PagePath.propTypes = {
+  page: React.PropTypes.object.isRequired,
+};
+
+PagePath.defaultProps = {
+  page: {},
+};

+ 18 - 3
resource/js/components/Search/SearchPage.js → resource/js/components/SearchPage.js

@@ -1,10 +1,9 @@
 // This is the root component for #search-page
 // This is the root component for #search-page
 
 
 import React from 'react';
 import React from 'react';
-
-import SearchForm from './SearchForm';
-import SearchResult from './SearchResult';
 import axios from 'axios'
 import axios from 'axios'
+import SearchForm from './SearchPage/SearchForm';
+import SearchResult from './SearchPage/SearchResult';
 
 
 export default class SearchPage extends React.Component {
 export default class SearchPage extends React.Component {
 
 
@@ -15,10 +14,12 @@ export default class SearchPage extends React.Component {
       location: location,
       location: location,
       searchingKeyword: this.props.query.q || '',
       searchingKeyword: this.props.query.q || '',
       searchedPages: [],
       searchedPages: [],
+      searchResultMeta: {},
       searchError: null,
       searchError: null,
     }
     }
 
 
     this.search = this.search.bind(this);
     this.search = this.search.bind(this);
+    this.changeURL = this.changeURL.bind(this);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -40,6 +41,16 @@ export default class SearchPage extends React.Component {
     return query;
     return query;
   }
   }
 
 
+  changeURL(keyword) {
+    // TODO 整理する
+    if (location && location.hash) {
+      location.hash = '';
+    }
+    if (window.history && window.history.pushState){
+      history.pushState('', '', `/_search?q=${keyword}`);
+    }
+  }
+
   search(data) {
   search(data) {
     const keyword = data.keyword;
     const keyword = data.keyword;
     if (keyword === '') {
     if (keyword === '') {
@@ -61,8 +72,11 @@ export default class SearchPage extends React.Component {
         this.setState({
         this.setState({
           searchingKeyword: keyword,
           searchingKeyword: keyword,
           searchedPages: res.data.data,
           searchedPages: res.data.data,
+          searchResultMeta: res.data.meta,
         });
         });
       }
       }
+
+      this.changeURL(keyword);
       // TODO error
       // TODO error
     })
     })
     .catch((res) => {
     .catch((res) => {
@@ -85,6 +99,7 @@ export default class SearchPage extends React.Component {
         <SearchResult
         <SearchResult
           pages={this.state.searchedPages}
           pages={this.state.searchedPages}
           searchingKeyword={this.state.searchingKeyword}
           searchingKeyword={this.state.searchingKeyword}
+          searchResultMeta={this.state.searchResultMeta}
           />
           />
       </div>
       </div>
     );
     );

+ 6 - 1
resource/js/components/Search/SearchForm.js → resource/js/components/SearchPage/SearchForm.js

@@ -40,7 +40,7 @@ export default class SearchForm extends React.Component {
 
 
   render() {
   render() {
     return (
     return (
-      <form className="form" onSubmit={this.handleSubmit}>
+      <form className="form form-group input-group" onSubmit={this.handleSubmit}>
         <input
         <input
           type="text"
           type="text"
           name="q"
           name="q"
@@ -48,6 +48,11 @@ export default class SearchForm extends React.Component {
           onChange={this.handleChange}
           onChange={this.handleChange}
           className="form-control"
           className="form-control"
           />
           />
+          <span className="input-group-btn">
+            <button type="submit" className="btn btn-default">
+              <i className="search-top-icon fa fa-search"></i>
+            </button>
+          </span>
       </form>
       </form>
     );
     );
   }
   }

+ 7 - 1
resource/js/components/Search/SearchResult.js → resource/js/components/SearchPage/SearchResult.js

@@ -11,7 +11,7 @@ export default class SearchResult extends React.Component {
     const listView = this.props.pages.map((page) => {
     const listView = this.props.pages.map((page) => {
       const pageId = "#" + page._id;
       const pageId = "#" + page._id;
       return (
       return (
-        <Page page={page} linkTo={pageId}>
+        <Page page={page} linkTo={pageId} key={page._id}>
           <div className="page-list-option">
           <div className="page-list-option">
             <a href={page.path}><i className="fa fa-arrow-circle-right" /></a>
             <a href={page.path}><i className="fa fa-arrow-circle-right" /></a>
           </div>
           </div>
@@ -19,6 +19,10 @@ export default class SearchResult extends React.Component {
       );
       );
     });
     });
 
 
+    /*
+    UI あとで考える
+    <span className="search-result-meta">Found: {this.props.searchResultMeta.total} pages with "{this.props.searchingKeyword}"</span>
+    */
     return (
     return (
       <div className="content-main" id="content-main">
       <div className="content-main" id="content-main">
         <div className="search-result row" id="search-result">
         <div className="search-result row" id="search-result">
@@ -30,6 +34,7 @@ export default class SearchResult extends React.Component {
             </nav>
             </nav>
           </div>
           </div>
           <div className="col-md-8 search-result-content" id="search-result-content">
           <div className="col-md-8 search-result-content" id="search-result-content">
+            <div className="search-result-meta">Found {this.props.searchResultMeta.total} pages with "{this.props.searchingKeyword}"</div>
             <SearchResultList
             <SearchResultList
               pages={this.props.pages}
               pages={this.props.pages}
               searchingKeyword={this.props.searchingKeyword}
               searchingKeyword={this.props.searchingKeyword}
@@ -48,5 +53,6 @@ SearchResult.propTypes = {
 SearchResult.defaultProps = {
 SearchResult.defaultProps = {
   searchedPages: [],
   searchedPages: [],
   searchingKeyword: '',
   searchingKeyword: '',
+  searchResultMeta: {},
 };
 };
 
 

+ 6 - 6
resource/js/components/Search/SearchResultList.js → resource/js/components/SearchPage/SearchResultList.js

@@ -14,8 +14,8 @@ export default class SearchResultList extends React.Component {
     let returnBody = body;
     let returnBody = body;
 
 
     this.props.searchingKeyword.split(' ').forEach((keyword) => {
     this.props.searchingKeyword.split(' ').forEach((keyword) => {
-      const keywordExp = new RegExp('(' + keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ')', 'g');
-      returnBody = returnBody.replace(keyword, '<span style="highlighted">$&</span>');
+      const keywordExp = new RegExp('(' + keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ')', 'ig');
+      returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
     });
     });
 
 
     //console.log(this.props.searchingKeyword, body);
     //console.log(this.props.searchingKeyword, body);
@@ -28,10 +28,10 @@ export default class SearchResultList extends React.Component {
       //console.log('resultList.page.path', page.path);
       //console.log('resultList.page.path', page.path);
       //console.log('resultList.pageBody', pageBody);
       //console.log('resultList.pageBody', pageBody);
       return (
       return (
-        <div id={page._id}>
-          <h2>{page.path}</h2>
-          <div>
-            <PageBody page={page} pageBody={pageBody} />
+        <div id={page._id} key={page._id} className="search-result-page">
+          <h2><a href={page.path}>{page.path}</a></h2>
+          <div className="wiki">
+            <PageBody className="hige" page={page} pageBody={pageBody} />
           </div>
           </div>
         </div>
         </div>
       );
       );

+ 17 - 13
webpack.config.js

@@ -1,7 +1,7 @@
 var path = require('path');
 var path = require('path');
 var webpack = require('webpack');
 var webpack = require('webpack');
 
 
-module.exports = {
+var config = {
   entry: {
   entry: {
     app: './resource/js/app.js',
     app: './resource/js/app.js',
     crowi: './resource/js/crowi.js',
     crowi: './resource/js/crowi.js',
@@ -30,17 +30,21 @@ module.exports = {
       }
       }
     ]
     ]
   },
   },
-  plugins: [
+  plugins: []
+};
+
+if (process.env && process.env.NODE_ENV == 'production') {
+  config.plugins = [
     new webpack.DefinePlugin({
     new webpack.DefinePlugin({
-      "process.env": {
-        NODE_ENV: JSON.stringify("production")
+      'process.env':{
+        'NODE_ENV': JSON.stringify('production')
       }
       }
-    })
-    //new webpack.ProvidePlugin({
-    //  jQuery: "jquery",
-    //  $: "jquery",
-    //  jqeury: "jquery",
-    //}),
-    //new webpack.optimize.DedupePlugin(),
-  ]
-};
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress:{
+        warnings: false
+      }
+    }),
+  ];
+}
+module.exports = config;