OptionsSelector.jsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { withTranslation } from 'react-i18next';
  4. import FormGroup from 'react-bootstrap/es/FormGroup';
  5. import FormControl from 'react-bootstrap/es/FormControl';
  6. import ControlLabel from 'react-bootstrap/es/ControlLabel';
  7. import Dropdown from 'react-bootstrap/es/Dropdown';
  8. import MenuItem from 'react-bootstrap/es/MenuItem';
  9. import { createSubscribedElement } from '../UnstatedUtils';
  10. import EditorContainer from '../../services/EditorContainer';
  11. export const defaultEditorOptions = {
  12. theme: 'elegant',
  13. keymapMode: 'default',
  14. styleActiveLine: false,
  15. };
  16. export const defaultPreviewOptions = {
  17. renderMathJaxInRealtime: false,
  18. };
  19. class OptionsSelector extends React.Component {
  20. constructor(props) {
  21. super(props);
  22. const config = this.props.crowi.getConfig();
  23. const isMathJaxEnabled = !!config.env.MATHJAX;
  24. this.state = {
  25. isCddMenuOpened: false,
  26. isMathJaxEnabled,
  27. };
  28. this.availableThemes = [
  29. 'eclipse', 'elegant', 'neo', 'mdn-like', 'material', 'dracula', 'monokai', 'twilight',
  30. ];
  31. this.keymapModes = {
  32. default: 'Default',
  33. vim: 'Vim',
  34. emacs: 'Emacs',
  35. sublime: 'Sublime Text',
  36. };
  37. this.onChangeTheme = this.onChangeTheme.bind(this);
  38. this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
  39. this.onClickStyleActiveLine = this.onClickStyleActiveLine.bind(this);
  40. this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
  41. this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
  42. }
  43. componentDidMount() {
  44. this.init();
  45. }
  46. init() {
  47. const { editorContainer } = this.props;
  48. this.themeSelectorInputEl.value = editorContainer.state.editorOptions.theme;
  49. this.keymapModeSelectorInputEl.value = editorContainer.state.editorOptions.keymapMode;
  50. }
  51. onChangeTheme() {
  52. const { editorContainer } = this.props;
  53. const newValue = this.themeSelectorInputEl.value;
  54. const newOpts = Object.assign(editorContainer.state.editorOptions, { theme: newValue });
  55. editorContainer.setState({ editorOptions: newOpts });
  56. // save to localStorage
  57. editorContainer.saveOptsToLocalStorage();
  58. }
  59. onChangeKeymapMode() {
  60. const { editorContainer } = this.props;
  61. const newValue = this.keymapModeSelectorInputEl.value;
  62. const newOpts = Object.assign(editorContainer.state.editorOptions, { keymapMode: newValue });
  63. editorContainer.setState({ editorOptions: newOpts });
  64. // save to localStorage
  65. editorContainer.saveOptsToLocalStorage();
  66. }
  67. onClickStyleActiveLine(event) {
  68. const { editorContainer } = this.props;
  69. // keep dropdown opened
  70. this._cddForceOpen = true;
  71. const newValue = !editorContainer.state.editorOptions.styleActiveLine;
  72. const newOpts = Object.assign(editorContainer.state.editorOptions, { styleActiveLine: newValue });
  73. editorContainer.setState({ editorOptions: newOpts });
  74. // save to localStorage
  75. editorContainer.saveOptsToLocalStorage();
  76. }
  77. onClickRenderMathJaxInRealtime(event) {
  78. const { editorContainer } = this.props;
  79. // keep dropdown opened
  80. this._cddForceOpen = true;
  81. const newValue = !editorContainer.state.previewOptions.renderMathJaxInRealtime;
  82. const newOpts = Object.assign(editorContainer.state.previewOptions, { renderMathJaxInRealtime: newValue });
  83. editorContainer.setState({ previewOptions: newOpts });
  84. // save to localStorage
  85. editorContainer.saveOptsToLocalStorage();
  86. }
  87. /*
  88. * see: https://github.com/react-bootstrap/react-bootstrap/issues/1490#issuecomment-207445759
  89. */
  90. onToggleConfigurationDropdown(newValue) {
  91. if (this._cddForceOpen) {
  92. this.setState({ isCddMenuOpened: true });
  93. this._cddForceOpen = false;
  94. }
  95. else {
  96. this.setState({ isCddMenuOpened: newValue });
  97. }
  98. }
  99. renderThemeSelector() {
  100. const optionElems = this.availableThemes.map((theme) => {
  101. return <option key={theme} value={theme}>{theme}</option>;
  102. });
  103. const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
  104. return (
  105. <FormGroup controlId="formControlsSelect" className="my-0">
  106. <ControlLabel>Theme:</ControlLabel>
  107. <FormControl
  108. componentClass="select"
  109. placeholder="select"
  110. bsClass={bsClassName}
  111. className="btn-group-sm selectpicker"
  112. onChange={this.onChangeTheme}
  113. // eslint-disable-next-line no-return-assign
  114. inputRef={(el) => { return this.themeSelectorInputEl = el }}
  115. >
  116. {optionElems}
  117. </FormControl>
  118. </FormGroup>
  119. );
  120. }
  121. renderKeymapModeSelector() {
  122. const optionElems = [];
  123. // eslint-disable-next-line guard-for-in, no-restricted-syntax
  124. for (const mode in this.keymapModes) {
  125. const label = this.keymapModes[mode];
  126. const dataContent = (mode === 'default')
  127. ? label
  128. : `<img src='/images/icons/${mode}.png' width='16px' class='m-r-5'></img> ${label}`;
  129. optionElems.push(
  130. <option key={mode} value={mode} data-content={dataContent}>{label}</option>,
  131. );
  132. }
  133. const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
  134. return (
  135. <FormGroup controlId="formControlsSelect" className="my-0">
  136. <ControlLabel>Keymap:</ControlLabel>
  137. <FormControl
  138. componentClass="select"
  139. placeholder="select"
  140. bsClass={bsClassName}
  141. className="btn-group-sm selectpicker"
  142. onChange={this.onChangeKeymapMode}
  143. // eslint-disable-next-line no-return-assign
  144. inputRef={(el) => { return this.keymapModeSelectorInputEl = el }}
  145. >
  146. {optionElems}
  147. </FormControl>
  148. </FormGroup>
  149. );
  150. }
  151. renderConfigurationDropdown() {
  152. return (
  153. <FormGroup controlId="formControlsSelect" className="my-0">
  154. <Dropdown
  155. dropup
  156. id="configurationDropdown"
  157. className="configuration-dropdown"
  158. open={this.state.isCddMenuOpened}
  159. onToggle={this.onToggleConfigurationDropdown}
  160. >
  161. <Dropdown.Toggle bsSize="sm">
  162. <i className="icon-settings"></i>
  163. </Dropdown.Toggle>
  164. <Dropdown.Menu>
  165. {this.renderActiveLineMenuItem()}
  166. {this.renderRealtimeMathJaxMenuItem()}
  167. {/* <MenuItem divider /> */}
  168. </Dropdown.Menu>
  169. </Dropdown>
  170. </FormGroup>
  171. );
  172. }
  173. renderActiveLineMenuItem() {
  174. const { t, editorContainer } = this.props;
  175. const isActive = editorContainer.state.editorOptions.styleActiveLine;
  176. const iconClasses = ['text-info'];
  177. if (isActive) {
  178. iconClasses.push('ti-check');
  179. }
  180. const iconClassName = iconClasses.join(' ');
  181. return (
  182. <MenuItem onClick={this.onClickStyleActiveLine}>
  183. <span className="icon-container"></span>
  184. <span className="menuitem-label">{ t('page_edit.Show active line') }</span>
  185. <span className="icon-container"><i className={iconClassName}></i></span>
  186. </MenuItem>
  187. );
  188. }
  189. renderRealtimeMathJaxMenuItem() {
  190. if (!this.state.isMathJaxEnabled) {
  191. return;
  192. }
  193. const { editorContainer } = this.props;
  194. const isEnabled = this.state.isMathJaxEnabled;
  195. const isActive = isEnabled && editorContainer.state.previewOptions.renderMathJaxInRealtime;
  196. const iconClasses = ['text-info'];
  197. if (isActive) {
  198. iconClasses.push('ti-check');
  199. }
  200. const iconClassName = iconClasses.join(' ');
  201. return (
  202. <MenuItem onClick={this.onClickRenderMathJaxInRealtime}>
  203. <span className="icon-container"><img src="/images/icons/fx.svg" width="14px" alt="fx"></img></span>
  204. <span className="menuitem-label">MathJax Rendering</span>
  205. <i className={iconClassName}></i>
  206. </MenuItem>
  207. );
  208. }
  209. render() {
  210. return (
  211. <div className="d-flex flex-row">
  212. <span className="m-l-5">{this.renderThemeSelector()}</span>
  213. <span className="m-l-5">{this.renderKeymapModeSelector()}</span>
  214. <span className="m-l-5">{this.renderConfigurationDropdown()}</span>
  215. </div>
  216. );
  217. }
  218. }
  219. /**
  220. * Wrapper component for using unstated
  221. */
  222. const OptionsSelectorWrapper = (props) => {
  223. return createSubscribedElement(OptionsSelector, props, [EditorContainer]);
  224. };
  225. OptionsSelector.propTypes = {
  226. t: PropTypes.func.isRequired, // i18next
  227. editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
  228. crowi: PropTypes.object.isRequired,
  229. };
  230. export default withTranslation()(OptionsSelectorWrapper);