2
0

OptionsSelector.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { withTranslation } from 'react-i18next';
  4. import {
  5. Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
  6. } from 'reactstrap';
  7. import { withUnstatedContainers } from '../UnstatedUtils';
  8. import AppContainer from '~/client/services/AppContainer';
  9. import EditorContainer from '~/client/services/EditorContainer';
  10. import { toastError } from '~/client/util/apiNotification';
  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.appContainer.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.typicalIndentSizes = [2, 4];
  38. this.onChangeTheme = this.onChangeTheme.bind(this);
  39. this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
  40. this.onClickStyleActiveLine = this.onClickStyleActiveLine.bind(this);
  41. this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
  42. this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
  43. this.switchTextlintEnabledHandler = this.switchTextlintEnabledHandler.bind(this);
  44. this.updateIsTextlintEnabledToDB = this.updateIsTextlintEnabledToDB.bind(this);
  45. this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
  46. this.onChangeIndentSize = this.onChangeIndentSize.bind(this);
  47. }
  48. onChangeTheme(newValue) {
  49. const { editorContainer } = this.props;
  50. const newOpts = Object.assign(editorContainer.state.editorOptions, { theme: newValue });
  51. editorContainer.setState({ editorOptions: newOpts });
  52. // save to localStorage
  53. editorContainer.saveOptsToLocalStorage();
  54. }
  55. onChangeKeymapMode(newValue) {
  56. const { editorContainer } = this.props;
  57. const newOpts = Object.assign(editorContainer.state.editorOptions, { keymapMode: newValue });
  58. editorContainer.setState({ editorOptions: newOpts });
  59. // save to localStorage
  60. editorContainer.saveOptsToLocalStorage();
  61. }
  62. onClickStyleActiveLine(event) {
  63. const { editorContainer } = this.props;
  64. // keep dropdown opened
  65. this._cddForceOpen = true;
  66. const newValue = !editorContainer.state.editorOptions.styleActiveLine;
  67. const newOpts = Object.assign(editorContainer.state.editorOptions, { styleActiveLine: newValue });
  68. editorContainer.setState({ editorOptions: newOpts });
  69. // save to localStorage
  70. editorContainer.saveOptsToLocalStorage();
  71. }
  72. onClickRenderMathJaxInRealtime(event) {
  73. const { editorContainer } = this.props;
  74. const newValue = !editorContainer.state.previewOptions.renderMathJaxInRealtime;
  75. const newOpts = Object.assign(editorContainer.state.previewOptions, { renderMathJaxInRealtime: newValue });
  76. editorContainer.setState({ previewOptions: newOpts });
  77. // save to localStorage
  78. editorContainer.saveOptsToLocalStorage();
  79. }
  80. onClickMarkdownTableAutoFormatting(event) {
  81. const { editorContainer } = this.props;
  82. const newValue = !editorContainer.state.editorOptions.ignoreMarkdownTableAutoFormatting;
  83. const newOpts = Object.assign(editorContainer.state.editorOptions, { ignoreMarkdownTableAutoFormatting: newValue });
  84. editorContainer.setState({ editorOptions: newOpts });
  85. // save to localStorage
  86. editorContainer.saveOptsToLocalStorage();
  87. }
  88. async updateIsTextlintEnabledToDB(newVal) {
  89. const { appContainer } = this.props;
  90. try {
  91. await appContainer.apiv3Put('/personal-setting/editor-settings', { textlintSettings: { isTextlintEnabled: newVal } });
  92. }
  93. catch (err) {
  94. toastError(err);
  95. }
  96. }
  97. async switchTextlintEnabledHandler() {
  98. const { editorContainer } = this.props;
  99. const newVal = !editorContainer.state.isTextlintEnabled;
  100. editorContainer.setState({ isTextlintEnabled: newVal });
  101. this.updateIsTextlintEnabledToDB(newVal);
  102. }
  103. onToggleConfigurationDropdown(newValue) {
  104. this.setState({ isCddMenuOpened: !this.state.isCddMenuOpened });
  105. }
  106. onChangeIndentSize(newValue) {
  107. const { editorContainer } = this.props;
  108. editorContainer.setState({ indentSize: newValue });
  109. }
  110. renderThemeSelector() {
  111. const { editorContainer } = this.props;
  112. const selectedTheme = editorContainer.state.editorOptions.theme;
  113. const menuItems = this.availableThemes.map((theme) => {
  114. return <button key={theme} className="dropdown-item" type="button" onClick={() => this.onChangeTheme(theme)}>{theme}</button>;
  115. });
  116. return (
  117. <div className="input-group flex-nowrap">
  118. <div className="input-group-prepend">
  119. <span className="input-group-text" id="igt-theme">Theme</span>
  120. </div>
  121. <div className="input-group-append dropup">
  122. <button
  123. type="button"
  124. className="btn btn-outline-secondary dropdown-toggle"
  125. data-toggle="dropdown"
  126. aria-haspopup="true"
  127. aria-expanded="false"
  128. aria-describedby="igt-theme"
  129. >
  130. {selectedTheme}
  131. </button>
  132. <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
  133. {menuItems}
  134. </div>
  135. </div>
  136. </div>
  137. );
  138. }
  139. renderKeymapModeSelector() {
  140. const { editorContainer } = this.props;
  141. const selectedKeymapMode = editorContainer.state.editorOptions.keymapMode;
  142. const menuItems = Object.keys(this.keymapModes).map((mode) => {
  143. const label = this.keymapModes[mode];
  144. const icon = (mode !== 'default')
  145. ? <img src={`/images/icons/${mode}.png`} width="16px" className="mr-2"></img>
  146. : null;
  147. return <button key={mode} className="dropdown-item" type="button" onClick={() => this.onChangeKeymapMode(mode)}>{icon}{label}</button>;
  148. });
  149. return (
  150. <div className="input-group flex-nowrap">
  151. <div className="input-group-prepend">
  152. <span className="input-group-text" id="igt-keymap">Keymap</span>
  153. </div>
  154. <div className="input-group-append dropup">
  155. <button
  156. type="button"
  157. className="btn btn-outline-secondary dropdown-toggle"
  158. data-toggle="dropdown"
  159. aria-haspopup="true"
  160. aria-expanded="false"
  161. aria-describedby="igt-keymap"
  162. >
  163. {selectedKeymapMode}
  164. </button>
  165. <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
  166. {menuItems}
  167. </div>
  168. </div>
  169. </div>
  170. );
  171. }
  172. renderConfigurationDropdown() {
  173. return (
  174. <div className="my-0 form-group">
  175. <Dropdown
  176. direction="up"
  177. className="grw-editor-configuration-dropdown"
  178. isOpen={this.state.isCddMenuOpened}
  179. toggle={this.onToggleConfigurationDropdown}
  180. >
  181. <DropdownToggle color="outline-secondary" caret>
  182. <i className="icon-settings"></i>
  183. </DropdownToggle>
  184. <DropdownMenu>
  185. {this.renderActiveLineMenuItem()}
  186. {this.renderRealtimeMathJaxMenuItem()}
  187. {this.renderMarkdownTableAutoFormattingMenuItem()}
  188. {this.renderIsTextlintEnabledMenuItem()}
  189. {/* <DropdownItem divider /> */}
  190. </DropdownMenu>
  191. </Dropdown>
  192. </div>
  193. );
  194. }
  195. renderActiveLineMenuItem() {
  196. const { t, editorContainer } = this.props;
  197. const isActive = editorContainer.state.editorOptions.styleActiveLine;
  198. const iconClasses = ['text-info'];
  199. if (isActive) {
  200. iconClasses.push('ti-check');
  201. }
  202. const iconClassName = iconClasses.join(' ');
  203. return (
  204. <DropdownItem toggle={false} onClick={this.onClickStyleActiveLine}>
  205. <div className="d-flex justify-content-between">
  206. <span className="icon-container"></span>
  207. <span className="menuitem-label">{ t('page_edit.Show active line') }</span>
  208. <span className="icon-container"><i className={iconClassName}></i></span>
  209. </div>
  210. </DropdownItem>
  211. );
  212. }
  213. renderRealtimeMathJaxMenuItem() {
  214. if (!this.state.isMathJaxEnabled) {
  215. return;
  216. }
  217. const { editorContainer } = this.props;
  218. const isEnabled = this.state.isMathJaxEnabled;
  219. const isActive = isEnabled && editorContainer.state.previewOptions.renderMathJaxInRealtime;
  220. const iconClasses = ['text-info'];
  221. if (isActive) {
  222. iconClasses.push('ti-check');
  223. }
  224. const iconClassName = iconClasses.join(' ');
  225. return (
  226. <DropdownItem toggle={false} onClick={this.onClickRenderMathJaxInRealtime}>
  227. <div className="d-flex justify-content-between">
  228. <span className="icon-container"><img src="/images/icons/fx.svg" width="14px" alt="fx"></img></span>
  229. <span className="menuitem-label">MathJax Rendering</span>
  230. <span className="icon-container"><i className={iconClassName}></i></span>
  231. </div>
  232. </DropdownItem>
  233. );
  234. }
  235. renderMarkdownTableAutoFormattingMenuItem() {
  236. const { t, editorContainer } = this.props;
  237. // Auto-formatting was enabled before optionalizing, so we made it a disabled option(ignoreMarkdownTableAutoFormatting).
  238. const isActive = !editorContainer.state.editorOptions.ignoreMarkdownTableAutoFormatting;
  239. const iconClasses = ['text-info'];
  240. if (isActive) {
  241. iconClasses.push('ti-check');
  242. }
  243. const iconClassName = iconClasses.join(' ');
  244. return (
  245. <DropdownItem toggle={false} onClick={this.onClickMarkdownTableAutoFormatting}>
  246. <div className="d-flex justify-content-between">
  247. <span className="icon-container"></span>
  248. <span className="menuitem-label">{ t('page_edit.auto_format_table') }</span>
  249. <span className="icon-container"><i className={iconClassName}></i></span>
  250. </div>
  251. </DropdownItem>
  252. );
  253. }
  254. renderIsTextlintEnabledMenuItem() {
  255. const isActive = this.props.editorContainer.state.isTextlintEnabled;
  256. const iconClasses = ['text-info'];
  257. if (isActive) {
  258. iconClasses.push('ti-check');
  259. }
  260. const iconClassName = iconClasses.join(' ');
  261. return (
  262. <DropdownItem toggle={false} onClick={this.switchTextlintEnabledHandler}>
  263. <div className="d-flex justify-content-between">
  264. <span className="icon-container"></span>
  265. <span className="menuitem-label">Textlint</span>
  266. <span className="icon-container"><i className={iconClassName}></i></span>
  267. </div>
  268. </DropdownItem>
  269. );
  270. }
  271. renderIndentSizeSelector() {
  272. const { appContainer, editorContainer } = this.props;
  273. const menuItems = this.typicalIndentSizes.map((indent) => {
  274. return <button key={indent} className="dropdown-item" type="button" onClick={() => this.onChangeIndentSize(indent)}>{indent}</button>;
  275. });
  276. return (
  277. <div className="input-group flex-nowrap">
  278. <div className="input-group-prepend">
  279. <span className="input-group-text" id="igt-indent">Indent</span>
  280. </div>
  281. <div className="input-group-append dropup">
  282. <button
  283. type="button"
  284. className="btn btn-outline-secondary dropdown-toggle"
  285. data-toggle="dropdown"
  286. aria-haspopup="true"
  287. aria-expanded="false"
  288. aria-describedby="igt-indent"
  289. disabled={appContainer.config.isIndentSizeForced}
  290. >
  291. {editorContainer.state.indentSize}
  292. </button>
  293. <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
  294. {menuItems}
  295. </div>
  296. </div>
  297. </div>
  298. );
  299. }
  300. render() {
  301. return (
  302. <div className="d-flex flex-row">
  303. <span>{this.renderThemeSelector()}</span>
  304. <span className="d-none d-sm-block ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
  305. <span className="ml-2 ml-sm-4">{this.renderIndentSizeSelector()}</span>
  306. <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
  307. </div>
  308. );
  309. }
  310. }
  311. /**
  312. * Wrapper component for using unstated
  313. */
  314. const OptionsSelectorWrapper = withUnstatedContainers(OptionsSelector, [AppContainer, EditorContainer]);
  315. OptionsSelector.propTypes = {
  316. t: PropTypes.func.isRequired, // i18next
  317. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  318. editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
  319. };
  320. export default withTranslation()(OptionsSelectorWrapper);