Jelajahi Sumber

add EditorOptionsContainer

Yuki Takei 6 tahun lalu
induk
melakukan
9bcb912acd

+ 24 - 38
src/client/js/app.js

@@ -23,7 +23,7 @@ import PageEditor from './components/PageEditor';
 // eslint-disable-next-line import/no-duplicates
 import OptionsSelector from './components/PageEditor/OptionsSelector';
 // eslint-disable-next-line import/no-duplicates
-import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSelector';
+import { defaultEditorOptions, defaultPreviewOptions } from './components/PageEditor/OptionsSelector';
 import SavePageControls from './components/SavePageControls';
 import PageEditorByHackmd from './components/PageEditorByHackmd';
 import Page from './components/Page';
@@ -48,6 +48,7 @@ import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
 
 import PageContainer from './services/PageContainer';
 import CommentContainer from './components/PageComment/CommentContainer';
+import EditorOptionsContainer from './services/EditorOptionsContainer';
 
 
 const logger = loggerFactory('growi:app');
@@ -118,6 +119,7 @@ window.crowiRenderer = crowiRenderer;
 // create unstated container instance
 const pageContainer = new PageContainer();
 const commentContainer = new CommentContainer(crowi, pageContainer);
+const editorOptionsContainer = new EditorOptionsContainer(defaultEditorOptions, defaultPreviewOptions);
 
 // FIXME
 const isEnabledPlugins = $('body').data('plugin-enabled');
@@ -294,8 +296,6 @@ if (!pageRevisionId && draft != null) {
   markdown = draft;
 }
 
-const pageEditorOptions = new EditorOptions(crowi.editorOptions);
-
 /**
  * define components
  *  key: id of element
@@ -323,7 +323,7 @@ let pageComments = null;
 if (pageId) {
   componentMappings['page-comments-list'] = (
     <I18nextProvider i18n={i18n}>
-      <Provider inject={[commentContainer]}>
+      <Provider inject={[commentContainer, editorOptionsContainer]}>
         <PageComments
           ref={(elem) => {
             if (pageComments == null) {
@@ -333,7 +333,6 @@ if (pageId) {
           revisionCreatedAt={pageRevisionCreatedAt}
           pageId={pageId}
           pagePath={pagePath}
-          editorOptions={pageEditorOptions}
           slackChannels={slackChannels}
           crowi={crowi}
           crowiOriginRenderer={crowiRenderer}
@@ -476,29 +475,27 @@ if (pageEditorWithHackmdElem) {
  * PageEditor
  */
 let pageEditor = null;
-const editorOptions = new EditorOptions(crowi.editorOptions);
-const previewOptions = new PreviewOptions(crowi.previewOptions);
 // render PageEditor
 const pageEditorElem = document.getElementById('page-editor');
 if (pageEditorElem) {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <PageEditor
-        ref={(elem) => {
-          if (pageEditor == null) {
-            pageEditor = elem;
-          }
-        }}
-        crowi={crowi}
-        crowiRenderer={crowiRenderer}
-        pageId={pageId}
-        revisionId={pageRevisionId}
-        pagePath={pagePath}
-        markdown={markdown}
-        editorOptions={editorOptions}
-        previewOptions={previewOptions}
-        onSaveWithShortcut={saveWithShortcut}
-      />
+      <Provider inject={[editorOptionsContainer]}>
+        <PageEditor
+          ref={(elem) => {
+            if (pageEditor == null) {
+              pageEditor = elem;
+            }
+          }}
+          crowi={crowi}
+          crowiRenderer={crowiRenderer}
+          pageId={pageId}
+          revisionId={pageRevisionId}
+          pagePath={pagePath}
+          markdown={markdown}
+          onSaveWithShortcut={saveWithShortcut}
+        />
+      </Provider>
     </I18nextProvider>,
     pageEditorElem,
   );
