import React, { useCallback, memo } from 'react'; import { commands } from 'codemirror'; import * as loadCssSync from 'load-css-file'; import PropTypes from 'prop-types'; import { Button } from 'reactstrap'; import * as loadScript from 'simple-load-script'; import { throttle, debounce } from 'throttle-debounce'; import urljoin from 'url-join'; import InterceptorManager from '~/services/interceptor-manager'; import { useHandsontableModal, useDrawioModal, useTemplateModal, useLinkEditModal, } from '~/stores/modal'; import loggerFactory from '~/utils/logger'; import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror'; import AbstractEditor from './AbstractEditor'; import CommentMentionHelper from './CommentMentionHelper'; import EditorIcon from './EditorIcon'; import EmojiPicker from './EmojiPicker'; import EmojiPickerHelper from './EmojiPickerHelper'; import GridEditModal from './GridEditModal'; // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248 // import geu from './GridEditorUtil'; import mdu from './MarkdownDrawioUtil'; import markdownLinkUtil from './MarkdownLinkUtil'; import markdownListUtil from './MarkdownListUtil'; import MarkdownTableInterceptor from './MarkdownTableInterceptor'; import mtu from './MarkdownTableUtil'; import pasteHelper from './PasteHelper'; import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor'; import SimpleCheatsheet from './SimpleCheatsheet'; import styles from './CodeMirrorEditor.module.scss'; require('codemirror/addon/hint/show-hint.css'); // Import from CodeMirrorEditor.module.scss not working require('codemirror/addon/display/placeholder'); require('codemirror/addon/edit/matchbrackets'); require('codemirror/addon/edit/matchtags'); require('codemirror/addon/edit/closetag'); require('codemirror/addon/edit/continuelist'); require('codemirror/addon/hint/show-hint'); require('codemirror/addon/search/searchcursor'); require('codemirror/addon/search/match-highlighter'); require('codemirror/addon/selection/active-line'); require('codemirror/addon/scroll/annotatescrollbar'); require('codemirror/addon/scroll/scrollpastend'); require('codemirror/addon/fold/foldcode'); require('codemirror/addon/fold/foldgutter'); require('codemirror/addon/fold/markdown-fold'); require('codemirror/addon/fold/brace-fold'); require('codemirror/addon/display/placeholder'); require('~/client/util/codemirror/autorefresh.ext'); require('~/client/util/codemirror/drawio-fold.ext'); require('~/client/util/codemirror/gfm-growi.mode'); // import modes to highlight require('codemirror/mode/clike/clike'); require('codemirror/mode/css/css'); require('codemirror/mode/django/django'); require('codemirror/mode/erlang/erlang'); require('codemirror/mode/gfm/gfm'); require('codemirror/mode/go/go'); require('codemirror/mode/javascript/javascript'); require('codemirror/mode/jsx/jsx'); require('codemirror/mode/mathematica/mathematica'); require('codemirror/mode/nginx/nginx'); require('codemirror/mode/perl/perl'); require('codemirror/mode/php/php'); require('codemirror/mode/python/python'); require('codemirror/mode/r/r'); require('codemirror/mode/ruby/ruby'); require('codemirror/mode/rust/rust'); require('codemirror/mode/sass/sass'); require('codemirror/mode/shell/shell'); require('codemirror/mode/sql/sql'); require('codemirror/mode/stex/stex'); require('codemirror/mode/stylus/stylus'); require('codemirror/mode/swift/swift'); require('codemirror/mode/toml/toml'); require('codemirror/mode/vb/vb'); require('codemirror/mode/vue/vue'); require('codemirror/mode/xml/xml'); require('codemirror/mode/yaml/yaml'); const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated'; const MARKDOWN_LINK_ACTIVATED_CLASS = 'markdown-link-activated'; class CodeMirrorEditor extends AbstractEditor { constructor(props) { super(props); this.logger = loggerFactory('growi:PageEditor:CodeMirrorEditor'); this.state = { isGfmMode: this.props.isGfmMode, isLoadingKeymap: false, isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value?.length === 0, isCheatsheetModalShown: false, additionalClassSet: new Set(), isEmojiPickerShown: false, emojiSearchText: '', startPosWithEmojiPickerModeTurnedOn: null, isEmojiPickerMode: false, isTemplateModalOpened: false, }; this.cm = React.createRef(); this.gridEditModal = React.createRef(); this.linkEditModal = React.createRef(); this.drawioModal = React.createRef(); this.init(); this.getCodeMirror = this.getCodeMirror.bind(this); this.getBol = this.getBol.bind(this); this.getEol = this.getEol.bind(this); this.loadTheme = this.loadTheme.bind(this); this.loadKeymapMode = this.loadKeymapMode.bind(this); this.setKeymapMode = this.setKeymapMode.bind(this); this.handleEnterKey = this.handleEnterKey.bind(this); this.handleCtrlEnterKey = this.handleCtrlEnterKey.bind(this); this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this); this.scrollCursorIntoViewHandlerThrottled = throttle(500, this.scrollCursorIntoViewHandler); this.pasteHandler = this.pasteHandler.bind(this); this.cursorHandler = this.cursorHandler.bind(this); this.cursorHandlerDebounced = debounce(200, throttle(500, this.cursorHandler)); this.changeHandler = this.changeHandler.bind(this); this.turnOnEmojiPickerMode = this.turnOnEmojiPickerMode.bind(this); this.turnOffEmojiPickerMode = this.turnOffEmojiPickerMode.bind(this); this.windowClickHandler = this.windowClickHandler.bind(this); this.keyDownHandler = this.keyDownHandler.bind(this); this.keyDownHandlerForEmojiPicker = this.keyDownHandlerForEmojiPicker.bind(this); this.keyDownHandlerForEmojiPickerThrottled = throttle(400, this.keyDownHandlerForEmojiPicker); this.showEmojiPicker = this.showEmojiPicker.bind(this); this.keyPressHandlerForEmojiPicker = this.keyPressHandlerForEmojiPicker.bind(this); this.keyPressHandlerForEmojiPickerThrottled = debounce(50, throttle(200, this.keyPressHandlerForEmojiPicker)); this.keyPressHandler = this.keyPressHandler.bind(this); this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this); this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this); this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this); this.makeHeaderHandler = this.makeHeaderHandler.bind(this); // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248 // this.showGridEditorHandler = this.showGridEditorHandler.bind(this); this.foldDrawioSection = this.foldDrawioSection.bind(this); this.clickDrawioIconHandler = this.clickDrawioIconHandler.bind(this); this.clickTableIconHandler = this.clickTableIconHandler.bind(this); this.showTemplateModal = this.showTemplateModal.bind(this); this.showLinkEditModal = this.showLinkEditModal.bind(this); } init() { this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0'; this.cmNoCdnScriptRoot = '/static/js/cdn'; this.cmNoCdnStyleRoot = '/static/styles/cdn'; this.interceptorManager = new InterceptorManager(); this.interceptorManager.addInterceptors([ new PreventMarkdownListInterceptor(), new MarkdownTableInterceptor(), ]); this.loadedThemeSet = new Set(['eclipse', 'elegant']); // themes imported in _vendor.scss this.loadedKeymapSet = new Set(); } componentDidMount() { // ensure to be able to resolve 'this' to use 'codemirror.commands.save' this.getCodeMirror().codeMirrorEditor = this; // mark clean this.getCodeMirror().getDoc().markClean(); // fold drawio section this.foldDrawioSection(); // initialize commentMentionHelper if comment editor is opened if (this.props.isComment) { this.commentMentionHelper = new CommentMentionHelper(this.getCodeMirror()); } this.emojiPickerHelper = new EmojiPickerHelper(this.getCodeMirror()); // HACKME: Find a better way to handle onClick for Editor document.addEventListener('click', this.windowClickHandler); } componentWillUnmount() { // HACKME: Find a better way to handle onClick for Editor document.removeEventListener('click', this.windowClickHandler); } componentWillReceiveProps(nextProps) { this.initializeEditorSettings(nextProps.editorSettings); // fold drawio section this.foldDrawioSection(); } initializeEditorSettings(editorSettings) { if (editorSettings == null) { return; } // load theme const theme = editorSettings.theme; if (theme != null) { this.loadTheme(theme); } // set keymap const keymapMode = editorSettings.keymapMode; if (keymapMode != null) { this.setKeymapMode(keymapMode); } } getCodeMirror() { return this.cm.current?.editor; } /** * @inheritDoc */ forceToFocus() { // use setInterval with reluctance -- 2018.01.11 Yuki Takei const intervalId = setInterval(() => { const editor = this.getCodeMirror(); editor.focus(); if (editor.hasFocus()) { clearInterval(intervalId); // refresh editor.refresh(); } }, 100); } /** * @inheritDoc */ setValue(newValue) { this.getCodeMirror().getDoc().setValue(newValue); // mark clean this.getCodeMirror().getDoc().markClean(); } /** * @inheritDoc */ setGfmMode(bool) { // update state this.setState({ isGfmMode: bool, }); this.updateCheatsheetStates(bool, null); // update CodeMirror option const mode = bool ? 'gfm' : undefined; this.getCodeMirror().setOption('mode', mode); } /** * @inheritDoc */ setCaretLine(line) { if (Number.isNaN(line)) { return; } const editor = this.getCodeMirror(); const linePosition = Math.max(0, line - 1); editor.setCursor({ line: linePosition }); // leave 'ch' field as null/undefined to indicate the end of line setTimeout(() => { this.setScrollTopByLine(linePosition); }, 100); } /** * @inheritDoc */ setScrollTopByLine(line) { if (Number.isNaN(line)) { return; } const editor = this.getCodeMirror(); // get top position of the line const top = editor.charCoords({ line: line - 1, ch: 0 }, 'local').top; editor.scrollTo(null, top); } /** * @inheritDoc */ getStrFromBol() { const editor = this.getCodeMirror(); const curPos = editor.getCursor(); return editor.getDoc().getRange(this.getBol(), curPos); } /** * @inheritDoc */ getStrToEol() { const editor = this.getCodeMirror(); const curPos = editor.getCursor(); return editor.getDoc().getRange(curPos, this.getEol()); } /** * @inheritDoc */ getStrFromBolToSelectedUpperPos() { const editor = this.getCodeMirror(); const pos = this.selectUpperPos(editor.getCursor('from'), editor.getCursor('to')); return editor.getDoc().getRange(this.getBol(), pos); } /** * @inheritDoc */ replaceBolToCurrentPos(text) { const editor = this.getCodeMirror(); const pos = this.selectLowerPos(editor.getCursor('from'), editor.getCursor('to')); editor.getDoc().replaceRange(text, this.getBol(), pos); } /** * @inheritDoc */ replaceLine(text) { const editor = this.getCodeMirror(); editor.getDoc().replaceRange(text, this.getBol(), this.getEol()); } /** * @inheritDoc */ insertText(text) { const editor = this.getCodeMirror(); editor.getDoc().replaceSelection(text); } /** * return the postion of the BOL(beginning of line) */ getBol() { const editor = this.getCodeMirror(); const curPos = editor.getCursor(); return { line: curPos.line, ch: 0 }; } /** * return the postion of the EOL(end of line) */ getEol() { const editor = this.getCodeMirror(); const curPos = editor.getCursor(); const lineLength = editor.getDoc().getLine(curPos.line).length; return { line: curPos.line, ch: lineLength }; } /** * select the upper position of pos1 and pos2 * @param {{line: number, ch: number}} pos1 * @param {{line: number, ch: number}} pos2 */ selectUpperPos(pos1, pos2) { // if both is in same line if (pos1.line === pos2.line) { return (pos1.ch < pos2.ch) ? pos1 : pos2; } return (pos1.line < pos2.line) ? pos1 : pos2; } /** * select the lower position of pos1 and pos2 * @param {{line: number, ch: number}} pos1 * @param {{line: number, ch: number}} pos2 */ selectLowerPos(pos1, pos2) { // if both is in same line if (pos1.line === pos2.line) { return (pos1.ch < pos2.ch) ? pos2 : pos1; } return (pos1.line < pos2.line) ? pos2 : pos1; } loadCss(source) { return new Promise((resolve) => { loadCssSync(source); resolve(); }); } /** * load Theme * @see https://codemirror.net/doc/manual.html#config * * @param {string} theme */ loadTheme(theme) { if (!this.loadedThemeSet.has(theme)) { const url = this.props.noCdn ? urljoin(this.cmNoCdnStyleRoot, `codemirror-theme-${theme}.css`) : urljoin(this.cmCdnRoot, `theme/${theme}.min.css`); this.loadCss(url); // update Set this.loadedThemeSet.add(theme); } } /** * load assets for Key Maps * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime' */ loadKeymapMode(keymapMode) { const loadCss = this.loadCss; const scriptList = []; const cssList = []; // add dependencies if (this.loadedKeymapSet.size === 0) { const dialogScriptUrl = this.props.noCdn ? urljoin(this.cmNoCdnScriptRoot, 'codemirror-dialog.js') : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js'); const dialogStyleUrl = this.props.noCdn ? urljoin(this.cmNoCdnStyleRoot, 'codemirror-dialog.css') : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css'); scriptList.push(loadScript(dialogScriptUrl)); cssList.push(loadCss(dialogStyleUrl)); } // load keymap if (!this.loadedKeymapSet.has(keymapMode)) { const keymapScriptUrl = this.props.noCdn ? urljoin(this.cmNoCdnScriptRoot, `codemirror-keymap-${keymapMode}.js`) : urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`); scriptList.push(loadScript(keymapScriptUrl)); // update Set this.loadedKeymapSet.add(keymapMode); } // set loading state this.setState({ isLoadingKeymap: true }); return Promise.all(scriptList.concat(cssList)) .then(() => { this.setState({ isLoadingKeymap: false }); }); } /** * set Key Maps * @see https://codemirror.net/doc/manual.html#keymaps * * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime' */ setKeymapMode(keymapMode) { if (!keymapMode.match(/^(vim|emacs|sublime)$/)) { // reset this.getCodeMirror().setOption('keyMap', 'default'); return; } this.loadKeymapMode(keymapMode) .then(() => { let errorCount = 0; const timer = setInterval(() => { if (errorCount > 10) { // cancel over 3000ms this.logger.error(`Timeout to load keyMap '${keymapMode}'`); clearInterval(timer); } try { this.getCodeMirror().setOption('keyMap', keymapMode); clearInterval(timer); } catch (e) { this.logger.info(`keyMap '${keymapMode}' has not been initialized. retry..`); // continue if error occured errorCount++; } }, 300); }); } /** * handle ENTER key */ handleEnterKey() { if (!this.state.isGfmMode) { commands.newlineAndIndent(this.getCodeMirror()); return; } const context = { handlers: [], // list of handlers which process enter key editor: this, autoFormatMarkdownTable: this.props.editorSettings.autoFormatMarkdownTable, }; const interceptorManager = this.interceptorManager; interceptorManager.process('preHandleEnter', context) .then(() => { if (context.handlers.length === 0) { markdownListUtil.newlineAndIndentContinueMarkdownList(this); } }); } /** * handle Ctrl+ENTER key */ handleCtrlEnterKey() { if (this.props.onCtrlEnter != null) { this.props.onCtrlEnter(); } } scrollCursorIntoViewHandler(editor, event) { if (this.props.onScrollCursorIntoView != null) { const line = editor.getCursor().line; this.props.onScrollCursorIntoView(line); } } cursorHandler(editor, event) { const { additionalClassSet } = this.state; const hasCustomClass = additionalClassSet.has(MARKDOWN_TABLE_ACTIVATED_CLASS); const hasLinkClass = additionalClassSet.has(MARKDOWN_LINK_ACTIVATED_CLASS); const isInTable = mtu.isInTable(editor); const isInLink = markdownLinkUtil.isInLink(editor); if (!hasCustomClass && isInTable) { additionalClassSet.add(MARKDOWN_TABLE_ACTIVATED_CLASS); this.setState({ additionalClassSet }); } if (hasCustomClass && !isInTable) { additionalClassSet.delete(MARKDOWN_TABLE_ACTIVATED_CLASS); this.setState({ additionalClassSet }); } if (!hasLinkClass && isInLink) { additionalClassSet.add(MARKDOWN_LINK_ACTIVATED_CLASS); this.setState({ additionalClassSet }); } if (hasLinkClass && !isInLink) { additionalClassSet.delete(MARKDOWN_LINK_ACTIVATED_CLASS); this.setState({ additionalClassSet }); } } changeHandler(editor, data, value) { if (this.props.onChange != null) { const isClean = data.origin == null || editor.isClean() || value === this.props.value; this.props.onChange(value, isClean); } this.updateCheatsheetStates(null, value); // Show username hint on comment editor if (this.props.isComment) { this.commentMentionHelper.showUsernameHint(); } } turnOnEmojiPickerMode(pos) { this.setState({ isEmojiPickerMode: true, startPosWithEmojiPickerModeTurnedOn: pos, }); } turnOffEmojiPickerMode() { this.setState({ isEmojiPickerMode: false, }); } showEmojiPicker(initialSearchingText) { // show emoji picker with a stored word this.setState({ isEmojiPickerShown: true, emojiSearchText: initialSearchingText ?? '', }); const resetStartPos = initialSearchingText == null; if (resetStartPos) { this.setState({ startPosWithEmojiPickerModeTurnedOn: null }); } this.turnOffEmojiPickerMode(); } keyPressHandlerForEmojiPicker(editor, event) { const char = event.key; const isEmojiPickerMode = this.state.isEmojiPickerMode; // evaluate whether emoji picker mode to be turned on if (!isEmojiPickerMode) { const startPos = this.emojiPickerHelper.shouldModeTurnOn(char); if (startPos == null) { return; } this.turnOnEmojiPickerMode(startPos); return; } // evaluate whether EmojiPicker to be opened const startPos = this.state.startPosWithEmojiPickerModeTurnedOn; if (this.emojiPickerHelper.shouldOpen(startPos)) { const initialSearchingText = this.emojiPickerHelper.getInitialSearchingText(startPos); this.showEmojiPicker(initialSearchingText); return; } this.turnOffEmojiPickerMode(); } keyPressHandler(editor, event) { this.keyPressHandlerForEmojiPickerThrottled(editor, event); } keyDownHandlerForEmojiPicker(editor, event) { const key = event.key; if (!this.state.isEmojiPickerMode) { return; } if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'BackSpace'].includes(key)) { this.turnOffEmojiPickerMode(); } } keyDownHandler(editor, event) { this.keyDownHandlerForEmojiPickerThrottled(editor, event); } windowClickHandler() { this.turnOffEmojiPickerMode(); } /** * CodeMirror paste event handler * see: https://codemirror.net/doc/manual.html#events * @param {any} editor An editor instance of CodeMirror * @param {any} event */ pasteHandler(editor, event) { const types = event.clipboardData.types; // files if (types.includes('Files')) { event.preventDefault(); this.dispatchPasteFiles(event); } // text else if (types.includes('text/plain')) { pasteHelper.pasteText(this, event); } } /** * update states which related to cheatsheet * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set) * @param {string} valueTmp (get value from codemirror if null is set) */ updateCheatsheetStates(isGfmModeTmp, valueTmp) { const isGfmMode = isGfmModeTmp || this.state.isGfmMode; const value = valueTmp || this.getCodeMirror().getDoc().getValue(); // update isSimpleCheatsheetShown const isSimpleCheatsheetShown = isGfmMode && value.length === 0; this.setState({ isSimpleCheatsheetShown }); } markdownHelpButtonClickedHandler() { if (this.props.onMarkdownHelpButtonClicked != null) { this.props.onMarkdownHelpButtonClicked(); } } renderLoadingKeymapOverlay() { // centering const style = { top: 0, right: 0, bottom: 0, left: 0, }; return this.state.isLoadingKeymap ? (
Loading Keymap ...
) : ''; } renderCheatsheetModalButton() { return ( ); } renderCheatsheetOverlay() { const cheatsheetModalButton = this.renderCheatsheetModalButton(); return (
{ this.state.isSimpleCheatsheetShown ? (
{cheatsheetModalButton}
) : (
{cheatsheetModalButton}
) }
); } renderEmojiPicker() { const { emojiSearchText } = this.state; return this.state.isEmojiPickerShown ? (
this.setState({ isEmojiPickerShown: false })} onSelected={emoji => this.emojiPickerHelper.addEmoji(emoji, this.state.startPosWithEmojiPickerModeTurnedOn)} emojiSearchText={emojiSearchText} emojiPickerHelper={this.emojiPickerHelper} isOpen={this.state.isEmojiPickerShown} />
) : ''; } /** * return a function to replace a selected range with prefix + selection + suffix * * The cursor after replacing is inserted between the selection and the suffix. */ createReplaceSelectionHandler(prefix, suffix) { return () => { const cm = this.getCodeMirror(); const selection = cm.getDoc().getSelection(); const curStartPos = cm.getCursor('from'); const curEndPos = cm.getCursor('to'); const curPosAfterReplacing = {}; curPosAfterReplacing.line = curEndPos.line; if (curStartPos.line === curEndPos.line) { curPosAfterReplacing.ch = curEndPos.ch + prefix.length; } else { curPosAfterReplacing.ch = curEndPos.ch; } cm.getDoc().replaceSelection(prefix + selection + suffix); cm.setCursor(curPosAfterReplacing); cm.focus(); }; } /** * return a function to add prefix to selected each lines * * The cursor after editing is inserted between the end of the selection. */ createAddPrefixToEachLinesHandler(prefix) { return () => { const cm = this.getCodeMirror(); const startLineNum = cm.getCursor('from').line; const endLineNum = cm.getCursor('to').line; const lines = []; for (let i = startLineNum; i <= endLineNum; i++) { lines.push(prefix + cm.getDoc().getLine(i)); } const replacement = `${lines.join('\n')}\n`; cm.getDoc().replaceRange(replacement, { line: startLineNum, ch: 0 }, { line: endLineNum + 1, ch: 0 }); cm.setCursor(endLineNum, cm.getDoc().getLine(endLineNum).length); cm.focus(); }; } /** * make a selected line a header * * The cursor after editing is inserted between the end of the line. */ makeHeaderHandler() { const cm = this.getCodeMirror(); const lineNum = cm.getCursor('from').line; const line = cm.getDoc().getLine(lineNum); let prefix = '#'; if (!line.startsWith('#')) { prefix += ' '; } cm.getDoc().replaceRange(prefix, { line: lineNum, ch: 0 }, { line: lineNum, ch: 0 }); cm.focus(); } // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248 // showGridEditorHandler() { // this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror())); // } showTemplateModal() { const onSubmit = templateText => this.insertText(templateText); this.props.onClickTemplateBtn({ onSubmit }); } showLinkEditModal() { const onSubmit = (linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText); }; const defaultMarkdownLink = markdownLinkUtil.getMarkdownLink(this.getCodeMirror()); this.props.onClickLinkEditBtn(defaultMarkdownLink, onSubmit); } // fold draw.io section (``` drawio ~ ```) foldDrawioSection() { const editor = this.getCodeMirror(); const lineNumbers = mdu.findAllDrawioSection(editor); lineNumbers.forEach((lineNumber) => { editor.foldCode({ line: lineNumber, ch: 0 }, { scanUp: false }, 'fold'); }); } clickDrawioIconHandler() { const drawioMxFile = mdu.getMarkdownDrawioMxfile(this.getCodeMirror()); this.props.onClickDrawioBtn( drawioMxFile, // onSave (drawioMxFile) => { mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioMxFile); // Fold the section after the drawio section (```drawio) has been updated. this.foldDrawioSection(); }, ); } clickTableIconHandler() { const markdownTable = mtu.getMarkdownTable(this.getCodeMirror()); this.props.onClickTableBtn( markdownTable, this.getCodeMirror(), this.props.editorSettings.autoFormatMarkdownTable, ); } getNavbarItems() { return [ , , , , , , , , , , , , // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248 // , , , , , ]; } render() { const additionalClasses = Array.from(this.state.additionalClassSet).join(' '); const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..'; const gutters = []; if (this.props.lineNumbers != null) { gutters.push('CodeMirror-linenumbers', 'CodeMirror-foldgutter'); } return (
{ cm.foldCode(cm.getCursor()) }, }, }} onCursor={this.cursorHandlerDebounced} onScroll={(editor, data) => { if (this.props.onScroll != null) { // add line data const line = editor.lineAtHeight(data.top, 'local'); data.line = line; this.props.onScroll(data); } }} onChange={this.changeHandler} onDragEnter={(editor, event) => { if (this.props.onDragEnter != null) { this.props.onDragEnter(event); } }} onKeyPress={this.keyPressHandler} onKeyDown={this.keyDownHandler} onPasteFiles={this.pasteHandler} onScrollCursorIntoView={this.scrollCursorIntoViewHandlerThrottled} /> { this.renderLoadingKeymapOverlay() } { this.renderCheatsheetOverlay() } { this.renderEmojiPicker() } {/* // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248 { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }} /> */}
); } } CodeMirrorEditor.propTypes = Object.assign({ lineNumbers: PropTypes.bool, editorSettings: PropTypes.object.isRequired, onMarkdownHelpButtonClicked: PropTypes.func, onAddAttachmentButtonClicked: PropTypes.func, }, AbstractEditor.propTypes); CodeMirrorEditor.defaultProps = { lineNumbers: true, }; const CodeMirrorEditorMemoized = memo(CodeMirrorEditor); const CodeMirrorEditorFc = React.forwardRef((props, ref) => { const { open: openDrawioModal } = useDrawioModal(); const { open: openHandsontableModal } = useHandsontableModal(); const { open: openTemplateModal } = useTemplateModal(); const { open: openLinkEditModal } = useLinkEditModal(); const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => { openDrawioModal(drawioMxFile, onSave); }, [openDrawioModal]); const openTableModalHandler = useCallback((markdownTable, editor, autoFormatMarkdownTable) => { openHandsontableModal(markdownTable, editor, autoFormatMarkdownTable); }, [openHandsontableModal]); const openTemplateModalHandler = useCallback((onSubmit) => { openTemplateModal(onSubmit); }, [openTemplateModal]); const openLinkEditModalHandler = useCallback((defaultMarkdownLink, onSubmit) => { openLinkEditModal(defaultMarkdownLink, onSubmit); }, [openLinkEditModal]); return ( ); }); CodeMirrorEditorFc.displayName = 'CodeMirrorEditorFc'; export default memo(CodeMirrorEditorFc);