Explorar el Código

Merge pull request #237 from weseek/imprv/enable-new-page-name-input-box-to-searchable

imprv/enable-new-page-name-input-box-to-searchable
Yuki Takei hace 8 años
padre
commit
d496ded02c

+ 5 - 1
lib/views/modal/create_page.html

@@ -33,7 +33,11 @@
               <h4>{{ t('Create under', parentPath(path)) }}</h4>
             </div>
             <div class="col-xs-10">
-              <input type="text" value="{{ parentPath(path) }}" class="page-name-input form-control " placeholder="{{ t('Input page name') }}" required>
+              {% if searchConfigured() %}
+              <div class="clearfix" id="page-name-inputter"></div>
+              {% else %}
+              <input type="text" value="{{ parentPath(path) }}" class="page-name-input form-control " placeholder="{{ t('Input page name') }}" required />
+              {% endif %}
             </div>
             <div class="col-xs-2">
               <button type="submit" class="btn btn-primary">{{ t('Create') }}</button>

+ 5 - 0
resource/js/app.js

@@ -17,6 +17,8 @@ import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
+import NewPageNameInputter from './components/NewPageNameInputter';
+import SearchTypeahead  from './components/SearchTypeahead';
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -84,6 +86,9 @@ const componentMappings = {
   //'revision-history': <PageHistory pageId={pageId} />,
   'seen-user-list': <SeenUserList pageId={pageId} crowi={crowi} />,
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
+
+  'page-name-inputter': <NewPageNameInputter crowi={crowi} parentPageName={pagePath} />,
+
 };
 // additional definitions if pagePath exists
 if (pagePath) {

+ 10 - 83
resource/js/components/HeaderSearchBox/SearchForm.js

@@ -3,7 +3,7 @@ import FormGroup from 'react-bootstrap/es/FormGroup';
 import Button from 'react-bootstrap/es/Button';
 import InputGroup from 'react-bootstrap/es/InputGroup';
 
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+import SearchTypeahead from '../SearchTypeahead';
 
 import UserPicture from '../User/UserPicture';
 import PageListMeta from '../PageList/PageListMeta';
@@ -19,19 +19,10 @@ export default class SearchForm extends React.Component {
     this.crowi = window.crowi; // FIXME
 
     this.state = {
-      input: '',
-      keyword: '',
-      searchedKeyword: '',
-      pages: [],
-      isLoading: false,
       searchError: null,
     };
 
-    this.search = this.search.bind(this);
-    this.clearForm = this.clearForm.bind(this);
-    this.getFormClearComponent = this.getFormClearComponent.bind(this);
-    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-    this.onInputChange = this.onInputChange.bind(this);
+    this.onSearchError = this.onSearchError.bind(this);
     this.onChange = this.onChange.bind(this);
   }
 
@@ -41,51 +32,10 @@ export default class SearchForm extends React.Component {
   componentWillUnmount() {
   }
 
-  search(keyword) {
-
-    if (keyword === '') {
-      this.setState({
-        keyword: '',
-        searchedKeyword: '',
-      });
-      return;
-    }
-
-    this.setState({isLoading: true});
-
-    this.crowi.apiGet('/search', {q: keyword})
-      .then(res => {
-        this.setState({
-          isLoading: false,
-          keyword: '',
-          pages: res.data,
-        });
-      })
-      .catch(err => {
-        this.setState({
-          isLoading: false,
-          searchError: err,
-        });
-      });
-  }
-
-  getFormClearComponent() {
-    let isHidden = (this.state.input.length === 0);
-
-    return isHidden ? <span></span> : (
-      <a className="btn btn-link search-top-clear" onClick={this.clearForm} hidden={isHidden}>
-        <i className="fa fa-times-circle" />
-      </a>
-    );
-  }
-
-  clearForm() {
-    this._typeahead.getInstance().clear();
-    this.setState({keyword: ''});
-  }
-
-  onInputChange(text) {
-    this.setState({input: text});
+  onSearchError(err) {
+    this.setState({
+      searchError: err,
+    });
   }
 
   onChange(selected) {
@@ -97,22 +47,10 @@ export default class SearchForm extends React.Component {
     }
   }
 
-  renderMenuItemChildren(option, props, index) {
-    const page = option;
-    return (
-      <span>
-        <UserPicture user={page.revision.author} />
-        <PagePath page={page} />
-        <PageListMeta page={page} />
-      </span>
-    );
-  }
-
   render() {
     const emptyLabel = (this.state.searchError !== null)
         ? 'Error on searching.'
         : 'No matches found on title... Hit [Enter] key so that search on contents.';
-    const formClear = this.getFormClearComponent();
 
     return (
       <form
@@ -121,23 +59,12 @@ export default class SearchForm extends React.Component {
       >
         <FormGroup>
           <InputGroup>
-            <AsyncTypeahead
-              ref={ref => this._typeahead = ref}
-              inputProps={{name: "q", autoComplete: "off"}}
-              isLoading={this.state.isLoading}
-              labelKey="path"
-              minLength={2}
-              options={this.state.pages}
-              placeholder="Search ..."
-              emptyLabel={emptyLabel}
-              align='left'
-              submitFormOnEnter={true}
-              onSearch={this.search}
-              onInputChange={this.onInputChange}
+            <SearchTypeahead
+              crowi={this.crowi}
               onChange={this.onChange}
-              renderMenuItemChildren={this.renderMenuItemChildren}
+              emptyLabel={emptyLabel}
+              placeholder="Search ..."
             />
-            {formClear}
             <InputGroup.Button>
               <Button type="submit">
                 <i className="search-top-icon fa fa-search"></i>

+ 78 - 0
resource/js/components/NewPageNameInputter.js

@@ -0,0 +1,78 @@
+import React from 'react';
+import { FormGroup, Button, InputGroup } from 'react-bootstrap';
+
+import UserPicture from './User/UserPicture';
+import PageListMeta from './PageList/PageListMeta';
+import PagePath from './PageList/PagePath';
+import PropTypes from 'prop-types';
+import SearchTypeahead from './SearchTypeahead';
+
+export default class NewPageNameInputter extends React.Component {
+
+  constructor(props) {
+
+    super(props);
+
+    this.state = {
+      searchError: null,
+    };
+    this.crowi = this.props.crowi;
+
+    this.onSearchError = this.onSearchError.bind(this);
+    this.getParentPageName = this.getParentPageName.bind(this);
+  }
+
+  componentDidMount() {
+  }
+
+  componentWillUnmount() {
+  }
+
+  onSearchError(err) {
+    this.setState({
+      searchError: err,
+    });
+  }
+
+  getParentPageName(path) {
+    if (path == '/') {
+      return path;
+    }
+
+    if (path.match(/.+\/$/)) {
+      return path;
+    }
+
+    return path + '/';
+  }
+
+  render() {
+    const emptyLabel = (this.state.searchError !== null)
+      ? 'Error on searching.'
+      : 'No matches found on title...';
+
+    return (
+      <form
+        action="/_search"
+        className=""
+      >
+        <SearchTypeahead
+          crowi={this.crowi}
+          onSearchError={this.onSearchError}
+          emptyLabel={emptyLabel}
+          placeholder="Input page name"
+          keywordOnInit={this.getParentPageName(this.props.parentPageName)}
+        />
+      </form>
+    );
+  }
+}
+
+NewPageNameInputter.propTypes = {
+  crowi:          PropTypes.object.isRequired,
+  parentPageName: PropTypes.string,
+};
+
+NewPageNameInputter.defaultProps = {
+  parentPageName: '',
+};

+ 225 - 0
resource/js/components/SearchTypeahead.js

@@ -0,0 +1,225 @@
+import {noop} from 'lodash';
+import React from 'react';
+
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+
+import UserPicture from './User/UserPicture';
+import PageListMeta from './PageList/PageListMeta';
+import PagePath from './PageList/PagePath';
+import PropTypes from 'prop-types';
+
+export default class SearchTypeahead extends React.Component {
+
+  constructor(props) {
+
+    super(props);
+
+    this.state = {
+      input: '',
+      keyword: '',
+      searchedKeyword: '',
+      pages: [],
+      isLoading: false,
+      searchError: null,
+      isFocused: false,
+    };
+    this.crowi = this.props.crowi;
+    this.emptyLabel = props.emptyLabel;
+
+    this.search = this.search.bind(this);
+    this.onInputChange = this.onInputChange.bind(this);
+    this.onChange = this.onChange.bind(this);
+    this.getRestoreFormButton = this.getRestoreFormButton.bind(this);
+    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
+    this.restoreInitialData = this.restoreInitialData.bind(this);
+    this.getTypeahead = this.getTypeahead.bind(this);
+    this.forceToFocus = this.forceToFocus.bind(this);
+    this.onInputBlur = this.onInputBlur.bind(this);
+    this.onInputFocus = this.onInputFocus.bind(this);
+  }
+
+  /**
+   * Get instance of AsyncTypeahead
+   */
+  getTypeahead() {
+    return this.refs.typeahead ? this.refs.typeahead.getInstance() : null;
+  }
+
+  componentDidMount() {
+    this.forceToFocus(); // cf. It is needed for displaing placeholder.
+                         //     And cannot focus on if set autoFocus=true to AsyncTypeahead,
+                         //       also set to inputProps of AsyncTypeahead.
+  }
+
+  componentWillUnmount() {
+  }
+
+  /**
+   * force to focus
+   */
+  forceToFocus() {
+    const typeahead = this.getTypeahead();
+    if (typeahead == null) return;
+    const intervalId = setInterval(() => {
+      this.getTypeahead().focus();
+      if (this.state.isFocused) {
+        clearInterval(intervalId);
+      }
+    }, 100);
+  }
+
+  onInputBlur() {
+    this.setState({isFocused: false});
+  }
+
+  onInputFocus() {
+    this.setState({isFocused: true});
+  }
+
+  search(keyword) {
+
+    if (keyword === '') {
+      this.setState({
+        keyword: '',
+        searchedKeyword: '',
+      });
+      return;
+    }
+
+    this.setState({isLoading: true});
+
+    this.crowi.apiGet('/search', {q: keyword})
+      .then(res => { this.onSearchSuccess(res) })
+      .catch(err => { this.onSearchError(err) });
+  }
+
+  /**
+   * Callback function which is occured when search is exit successfully
+   * @param {*} pages
+   */
+  onSearchSuccess(res) {
+    this.setState({
+      isLoading: false,
+      keyword: '',
+      pages: res.data,
+    });
+    this.props.onSearchSuccess && this.props.onSearchSuccess(res);
+  }
+
+  /**
+   * Callback function which is occured when search is exit abnormaly
+   * @param {*} err
+   */
+  onSearchError(err) {
+    this.setState({
+      isLoading: false,
+      searchError: err,
+    });
+    this.props.onSearchError && this.props.onSearchError(err);
+  }
+
+  onInputChange(text) {
+    this.setState({input: text});
+  }
+
+  onChange(selected) {
+    const page = selected[0];  // should be single page selected
+
+    // navigate to page
+    if (page != null) {
+        window.location = page.path;
+    }
+  }
+
+  renderMenuItemChildren(option, props, index) {
+    const page = option;
+    return (
+      <span>
+      <UserPicture user={page.revision.author} />
+      <PagePath page={page} />
+      <PageListMeta page={page} />
+      </span>
+    );
+  }
+
+  /**
+   * Initialize keyword
+   */
+  restoreInitialData() {
+    this.refs.typeahead.getInstance().clear();
+    this.refs.typeahead.getInstance()._updateText(this.props.keywordOnInit);
+  }
+
+  /**
+   * Get restore form button to initialize button
+   */
+  getRestoreFormButton() {
+    let isHidden = (this.state.input.length === 0);
+
+    return isHidden ? <span></span> : (
+      <a className="btn btn-link search-top-clear" onClick={this.restoreInitialData} hidden={isHidden}>
+        <i className="fa fa-times-circle" />
+      </a>
+    );
+  }
+
+  render() {
+    const emptyLabel = (this.state.searchError !== null)
+      ? 'Error on searching.'
+      : 'No matches found on title...';
+    const restoreFormButton = this.getRestoreFormButton();
+    const defaultSelected = (this.props.keywordOnInit != "")
+      ? [{path: this.props.keywordOnInit}]
+      : [];
+
+    return (
+      <span>
+        <AsyncTypeahead
+          {...this.props}
+          ref="typeahead"
+          inputProps={{name: "q", autoComplete: "off"}}
+          isLoading={this.state.isLoading}
+          labelKey="path"
+          minLength={2}
+          options={this.state.pages} // Search result (Some page names)
+          emptyLabel={this.emptyLabel ? this.emptyLabel : emptyLabel}
+          align='left'
+          submitFormOnEnter={true}
+          onSearch={this.search}
+          onInputChange={this.onInputChange}
+          renderMenuItemChildren={this.renderMenuItemChildren}
+          caseSensitive={false}
+          defaultSelected={defaultSelected}
+          onBlur={this.onInputBlur}
+          onFocus={this.onInputFocus}
+        />
+        {restoreFormButton}
+      </span>
+    );
+  }
+}
+
+/**
+ * Properties
+ */
+SearchTypeahead.propTypes = {
+  crowi:           PropTypes.object.isRequired,
+  onSearchSuccess: PropTypes.func,
+  onSearchError:   PropTypes.func,
+  onChange:        PropTypes.func,
+  emptyLabel:      PropTypes.string,
+  placeholder:     PropTypes.string,
+  keywordOnInit:   PropTypes.string,
+};
+
+/**
+ * Properties
+ */
+SearchTypeahead.defaultProps = {
+  onSearchSuccess: noop,
+  onSearchError:   noop,
+  onChange:        noop,
+  emptyLabel:      null,
+  placeholder:     "",
+  keywordOnInit:   "",
+};