@@ -511,12 +508,11 @@ if (pageEditorElem) {
 const writeCommentElem = document.getElementById('page-comment-write');
 if (writeCommentElem) {
   ReactDOM.render(
-    <Provider inject={[commentContainer]}>
+    <Provider inject={[commentContainer, editorOptionsContainer]}>
       <I18nextProvider i18n={i18n}>
         <CommentEditorLazyRenderer
           crowi={crowi}
           crowiOriginRenderer={crowiRenderer}
-          editorOptions={pageEditorOptions}
           slackChannels={slackChannels}
         >
         </CommentEditorLazyRenderer>
@@ -531,19 +527,9 @@ const pageEditorOptionsSelectorElem = document.getElementById('page-editor-optio
 if (pageEditorOptionsSelectorElem) {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <OptionsSelector
-        crowi={crowi}
-        editorOptions={editorOptions}
-        previewOptions={previewOptions}
-        onChange={(newEditorOptions, newPreviewOptions) => { // set onChange event handler
-          // set options
-          pageEditor.setEditorOptions(newEditorOptions);
-          pageEditor.setPreviewOptions(newPreviewOptions);
-          // save
-          crowi.saveEditorOptions(newEditorOptions);
-          crowi.savePreviewOptions(newPreviewOptions);
-        }}
-      />
+      <Provider inject={[editorOptionsContainer]}>
+        <OptionsSelector crowi={crowi} />
+      </Provider>
     </I18nextProvider>,
     pageEditorOptionsSelectorElem,
   );

+ 0 - 3
src/client/js/components/PageComment/CommentEditor.jsx

@@ -256,7 +256,6 @@ class CommentEditor extends React.Component {
                       ref={(c) => { this.editor = c }}
                       value={this.state.comment}
                       isGfmMode={this.state.isMarkdown}
-                      editorOptions={this.props.editorOptions}
                       lineNumbers={false}
                       isMobile={this.props.crowi.isMobile}
                       isUploadable={this.state.isUploadable && this.state.isLayoutTypeGrowi} // enable only when GROWI layout
@@ -352,13 +351,11 @@ CommentEditor.propTypes = {
   crowi: PropTypes.object.isRequired,
   crowiOriginRenderer: PropTypes.object.isRequired,
   commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
-  editorOptions: PropTypes.object,
   slackChannels: PropTypes.string,
 };
 CommentEditorWrapper.propTypes = {
   crowi: PropTypes.object.isRequired,
   crowiOriginRenderer: PropTypes.object.isRequired,
-  editorOptions: PropTypes.object,
   slackChannels: PropTypes.string,
 };
 

+ 0 - 1
src/client/js/components/PageComment/CommentEditorLazyRenderer.jsx

@@ -54,6 +54,5 @@ export default class CommentEditorLazyRenderer extends React.Component {
 CommentEditorLazyRenderer.propTypes = {
   crowi: PropTypes.object.isRequired,
   crowiOriginRenderer: PropTypes.object.isRequired,
-  editorOptions: PropTypes.object,
   slackChannels: PropTypes.string,
 };

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

@@ -140,7 +140,6 @@ class PageComments extends React.Component {
             <CommentEditor
               crowi={this.props.crowi}
               crowiOriginRenderer={this.props.crowiOriginRenderer}
-              editorOptions={this.props.editorOptions}
             />
           )}
         </div>
@@ -299,7 +298,6 @@ PageCommentsWrapper.propTypes = {
   revisionId: PropTypes.string.isRequired,
   revisionCreatedAt: PropTypes.number,
   pagePath: PropTypes.string,
-  editorOptions: PropTypes.object,
   slackChannels: PropTypes.string,
 };
 PageComments.propTypes = {
@@ -311,7 +309,6 @@ PageComments.propTypes = {
   revisionId: PropTypes.string.isRequired,
   revisionCreatedAt: PropTypes.number,
   pagePath: PropTypes.string,
-  editorOptions: PropTypes.object,
   slackChannels: PropTypes.string,
 };
 

+ 0 - 23
src/client/js/components/PageEditor.js

@@ -6,7 +6,6 @@ import { throttle, debounce } from 'throttle-debounce';
 import * as toastr from 'toastr';
 import GrowiRenderer from '../util/GrowiRenderer';
 
-import { EditorOptions, PreviewOptions } from './PageEditor/OptionsSelector';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
@@ -29,8 +28,6 @@ export default class PageEditor extends React.Component {
       isUploadable,
       isUploadableFile,
       isMathJaxEnabled,
-      editorOptions: this.props.editorOptions,
-      previewOptions: this.props.previewOptions,
     };
 
     this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, { mode: 'editor' });
@@ -104,22 +101,6 @@ export default class PageEditor extends React.Component {
     scrollSyncHelper.scrollPreview(this.previewElement, line);
   }
 
-  /**
-   * set options (used from the outside)
-   * @param {object} editorOptions
-   */
-  setEditorOptions(editorOptions) {
-    this.setState({ editorOptions });
-  }
-
-  /**
-   * set options (used from the outside)
-   * @param {object} previewOptions
-   */
-  setPreviewOptions(previewOptions) {
-    this.setState({ previewOptions });
-  }
-
   /**
    * the change event handler for `markdown` state
    * @param {string} value
@@ -342,7 +323,6 @@ export default class PageEditor extends React.Component {
           <Editor
             ref={(c) => { this.editor = c }}
             value={this.state.markdown}
-            editorOptions={this.state.editorOptions}
             noCdn={noCdn}
             isMobile={this.props.crowi.isMobile}
             isUploadable={this.state.isUploadable}
@@ -362,7 +342,6 @@ export default class PageEditor extends React.Component {
             inputRef={(el) => { return this.previewElement = el }}
             isMathJaxEnabled={this.state.isMathJaxEnabled}
             renderMathJaxOnInit={false}
-            previewOptions={this.state.previewOptions}
             onScroll={this.onPreviewScroll}
           />
         </div>
@@ -380,6 +359,4 @@ PageEditor.propTypes = {
   pageId: PropTypes.string,
   revisionId: PropTypes.string,
   pagePath: PropTypes.string,
-  editorOptions: PropTypes.instanceOf(EditorOptions),
-  previewOptions: PropTypes.instanceOf(PreviewOptions),
 };

+ 0 - 1
src/client/js/components/PageEditor/AbstractEditor.js

@@ -124,7 +124,6 @@ export default class AbstractEditor extends React.Component {
 AbstractEditor.propTypes = {
   value: PropTypes.string,
   isGfmMode: PropTypes.bool,
-  editorOptions: PropTypes.object,
   onChange: PropTypes.func,
   onScroll: PropTypes.func,
   onScrollCursorIntoView: PropTypes.func,

+ 38 - 34
src/client/js/components/PageEditor/CodeMirrorEditor.js

@@ -114,6 +114,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
       this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
       this.setState({ isEnabledEmojiAutoComplete: true });
     }
+
+    // load theme
+    const theme = this.props.editorOptions.theme;
+    this.loadTheme(theme);
+
+    // set keymap
+    const keymapMode = this.props.editorOptions.keymapMode;
+    this.setKeymapMode(keymapMode);
   }
 
   componentDidMount() {
@@ -717,12 +725,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
   render() {
     const mode = this.state.isGfmMode ? 'gfm' : undefined;
-    const defaultEditorOptions = {
-      theme: 'elegant',
-      lineNumbers: true,
-    };
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
-    const editorOptions = Object.assign(defaultEditorOptions, this.props.editorOptions || {});
 
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
 
@@ -740,35 +743,35 @@ export default class CodeMirrorEditor extends AbstractEditor {
         }}
           value={this.state.value}
           options={{
-          mode,
-          theme: editorOptions.theme,
-          styleActiveLine: editorOptions.styleActiveLine,
-          lineNumbers: this.props.lineNumbers,
-          tabSize: 4,
-          indentUnit: 4,
-          lineWrapping: true,
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          autoCloseTags: true,
-          placeholder,
-          matchBrackets: true,
-          matchTags: { bothTags: true },
-          // folding
-          foldGutter: this.props.lineNumbers,
-          gutters: this.props.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : [],
-          // match-highlighter, matchesonscrollbar, annotatescrollbar options
-          highlightSelectionMatches: { annotateScrollbar: true },
-          // markdown mode options
-          highlightFormatting: true,
-          // continuelist, indentlist
-          extraKeys: {
-            Enter: this.handleEnterKey,
-            'Ctrl-Enter': this.handleCtrlEnterKey,
-            'Cmd-Enter': this.handleCtrlEnterKey,
-            Tab: 'indentMore',
-            'Shift-Tab': 'indentLess',
-            'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
-          },
-        }}
+            mode,
+            theme: this.props.editorOptions.theme,
+            styleActiveLine: this.props.editorOptions.styleActiveLine,
+            lineNumbers: this.props.lineNumbers,
+            tabSize: 4,
+            indentUnit: 4,
+            lineWrapping: true,
+            autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
+            autoCloseTags: true,
+            placeholder,
+            matchBrackets: true,
+            matchTags: { bothTags: true },
+            // folding
+            foldGutter: this.props.lineNumbers,
+            gutters: this.props.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : [],
+            // match-highlighter, matchesonscrollbar, annotatescrollbar options
+            highlightSelectionMatches: { annotateScrollbar: true },
+            // markdown mode options
+            highlightFormatting: true,
+            // continuelist, indentlist
+            extraKeys: {
+              Enter: this.handleEnterKey,
+              'Ctrl-Enter': this.handleCtrlEnterKey,
+              'Cmd-Enter': this.handleCtrlEnterKey,
+              Tab: 'indentMore',
+              'Shift-Tab': 'indentLess',
+              'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
+            },
+          }}
           onCursor={this.cursorHandler}
           onScroll={(editor, data) => {
           if (this.props.onScroll != null) {
@@ -804,6 +807,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 }
 
 CodeMirrorEditor.propTypes = Object.assign({
+  editorOptions: PropTypes.object.isRequired,
   emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
 }, AbstractEditor.propTypes);

+ 17 - 10
src/client/js/components/PageEditor/Editor.jsx

@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { Subscribe } from 'unstated';
+
 import Dropzone from 'react-dropzone';
 import AbstractEditor from './AbstractEditor';
 import CodeMirrorEditor from './CodeMirrorEditor';
@@ -8,6 +10,7 @@ import TextAreaEditor from './TextAreaEditor';
 
 
 import pasteHelper from './PasteHelper';
+import EditorOptionsContainer from '../../services/EditorOptionsContainer';
 
 export default class Editor extends AbstractEditor {
 
@@ -271,14 +274,19 @@ export default class Editor extends AbstractEditor {
 
                 {/* for PC */}
                 { !isMobile && (
-                  <CodeMirrorEditor
-                    ref={(c) => { this.cmEditor = c }}
-                    onPasteFiles={this.pasteFilesHandler}
-                    onDragEnter={this.dragEnterHandler}
-                    {...this.props}
-                  />
-                  )
-                }
+                  <Subscribe to={[EditorOptionsContainer]}>
+                    { editorOptionsContainer => (
+                      // eslint-disable-next-line arrow-body-style
+                      <CodeMirrorEditor
+                        ref={(c) => { this.cmEditor = c }}
+                        editorOptions={editorOptionsContainer.state.editorOptions}
+                        onPasteFiles={this.pasteFilesHandler}
+                        onDragEnter={this.dragEnterHandler}
+                        {...this.props}
+                      />
+                    )}
+                  </Subscribe>
+                )}
 
                 {/* for mobile */}
                 { isMobile && (
@@ -288,8 +296,7 @@ export default class Editor extends AbstractEditor {
                     onDragEnter={this.dragEnterHandler}
                     {...this.props}
                   />
-                  )
-                }
+                )}
 
                 <input {...getInputProps()} />
               </div>

+ 70 - 54
src/client/js/components/PageEditor/OptionsSelector.jsx

@@ -1,5 +1,7 @@
+/* eslint-disable react/no-multi-comp */
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Subscribe } from 'unstated';
 import { withTranslation } from 'react-i18next';
 
 import FormGroup from 'react-bootstrap/es/FormGroup';
