Browse Source

Merge pull request #2244 from weseek/feat/impl-search-for-mobile

Feat/impl search for mobile
Yuki Takei 5 years ago
parent
commit
62c5f40d84

+ 2 - 3
src/client/js/bootstrap.jsx

@@ -3,7 +3,7 @@ import React from 'react';
 import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
 
-import HeaderSearchBox from './components/HeaderSearchBox';
+import SearchTop from './components/Navbar/SearchTop';
 import NavbarToggler from './components/Navbar/NavbarToggler';
 import PersonalDropdown from './components/Navbar/PersonalDropdown';
 import Sidebar from './components/Sidebar';
@@ -42,8 +42,7 @@ appContainer.injectToWindow();
 const componentMappings = {
   'grw-navbar-toggler': <NavbarToggler />,
 
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+  'grw-search-top': <SearchTop />,
   'personal-dropdown': <PersonalDropdown />,
 
   'create-page-button': <PageCreateButton />,

+ 3 - 13
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -61,7 +61,7 @@ const GrowiSubNavigation = (props) => {
   const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page') === 'true';
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isHeaderSticky, isSubnavCompact,
+    pageId, path, createdAt, creator, updatedAt, revisionAuthor,
   } = pageContainer.state;
 
   const isPageNotFound = pageId == null;
@@ -77,16 +77,6 @@ const GrowiSubNavigation = (props) => {
   }
 
   const additionalClassNames = ['grw-subnavbar'];
-  const layoutType = appContainer.getConfig().layoutType;
-
-  if (layoutType === 'growi') {
-    if (isHeaderSticky) {
-      additionalClassNames.push('grw-subnavbar-sticky');
-    }
-    if (isSubnavCompact) {
-      additionalClassNames.push('grw-subnavbar-compact');
-    }
-  }
 
   return (
     <div className={`d-flex align-items-center justify-content-between px-3 py-1 ${additionalClassNames.join(' ')}`}>
@@ -116,12 +106,12 @@ const GrowiSubNavigation = (props) => {
         <ul className="authors text-nowrap d-none d-lg-block d-edit-none">
           { creator != null && (
             <li>
-              <PageCreator creator={creator} createdAt={createdAt} isCompactMode={isSubnavCompact} />
+              <PageCreator creator={creator} createdAt={createdAt} />
             </li>
           ) }
           { revisionAuthor != null && (
             <li className="mt-1">
-              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} isCompactMode={isSubnavCompact} />
+              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} />
             </li>
           ) }
         </ul>

+ 2 - 10
src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx

@@ -36,22 +36,14 @@ const GrowiSubNavigationForUserPage = (props) => {
   const pageUser = JSON.parse(document.querySelector('#grw-subnav-for-user-page').getAttribute('data-page-user'));
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, isHeaderSticky, isSubnavCompact,
+    pageId, path,
   } = pageContainer.state;
 
   const additionalClassNames = ['grw-subnavbar', 'grw-subnavbar-user-page'];
   const layoutType = appContainer.getConfig().layoutType;
 
   if (layoutType === 'growi') {
-    if (isHeaderSticky) {
-      additionalClassNames.push('grw-subnavbar-sticky');
-    }
-    if (isSubnavCompact) {
-      additionalClassNames.push('py-2 grw-subnavbar-compact');
-    }
-    else {
-      additionalClassNames.push('py-3');
-    }
+    additionalClassNames.push('py-3');
   }
 
   return (

+ 42 - 15
src/client/js/components/HeaderSearchBox.jsx → src/client/js/components/Navbar/SearchTop.jsx

@@ -2,13 +2,13 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { createSubscribedElement } from './UnstatedUtils';
-import AppContainer from '../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
 
-import SearchForm from './SearchForm';
+import SearchForm from '../SearchForm';
 
 
-class HeaderSearchBox extends React.Component {
+class SearchTop extends React.Component {
 
   constructor(props) {
     super(props);
@@ -16,6 +16,7 @@ class HeaderSearchBox extends React.Component {
     this.state = {
       text: '',
       isScopeChildren: false,
+      isCollapsed: true,
     };
 
     this.onInputChange = this.onInputChange.bind(this);
@@ -24,10 +25,14 @@ class HeaderSearchBox extends React.Component {
     this.search = this.search.bind(this);
   }
 
-  componentDidMount() {
+  componentWillMount() {
+    this.initBreakpointEvents();
   }
 
-  componentWillUnmount() {
+  initBreakpointEvents() {
+    this.props.appContainer.addBreakpointListener('md', (mql) => {
+      this.setState({ isCollapsed: !mql.matches });
+    }, true);
   }
 
   onInputChange(text) {
@@ -56,7 +61,23 @@ class HeaderSearchBox extends React.Component {
     window.location.href = url.href;
   }
 
-  render() {
+  Root = ({ children }) => {
+    const { isCollapsed } = this.state;
+
+    return isCollapsed
+      ? (
+        <div id="grw-search-top-collapse" className="collapse bg-dark p-3">
+          {children}
+        </div>
+      )
+      : (
+        <div className="grw-search-top-fixed position-fixed">
+          {children}
+        </div>
+      );
+  };
+
+  SearchTopForm = () => {
     const { t, appContainer } = this.props;
     const scopeLabel = this.state.isScopeChildren
       ? t('header_search_box.label.This tree')
@@ -94,19 +115,25 @@ class HeaderSearchBox extends React.Component {
     );
   }
 
+  render() {
+    const { Root, SearchTopForm } = this;
+    return (
+      <Root><SearchTopForm /></Root>
+    );
+  }
+
 }
 
+SearchTop.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
 
 /**
  * Wrapper component for using unstated
  */
-const HeaderSearchBoxWrapper = (props) => {
-  return createSubscribedElement(HeaderSearchBox, props, [AppContainer]);
-};
-
-HeaderSearchBox.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+const SearchTopWrapper = (props) => {
+  return createSubscribedElement(SearchTop, props, [AppContainer]);
 };
 
-export default withTranslation()(HeaderSearchBoxWrapper);
+export default withTranslation()(SearchTopWrapper);

+ 25 - 37
src/client/js/components/Sidebar.jsx

@@ -38,44 +38,32 @@ class Sidebar extends React.Component {
   initBreakpointEvents() {
     const { appContainer, navigationUIController } = this.props;
 
-    document.addEventListener('DOMContentLoaded', () => {
-      // get the value of '--breakpoint-*'
-      // const breakpointSm = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-sm'), 10);
-      const breakpointMd = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-md'), 10);
-
-      const smHandler = (mql) => {
-        if (mql.matches) {
-          // cache width
-          this.sidebarWidthCached = navigationUIController.state.productNavWidth;
-
-          appContainer.setState({ isDrawerOpened: false });
-          navigationUIController.disableResize();
-          navigationUIController.expand();
-
-          // fix width
-          navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+    const mdOrAvobeHandler = (mql) => {
+      // sm -> md
+      if (mql.matches) {
+        appContainer.setState({ isDrawerOpened: false });
+        navigationUIController.enableResize();
+
+        // restore width
+        if (this.sidebarWidthCached != null) {
+          navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
         }
-        else {
-          appContainer.setState({ isDrawerOpened: false });
-          navigationUIController.enableResize();
-
-          // restore width
-          if (this.sidebarWidthCached != null) {
-            navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
-          }
-        }
-      };
-
-      // const mediaQueryForXs = window.matchMedia(`(max-width: ${breakpointSm}px)`);
-      const mediaQueryForSm = window.matchMedia(`(max-width: ${breakpointMd}px)`);
-
-      // add event listener
-      // mediaQueryForXs.addListener(xsHandler);
-      mediaQueryForSm.addListener(smHandler);
-      // initialize
-      // xsHandler(mediaQueryForXs);
-      smHandler(mediaQueryForSm);
-    });
+      }
+      // md -> sm
+      else {
+        // cache width
+        this.sidebarWidthCached = navigationUIController.state.productNavWidth;
+
+        appContainer.setState({ isDrawerOpened: false });
+        navigationUIController.disableResize();
+        navigationUIController.expand();
+
+        // fix width
+        navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+      }
+    };
+
+    appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
   }
 
   backdropClickedHandler = () => {

+ 23 - 1
src/client/js/services/AppContainer.js

@@ -33,6 +33,7 @@ export default class AppContainer extends Container {
       editorMode: null,
       preferDarkModeByMediaQuery: false,
       preferDarkModeByUser: null,
+      breakpoint: 'xs',
       isDrawerOpened: false,
 
       isPageCreateModalShown: false,
@@ -108,7 +109,6 @@ export default class AppContainer extends Container {
   }
 
   init() {
-    // this.initBreakpointEvents();
     this.initColorScheme();
     this.initPlugins();
   }
@@ -233,6 +233,28 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
+  /**
+   *
+   * @param {string} breakpoint id of breakpoint
+   * @param {function} handler event handler for media query
+   * @param {boolean} invokeOnInit invoke handler after the initialization if true
+   */
+  addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
+    document.addEventListener('DOMContentLoaded', () => {
+      // get the value of '--breakpoint-*'
+      const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
+
+      const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
+
+      // add event listener
+      mediaQuery.addListener(handler);
+      // initialize
+      if (invokeOnInit) {
+        handler(mediaQuery);
+      }
+    });
+  }
+
   getOriginRenderer() {
     return this.originRenderer;
   }

+ 1 - 5
src/client/styles/scss/_admin.scss

@@ -1,8 +1,4 @@
 .admin-page {
-  header.grw-header {
-    height: unset;
-  }
-
   .admin-user-menu {
     .dropdown-menu {
       right: 0;
@@ -168,4 +164,4 @@
 
 .admin-navigation > a + a {
   margin-top: 2px;
-}
+}

+ 0 - 3
src/client/styles/scss/_me.scss

@@ -1,5 +1,2 @@
 .user-settings-page {
-  header.grw-header {
-    height: unset;
-  }
 }

+ 18 - 9
src/client/styles/scss/_search.scss

@@ -18,6 +18,7 @@
 
 .search-typeahead {
   position: relative;
+  width: 100%;
 
   .search-clear {
     position: absolute;
@@ -102,17 +103,25 @@
 
 // layout
 .search-top {
-  .rbt-input.form-control {
-    width: 200px;
-    transition: 0.3s ease-out;
+  .grw-search-top-fixed {
+    // centering on navbar
+    top: $grw-navbar-height / 2;
+    left: 50vw;
+    z-index: $zindex-fixed + 1;
+    transform: translate(-50%, -50%);
 
-    // focus
-    &.focus {
-      width: 300px;
-    }
+    .rbt-input.form-control {
+      width: 200px;
+      transition: 0.3s ease-out;
 
-    @include media-breakpoint-up(md) {
-      width: 300px;
+      // focus
+      &.focus {
+        width: 300px;
+      }
+
+      @include media-breakpoint-up(md) {
+        width: 300px;
+      }
     }
   }
 }

+ 1 - 1
src/client/styles/scss/_sidebar.scss

@@ -51,7 +51,6 @@
   $navbar-total-height: $grw-navbar-height + $grw-navbar-border-width;
   div[class$='-LayoutContainer'] {
     height: calc(100vh - #{$navbar-total-height});
-    margin-top: $navbar-total-height;
   }
   div[class$='-NavigationContainer'] {
     top: $navbar-total-height;
@@ -118,6 +117,7 @@
   }
   div[class$='-ScrollableTransitionGroup'] {
     // remove horizontal line
+    > div,
     > div > div {
       &:before,
       &:after {

+ 0 - 42
src/client/styles/scss/_subnav.scss

@@ -5,48 +5,6 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   transition: all 300ms $easeInOutCubic;
 }
 
-/*
- * layout for sticky
- */
-header.grw-header {
-  top: $grw-navbar-height + $grw-navbar-border-width;
-
-  // Adjust to be on top of the growi subnavigation
-  z-index: $zindex-sticky;
-
-  height: 110px;
-  pointer-events: none; // disable pointer events for sticky
-
-  .grw-subnav {
-    overflow: unset;
-    pointer-events: all; // enable pointer events
-  }
-}
-
-/*
- * Compact Mode Switching
- */
-.grw-subnavbar {
-  &.grw-subnavbar-compact {
-    @extend %transitionForCompactMode;
-
-    h1 {
-      @include variable-font-size(18px);
-      @extend %transitionForCompactMode;
-    }
-  }
-}
-
-/*
- * Sticky Mode Switching
- */
-.grw-subnavbar {
-  &.grw-subnavbar-sticky {
-    // set transition-duration (init -> sticky)
-    transition: all 400ms linear !important;
-  }
-}
-
 /*
  * Styles
  */

+ 0 - 4
src/client/styles/scss/_tag.scss

@@ -1,8 +1,4 @@
 .tags-page {
-  header.grw-header {
-    height: unset;
-  }
-
   .list-tag-count {
     background: rgba(0, 0, 0, 0.08);
   }

+ 0 - 35
src/client/styles/scss/_user.scss

@@ -5,41 +5,6 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   transition: all 300ms $easeInOutCubic;
 }
 
-.grw-header.grw-header-user-page {
-  height: 150px;
-}
-
-/*
- * Compact Mode Switching
- */
-.grw-subnavbar.grw-subnavbar-user-page {
-  &.grw-subnavbar-compact {
-    .grw-user-page-path {
-      margin-bottom: 0;
-      font-size: 14px;
-
-      @extend %transitionForCompactMode;
-    }
-    .picture {
-      width: 62px;
-      height: 62px;
-
-      @extend %transitionForCompactMode;
-    }
-    h1 {
-      font-size: 1.5em;
-      line-height: 30px;
-
-      @extend %transitionForCompactMode;
-    }
-    .users-meta {
-      margin-left: 15px;
-
-      @extend %transitionForCompactMode;
-    }
-  }
-}
-
 /*
  * Styles
  */

+ 1 - 1
src/server/views/layout-growi/base/layout.html

@@ -9,7 +9,7 @@
 {% block layout_main %}
 
 {% block content_header_wrapper %}
-<header class="py-0 position-sticky grw-header">
+<header class="py-0 grw-header">
   {% block content_header %}
   {% endblock %}
 </header>

+ 1 - 1
src/server/views/layout-growi/user_page.html

@@ -6,7 +6,7 @@
 {% endblock %}
 
 {% block content_header_wrapper %}
-  <header class="py-0 position-sticky grw-header grw-header-user-page">
+  <header class="py-0 grw-header grw-header-user-page">
     {% if pageUser %}
       <div id="grw-subnav-for-user-page" class="grw-subnav" data-page-user="{{ pageUser|json }}"></div>
     {% else %}

+ 27 - 22
src/server/views/layout/layout.html

@@ -86,48 +86,53 @@
       </a>
     </div>
 
-    {# Search #}
-    <ul class="navbar-nav grw-navbar-search d-none d-md-block">
-      <li>
-        {% if isSearchServiceConfigured() %}
-        <div class="search-top" role="search" id="search-top"></div>
-        {% endif %}
-      </li>
-    </ul>
-
     {# Navbar Right #}
     <ul class="navbar-nav ml-auto">
       {% if user %}
-      <!-- TODO GW-79 enable after refactoring  <li id="create-page-button" class="nav-item d-none d-md-block"></li> -->
-      <li class="nav-item d-none d-md-block">
-        <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
-          <i class="icon-pencil mr-2"></i>
-          <span>{{ t('New') }}</span>
-        </a>
-      </li>
-      <li id="personal-dropdown" class="nav-item dropdown dropdown-toggle"></li>
+        <!-- TODO GW-79 enable after refactoring  <li id="create-page-button" class="nav-item d-none d-md-block"></li> -->
+        <li class="nav-item d-none d-md-block">
+          <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
+            <i class="icon-pencil mr-2"></i>
+            <span>{{ t('New') }}</span>
+          </a>
+        </li>
+        {% if isSearchServiceConfigured() %}
+          <li class="nav-item d-md-none">
+            <a type="button" class="nav-link px-4" data-target="#grw-search-top-collapse" data-toggle="collapse">
+              <i class="icon-magnifier mr-2"></i>
+            </a>
+          </li>
+        {% endif %}
+        <li id="personal-dropdown" class="nav-item dropdown dropdown-toggle"></li>
       {% else %}
-      <li id="login-user" class="nav-item"><a class="nav-link" href="/login">Login</a></li>
+        <li id="login-user" class="nav-item"><a class="nav-link" href="/login">Login</a></li>
       {% endif %}
+
       {% if getConfig('crowi', 'app:confidential') %}
       <li class="nav-item confidential text-light">{{ getConfig('crowi', 'app:confidential') }}</li>
       {% endif %}
     </ul>
 
   </nav>
+
   {% include '../modal/create_page.html' %}
   {% endblock  %} {# layout_head_nav #}
 
   {% block head_warn_breaking_changes %}{% include '../widget/alert_breaking_changes.html' %}{% endblock %}
 
-  <div class="d-flex">
+  <div id="page-wrapper" class="page-wrapper d-flex">
     {# Sidebar #}
     <div id="grw-sidebar-wrapper"></div>
 
-    <div id="page-wrapper" class="flex-grow-1">
+    <div class="flex-grow-1">
       {% block head_warn_alert_siteurl_undefined %}{% include '../widget/alert_siteurl_undefined.html' %}{% endblock %}
-      {% block layout_main %}
-      {% endblock %} {# layout_main #}
+
+      {# Search #}
+      {% if isSearchServiceConfigured() %}
+        <div id="grw-search-top" class="search-top" role="search"></div>
+      {% endif %}
+
+      {% block layout_main %}{% endblock %}
     </div>
   </div>
 

+ 1 - 1
src/server/views/tags.html

@@ -5,7 +5,7 @@
 {% block html_base_css %}tags-page{% endblock %}
 
 {% block layout_main %}
-<header class="py-0 position-sticky grw-header">
+<header class="py-0 grw-header">
   <h1 class="title">{{ t('Tags') }}</h1>
 </header>