Yuki Takei 5 лет назад
Родитель
Сommit
1c04956537

+ 3 - 1
src/client/js/app.jsx

@@ -32,6 +32,7 @@ import LikerList from './components/User/LikerList';
 import TableOfContents from './components/TableOfContents';
 import TableOfContents from './components/TableOfContents';
 
 
 import PersonalSettings from './components/Me/PersonalSettings';
 import PersonalSettings from './components/Me/PersonalSettings';
+import NavigationContainer from './services/NavigationContainer';
 import PageContainer from './services/PageContainer';
 import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
@@ -50,13 +51,14 @@ const { i18n } = appContainer;
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 
 // create unstated container instance
 // create unstated container instance
+const navigationContainer = new NavigationContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const tagContainer = new TagContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [
-  appContainer, websocketContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
+  appContainer, websocketContainer, navigationContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
 ];
 ];
 
 
 logger.info('unstated containers have been initialized');
 logger.info('unstated containers have been initialized');

+ 4 - 5
src/client/js/components/Drawio.jsx

@@ -23,11 +23,9 @@ class Drawio extends React.Component {
   }
   }
 
 
   onEdit() {
   onEdit() {
-    if (window.crowi != null) {
-      window.crowi.launchDrawioModal('page',
-        this.props.rangeLineNumberOfMarkdown.beginLineNumber,
-        this.props.rangeLineNumberOfMarkdown.endLineNumber);
-    }
+    const { appContainer, rangeLineNumberOfMarkdown } = this.props;
+    const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
+    appContainer.launchDrawioModal('page', beginLineNumber, endLineNumber);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -77,6 +75,7 @@ class Drawio extends React.Component {
 Drawio.propTypes = {
 Drawio.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.object.isRequired,
   appContainer: PropTypes.object.isRequired,
+
   drawioContent: PropTypes.any.isRequired,
   drawioContent: PropTypes.any.isRequired,
   isPreview: PropTypes.bool,
   isPreview: PropTypes.bool,
   rangeLineNumberOfMarkdown: PropTypes.object.isRequired,
   rangeLineNumberOfMarkdown: PropTypes.object.isRequired,

+ 5 - 5
src/client/js/components/Navbar/NavbarToggler.jsx

@@ -4,14 +4,14 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 const NavbarToggler = (props) => {
 const NavbarToggler = (props) => {
 
 
-  const { appContainer } = props;
+  const { navigationContainer } = props;
 
 
   const clickHandler = () => {
   const clickHandler = () => {
-    appContainer.toggleDrawer();
+    navigationContainer.toggleDrawer();
   };
   };
 
 
   return (
   return (
@@ -31,12 +31,12 @@ const NavbarToggler = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const NavbarTogglerWrapper = withUnstatedContainers(NavbarToggler, [AppContainer]);
+const NavbarTogglerWrapper = withUnstatedContainers(NavbarToggler, [NavigationContainer]);
 
 
 
 
 NavbarToggler.propTypes = {
 NavbarToggler.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(NavbarTogglerWrapper);
 export default withTranslation()(NavbarTogglerWrapper);

+ 6 - 6
src/client/js/components/Navbar/PageCreateButton.jsx

@@ -4,21 +4,21 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 const PageCreateButton = (props) => {
 const PageCreateButton = (props) => {
-  const { t, appContainer, isIcon } = props;
+  const { t, navigationContainer, isIcon } = props;
 
 
   if (isIcon) {
   if (isIcon) {
     return (
     return (
-      <button className="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" onClick={appContainer.openPageCreateModal}>
+      <button className="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" onClick={navigationContainer.openPageCreateModal}>
         <i className="icon-pencil"></i>
         <i className="icon-pencil"></i>
       </button>
       </button>
     );
     );
   }
   }
 
 
   return (
   return (
-    <button className="px-md-2 nav-link create-page border-0 bg-transparent" type="button" onClick={appContainer.openPageCreateModal}>
+    <button className="px-md-2 nav-link create-page border-0 bg-transparent" type="button" onClick={navigationContainer.openPageCreateModal}>
       <i className="icon-pencil mr-2"></i>
       <i className="icon-pencil mr-2"></i>
       <span className="d-none d-lg-block">{ t('New') }</span>
       <span className="d-none d-lg-block">{ t('New') }</span>
     </button>
     </button>
@@ -28,12 +28,12 @@ const PageCreateButton = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageCreateButtonWrapper = withUnstatedContainers(PageCreateButton, [AppContainer]);
+const PageCreateButtonWrapper = withUnstatedContainers(PageCreateButton, [NavigationContainer]);
 
 
 
 
 PageCreateButton.propTypes = {
 PageCreateButton.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 
 
   isIcon: PropTypes.bool,
   isIcon: PropTypes.bool,
 };
 };

+ 7 - 5
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -7,12 +7,13 @@ import { UncontrolledTooltip } from 'reactstrap';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 
 
 const PersonalDropdown = (props) => {
 const PersonalDropdown = (props) => {
 
 
-  const { t, appContainer } = props;
+  const { t, appContainer, navigationContainer } = props;
   const user = appContainer.currentUser || {};
   const user = appContainer.currentUser || {};
 
 
   const logoutHandler = () => {
   const logoutHandler = () => {
@@ -28,11 +29,11 @@ const PersonalDropdown = (props) => {
   };
   };
 
 
   const preferDrawerModeSwitchModifiedHandler = (bool) => {
   const preferDrawerModeSwitchModifiedHandler = (bool) => {
-    appContainer.setDrawerModePreference(bool);
+    navigationContainer.setDrawerModePreference(bool);
   };
   };
 
 
   const preferDrawerModeOnEditSwitchModifiedHandler = (bool) => {
   const preferDrawerModeOnEditSwitchModifiedHandler = (bool) => {
-    appContainer.setDrawerModePreferenceOnEdit(bool);
+    navigationContainer.setDrawerModePreferenceOnEdit(bool);
   };
   };
 
 
   const followOsCheckboxModifiedHandler = (bool) => {
   const followOsCheckboxModifiedHandler = (bool) => {
@@ -56,7 +57,7 @@ const PersonalDropdown = (props) => {
    */
    */
   const {
   const {
     preferDarkModeByMediaQuery, preferDarkModeByUser, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
     preferDarkModeByMediaQuery, preferDarkModeByUser, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
-  } = appContainer.state;
+  } = navigationContainer.state;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isDarkMode = () => {
   const isDarkMode = () => {
     if (isUserPreferenceExists) {
     if (isUserPreferenceExists) {
@@ -205,12 +206,13 @@ const PersonalDropdown = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer]);
+const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer, NavigationContainer]);
 
 
 
 
 PersonalDropdown.propTypes = {
 PersonalDropdown.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(PersonalDropdownWrapper);
 export default withTranslation()(PersonalDropdownWrapper);

+ 4 - 2
src/client/js/components/Navbar/SearchTop.jsx

@@ -4,6 +4,7 @@ import { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 import SearchForm from '../SearchForm';
 import SearchForm from '../SearchForm';
 
 
@@ -51,7 +52,7 @@ class SearchTop extends React.Component {
   }
   }
 
 
   Root = ({ children }) => {
   Root = ({ children }) => {
-    const { isDeviceSmallerThanMd: isCollapsed } = this.props.appContainer.state;
+    const { isDeviceSmallerThanMd: isCollapsed } = this.props.navigationContainer.state;
 
 
     return isCollapsed
     return isCollapsed
       ? (
       ? (
@@ -116,11 +117,12 @@ class SearchTop extends React.Component {
 SearchTop.propTypes = {
 SearchTop.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SearchTopWrapper = withUnstatedContainers(SearchTop, [AppContainer]);
+const SearchTopWrapper = withUnstatedContainers(SearchTop, [AppContainer, NavigationContainer]);
 
 
 export default withTranslation()(SearchTopWrapper);
 export default withTranslation()(SearchTopWrapper);

+ 6 - 4
src/client/js/components/Page/TagLabels.jsx

@@ -6,6 +6,7 @@ import * as toastr from 'toastr';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
 import EditorContainer from '../../services/EditorContainer';
 import EditorContainer from '../../services/EditorContainer';
 
 
@@ -30,7 +31,7 @@ class TagLabels extends React.Component {
    *   2. editorContainer.state.tags if editorMode is not null
    *   2. editorContainer.state.tags if editorMode is not null
    */
    */
   getEditTargetData() {
   getEditTargetData() {
-    const { editorMode } = this.props.appContainer.state;
+    const { editorMode } = this.props.navigationContainer.state;
     return (editorMode == null)
     return (editorMode == null)
       ? this.props.pageContainer.state.tags
       ? this.props.pageContainer.state.tags
       : this.props.editorContainer.state.tags;
       : this.props.editorContainer.state.tags;
@@ -41,8 +42,8 @@ class TagLabels extends React.Component {
   }
   }
 
 
   async tagsUpdatedHandler(tags) {
   async tagsUpdatedHandler(tags) {
-    const { appContainer, editorContainer } = this.props;
-    const { editorMode } = appContainer.state;
+    const { appContainer, navigationContainer, editorContainer } = this.props;
+    const { editorMode } = navigationContainer.state;
 
 
     // post api request and update tags
     // post api request and update tags
     if (editorMode == null) {
     if (editorMode == null) {
@@ -137,12 +138,13 @@ class TagLabels extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, PageContainer, EditorContainer]);
+const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, NavigationContainer, PageContainer, EditorContainer]);
 
 
 
 
 TagLabels.propTypes = {
 TagLabels.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };
 };

+ 9 - 5
src/client/js/components/PageCreateModal.jsx

@@ -10,13 +10,15 @@ import urljoin from 'url-join';
 
 
 import { userPageRoot } from '@commons/util/path-utils';
 import { userPageRoot } from '@commons/util/path-utils';
 import { pathUtils } from 'growi-commons';
 import { pathUtils } from 'growi-commons';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
+import NavigationContainer from '../services/NavigationContainer';
+import { withUnstatedContainers } from './UnstatedUtils';
+
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 
 const PageCreateModal = (props) => {
 const PageCreateModal = (props) => {
-  const { t, appContainer } = props;
+  const { t, appContainer, navigationContainer } = props;
 
 
   const config = appContainer.getConfig();
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
   const isReachable = config.isSearchServiceReachable;
@@ -240,9 +242,10 @@ const PageCreateModal = (props) => {
       </div>
       </div>
     );
     );
   }
   }
+
   return (
   return (
-    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={appContainer.closePageCreateModal} className="bg-primary text-light">
+    <Modal size="lg" isOpen={navigationContainer.state.isPageCreateModalShown} toggle={navigationContainer.closePageCreateModal} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={navigationContainer.closePageCreateModal} className="bg-primary text-light">
         { t('New Page') }
         { t('New Page') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
@@ -259,12 +262,13 @@ const PageCreateModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
+const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer, NavigationContainer]);
 
 
 
 
 PageCreateModal.propTypes = {
 PageCreateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(ModalControlWrapper);
 export default withTranslation()(ModalControlWrapper);

+ 9 - 6
src/client/js/components/Sidebar.jsx

@@ -10,6 +10,7 @@ import {
 
 
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
+import NavigationContainer from '../services/NavigationContainer';
 
 
 import SidebarNav from './Sidebar/SidebarNav';
 import SidebarNav from './Sidebar/SidebarNav';
 import RecentChanges from './Sidebar/RecentChanges';
 import RecentChanges from './Sidebar/RecentChanges';
@@ -22,6 +23,7 @@ class Sidebar extends React.Component {
 
 
   static propTypes = {
   static propTypes = {
     appContainer: PropTypes.instanceOf(AppContainer).isRequired,
     appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+    navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
     navigationUIController: PropTypes.any.isRequired,
     navigationUIController: PropTypes.any.isRequired,
     isDrawerModeOnInit: PropTypes.bool,
     isDrawerModeOnInit: PropTypes.bool,
   };
   };
@@ -58,7 +60,7 @@ class Sidebar extends React.Component {
    * return whether drawer mode or not
    * return whether drawer mode or not
    */
    */
   get isDrawerMode() {
   get isDrawerMode() {
-    let isDrawerMode = this.props.appContainer.state.isDrawerMode;
+    let isDrawerMode = this.props.navigationContainer.state.isDrawerMode;
     if (isDrawerMode == null) {
     if (isDrawerMode == null) {
       isDrawerMode = this.props.isDrawerModeOnInit;
       isDrawerMode = this.props.isDrawerModeOnInit;
     }
     }
@@ -120,8 +122,8 @@ class Sidebar extends React.Component {
   }
   }
 
 
   backdropClickedHandler = () => {
   backdropClickedHandler = () => {
-    const { appContainer } = this.props;
-    appContainer.setState({ isDrawerOpened: false });
+    const { navigationContainer } = this.props;
+    navigationContainer.setState({ isDrawerOpened: false });
   }
   }
 
 
   itemSelectedHandler = (contentsId) => {
   itemSelectedHandler = (contentsId) => {
@@ -156,7 +158,7 @@ class Sidebar extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { isDrawerOpened } = this.props.appContainer.state;
+    const { isDrawerOpened } = this.props.navigationContainer.state;
 
 
     return (
     return (
       <>
       <>
@@ -199,7 +201,7 @@ const SidebarWithNavigationUIController = withNavigationUIController(Sidebar);
  */
  */
 
 
 const SidebarWithNavigation = (props) => {
 const SidebarWithNavigation = (props) => {
-  const { preferDrawerModeByUser: isDrawerModeOnInit } = props.appContainer.state;
+  const { preferDrawerModeByUser: isDrawerModeOnInit } = props.navigationContainer.state;
 
 
   const initUICForDrawerMode = isDrawerModeOnInit
   const initUICForDrawerMode = isDrawerModeOnInit
     // generate initialUIController for Drawer mode
     // generate initialUIController for Drawer mode
@@ -220,6 +222,7 @@ const SidebarWithNavigation = (props) => {
 
 
 SidebarWithNavigation.propTypes = {
 SidebarWithNavigation.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
-export default withUnstatedContainers(SidebarWithNavigation, [AppContainer]);
+export default withUnstatedContainers(SidebarWithNavigation, [AppContainer, NavigationContainer]);

+ 10 - 5
src/client/js/legacy/crowi.js

@@ -240,10 +240,12 @@ $(() => {
 
 
   // tab changing handling
   // tab changing handling
   $('a[href="#revision-body"]').on('show.bs.tab', () => {
   $('a[href="#revision-body"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode(null);
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode(null);
   });
   });
   $('a[href="#edit"]').on('show.bs.tab', () => {
   $('a[href="#edit"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode('builtin');
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode('builtin');
     $('body').addClass('on-edit');
     $('body').addClass('on-edit');
     $('body').addClass('builtin-editor');
     $('body').addClass('builtin-editor');
   });
   });
@@ -252,7 +254,8 @@ $(() => {
     $('body').removeClass('builtin-editor');
     $('body').removeClass('builtin-editor');
   });
   });
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode('hackmd');
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode('hackmd');
     $('body').addClass('on-edit');
     $('body').addClass('on-edit');
     $('body').addClass('hackmd');
     $('body').addClass('hackmd');
   });
   });
@@ -317,8 +320,10 @@ window.addEventListener('load', (e) => {
 
 
   // hash on page
   // hash on page
   if (window.location.hash) {
   if (window.location.hash) {
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+
     if ((window.location.hash === '#edit' || window.location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
     if ((window.location.hash === '#edit' || window.location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
-      appContainer.setState({ editorMode: 'builtin' });
+      navigationContainer.setEditorMode('builtin');
 
 
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
       $('body').addClass('on-edit');
       $('body').addClass('on-edit');
@@ -328,7 +333,7 @@ window.addEventListener('load', (e) => {
       Crowi.setCaretLineAndFocusToEditor();
       Crowi.setCaretLineAndFocusToEditor();
     }
     }
     else if (window.location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
     else if (window.location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
-      appContainer.setState({ editorMode: 'hackmd' });
+      navigationContainer.setEditorMode('hackmd');
 
 
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('body').addClass('on-edit');
       $('body').addClass('on-edit');

+ 5 - 126
src/client/js/services/AppContainer.js

@@ -36,22 +36,8 @@ export default class AppContainer extends Container {
   constructor() {
   constructor() {
     super();
     super();
 
 
-    const { localStorage } = window;
-
     this.state = {
     this.state = {
-      // minimalized states for app
-      isDeviceSmallerThanMd: null,
-      preferDarkModeByMediaQuery: false,
-      preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true',
-      preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
-      preferDrawerModeOnEditByUser: // default: true
-        localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
-      isDrawerMode: null,
-      isDrawerOpened: false,
-
       // stetes for contents
       // stetes for contents
-      editorMode: null,
-      isPageCreateModalShown: false,
       recentlyUpdatedPages: [],
       recentlyUpdatedPages: [],
     };
     };
 
 
@@ -68,16 +54,10 @@ export default class AppContainer extends Container {
     const userlang = body.dataset.userlang;
     const userlang = body.dataset.userlang;
     this.i18n = i18nFactory(userlang);
     this.i18n = i18nFactory(userlang);
 
 
-    if (this.isLoggedin) {
-      // remove old user cache
-      this.removeOldUserCache();
-    }
-
     this.containerInstances = {};
     this.containerInstances = {};
     this.componentInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
     this.rendererInstances = {};
 
 
-    this.removeOldUserCache = this.removeOldUserCache.bind(this);
     this.apiGet = this.apiGet.bind(this);
     this.apiGet = this.apiGet.bind(this);
     this.apiPost = this.apiPost.bind(this);
     this.apiPost = this.apiPost.bind(this);
     this.apiDelete = this.apiDelete.bind(this);
     this.apiDelete = this.apiDelete.bind(this);
@@ -90,9 +70,6 @@ export default class AppContainer extends Container {
       put: this.apiv3Put.bind(this),
       put: this.apiv3Put.bind(this),
       delete: this.apiv3Delete.bind(this),
       delete: this.apiv3Delete.bind(this),
     };
     };
-
-    this.openPageCreateModal = this.openPageCreateModal.bind(this);
-    this.closePageCreateModal = this.closePageCreateModal.bind(this);
   }
   }
 
 
   /**
   /**
@@ -103,7 +80,6 @@ export default class AppContainer extends Container {
   }
   }
 
 
   initApp() {
   initApp() {
-    this.initDeviceSize();
     this.initMediaQueryForColorScheme();
     this.initMediaQueryForColorScheme();
 
 
     this.injectToWindow();
     this.injectToWindow();
@@ -128,48 +104,19 @@ export default class AppContainer extends Container {
     this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
     this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
     this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
     this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
 
 
+    if (this.isLoggedin) {
+      // remove old user cache
+      this.removeOldUserCache();
+    }
+
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
     if (isPluginEnabled) {
       this.initPlugins();
       this.initPlugins();
     }
     }
 
 
-    window.addEventListener('keydown', (event) => {
-      const target = event.target;
-
-      // ignore when target dom is input
-      const inputPattern = /^input|textinput|textarea$/i;
-      if (inputPattern.test(target.tagName) || target.isContentEditable) {
-        return;
-      }
-
-      if (event.key === 'c') {
-        this.setState({ isPageCreateModalShown: true });
-      }
-    });
-
     this.injectToWindow();
     this.injectToWindow();
   }
   }
 
 
-  initDeviceSize() {
-    const mdOrAvobeHandler = async(mql) => {
-      let isDeviceSmallerThanMd;
-
-      // sm -> md
-      if (mql.matches) {
-        isDeviceSmallerThanMd = false;
-      }
-      // md -> sm
-      else {
-        isDeviceSmallerThanMd = true;
-      }
-
-      this.setState({ isDeviceSmallerThanMd });
-      this.updateDrawerMode({ ...this.state, isDeviceSmallerThanMd }); // generate newest state object
-    };
-
-    this.addBreakpointListener('md', mdOrAvobeHandler, true);
-  }
-
   async initMediaQueryForColorScheme() {
   async initMediaQueryForColorScheme() {
     const switchStateByMediaQuery = async(mql) => {
     const switchStateByMediaQuery = async(mql) => {
       const preferDarkMode = mql.matches;
       const preferDarkMode = mql.matches;
@@ -343,16 +290,6 @@ export default class AppContainer extends Container {
     this.setState({ recentlyUpdatedPages: data.pages });
     this.setState({ recentlyUpdatedPages: data.pages });
   }
   }
 
 
-  setEditorMode(editorMode) {
-    this.setState({ editorMode });
-    this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
-  }
-
-  toggleDrawer() {
-    const { isDrawerOpened } = this.state;
-    this.setState({ isDrawerOpened: !isDrawerOpened });
-  }
-
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     let targetComponent;
     switch (componentKind) {
     switch (componentKind) {
@@ -373,56 +310,6 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
   }
 
 
-  /**
-   * Set Sidebar mode preference by user
-   * @param {boolean} preferDockMode
-   */
-  async setDrawerModePreference(bool) {
-    this.setState({ preferDrawerModeByUser: bool });
-    this.updateDrawerMode({ ...this.state, preferDrawerModeByUser: bool }); // generate newest state object
-
-    // store settings to localStorage
-    const { localStorage } = window;
-    localStorage.preferDrawerModeByUser = bool;
-  }
-
-  /**
-   * Set Sidebar mode preference by user
-   * @param {boolean} preferDockMode
-   */
-  async setDrawerModePreferenceOnEdit(bool) {
-    this.setState({ preferDrawerModeOnEditByUser: bool });
-    this.updateDrawerMode({ ...this.state, preferDrawerModeOnEditByUser: bool }); // generate newest state object
-
-    // store settings to localStorage
-    const { localStorage } = window;
-    localStorage.preferDrawerModeOnEditByUser = bool;
-  }
-
-  /**
-   * Update drawer related state by specified 'newState' object
-   * @param {object} newState A newest state object
-   *
-   * Specify 'newState' like following code:
-   *
-   *   { ...this.state, overwriteParam: overwriteValue }
-   *
-   * because updating state of unstated container will be delayed unless you use await
-   */
-  updateDrawerMode(newState) {
-    const {
-      editorMode, isDeviceSmallerThanMd, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
-    } = newState;
-
-    // get preference on view or edit
-    const preferDrawerMode = editorMode != null ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
-
-    const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
-    const isDrawerOpened = false; // close Drawer anyway
-
-    this.setState({ isDrawerMode, isDrawerOpened });
-  }
-
   /**
   /**
    * Set color scheme preference by user
    * Set color scheme preference by user
    * @param {boolean} isDarkMode
    * @param {boolean} isDarkMode
@@ -433,14 +320,6 @@ export default class AppContainer extends Container {
     applyColorScheme();
     applyColorScheme();
   }
   }
 
 
-  openPageCreateModal() {
-    this.setState({ isPageCreateModalShown: true });
-  }
-
-  closePageCreateModal() {
-    this.setState({ isPageCreateModalShown: false });
-  }
-
   async apiGet(path, params) {
   async apiGet(path, params) {
     return this.apiRequest('get', path, { params });
     return this.apiRequest('get', path, { params });
   }
   }

+ 148 - 0
src/client/js/services/NavigationContainer.js

@@ -0,0 +1,148 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container related to options for Application
+ * @extends {Container} unstated Container
+ */
+export default class NavigationContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+    this.appContainer.registerContainer(this);
+
+    const { localStorage } = window;
+
+    this.state = {
+      editorMode: null,
+
+      isDeviceSmallerThanMd: null,
+      preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
+      preferDrawerModeOnEditByUser: // default: true
+        localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
+      isDrawerMode: null,
+      isDrawerOpened: false,
+
+      isPageCreateModalShown: false,
+    };
+
+    this.openPageCreateModal = this.openPageCreateModal.bind(this);
+    this.closePageCreateModal = this.closePageCreateModal.bind(this);
+
+    this.initHotkeys();
+    this.initDeviceSize();
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'NavigationContainer';
+  }
+
+  initHotkeys() {
+    window.addEventListener('keydown', (event) => {
+      const target = event.target;
+
+      // ignore when target dom is input
+      const inputPattern = /^input|textinput|textarea$/i;
+      if (inputPattern.test(target.tagName) || target.isContentEditable) {
+        return;
+      }
+
+      if (event.key === 'c') {
+        this.setState({ isPageCreateModalShown: true });
+      }
+    });
+  }
+
+  initDeviceSize() {
+    const mdOrAvobeHandler = async(mql) => {
+      let isDeviceSmallerThanMd;
+
+      // sm -> md
+      if (mql.matches) {
+        isDeviceSmallerThanMd = false;
+      }
+      // md -> sm
+      else {
+        isDeviceSmallerThanMd = true;
+      }
+
+      this.setState({ isDeviceSmallerThanMd });
+      this.updateDrawerMode({ ...this.state, isDeviceSmallerThanMd }); // generate newest state object
+    };
+
+    this.appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
+  }
+
+  setEditorMode(editorMode) {
+    this.setState({ editorMode });
+    this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
+  }
+
+  toggleDrawer() {
+    const { isDrawerOpened } = this.state;
+    this.setState({ isDrawerOpened: !isDrawerOpened });
+  }
+
+  /**
+   * Set Sidebar mode preference by user
+   * @param {boolean} preferDockMode
+   */
+  async setDrawerModePreference(bool) {
+    this.setState({ preferDrawerModeByUser: bool });
+    this.updateDrawerMode({ ...this.state, preferDrawerModeByUser: bool }); // generate newest state object
+
+    // store settings to localStorage
+    const { localStorage } = window;
+    localStorage.preferDrawerModeByUser = bool;
+  }
+
+  /**
+   * Set Sidebar mode preference by user
+   * @param {boolean} preferDockMode
+   */
+  async setDrawerModePreferenceOnEdit(bool) {
+    this.setState({ preferDrawerModeOnEditByUser: bool });
+    this.updateDrawerMode({ ...this.state, preferDrawerModeOnEditByUser: bool }); // generate newest state object
+
+    // store settings to localStorage
+    const { localStorage } = window;
+    localStorage.preferDrawerModeOnEditByUser = bool;
+  }
+
+  /**
+   * Update drawer related state by specified 'newState' object
+   * @param {object} newState A newest state object
+   *
+   * Specify 'newState' like following code:
+   *
+   *   { ...this.state, overwriteParam: overwriteValue }
+   *
+   * because updating state of unstated container will be delayed unless you use await
+   */
+  updateDrawerMode(newState) {
+    const {
+      editorMode, isDeviceSmallerThanMd, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
+    } = newState;
+
+    // get preference on view or edit
+    const preferDrawerMode = editorMode != null ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
+
+    const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
+    const isDrawerOpened = false; // close Drawer anyway
+
+    this.setState({ isDrawerMode, isDrawerOpened });
+  }
+
+  openPageCreateModal() {
+    this.setState({ isPageCreateModalShown: true });
+  }
+
+  closePageCreateModal() {
+    this.setState({ isPageCreateModalShown: false });
+  }
+
+}

+ 7 - 3
src/client/js/services/PageContainer.js

@@ -179,6 +179,10 @@ export default class PageContainer extends Container {
     }
     }
   }
   }
 
 
+  get navigationContainer() {
+    return this.appContainer.getContainer('NavigationContainer');
+  }
+
   setLatestRemotePageData(page, user) {
   setLatestRemotePageData(page, user) {
     this.setState({
     this.setState({
       remoteRevisionId: page.revision._id,
       remoteRevisionId: page.revision._id,
@@ -199,7 +203,7 @@ export default class PageContainer extends Container {
    * @param {Array[Tag]} tags Array of Tag
    * @param {Array[Tag]} tags Array of Tag
    */
    */
   updateStateAfterSave(page, tags) {
   updateStateAfterSave(page, tags) {
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
 
 
     // update state of PageContainer
     // update state of PageContainer
     const newState = {
     const newState = {
@@ -243,7 +247,7 @@ export default class PageContainer extends Container {
    * @return {object} { page: Page, tags: Tag[] }
    * @return {object} { page: Page, tags: Tag[] }
    */
    */
   async save(markdown, optionsToSave = {}) {
   async save(markdown, optionsToSave = {}) {
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
 
 
     const { pageId, path } = this.state;
     const { pageId, path } = this.state;
     let { revisionId } = this.state;
     let { revisionId } = this.state;
@@ -274,7 +278,7 @@ export default class PageContainer extends Container {
       throw new Error(msg);
       throw new Error(msg);
     }
     }
 
 
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
     if (editorMode == null) {
     if (editorMode == null) {
       logger.warn('\'saveAndReload\' requires the \'errorMode\' param');
       logger.warn('\'saveAndReload\' requires the \'errorMode\' param');
       return;
       return;