@@ -9,27 +11,18 @@ import ControlLabel from 'react-bootstrap/es/ControlLabel';
 import Dropdown from 'react-bootstrap/es/Dropdown';
 import MenuItem from 'react-bootstrap/es/MenuItem';
 
-export class EditorOptions {
+import EditorOptionsContainer from '../../services/EditorOptionsContainer';
 
-  constructor(props) {
-    this.theme = 'elegant';
-    this.keymapMode = 'default';
-    this.styleActiveLine = false;
-
-    Object.assign(this, props);
-  }
-
-}
-
-export class PreviewOptions {
-
-  constructor(props) {
-    this.renderMathJaxInRealtime = false;
 
-    Object.assign(this, props);
-  }
+export const defaultEditorOptions = {
+  theme: 'elegant',
+  keymapMode: 'default',
+  styleActiveLine: false,
+};
 
-}
+export const defaultPreviewOptions = {
+  renderMathJaxInRealtime: false,
+};
 
 class OptionsSelector extends React.Component {
 
@@ -40,8 +33,6 @@ class OptionsSelector extends React.Component {
     const isMathJaxEnabled = !!config.env.MATHJAX;
 
     this.state = {
-      editorOptions: this.props.editorOptions || new EditorOptions(),
-      previewOptions: this.props.previewOptions || new PreviewOptions(),
       isCddMenuOpened: false,
       isMathJaxEnabled,
     };
@@ -68,50 +59,60 @@ class OptionsSelector extends React.Component {
   }
 
   init() {
-    this.themeSelectorInputEl.value = this.state.editorOptions.theme;
-    this.keymapModeSelectorInputEl.value = this.state.editorOptions.keymapMode;
+    const { editorOptionsContainer } = this.props;
+
+    this.themeSelectorInputEl.value = editorOptionsContainer.state.editorOptions.theme;
+    this.keymapModeSelectorInputEl.value = editorOptionsContainer.state.editorOptions.keymapMode;
   }
 
   onChangeTheme() {
+    const { editorOptionsContainer } = this.props;
+
     const newValue = this.themeSelectorInputEl.value;
-    const newOpts = Object.assign(this.state.editorOptions, { theme: newValue });
-    this.setState({ editorOptions: newOpts });
+    const newOpts = Object.assign(editorOptionsContainer.state.editorOptions, { theme: newValue });
+    editorOptionsContainer.setState({ editorOptions: newOpts });
 
-    // dispatch event
-    this.dispatchOnChange();
+    // save to localStorage
+    editorOptionsContainer.saveToLocalStorage();
   }
 
   onChangeKeymapMode() {
+    const { editorOptionsContainer } = this.props;
+
     const newValue = this.keymapModeSelectorInputEl.value;
-    const newOpts = Object.assign(this.state.editorOptions, { keymapMode: newValue });
-    this.setState({ editorOptions: newOpts });
+    const newOpts = Object.assign(editorOptionsContainer.state.editorOptions, { keymapMode: newValue });
+    editorOptionsContainer.setState({ editorOptions: newOpts });
 
-    // dispatch event
-    this.dispatchOnChange();
+    // save to localStorage
+    editorOptionsContainer.saveToLocalStorage();
   }
 
   onClickStyleActiveLine(event) {
+    const { editorOptionsContainer } = this.props;
+
     // keep dropdown opened
     this._cddForceOpen = true;
 
-    const newValue = !this.state.editorOptions.styleActiveLine;
-    const newOpts = Object.assign(this.state.editorOptions, { styleActiveLine: newValue });
-    this.setState({ editorOptions: newOpts });
+    const newValue = !editorOptionsContainer.state.editorOptions.styleActiveLine;
+    const newOpts = Object.assign(editorOptionsContainer.state.editorOptions, { styleActiveLine: newValue });
+    editorOptionsContainer.setState({ editorOptions: newOpts });
 
-    // dispatch event
-    this.dispatchOnChange();
+    // save to localStorage
+    editorOptionsContainer.saveToLocalStorage();
   }
 
   onClickRenderMathJaxInRealtime(event) {
+    const { editorOptionsContainer } = this.props;
+
     // keep dropdown opened
     this._cddForceOpen = true;
 
-    const newValue = !this.state.previewOptions.renderMathJaxInRealtime;
-    const newOpts = Object.assign(this.state.previewOptions, { renderMathJaxInRealtime: newValue });
-    this.setState({ previewOptions: newOpts });
+    const newValue = !editorOptionsContainer.state.previewOptions.renderMathJaxInRealtime;
+    const newOpts = Object.assign(editorOptionsContainer.state.previewOptions, { renderMathJaxInRealtime: newValue });
+    editorOptionsContainer.setState({ previewOptions: newOpts });
 
-    // dispatch event
-    this.dispatchOnChange();
+    // save to localStorage
+    editorOptionsContainer.saveToLocalStorage();
   }
 
   /*
@@ -127,13 +128,6 @@ class OptionsSelector extends React.Component {
     }
   }
 
-  /**
-   * dispatch onChange event
-   */
-  dispatchOnChange() {
-    this.props.onChange(this.state.editorOptions, this.state.previewOptions);
-  }
-
   renderThemeSelector() {
     const optionElems = this.availableThemes.map((theme) => {
       return <option key={theme} value={theme}>{theme}</option>;
@@ -225,8 +219,8 @@ class OptionsSelector extends React.Component {
   }
 
   renderActiveLineMenuItem() {
-    const { t } = this.props;
-    const isActive = this.state.editorOptions.styleActiveLine;
+    const { t, editorOptionsContainer } = this.props;
+    const isActive = editorOptionsContainer.state.editorOptions.styleActiveLine;
 
     const iconClasses = ['text-info'];
     if (isActive) {
@@ -248,8 +242,10 @@ class OptionsSelector extends React.Component {
       return;
     }
 
+    const { editorOptionsContainer } = this.props;
+
     const isEnabled = this.state.isMathJaxEnabled;
-    const isActive = isEnabled && this.state.previewOptions.renderMathJaxInRealtime;
+    const isActive = isEnabled && editorOptionsContainer.state.previewOptions.renderMathJaxInRealtime;
 
     const iconClasses = ['text-info'];
     if (isActive) {
@@ -278,13 +274,33 @@ class OptionsSelector extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+class OptionsSelectorWrapper extends React.Component {
+
+  render() {
+    return (
+      <Subscribe to={[EditorOptionsContainer]}>
+        { editorOptionsContainer => (
+          // eslint-disable-next-line arrow-body-style
+          <OptionsSelector editorOptionsContainer={editorOptionsContainer} {...this.props} />
+        )}
+      </Subscribe>
+    );
+  }
+
+}
+
 
 OptionsSelector.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   crowi: PropTypes.object.isRequired,
-  editorOptions: PropTypes.instanceOf(EditorOptions).isRequired,
-  previewOptions: PropTypes.instanceOf(PreviewOptions).isRequired,
-  onChange: PropTypes.func.isRequired,
+  editorOptionsContainer: PropTypes.instanceOf(EditorOptionsContainer).isRequired,
+};
+OptionsSelectorWrapper.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  crowi: PropTypes.object.isRequired,
 };
 
-export default withTranslation()(OptionsSelector);
+export default withTranslation()(OptionsSelectorWrapper);

+ 25 - 22
src/client/js/components/PageEditor/Preview.js

@@ -1,9 +1,11 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { Subscribe } from 'unstated';
+
 import RevisionBody from '../Page/RevisionBody';
 
-import { PreviewOptions } from './OptionsSelector';
+import EditorOptionsContainer from '../../services/EditorOptionsContainer';
 
 /**
  * Wrapper component for Page/RevisionBody
@@ -11,27 +13,29 @@ import { PreviewOptions } from './OptionsSelector';
 export default class Preview extends React.Component {
 
   render() {
-    const renderMathJaxInRealtime = this.props.previewOptions.renderMathJaxInRealtime;
-
     return (
-      <div
-        className="page-editor-preview-body"
-        ref={(elm) => {
-            this.previewElement = elm;
-            this.props.inputRef(elm);
-          }}
-        onScroll={(event) => {
-            if (this.props.onScroll != null) {
-              this.props.onScroll(event.target.scrollTop);
-            }
-          }}
-      >
-
-        <RevisionBody
-          {...this.props}
-          renderMathJaxInRealtime={renderMathJaxInRealtime}
-        />
-      </div>
+      <Subscribe to={[EditorOptionsContainer]}>
+        { editorOptionsContainer => (
+          // eslint-disable-next-line arrow-body-style
+          <div
+            className="page-editor-preview-body"
+            ref={(elm) => {
+                this.previewElement = elm;
+                this.props.inputRef(elm);
+              }}
+            onScroll={(event) => {
+                if (this.props.onScroll != null) {
+                  this.props.onScroll(event.target.scrollTop);
+                }
+              }}
+          >
+            <RevisionBody
+              {...this.props}
+              renderMathJaxInRealtime={editorOptionsContainer.state.previewOptions.renderMathJaxInRealtime}
+            />
+          </div>
+        )}
+      </Subscribe>
     );
   }
 
@@ -42,6 +46,5 @@ Preview.propTypes = {
   inputRef: PropTypes.func.isRequired, // for getting div element
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
-  previewOptions: PropTypes.instanceOf(PreviewOptions),
   onScroll: PropTypes.func,
 };

+ 45 - 0
src/client/js/services/EditorOptionsContainer.js

@@ -0,0 +1,45 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container related to options for Editor/Preview
+ * @extends {Container} unstated Container
+ */
+export default class EditorOptionsContainer extends Container {
+
+  constructor(defaultEditorOptions, defaultPreviewOptions) {
+    super();
+
+    this.state = {
+      editorOptions: {},
+      previewOptions: {},
+    };
+
+    this.initOptions('editorOptions', 'editorOptions', defaultEditorOptions);
+    this.initOptions('previewOptions', 'previewOptions', defaultPreviewOptions);
+  }
+
+  initOptions(stateKey, localStorageKey, defaultOptions) {
+    // load from localStorage
+    const optsStr = window.localStorage[localStorageKey];
+
+    let loadedOpts = {};
+    // JSON.parseparse
+    if (optsStr != null) {
+      try {
+        loadedOpts = JSON.parse(optsStr);
+      }
+      catch (e) {
+        this.localStorage.removeItem(localStorageKey);
+      }
+    }
+
+    // set to state obj
+    this.state[stateKey] = Object.assign(defaultOptions, loadedOpts);
+  }
+
+  saveToLocalStorage() {
+    window.localStorage.setItem('editorOptions', JSON.stringify(this.state.editorOptions));
+    window.localStorage.setItem('previewOptions', JSON.stringify(this.state.previewOptions));
+  }
+
+}

+ 0 - 10
src/client/js/util/Crowi.js

@@ -106,8 +106,6 @@ export default class Crowi {
       'userById',
       'users',
       'draft',
-      'editorOptions',
-      'previewOptions',
     ];
 
     keys.forEach((key) => {
@@ -190,14 +188,6 @@ export default class Crowi {
     return null;
   }
 
-  saveEditorOptions(options) {
-    this.localStorage.setItem('editorOptions', JSON.stringify(options));
-  }
-
-  savePreviewOptions(options) {
-    this.localStorage.setItem('previewOptions', JSON.stringify(options));
-  }
-
   findUserById(userId) {
     if (this.userById && this.userById[userId]) {
       return this.userById[userId];