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

refactor instanciation of GrowiRenderer

Yuki Takei 6 лет назад
Родитель
Сommit
ebcec9933a

+ 14 - 23
src/client/js/app.js

@@ -10,9 +10,6 @@ import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
 import i18nFactory from './i18n';
 
-
-import GrowiRenderer from './util/GrowiRenderer';
-
 import HeaderSearchBox from './components/HeaderSearchBox';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
@@ -80,26 +77,23 @@ window.appContainer = appContainer;
 
 logger.info('unstated containers have been initialized');
 
-// backward compatibility
-const crowi = appContainer;
-window.crowi = appContainer;
-
 if (isLoggedin) {
   appContainer.fetchUsers();
 }
 
-const crowiRenderer = new GrowiRenderer(crowi, null, {
-  mode: 'page',
-  isAutoSetup: false, // manually setup because plugins may configure it
-  renderToc: appContainer.getCrowiForJquery().renderTocContent, // function for rendering Table Of Contents
-});
-window.crowiRenderer = crowiRenderer;
+const originRenderer = appContainer.getOriginRenderer();
+window.growiRenderer = originRenderer;
+
+// backward compatibility
+const crowi = appContainer;
+window.crowi = appContainer;
+window.crowiRenderer = originRenderer;
 
 // FIXME
 const isEnabledPlugins = $('body').data('plugin-enabled');
 if (isEnabledPlugins) {
   const crowiPlugin = window.crowiPlugin;
-  crowiPlugin.installAll(crowi, crowiRenderer);
+  crowiPlugin.installAll(appContainer, originRenderer);
 }
 
 /**
@@ -253,9 +247,6 @@ const saveWithSubmitButton = function(submitOpts) {
     .catch(errorHandler);
 };
 
-// setup renderer after plugins are installed
-crowiRenderer.setup();
-
 /**
  * define components
  *  key: id of element
@@ -264,29 +255,29 @@ crowiRenderer.setup();
 let componentMappings = {
   'search-top': <HeaderSearchBox crowi={crowi} />,
   'search-sidebar': <HeaderSearchBox crowi={crowi} />,
-  'search-page': <SearchPage crowi={crowi} crowiRenderer={crowiRenderer} />,
+  'search-page': <SearchPage crowi={crowi} />,
 
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={crowi} />,
 
   'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pageContainer.state.path} addTrailingSlash />,
 
-  'page-editor': <PageEditor crowiRenderer={crowiRenderer} onSaveWithShortcut={saveWithShortcut} />,
+  'page-editor': <PageEditor onSaveWithShortcut={saveWithShortcut} />,
   'page-editor-options-selector': <OptionsSelector crowi={crowi} />,
   'page-status-alert': <PageStatusAlert />,
   'save-page-controls': <SavePageControls onSubmit={saveWithSubmitButton} />,
 
   'user-created-list': <RecentCreated />,
-  'user-draft-list': <MyDraftList crowiOriginRenderer={crowiRenderer} />,
+  'user-draft-list': <MyDraftList />,
 };
 
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
   componentMappings = Object.assign({
     'page-editor-with-hackmd': <PageEditorByHackmd onSaveWithShortcut={saveWithShortcut} />,
-    'page-comments-list': <PageComments crowiOriginRenderer={crowiRenderer} />,
+    'page-comments-list': <PageComments />,
     'page-attachment':  <PageAttachment />,
-    'page-comment-write':  <CommentEditorLazyRenderer crowiOriginRenderer={crowiRenderer} />,
+    'page-comment-write':  <CommentEditorLazyRenderer />,
     'bookmark-button':  <BookmarkButton pageId={pageContainer.state.pageId} crowi={crowi} />,
     'bookmark-button-lg':  <BookmarkButton pageId={pageContainer.state.pageId} crowi={crowi} size="lg" />,
     'rename-page-name-input':  <PagePathAutoComplete crowi={crowi} initializedPath={pageContainer.state.path} />,
@@ -296,7 +287,7 @@ if (pageContainer.state.pageId != null) {
 if (pageContainer.state.path != null) {
   componentMappings = Object.assign({
     // eslint-disable-next-line quote-props
-    'page': <Page crowiRenderer={crowiRenderer} onSaveWithShortcut={saveWithShortcut} />,
+    'page': <Page onSaveWithShortcut={saveWithShortcut} />,
     'revision-path':  <RevisionPath pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} crowi={crowi} />,
     'tag-label':  <TagLabels />,
   }, componentMappings);

+ 7 - 6
src/client/js/components/Page.jsx

@@ -1,13 +1,14 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 
-import { createSubscribedElement } from './UnstatedUtils';
+import MarkdownTable from '../models/MarkdownTable';
+
 import RevisionRenderer from './Page/RevisionRenderer';
 import HandsontableModal from './PageEditor/HandsontableModal';
-import MarkdownTable from '../models/MarkdownTable';
 import mtu from './PageEditor/MarkdownTableUtil';
 
 class Page extends React.Component {
@@ -19,6 +20,8 @@ class Page extends React.Component {
       currentTargetTableArea: null,
     };
 
+    this.growiRenderer = this.props.appContainer.getRenderer('page');
+
     this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
   }
 
@@ -47,12 +50,11 @@ class Page extends React.Component {
 
   render() {
     const isMobile = this.props.appContainer.isMobile;
+    const { markdown } = this.props.pageContainer.state;
 
     return (
       <div className={isMobile ? 'page-mobile' : ''}>
-        <RevisionRenderer
-          crowiRenderer={this.props.crowiRenderer}
-        />
+        <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
         <HandsontableModal ref={(c) => { this.handsontableModal = c }} onSave={this.saveHandlerForHandsontableModal} />
       </div>
     );
@@ -72,7 +74,6 @@ Page.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
-  crowiRenderer: PropTypes.object.isRequired,
   onSaveWithShortcut: PropTypes.func.isRequired,
 };
 

+ 19 - 6
src/client/js/components/Page/RevisionLoader.jsx

@@ -3,12 +3,16 @@ import PropTypes from 'prop-types';
 
 import { Waypoint } from 'react-waypoint';
 
+import { createSubscribedElement } from '../UnstatedUtils';
+import GrowiRenderer from '../../util/GrowiRenderer';
+import AppContainer from '../../services/AppContainer';
+
 import RevisionRenderer from './RevisionRenderer';
 
 /**
  * Load data from server and render RevisionBody component
  */
-export default class RevisionLoader extends React.Component {
+class RevisionLoader extends React.Component {
 
   constructor(props) {
     super(props);
@@ -42,7 +46,7 @@ export default class RevisionLoader extends React.Component {
     };
 
     // load data with REST API
-    this.props.crowi.apiGet('/revisions.get', requestData)
+    this.props.appContainer.apiGet('/revisions.get', requestData)
       .then((res) => {
         if (!res.ok) {
           throw new Error(res.error);
@@ -96,8 +100,7 @@ export default class RevisionLoader extends React.Component {
 
     return (
       <RevisionRenderer
-        crowi={this.props.crowi}
-        crowiRenderer={this.props.crowiRenderer}
+        growiRenderer={this.props.growiRenderer}
         pagePath={this.props.pagePath}
         markdown={markdown}
         highlightKeywords={this.props.highlightKeywords}
@@ -107,12 +110,22 @@ export default class RevisionLoader extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const RevisionLoaderWrapper = (props) => {
+  return createSubscribedElement(RevisionLoader, props, [AppContainer]);
+};
+
 RevisionLoader.propTypes = {
-  crowi: PropTypes.object.isRequired,
-  crowiRenderer: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   pageId: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   revisionId: PropTypes.string.isRequired,
   lazy: PropTypes.bool,
   highlightKeywords: PropTypes.string,
 };
+
+export default RevisionLoaderWrapper;

+ 11 - 10
src/client/js/components/Page/RevisionRenderer.jsx

@@ -1,10 +1,11 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
+import GrowiRenderer from '../../util/GrowiRenderer';
 
-import { createSubscribedElement } from '../UnstatedUtils';
 import RevisionBody from './RevisionBody';
 
 class RevisionRenderer extends React.Component {
@@ -21,13 +22,11 @@ class RevisionRenderer extends React.Component {
   }
 
   componentWillMount() {
-    const { pageContainer } = this.props;
-    this.renderHtml(pageContainer.state.markdown, this.props.highlightKeywords);
+    this.renderHtml(this.props.markdown, this.props.highlightKeywords);
   }
 
   componentWillReceiveProps(nextProps) {
-    const { pageContainer } = nextProps;
-    this.renderHtml(pageContainer.state.markdown, this.props.highlightKeywords);
+    this.renderHtml(nextProps.markdown, this.props.highlightKeywords);
   }
 
   /**
@@ -60,20 +59,20 @@ class RevisionRenderer extends React.Component {
       currentPagePath: pageContainer.state.path,
     };
 
-    const crowiRenderer = this.props.crowiRenderer;
+    const growiRenderer = this.props.growiRenderer;
     const interceptorManager = this.props.appContainer.interceptorManager;
     interceptorManager.process('preRender', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
-        context.markdown = crowiRenderer.preProcess(context.markdown);
+        context.markdown = growiRenderer.preProcess(context.markdown);
       })
       .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
-        context.parsedHTML = crowiRenderer.process(context.markdown);
+        context.parsedHTML = growiRenderer.process(context.markdown);
       })
       .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
-        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML);
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
 
         // highlight
         if (this.props.highlightKeywords != null) {
@@ -115,7 +114,9 @@ const RevisionRendererWrapper = (props) => {
 RevisionRenderer.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  crowiRenderer: PropTypes.object.isRequired,
+
+  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
+  markdown: PropTypes.string.isRequired,
   highlightKeywords: PropTypes.string,
 };
 

+ 6 - 6
src/client/js/components/PageComment/Comment.jsx

@@ -101,21 +101,21 @@ class Comment extends React.Component {
       markdown,
     };
 
-    const crowiRenderer = this.props.crowiRenderer;
+    const growiRenderer = this.props.growiRenderer;
     const interceptorManager = this.props.appContainer.interceptorManager;
     interceptorManager.process('preRenderComment', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
-        context.markdown = crowiRenderer.preProcess(context.markdown);
+        context.markdown = growiRenderer.preProcess(context.markdown);
       })
       .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
-        const parsedHTML = crowiRenderer.process(context.markdown);
+        const parsedHTML = growiRenderer.process(context.markdown);
         context.parsedHTML = parsedHTML;
       })
       .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
-        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML);
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
       })
       .then(() => { return interceptorManager.process('postPostProcess', context) })
       .then(() => { return interceptorManager.process('preRenderCommentHtml', context) })
@@ -139,7 +139,7 @@ class Comment extends React.Component {
           <CommentWrapper
             comment={reply}
             deleteBtnClicked={this.props.deleteBtnClicked}
-            crowiRenderer={this.props.crowiRenderer}
+            growiRenderer={this.props.growiRenderer}
             replyList={[]}
           />
         </div>
@@ -218,7 +218,7 @@ Comment.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   comment: PropTypes.object.isRequired,
-  crowiRenderer: PropTypes.object.isRequired,
+  growiRenderer: PropTypes.object.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
   replyList: PropTypes.array,
 };

+ 2 - 4
src/client/js/components/PageComment/CommentEditor.jsx

@@ -47,8 +47,6 @@ class CommentEditor extends React.Component {
       hasSlackConfig: config.hasSlackConfig,
     };
 
-    this.growiRenderer = new GrowiRenderer(window.crowi, this.props.crowiOriginRenderer, { mode: 'comment' });
-
     this.updateState = this.updateState.bind(this);
     this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
 
@@ -175,7 +173,7 @@ class CommentEditor extends React.Component {
       markdown,
     };
 
-    const growiRenderer = this.growiRenderer;
+    const { growiRenderer } = this.props;
     const interceptorManager = this.props.appContainer.interceptorManager;
     interceptorManager.process('preRenderCommnetPreview', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
@@ -331,7 +329,7 @@ CommentEditor.propTypes = {
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
   commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
 
-  crowiOriginRenderer: PropTypes.object.isRequired,
+  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   replyTo: PropTypes.string,
   commentButtonClickedHandler: PropTypes.func.isRequired,
 };

+ 3 - 3
src/client/js/components/PageComment/CommentEditorLazyRenderer.jsx

@@ -17,6 +17,8 @@ class CommentEditorLazyRenderer extends React.Component {
       isLayoutTypeGrowi: false,
     };
 
+    this.growiRenderer = this.props.appContainer.getRenderer('comment');
+
     this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
   }
 
@@ -71,7 +73,7 @@ class CommentEditorLazyRenderer extends React.Component {
         { this.state.isEditorShown
           && (
           <CommentEditor
-            {...this.props}
+            growiRenderer={this.growiRenderer}
             replyTo={undefined}
             commentButtonClickedHandler={this.showCommentFormBtnClickHandler}
           >
@@ -92,8 +94,6 @@ const CommentEditorLazyRendererWrapper = (props) => {
 };
 
 CommentEditorLazyRenderer.propTypes = {
-  crowiOriginRenderer: PropTypes.object.isRequired,
-
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 

+ 3 - 5
src/client/js/components/PageComments.jsx

@@ -42,7 +42,7 @@ class PageComments extends React.Component {
       showEditorIds: new Set(),
     };
 
-    this.growiRenderer = new GrowiRenderer(window.crowi, this.props.crowiOriginRenderer, { mode: 'comment' });
+    this.growiRenderer = this.props.appContainer.getRenderer('comment');
 
     this.init = this.init.bind(this);
     this.confirmToDeleteComment = this.confirmToDeleteComment.bind(this);
@@ -143,7 +143,7 @@ class PageComments extends React.Component {
           <Comment
             comment={comment}
             deleteBtnClicked={this.confirmToDeleteComment}
-            crowiRenderer={this.growiRenderer}
+            growiRenderer={this.growiRenderer}
             replyList={replyList}
           />
           <div className="container-fluid">
@@ -168,7 +168,7 @@ class PageComments extends React.Component {
                 )}
                 { showEditor && (
                   <CommentEditor
-                    crowiOriginRenderer={this.props.crowiOriginRenderer}
+                    growiRenderer={this.growiRenderer}
                     replyTo={commentId}
                     commentButtonClickedHandler={this.commentButtonClickedHandler}
                   />
@@ -244,8 +244,6 @@ PageComments.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
-
-  crowiOriginRenderer: PropTypes.object.isRequired,
 };
 
 export default withTranslation()(PageCommentsWrapper);

+ 3 - 3
src/client/js/components/PageEditor.jsx

@@ -32,8 +32,6 @@ class PageEditor extends React.Component {
       isMathJaxEnabled,
     };
 
-    this.growiRenderer = new GrowiRenderer(window.crowi, this.props.crowiRenderer, { mode: 'editor' });
-
     this.setCaretLine = this.setCaretLine.bind(this);
     this.focusToEditor = this.focusToEditor.bind(this);
     this.onMarkdownChanged = this.onMarkdownChanged.bind(this);
@@ -47,6 +45,9 @@ class PageEditor extends React.Component {
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
     this.showUnsavedWarning = this.showUnsavedWarning.bind(this);
 
+    // get renderer
+    this.growiRenderer = this.props.appContainer.getRenderer('editor');
+
     // for scrolling
     this.lastScrolledDateWithCursor = null;
     this.isOriginOfScrollSyncEditor = false;
@@ -369,7 +370,6 @@ PageEditor.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
-  crowiRenderer: PropTypes.object.isRequired,
   onSaveWithShortcut: PropTypes.func.isRequired,
 };
 

+ 1 - 3
src/client/js/components/PageList/PagePath.js

@@ -30,7 +30,7 @@ export default class PagePath extends React.Component {
   render() {
     const page = this.props.page;
     const isShortPathOnly = this.props.isShortPathOnly;
-    const pagePath = decodeURIComponent(page.path.replace(this.props.excludePathString.replace(/^\//, ''), ''));
+    const pagePath = decodeURIComponent(page.path);
     const shortPath = this.getShortPath(pagePath);
 
     const shortPathEscaped = escapeStringRegexp(shortPath);
@@ -51,11 +51,9 @@ export default class PagePath extends React.Component {
 PagePath.propTypes = {
   page: PropTypes.object.isRequired,
   isShortPathOnly: PropTypes.bool,
-  excludePathString: PropTypes.string,
   additionalClassNames: PropTypes.array,
 };
 
 PagePath.defaultProps = {
   additionalClassNames: [],
-  excludePathString: '',
 };

+ 15 - 3
src/client/js/components/SearchForm.js

@@ -1,9 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { createSubscribedElement } from './UnstatedUtils';
+import AppContainer from '../services/AppContainer';
+
 import SearchTypeahead from './SearchTypeahead';
 
 // SearchTypeahead wrapper
-export default class SearchForm extends React.Component {
+class SearchForm extends React.Component {
 
   constructor(props) {
     super(props);
@@ -93,7 +96,6 @@ export default class SearchForm extends React.Component {
 
     return (
       <SearchTypeahead
-        crowi={this.props.crowi}
         onChange={this.onChange}
         onSubmit={this.props.onSubmit}
         onInputChange={this.props.onInputChange}
@@ -108,9 +110,17 @@ export default class SearchForm extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const SearchFormWrapper = (props) => {
+  return createSubscribedElement(SearchForm, props, [AppContainer]);
+};
+
 SearchForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  crowi: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   keyword: PropTypes.string,
   onSubmit: PropTypes.func.isRequired,
   onInputChange: PropTypes.func,
@@ -119,3 +129,5 @@ SearchForm.propTypes = {
 SearchForm.defaultProps = {
   onInputChange: () => {},
 };
+
+export default SearchFormWrapper;

+ 14 - 7
src/client/js/components/SearchPage.js

@@ -4,6 +4,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import { createSubscribedElement } from './UnstatedUtils';
+import AppContainer from '../services/AppContainer';
+
 import SearchPageForm from './SearchPage/SearchPageForm';
 import SearchResult from './SearchPage/SearchResult';
 
@@ -69,7 +72,7 @@ class SearchPage extends React.Component {
       searchingKeyword: keyword,
     });
 
-    this.props.crowi.apiGet('/search', { q: keyword })
+    this.props.appContainer.apiGet('/search', { q: keyword })
       .then((res) => {
         this.changeURL(keyword);
 
@@ -92,14 +95,11 @@ class SearchPage extends React.Component {
         <div className="search-page-input">
           <SearchPageForm
             t={this.props.t}
-            crowi={this.props.crowi}
             onSearchFormChanged={this.search}
             keyword={this.state.searchingKeyword}
           />
         </div>
         <SearchResult
-          crowi={this.props.crowi}
-          crowiRenderer={this.props.crowiRenderer}
           pages={this.state.searchedPages}
           searchingKeyword={this.state.searchingKeyword}
           searchResultMeta={this.state.searchResultMeta}
@@ -110,10 +110,17 @@ class SearchPage extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const SearchPageWrapper = (props) => {
+  return createSubscribedElement(SearchPage, props, [AppContainer]);
+};
+
 SearchPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  crowi: PropTypes.object.isRequired,
-  crowiRenderer: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   query: PropTypes.object,
 };
 SearchPage.defaultProps = {
@@ -121,4 +128,4 @@ SearchPage.defaultProps = {
   query: SearchPage.getQueryByLocation(window.location || {}),
 };
 
-export default withTranslation()(SearchPage);
+export default withTranslation()(SearchPageWrapper);

+ 15 - 3
src/client/js/components/SearchPage/SearchPageForm.js

@@ -5,10 +5,13 @@ import FormGroup from 'react-bootstrap/es/FormGroup';
 import Button from 'react-bootstrap/es/Button';
 import InputGroup from 'react-bootstrap/es/InputGroup';
 
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
 import SearchForm from '../SearchForm';
 
 // Search.SearchForm
-export default class SearchPageForm extends React.Component {
+class SearchPageForm extends React.Component {
 
   constructor(props) {
     super(props);
@@ -38,7 +41,6 @@ export default class SearchPageForm extends React.Component {
         <InputGroup>
           <SearchForm
             t={this.props.t}
-            crowi={this.props.crowi}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             onInputChange={this.onInputChange}
@@ -55,11 +57,21 @@ export default class SearchPageForm extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const SearchPageFormWrapper = (props) => {
+  return createSubscribedElement(SearchPageForm, props, [AppContainer]);
+};
+
 SearchPageForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  crowi: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   keyword: PropTypes.string,
   onSearchFormChanged: PropTypes.func.isRequired,
 };
 SearchPageForm.defaultProps = {
 };
+
+export default SearchPageFormWrapper;

+ 26 - 20
src/client/js/components/SearchPage/SearchResult.js

@@ -7,9 +7,10 @@ import * as toastr from 'toastr';
 import Page from '../PageList/Page';
 import SearchResultList from './SearchResultList';
 import DeletePageListModal from './DeletePageListModal';
+import AppContainer from '../../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
 
-// Search.SearchResult
-export default class SearchResult extends React.Component {
+class SearchResult extends React.Component {
 
   constructor(props) {
     super(props);
@@ -117,7 +118,8 @@ export default class SearchResult extends React.Component {
       return new Promise((resolve, reject) => {
         const pageId = page._id;
         const revisionId = page.revision._id;
-        this.props.crowi.apiPost('/pages.remove', { page_id: pageId, revision_id: revisionId, completely: deleteCompletely })
+
+        this.props.appContainer.apiPost('/pages.remove', { page_id: pageId, revision_id: revisionId, completely: deleteCompletely })
           .then((res) => {
             if (res.ok) {
               this.state.selectedPages.delete(page);
@@ -171,8 +173,6 @@ export default class SearchResult extends React.Component {
   }
 
   render() {
-    const excludePathString = this.props.tree;
-
     // console.log(this.props.searchError);
     // console.log(this.isError());
     if (this.isError()) {
@@ -189,7 +189,7 @@ export default class SearchResult extends React.Component {
 
     if (this.isNotFound()) {
       let under = '';
-      if (this.props.tree !== '') {
+      if (this.props.tree !== null) {
         under = ` under "${this.props.tree}"`;
       }
       return (
@@ -249,18 +249,17 @@ export default class SearchResult extends React.Component {
           page={page}
           linkTo={pageId}
           key={page._id}
-          excludePathString={excludePathString}
         >
           { this.state.deletionMode
             && (
-            <input
-              type="checkbox"
-              className="search-result-list-delete-checkbox"
-              value={pageId}
-              checked={this.state.selectedPages.has(page)}
-              onClick={() => { return this.toggleCheckbox(page) }}
-            />
-)
+              <input
+                type="checkbox"
+                className="search-result-list-delete-checkbox"
+                value={pageId}
+                checked={this.state.selectedPages.has(page)}
+                onClick={() => { return this.toggleCheckbox(page) }}
+              />
+            )
             }
           <div className="page-list-option">
             <a href={page.path}><i className="icon-login" /></a>
@@ -300,8 +299,6 @@ export default class SearchResult extends React.Component {
           </div>
           <div className="col-md-8 search-result-content" id="search-result-content">
             <SearchResultList
-              crowi={this.props.crowi}
-              crowiRenderer={this.props.crowiRenderer}
               pages={this.props.pages}
               searchingKeyword={this.props.searchingKeyword}
             />
@@ -322,15 +319,24 @@ export default class SearchResult extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const SearchResultWrapper = (props) => {
+  return createSubscribedElement(SearchResult, props, [AppContainer]);
+};
+
 SearchResult.propTypes = {
-  crowi: PropTypes.object.isRequired,
-  crowiRenderer: PropTypes.object,
-  tree: PropTypes.string.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   pages: PropTypes.array.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
   searchResultMeta: PropTypes.object.isRequired,
   searchError: PropTypes.object,
+  tree: PropTypes.string,
 };
 SearchResult.defaultProps = {
   searchError: null,
 };
+
+export default SearchResultWrapper;

+ 16 - 8
src/client/js/components/SearchPage/SearchResultList.js

@@ -1,16 +1,16 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import GrowiRenderer from '../../util/GrowiRenderer';
-
 import RevisionLoader from '../Page/RevisionLoader';
+import AppContainer from '../../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
 
-export default class SearchResultList extends React.Component {
+class SearchResultList extends React.Component {
 
   constructor(props) {
     super(props);
 
-    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, { mode: 'searchresult' });
+    this.growiRenderer = this.props.appContainer.getRenderer('searchresult');
   }
 
   render() {
@@ -22,8 +22,7 @@ export default class SearchResultList extends React.Component {
             <span><i className="tag-icon icon-tag"></i> {page.tags.join(', ')}</span>
           )}
           <RevisionLoader
-            crowi={this.props.crowi}
-            crowiRenderer={this.growiRenderer}
+            growiRenderer={this.growiRenderer}
             pageId={page._id}
             pagePath={page.path}
             revisionId={page.revision}
@@ -42,12 +41,21 @@ export default class SearchResultList extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const SearchResultListWrapper = (props) => {
+  return createSubscribedElement(SearchResultList, props, [AppContainer]);
+};
+
 SearchResultList.propTypes = {
-  crowi: PropTypes.object.isRequired,
-  crowiRenderer: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   pages: PropTypes.array.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
 };
 
 SearchResultList.defaultProps = {
 };
+
+export default SearchResultListWrapper;

+ 15 - 4
src/client/js/components/SearchTypeahead.js

@@ -7,8 +7,10 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import UserPicture from './User/UserPicture';
 import PageListMeta from './PageList/PageListMeta';
 import PagePath from './PageList/PagePath';
+import AppContainer from '../services/AppContainer';
+import { createSubscribedElement } from './UnstatedUtils';
 
-export default class SearchTypeahead extends React.Component {
+class SearchTypeahead extends React.Component {
 
   constructor(props) {
 
@@ -20,7 +22,6 @@ export default class SearchTypeahead extends React.Component {
       isLoading: false,
       searchError: null,
     };
-    this.crowi = this.props.crowi;
 
     this.restoreInitialData = this.restoreInitialData.bind(this);
     this.search = this.search.bind(this);
@@ -68,7 +69,7 @@ export default class SearchTypeahead extends React.Component {
 
     this.setState({ isLoading: true });
 
-    this.crowi.apiGet('/search', { q: keyword })
+    this.props.appContainer.apiGet('/search', { q: keyword })
       .then((res) => { this.onSearchSuccess(res) })
       .catch((err) => { this.onSearchError(err) });
   }
@@ -205,11 +206,19 @@ export default class SearchTypeahead extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const SearchTypeaheadWrapper = (props) => {
+  return createSubscribedElement(SearchTypeahead, props, [AppContainer]);
+};
+
 /**
  * Properties
  */
 SearchTypeahead.propTypes = {
-  crowi:           PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   onSearchSuccess: PropTypes.func,
   onSearchError:   PropTypes.func,
   onChange:        PropTypes.func,
@@ -234,3 +243,5 @@ SearchTypeahead.defaultProps = {
   keywordOnInit:   '',
   onInputChange: () => {},
 };
+
+export default SearchTypeaheadWrapper;

+ 12 - 14
src/client/js/legacy/crowi.js

@@ -4,6 +4,8 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
+import { Provider } from 'unstated';
+
 import { debounce } from 'throttle-debounce';
 
 import { pathUtils } from 'growi-commons';
@@ -257,9 +259,6 @@ $(() => {
   const websocketContainer = appContainer.getContainer('WebsocketContainer');
   const config = appContainer.getConfig();
 
-  // backward compatibility
-  const crowi = appContainer;
-
   const pageId = $('#content-main').data('page-id');
   // const revisionId = $('#content-main').data('page-revision-id');
   // const revisionCreatedAt = $('#content-main').data('page-revision-created');
@@ -535,9 +534,7 @@ $(() => {
     const isShown = $('#view-timeline').data('shown');
 
     if (growiRendererForTimeline == null) {
-      const crowi = window.crowi;
-      const crowiRenderer = window.crowiRenderer;
-      growiRendererForTimeline = new GrowiRenderer(crowi, crowiRenderer, { mode: 'timeline' });
+      growiRendererForTimeline = GrowiRenderer.generate('timeline');
     }
 
     if (isShown === 0) {
@@ -552,14 +549,15 @@ $(() => {
         const revisionId = timelineElm.getAttribute('data-revision');
 
         ReactDOM.render(
-          <RevisionLoader
-            lazy
-            crowi={crowi}
-            crowiRenderer={growiRendererForTimeline}
-            pageId={pageId}
-            pagePath={pagePath}
-            revisionId={revisionId}
-          />,
+          <Provider inject={[appContainer]}>
+            <RevisionLoader
+              lazy
+              growiRenderer={growiRendererForTimeline}
+              pageId={pageId}
+              pagePath={pagePath}
+              revisionId={revisionId}
+            />
+          </Provider>,
           revisionBodyElem,
         );
       });

+ 4 - 4
src/client/js/plugin.js

@@ -7,12 +7,12 @@ export default class CrowiPlugin {
   /**
    * process plugin entry
    *
-   * @param {Crowi} crowi Crowi context class
-   * @param {CrowiRenderer} crowiRenderer CrowiRenderer
+   * @param {AppContainer} appContainer
+   * @param {GrowiRenderer} originRenderer The origin instance of GrowiRenderer
    *
    * @memberof CrowiPlugin
    */
-  installAll(crowi, crowiRenderer) {
+  installAll(appContainer, originRenderer) {
     // import plugin definitions
     let definitions = [];
     try {
@@ -34,7 +34,7 @@ export default class CrowiPlugin {
         // v2 or above
         default:
           definition.entries.forEach((entry) => {
-            entry(crowi, crowiRenderer);
+            entry(appContainer, originRenderer);
           });
       }
     });

+ 30 - 4
src/client/js/services/AppContainer.js

@@ -10,6 +10,7 @@ import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
 } from '../util/interceptor/detach-code-blocks';
+import GrowiRenderer from '../util/GrowiRenderer.js';
 
 /**
  * Service container related to options for Application
@@ -37,10 +38,7 @@ export default class AppContainer extends Container {
 
     this.isDocSaved = true;
 
-    this.fetchUsers = this.fetchUsers.bind(this);
-    this.apiGet = this.apiGet.bind(this);
-    this.apiPost = this.apiPost.bind(this);
-    this.apiRequest = this.apiRequest.bind(this);
+    this.originRenderer = new GrowiRenderer(this);
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
@@ -53,6 +51,12 @@ export default class AppContainer extends Container {
 
     this.containerInstances = {};
     this.componentInstances = {};
+    this.rendererInstances = {};
+
+    this.fetchUsers = this.fetchUsers.bind(this);
+    this.apiGet = this.apiGet.bind(this);
+    this.apiPost = this.apiPost.bind(this);
+    this.apiRequest = this.apiRequest.bind(this);
   }
 
   /**
@@ -121,6 +125,28 @@ export default class AppContainer extends Container {
     return this.componentInstances[className];
   }
 
+  getOriginRenderer() {
+    return this.originRenderer;
+  }
+
+  /**
+   * factory method
+   */
+  getRenderer(mode) {
+    if (this.rendererInstances[mode] != null) {
+      return this.rendererInstances[mode];
+    }
+
+    const renderer = new GrowiRenderer(this, this.originRenderer);
+    // setup
+    renderer.initMarkdownItConfigurers(mode);
+    renderer.setup(mode);
+    // register
+    this.rendererInstances[mode] = renderer;
+
+    return renderer;
+  }
+
   setIsDocSaved(isSaved) {
     this.isDocSaved = isSaved;
   }

+ 5 - 1
src/client/js/services/TagContainer.js

@@ -27,10 +27,14 @@ export default class TagContainer extends Container {
     const pageContainer = this.appContainer.getContainer('PageContainer');
     const editorContainer = this.appContainer.getContainer('EditorContainer');
 
+    if (Object.keys(pageContainer.state).length === 0) {
+      logger.debug('There is no need to initialize TagContainer because this is not a Page');
+      return;
+    }
+
     const { pageId, templateTagData } = pageContainer.state;
 
     let tags;
-
     // when the page exists
     if (pageId != null) {
       const res = await this.appContainer.apiGet('/pages.getPageTag', { pageId });

+ 47 - 53
src/client/js/util/GrowiRenderer.js

@@ -20,38 +20,40 @@ import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
 
 const logger = require('@alias/logger')('growi:util:GrowiRenderer');
 
-
 export default class GrowiRenderer {
 
   /**
    *
-   * @param {Crowi} crowi
-   * @param {GrowiRenderer} originRenderer may be customized by plugins
-   * @param {object} options
+   * @param {AppContainer} appContainer
+   * @param {GrowiRenderer} originRenderer
+   * @param {string} mode
    */
-  constructor(crowi, originRenderer, options) {
-    this.crowi = crowi;
-    this.originRenderer = originRenderer || {};
-    this.options = Object.assign( //  merge options
-      { isAutoSetup: true }, //       default options
-      options || {}, //               specified options
-    );
-
-    // initialize processors
-    //  that will be retrieved if originRenderer exists
-    this.preProcessors = this.originRenderer.preProcessors || [
-      new Linker(crowi),
-      new CsvToTable(crowi),
-      new XssFilter(crowi),
-    ];
-    this.postProcessors = this.originRenderer.postProcessors || [
-      new CrowiTemplate(crowi),
-    ];
+  constructor(appContainer, originRenderer) {
+    this.appContainer = appContainer;
+
+    if (originRenderer != null) {
+      this.preProcessors = originRenderer.preProcessors;
+      this.postProcessors = originRenderer.postProcessors;
+    }
+    else {
+      this.preProcessors = [
+        new Linker(appContainer),
+        new CsvToTable(appContainer),
+        new XssFilter(appContainer),
+      ];
+      this.postProcessors = [
+        new CrowiTemplate(appContainer),
+      ];
+    }
 
     this.initMarkdownItConfigurers = this.initMarkdownItConfigurers.bind(this);
     this.setup = this.setup.bind(this);
     this.process = this.process.bind(this);
     this.codeRenderer = this.codeRenderer.bind(this);
+  }
+
+  initMarkdownItConfigurers(mode) {
+    const appContainer = this.appContainer;
 
     // init markdown-it
     this.md = new MarkdownIt({
@@ -59,52 +61,44 @@ export default class GrowiRenderer {
       linkify: true,
       highlight: this.codeRenderer,
     });
-    this.initMarkdownItConfigurers(options);
-
-    // auto setup
-    if (this.options.isAutoSetup) {
-      this.setup(crowi.getConfig());
-    }
-  }
-
-  initMarkdownItConfigurers(options) {
-    const crowi = this.crowi;
 
     this.isMarkdownItConfigured = false;
 
     this.markdownItConfigurers = [
-      new TaskListsConfigurer(crowi),
-      new HeaderConfigurer(crowi),
-      new EmojiConfigurer(crowi),
-      new MathJaxConfigurer(crowi),
-      new PlantUMLConfigurer(crowi),
-      new BlockdiagConfigurer(crowi),
+      new TaskListsConfigurer(appContainer),
+      new HeaderConfigurer(appContainer),
+      new EmojiConfigurer(appContainer),
+      new MathJaxConfigurer(appContainer),
+      new PlantUMLConfigurer(appContainer),
+      new BlockdiagConfigurer(appContainer),
     ];
 
     // add configurers according to mode
-    const mode = options.mode;
     switch (mode) {
-      case 'page':
+      case 'page': {
+        const renderToc = appContainer.getCrowiForJquery().renderTocContent;
+
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(crowi),
-          new TocAndAnchorConfigurer(crowi, options.renderToc),
-          new HeaderLineNumberConfigurer(crowi),
-          new HeaderWithEditLinkConfigurer(crowi),
-          new TableWithHandsontableButtonConfigurer(crowi),
+          new FooternoteConfigurer(appContainer),
+          new TocAndAnchorConfigurer(appContainer, renderToc),
+          new HeaderLineNumberConfigurer(appContainer),
+          new HeaderWithEditLinkConfigurer(appContainer),
+          new TableWithHandsontableButtonConfigurer(appContainer),
         ]);
         break;
+      }
       case 'editor':
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(crowi),
-          new HeaderLineNumberConfigurer(crowi),
-          new TableConfigurer(crowi),
+          new FooternoteConfigurer(appContainer),
+          new HeaderLineNumberConfigurer(appContainer),
+          new TableConfigurer(appContainer),
         ]);
         break;
       // case 'comment':
       //   break;
       default:
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new TableConfigurer(crowi),
+          new TableConfigurer(appContainer),
         ]);
         break;
     }
@@ -113,11 +107,11 @@ export default class GrowiRenderer {
   /**
    * setup with crowi config
    */
-  setup() {
-    const crowiConfig = this.crowi.config;
+  setup(mode) {
+    const crowiConfig = this.appContainer.config;
 
     let isEnabledLinebreaks;
-    switch (this.options.mode) {
+    switch (mode) {
       case 'comment':
         isEnabledLinebreaks = crowiConfig.isEnabledLinebreaksInComments;
         break;
@@ -166,7 +160,7 @@ export default class GrowiRenderer {
   }
 
   codeRenderer(code, langExt) {
-    const config = this.crowi.getConfig();
+    const config = this.appContainer.getConfig();
     const noborder = (!config.highlightJsStyleBorder) ? 'hljs-no-border' : '';
 
     let citeTag = '';

+ 8 - 12
src/client/js/util/reveal/plugins/growi-renderer.js

@@ -1,24 +1,21 @@
-import GrowiRenderer from '../../GrowiRenderer';
-
 /**
  * reveal.js growi-renderer plugin.
  */
 (function(root, factory) {
-  // parent window DOM (crowi.js) of presentation window.
-  const parentWindow = window.parent;
-
-  // create GrowiRenderer instance and setup.
-  const growiRenderer = new GrowiRenderer(parentWindow.crowi, parentWindow.crowiRenderer, { mode: 'editor' });
+  // get AppContainer instance from parent window
+  const appContainer = window.parent.appContainer;
 
-  const growiRendererPlugin = factory(growiRenderer);
+  const growiRendererPlugin = factory(appContainer);
   growiRendererPlugin.initialize();
-}(this, (growiRenderer) => {
+}(this, (appContainer) => {
   /* eslint-disable no-useless-escape */
   const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
   const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
   const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
   /* eslint-enable no-useless-escape */
 
+  const growiRenderer = appContainer.getRenderer('editor');
+
   let marked;
 
   /**
@@ -31,7 +28,7 @@ import GrowiRenderer from '../../GrowiRenderer';
       const section = sections[i];
       const markdown = marked.getMarkdownFromSlide(section);
       const context = { markdown };
-      const interceptorManager = growiRenderer.crowi.interceptorManager;
+      const interceptorManager = appContainer.interceptorManager;
       let dataSeparator = section.getAttribute('data-separator') || DEFAULT_SLIDE_SEPARATOR;
       // replace string '\n' to LF code.
       dataSeparator = dataSeparator.replace(/\\n/g, '\n');
@@ -54,7 +51,7 @@ import GrowiRenderer from '../../GrowiRenderer';
   function convertSlides() {
     const sections = document.querySelectorAll('[data-markdown]');
     let markdown;
-    const interceptorManager = growiRenderer.crowi.interceptorManager;
+    const interceptorManager = appContainer.interceptorManager;
 
     for (let i = 0, len = sections.length; i < len; i++) {
       const section = sections[i];
@@ -104,7 +101,6 @@ import GrowiRenderer from '../../GrowiRenderer';
   // API
   return {
     async initialize() {
-      growiRenderer.setup();
       marked = require('./markdown').default(growiRenderer.process);
       divideSlides();
       marked.processSlides();