From 12217ef70569ddc6b362f196e6e4254ee8a23ca7 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 23 Jun 2023 11:52:58 +0200 Subject: [PATCH 01/30] feat: add rich text editor/parser components "Moved" from d2-ui-rich-text. --- package.json | 2 +- src/components/RichText/editor/Editor.js | 29 +++ .../RichText/editor/__tests__/Editor.spec.js | 37 +++ .../editor/__tests__/convertCtrlKey.spec.js | 230 ++++++++++++++++++ .../RichText/editor/convertCtrlKey.js | 103 ++++++++ src/components/RichText/index.js | 5 + src/components/RichText/parser/MdParser.js | 119 +++++++++ src/components/RichText/parser/Parser.js | 38 +++ .../parser/__tests__/MdParser.spec.js | 134 ++++++++++ .../RichText/parser/__tests__/Parser.spec.js | 44 ++++ src/index.js | 2 + yarn.lock | 43 ++-- 12 files changed, 764 insertions(+), 22 deletions(-) create mode 100644 src/components/RichText/editor/Editor.js create mode 100644 src/components/RichText/editor/__tests__/Editor.spec.js create mode 100644 src/components/RichText/editor/__tests__/convertCtrlKey.spec.js create mode 100644 src/components/RichText/editor/convertCtrlKey.js create mode 100644 src/components/RichText/index.js create mode 100644 src/components/RichText/parser/MdParser.js create mode 100644 src/components/RichText/parser/Parser.js create mode 100644 src/components/RichText/parser/__tests__/MdParser.spec.js create mode 100644 src/components/RichText/parser/__tests__/Parser.spec.js diff --git a/package.json b/package.json index 1062504ed..7bf1e261a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "styled-jsx": "^4.0.1" }, "dependencies": { - "@dhis2/d2-ui-rich-text": "^7.4.1", "@dhis2/multi-calendar-dates": "1.0.0", "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", @@ -71,6 +70,7 @@ "d3-color": "^1.2.3", "highcharts": "^10.3.3", "lodash": "^4.17.21", + "markdown-it": "^13.0.1", "mathjs": "^9.4.2", "react-beautiful-dnd": "^10.1.1", "resize-observer-polyfill": "^1.5.1" diff --git a/src/components/RichText/editor/Editor.js b/src/components/RichText/editor/Editor.js new file mode 100644 index 000000000..7acc51b51 --- /dev/null +++ b/src/components/RichText/editor/Editor.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import convertCtrlKey from './convertCtrlKey.js' + +class Editor extends Component { + onKeyDown = (event) => { + convertCtrlKey(event, this.props.onEdit) + } + + render() { + const { children } = this.props + + return
{children}
+ } +} + +Editor.defaultProps = { + onEdit: null, +} + +Editor.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + onEdit: PropTypes.func, +} + +export default Editor diff --git a/src/components/RichText/editor/__tests__/Editor.spec.js b/src/components/RichText/editor/__tests__/Editor.spec.js new file mode 100644 index 000000000..ffb48ed46 --- /dev/null +++ b/src/components/RichText/editor/__tests__/Editor.spec.js @@ -0,0 +1,37 @@ +import { shallow } from 'enzyme' +import React from 'react' +import convertCtrlKey from '../convertCtrlKey.js' +import Editor from '../Editor.js' + +jest.mock('../convertCtrlKey') + +describe('RichText: Editor component', () => { + let richTextEditor + const componentProps = { + onEdit: jest.fn(), + } + + beforeEach(() => { + convertCtrlKey.mockClear() + }) + + const renderComponent = (props) => { + return shallow( + + + + ) + } + + it('renders a result', () => { + richTextEditor = renderComponent(componentProps) + + expect(richTextEditor).toHaveLength(1) + }) + + it('calls convertCtrlKey on keydown', () => { + richTextEditor = renderComponent(componentProps) + richTextEditor.simulate('keyDown') + expect(convertCtrlKey).toHaveBeenCalled() + }) +}) diff --git a/src/components/RichText/editor/__tests__/convertCtrlKey.spec.js b/src/components/RichText/editor/__tests__/convertCtrlKey.spec.js new file mode 100644 index 000000000..019e0df20 --- /dev/null +++ b/src/components/RichText/editor/__tests__/convertCtrlKey.spec.js @@ -0,0 +1,230 @@ +import convertCtrlKey from '../convertCtrlKey.js' + +describe('convertCtrlKey', () => { + it('does not trigger callback if no ctrl key', () => { + const cb = jest.fn() + const e = { key: 'j', preventDefault: () => {} } + + convertCtrlKey(e, cb) + + expect(cb).not.toHaveBeenCalled() + }) + + describe('when ctrl key + "b" pressed', () => { + it('triggers callback with open/close markers and caret pos in between', () => { + const cb = jest.fn() + const e = { + key: 'b', + ctrlKey: true, + target: { + selectionStart: 0, + selectionEnd: 0, + value: 'rainbow dash', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('** rainbow dash', 1) + }) + + it('triggers callback with open/close markers and caret pos in between (end of text)', () => { + const cb = jest.fn() + const e = { + key: 'b', + ctrlKey: true, + target: { + selectionStart: 22, + selectionEnd: 22, + value: 'rainbow dash is purple', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('rainbow dash is purple **', 24) + }) + + it('triggers callback with open/close markers mid-text with surrounding spaces (1)', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 4, // caret located just before "quick" + selectionEnd: 4, + value: 'the quick brown fox', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('the ** quick brown fox', 5) + }) + + it('triggers callback with open/close markers mid-text with surrounding spaces (2)', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 3, // caret located just after "the" + selectionEnd: 3, + value: 'the quick brown fox', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('the ** quick brown fox', 5) + }) + + it('triggers callback with correct double markers and padding', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 9, // between the underscores + selectionEnd: 9, + value: 'rainbow __', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('rainbow _**_', 10) + }) + + describe('selected text', () => { + it('triggers callback with open/close markers around text and caret pos after closing marker', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 5, // "ow da" is selected + selectionEnd: 10, + value: 'rainbow dash is purple', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith( + 'rainb *ow da* sh is purple', + 13 + ) + }) + + it('triggers callback with open/close markers around text when starting at beginning of line', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 0, // "rainbow" is selected + selectionEnd: 7, + value: 'rainbow dash is purple', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('*rainbow* dash is purple', 9) + }) + + it('triggers callback with open/close markers around text when ending at end of line', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 16, // "purple" is selected + selectionEnd: 22, + value: 'rainbow dash is purple', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('rainbow dash is *purple*', 24) + }) + + it('triggers callback with open/close markers around word', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 8, // "dash" is selected + selectionEnd: 12, + value: 'rainbow dash is purple', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('rainbow *dash* is purple', 14) + }) + + it('triggers callback with leading/trailing spaces trimmed from selection', () => { + const cb = jest.fn() + const e = { + key: 'b', + metaKey: true, + target: { + selectionStart: 8, // " dash " is selected (note leading and trailing space) + selectionEnd: 13, + value: 'rainbow dash is purple', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('rainbow *dash* is purple', 14) + }) + }) + }) + + describe('when ctrl key + "i" pressed', () => { + it('triggers callback with open/close italics markers and caret pos in between', () => { + const cb = jest.fn() + const e = { + key: 'i', + ctrlKey: true, + target: { + selectionStart: 0, + selectionEnd: 0, + value: '', + }, + preventDefault: () => {}, + } + + convertCtrlKey(e, cb) + + expect(cb).toHaveBeenCalled() + expect(cb).toHaveBeenCalledWith('__', 1) + }) + }) +}) diff --git a/src/components/RichText/editor/convertCtrlKey.js b/src/components/RichText/editor/convertCtrlKey.js new file mode 100644 index 000000000..e962c8fc2 --- /dev/null +++ b/src/components/RichText/editor/convertCtrlKey.js @@ -0,0 +1,103 @@ +const state = { + boldMode: false, + italicMode: false, + element: null, +} + +const markerMap = { + italic: '_', + bold: '*', +} + +const trim = (str) => { + const leftSpaces = /^\s+/ + const rightSpaces = /\s+$/ + + return str.replace(leftSpaces, '').replace(rightSpaces, '') +} + +const toggleMode = (mode) => { + const prop = `${mode}Mode` + + state[prop] = !state[prop] +} + +const insertMarkers = (mode, cb) => { + const { selectionStart: start, selectionEnd: end, value } = state.element + const marker = markerMap[mode] || null + if (!marker || !cb || start < 0) { + return + } + + toggleMode(mode) + + let newValue + let caretPos = end + 1 + + const padMarkers = (text) => { + // is caret between two markers (i.e., "**" or "__")? Then do not add padding + if (start === end && value.length && start > 0) { + if ( + (value[start - 1] === markerMap.bold && + value[start] === markerMap.bold) || + (value[start - 1] === markerMap.italic && + value[start] === markerMap.italic) + ) { + return text + } + } + + if (value.length && start > 0 && value[start - 1] !== ' ') { + text = ` ${text}` + ++caretPos + } + + if (value.length && end !== value.length && value[end] !== ' ') { + text = `${text} ` + } + + return text + } + + if (start === end) { + //no text + const valueArr = value.split('') + + valueArr.splice(start, 0, padMarkers(`${marker}${marker}`)) + newValue = valueArr.join('') + } else { + const text = value.slice(start, end) + const trimmedText = trim(text) + + // adjust caretPos based on trimmed text selection + caretPos = caretPos - (text.length - trimmedText.length) + 1 + + newValue = [ + value.slice(0, start), + padMarkers(`${marker}${trimmedText}${marker}`), + value.slice(end), + ].join('') + + toggleMode(mode) + } + + cb(newValue, caretPos) +} + +const convertCtrlKey = (event, cb) => { + const { key, ctrlKey, metaKey } = event + + const element = event.target + + state.element = element + + if (key === 'b' && (ctrlKey || metaKey)) { + event.preventDefault() + insertMarkers('bold', cb) + } else if (key === 'i' && (ctrlKey || metaKey)) { + event.preventDefault() + insertMarkers('italic', cb) + } +} + +export default convertCtrlKey diff --git a/src/components/RichText/index.js b/src/components/RichText/index.js new file mode 100644 index 000000000..f25c37d28 --- /dev/null +++ b/src/components/RichText/index.js @@ -0,0 +1,5 @@ +export { default as RichTextEditor } from './editor/Editor.js' +export { default as RichTextParser } from './parser/Parser.js' + +export { default as convertCtrlKey } from './editor/convertCtrlKey.js' +export { default as ClassMdParser } from './parser/MdParser.js' diff --git a/src/components/RichText/parser/MdParser.js b/src/components/RichText/parser/MdParser.js new file mode 100644 index 000000000..25995e67e --- /dev/null +++ b/src/components/RichText/parser/MdParser.js @@ -0,0 +1,119 @@ +import MarkdownIt from 'markdown-it' + +const emojiDb = { + ':-)': '\u{1F642}', + ':)': '\u{1F642}', + ':-(': '\u{1F641}', + ':(': '\u{1F641}', + ':+1': '\u{1F44D}', + ':-1': '\u{1F44E}', +} + +const codes = { + bold: { + name: 'bold', + char: '*', + domEl: 'strong', + encodedChar: 0x2a, + // see https://regex101.com/r/evswdV/8 for explanation of regexp + regexString: '\\B\\*((?!\\s)[^*]+(?:\\b|[^*\\s]))\\*\\B', + contentFn: (val) => val, + }, + italic: { + name: 'italic', + char: '_', + domEl: 'em', + encodedChar: 0x5f, + // see https://regex101.com/r/p6LpjK/6 for explanation of regexp + regexString: '\\b_((?!\\s)[^_]+(?:\\B|[^_\\s]))_\\b', + contentFn: (val) => val, + }, + emoji: { + name: 'emoji', + char: ':', + domEl: 'span', + encodedChar: 0x3a, + regexString: '^(:-\\)|:\\)|:\\(|:-\\(|:\\+1|:-1)', + contentFn: (val) => emojiDb[val], + }, +} + +let md +let linksInText + +const markerIsInLinkText = (pos) => + linksInText.some((link) => pos >= link.index && pos <= link.lastIndex) + +const parse = (code) => (state, silent) => { + if (silent) { + return false + } + + const start = state.pos + + // skip parsing emphasis if marker is within a link + if (markerIsInLinkText(start)) { + return false + } + + const marker = state.src.charCodeAt(start) + + // marker character: "_", "*", ":" + if (marker !== codes[code].encodedChar) { + return false + } + + const MARKER_REGEX = new RegExp(codes[code].regexString) + const token = state.src.slice(start) + + if (MARKER_REGEX.test(token)) { + const markerMatch = token.match(MARKER_REGEX) + + // skip parsing sections where the marker is not at the start of the token + if (markerMatch.index !== 0) { + return false + } + + const text = markerMatch[1] + + state.push(`${codes[code].domEl}_open`, codes[code].domEl, 1) + + const t = state.push('text', '', 0) + t.content = codes[code].contentFn(text) + + state.push(`${codes.bold.domEl}_close`, codes[code].domEl, -1) + state.pos += markerMatch[0].length + + return true + } + + return false +} + +class MdParser { + constructor() { + // disable all rules, enable autolink for URLs and email addresses + md = new MarkdownIt('zero', { linkify: true }) + + // *bold* -> bold + md.inline.ruler.push('strong', parse(codes.bold.name)) + + // _italic_ -> italic + md.inline.ruler.push('italic', parse(codes.italic.name)) + + // :-) :) :-( :( :+1 :-1 -> [unicode] + md.inline.ruler.push('emoji', parse(codes.emoji.name)) + + md.enable(['link', 'linkify', 'strong', 'italic', 'emoji']) + + return this + } + + render(text) { + linksInText = md.linkify.match(text) || [] + + return md.renderInline(text) + } +} + +export default MdParser diff --git a/src/components/RichText/parser/Parser.js b/src/components/RichText/parser/Parser.js new file mode 100644 index 000000000..a96062f73 --- /dev/null +++ b/src/components/RichText/parser/Parser.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import MdParserClass from './MdParser.js' + +class Parser extends Component { + constructor(props) { + super(props) + + this.MdParser = new MdParserClass() + } + + render() { + const { children, style } = this.props + + return children ? ( +

+ ) : null + } +} + +Parser.defaultProps = { + style: null, +} + +Parser.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + style: PropTypes.object, +} + +export default Parser diff --git a/src/components/RichText/parser/__tests__/MdParser.spec.js b/src/components/RichText/parser/__tests__/MdParser.spec.js new file mode 100644 index 000000000..dc7543e9a --- /dev/null +++ b/src/components/RichText/parser/__tests__/MdParser.spec.js @@ -0,0 +1,134 @@ +import MdParser from '../MdParser.js' + +const Parser = new MdParser() + +describe('MdParser class', () => { + it('converts text into HTML', () => { + const tests = [ + ['_italic_', 'italic'], + ['*bold*', 'bold'], + [ + '* not bold because there is a space *', + '* not bold because there is a space *', + ], + [ + '_ not italic because there is a space _', + '_ not italic because there is a space _', + ], + [':-)', '\u{1F642}'], + [':)', '\u{1F642}'], + [':-(', '\u{1F641}'], + [':(', '\u{1F641}'], + [':+1', '\u{1F44D}'], + [':-1', '\u{1F44E}'], + [ + 'mixed _italic_ *bold* and :+1', + 'mixed italic bold and \u{1F44D}', + ], + ['_italic with * inside_', 'italic with * inside'], + ['*bold with _ inside*', 'bold with _ inside'], + + // italic marker followed by : should work + ['_italic_:', 'italic:'], + [ + '_italic_: some text, *bold*: some other text', + 'italic: some text, bold: some other text', + ], + // bold marker followed by : should work + ['*bold*:', 'bold:'], + [ + '*bold*: some text, _italic_: some other text', + 'bold: some text, italic: some other text', + ], + + // italic marker inside an italic string not allowed + ['_italic with _ inside_', '_italic with _ inside_'], + // bold marker inside a bold string not allowed + ['*bold with * inside*', '*bold with * inside*'], + [ + '_multiple_ italic in the _same line_', + 'multiple italic in the same line', + ], + // nested italic/bold combinations not allowed + [ + '_italic with *bold* inside_', + 'italic with *bold* inside', + ], + [ + '*bold with _italic_ inside*', + 'bold with _italic_ inside', + ], + ['text with : and :)', 'text with : and \u{1F642}'], + [ + '(parenthesis and :))', + '(parenthesis and \u{1F642})', + ], + [ + ':((parenthesis:))', + '\u{1F641}(parenthesis\u{1F642})', + ], + [':+1+1', '\u{1F44D}+1'], + ['-1:-1', '-1\u{1F44E}'], + + // links + [ + 'example.com/path', + 'example.com/path', + ], + + // not recognized links with italic marker inside not converted + [ + 'example_with_underscore.com/path', + 'example_with_underscore.com/path', + ], + [ + 'example_with_underscore.com/path_with_underscore', + 'example_with_underscore.com/path_with_underscore', + ], + + // markers around non-recognized links + [ + 'link example_with_underscore.com/path should _not_ be converted', + 'link example_with_underscore.com/path should not be converted', + ], + [ + 'link example_with_underscore.com/path should *not* be converted', + 'link example_with_underscore.com/path should not be converted', + ], + + // italic marker inside links not converted + [ + 'example.com/path_with_underscore', + 'example.com/path_with_underscore', + ], + [ + '_italic_ and *bold* with a example.com/link_with_underscore', + 'italic and bold with a example.com/link_with_underscore', + ], + [ + 'example.com/path with *bold* after :)', + 'example.com/path with bold after \u{1F642}', + ], + [ + '_before_ example.com/path_with_underscore *after* :)', + 'before example.com/path_with_underscore after \u{1F642}', + ], + + // italic/bold markers right after non-word characters + [ + '_If % of ART retention rate after 12 months >90(%)_: Sustain the efforts.', + 'If % of ART retention rate after 12 months >90(%): Sustain the efforts.', + ], + [ + '*If % of ART retention rate after 12 months >90(%)*: Sustain the efforts.', + 'If % of ART retention rate after 12 months >90(%): Sustain the efforts.', + ], + ] + + tests.forEach((test) => { + const renderedText = Parser.render(test[0]) + + expect(renderedText).toEqual(test[1]) + }) + }) +}) diff --git a/src/components/RichText/parser/__tests__/Parser.spec.js b/src/components/RichText/parser/__tests__/Parser.spec.js new file mode 100644 index 000000000..433662cbe --- /dev/null +++ b/src/components/RichText/parser/__tests__/Parser.spec.js @@ -0,0 +1,44 @@ +import { shallow } from 'enzyme' +import React from 'react' +import Parser from '../Parser.js' +import '../MdParser.js' + +jest.mock('../MdParser', () => { + return jest.fn().mockImplementation(() => { + return { render: () => 'converted text' } + }) +}) + +describe('RichText: Parser component', () => { + let richTextParser + const defaultProps = { + style: { color: 'blue' }, + } + + const renderComponent = (props, text) => { + return shallow({text}) + } + + it('should have rendered a result', () => { + richTextParser = renderComponent({}, 'test') + + expect(richTextParser).toHaveLength(1) + }) + + it('should have rendered a result with the style prop', () => { + richTextParser = renderComponent(defaultProps, 'test prop') + + expect(richTextParser.props().style).toEqual(defaultProps.style) + }) + + it('should have rendered content', () => { + richTextParser = renderComponent({}, 'plain text') + + expect(richTextParser.html()).toEqual('

converted text

') + }) + + it('should return null if no children is passed', () => { + richTextParser = renderComponent({}, undefined) + expect(richTextParser.html()).toBe(null) + }) +}) diff --git a/src/index.js b/src/index.js index 8db29cbb2..6c33ddb38 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,8 @@ export { useCachedDataQuery, } from './components/CachedDataQueryProvider.js' +export * from './components/RichText/index.js' + // Api export { default as Analytics } from './api/analytics/Analytics.js' diff --git a/yarn.lock b/yarn.lock index 08374ba9f..cef802298 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2433,15 +2433,6 @@ i18next "^10.3" moment "^2.24.0" -"@dhis2/d2-ui-rich-text@^7.4.1": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@dhis2/d2-ui-rich-text/-/d2-ui-rich-text-7.4.1.tgz#8764208c59c6758bf34765b1dbe01762ce435d11" - integrity sha512-/n5nE0b4EDI/kX0/aN+vFDOswoWT5JQ3lwtHsUxailvnEHMu4/3l27Q38Z+5qhKwl+jYNB9GOFxWoSiymUgBbw== - dependencies: - babel-runtime "^6.26.0" - markdown-it "^8.4.2" - prop-types "^15.6.2" - "@dhis2/multi-calendar-dates@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-1.0.0.tgz#bf7f49aecdffa9781837a5d60d56a094b74ab4df" @@ -6061,6 +6052,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -9508,6 +9504,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" + integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== + enzyme-adapter-react-16@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901" @@ -14165,10 +14166,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" - integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== +linkify-it@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" + integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== dependencies: uc.micro "^1.0.1" @@ -14615,14 +14616,14 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== -markdown-it@^8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" - integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== +markdown-it@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" + integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== dependencies: - argparse "^1.0.7" - entities "~1.1.1" - linkify-it "^2.0.0" + argparse "^2.0.1" + entities "~3.0.1" + linkify-it "^4.0.1" mdurl "^1.0.1" uc.micro "^1.0.5" @@ -14707,7 +14708,7 @@ mdn-data@2.0.6: mdurl@^1.0.0, mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== media-typer@0.3.0: version "0.3.0" From e83d9543aa4d60ea2b4f8ec624225785a12fb975 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 23 Jun 2023 11:55:13 +0200 Subject: [PATCH 02/30] refactor: use rich text parser/editor components from analytics --- src/components/AboutAOUnit/AboutAOUnit.js | 8 ++++---- src/components/Interpretations/common/Message/Message.js | 2 +- .../common/RichTextEditor/RichTextEditor.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/AboutAOUnit/AboutAOUnit.js b/src/components/AboutAOUnit/AboutAOUnit.js index 76002a453..5f26de675 100644 --- a/src/components/AboutAOUnit/AboutAOUnit.js +++ b/src/components/AboutAOUnit/AboutAOUnit.js @@ -4,7 +4,6 @@ import { useTimeZoneConversion, } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text' import { Button, CircularLoader, @@ -29,6 +28,7 @@ import React, { useImperativeHandle, } from 'react' import { formatList } from '../../modules/list.js' +import { RichTextParser } from '../RichText/index.js' import styles from './styles/AboutAOUnit.style.js' import { getTranslatedString, AOTypeMap } from './utils.js' @@ -191,7 +191,7 @@ const AboutAOUnit = forwardRef(({ type, id, renderId }, ref) => { )} {data && (
-

{ {data.ao.displayDescription} ) : ( - i18n.t('No description') +

{i18n.t('No description')}

)} -

+

diff --git a/src/components/Interpretations/common/Message/Message.js b/src/components/Interpretations/common/Message/Message.js index 016e8b9d0..94bd37b5a 100644 --- a/src/components/Interpretations/common/Message/Message.js +++ b/src/components/Interpretations/common/Message/Message.js @@ -1,9 +1,9 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime' -import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text' import { UserAvatar, spacers, colors } from '@dhis2/ui' import moment from 'moment' import PropTypes from 'prop-types' import React from 'react' +import { RichTextParser } from '../../../RichText/index.js' const Message = ({ children, text, created, username }) => { const { fromServerDate } = useTimeZoneConversion() diff --git a/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js b/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js index e8ad9216d..cd0b66b98 100644 --- a/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js +++ b/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js @@ -1,5 +1,4 @@ import i18n from '@dhis2/d2-i18n' -import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text' import { Button, Popover, @@ -14,6 +13,7 @@ import { } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { forwardRef, useRef, useEffect, useState } from 'react' +import { RichTextParser } from '../../../RichText/index.js' import { UserMentionWrapper } from '../UserMention/UserMentionWrapper.js' import { convertCtrlKey, From 9b06ca7cad97d8bea32f0efca3d7ca7631052882 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Fri, 23 Jun 2023 12:01:33 +0200 Subject: [PATCH 03/30] refactor: convert to functional component --- src/components/RichText/parser/Parser.js | 30 +++++++++--------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/components/RichText/parser/Parser.js b/src/components/RichText/parser/Parser.js index a96062f73..50a98039d 100644 --- a/src/components/RichText/parser/Parser.js +++ b/src/components/RichText/parser/Parser.js @@ -1,26 +1,18 @@ import PropTypes from 'prop-types' -import React, { Component } from 'react' +import React, { useMemo } from 'react' import MdParserClass from './MdParser.js' -class Parser extends Component { - constructor(props) { - super(props) +const Parser = ({ children, style }) => { + const MdParser = useMemo(() => new MdParserClass(), []) - this.MdParser = new MdParserClass() - } - - render() { - const { children, style } = this.props - - return children ? ( -

- ) : null - } + return children ? ( +

+ ) : null } Parser.defaultProps = { From bb62385784174d4ab459922c6c3822a5178ef0a1 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 6 Feb 2024 16:11:30 +0100 Subject: [PATCH 04/30] fix: attempt to fix issue with rebase --- src/components/RichText/{editor => Editor}/Editor.js | 0 .../RichText/{editor => Editor}/__tests__/Editor.spec.js | 0 .../RichText/{editor => Editor}/__tests__/convertCtrlKey.spec.js | 0 src/components/RichText/{editor => Editor}/convertCtrlKey.js | 0 src/components/RichText/{parser => Parser}/MdParser.js | 0 src/components/RichText/{parser => Parser}/Parser.js | 0 .../RichText/{parser => Parser}/__tests__/MdParser.spec.js | 0 .../RichText/{parser => Parser}/__tests__/Parser.spec.js | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename src/components/RichText/{editor => Editor}/Editor.js (100%) rename src/components/RichText/{editor => Editor}/__tests__/Editor.spec.js (100%) rename src/components/RichText/{editor => Editor}/__tests__/convertCtrlKey.spec.js (100%) rename src/components/RichText/{editor => Editor}/convertCtrlKey.js (100%) rename src/components/RichText/{parser => Parser}/MdParser.js (100%) rename src/components/RichText/{parser => Parser}/Parser.js (100%) rename src/components/RichText/{parser => Parser}/__tests__/MdParser.spec.js (100%) rename src/components/RichText/{parser => Parser}/__tests__/Parser.spec.js (100%) diff --git a/src/components/RichText/editor/Editor.js b/src/components/RichText/Editor/Editor.js similarity index 100% rename from src/components/RichText/editor/Editor.js rename to src/components/RichText/Editor/Editor.js diff --git a/src/components/RichText/editor/__tests__/Editor.spec.js b/src/components/RichText/Editor/__tests__/Editor.spec.js similarity index 100% rename from src/components/RichText/editor/__tests__/Editor.spec.js rename to src/components/RichText/Editor/__tests__/Editor.spec.js diff --git a/src/components/RichText/editor/__tests__/convertCtrlKey.spec.js b/src/components/RichText/Editor/__tests__/convertCtrlKey.spec.js similarity index 100% rename from src/components/RichText/editor/__tests__/convertCtrlKey.spec.js rename to src/components/RichText/Editor/__tests__/convertCtrlKey.spec.js diff --git a/src/components/RichText/editor/convertCtrlKey.js b/src/components/RichText/Editor/convertCtrlKey.js similarity index 100% rename from src/components/RichText/editor/convertCtrlKey.js rename to src/components/RichText/Editor/convertCtrlKey.js diff --git a/src/components/RichText/parser/MdParser.js b/src/components/RichText/Parser/MdParser.js similarity index 100% rename from src/components/RichText/parser/MdParser.js rename to src/components/RichText/Parser/MdParser.js diff --git a/src/components/RichText/parser/Parser.js b/src/components/RichText/Parser/Parser.js similarity index 100% rename from src/components/RichText/parser/Parser.js rename to src/components/RichText/Parser/Parser.js diff --git a/src/components/RichText/parser/__tests__/MdParser.spec.js b/src/components/RichText/Parser/__tests__/MdParser.spec.js similarity index 100% rename from src/components/RichText/parser/__tests__/MdParser.spec.js rename to src/components/RichText/Parser/__tests__/MdParser.spec.js diff --git a/src/components/RichText/parser/__tests__/Parser.spec.js b/src/components/RichText/Parser/__tests__/Parser.spec.js similarity index 100% rename from src/components/RichText/parser/__tests__/Parser.spec.js rename to src/components/RichText/Parser/__tests__/Parser.spec.js From 903adeaaf499e5235ba55d5fea91184fc7325068 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Wed, 28 Jun 2023 12:15:40 +0200 Subject: [PATCH 05/30] feat: moved RichText and UserMention out of Intepretation components RichText has the Jest tests from the old d2-ui implementation. --- i18n/en.pot | 70 ++--- .../InterpretationModal/CommentAddForm.js | 2 +- .../InterpretationModal/CommentUpdateForm.js | 7 +- .../InterpretationsUnit/InterpretationForm.js | 7 +- .../InterpretationUpdateForm.js | 2 +- .../common/RichTextEditor/RichTextEditor.js | 277 ----------------- .../common/RichTextEditor/index.js | 1 - .../Interpretations/common/index.js | 1 - src/components/RichText/Editor/Editor.js | 286 ++++++++++++++++-- .../RichText/Editor/__tests__/Editor.spec.js | 25 +- .../Editor/__tests__/convertCtrlKey.spec.js | 2 +- .../RichText/Editor/convertCtrlKey.js | 103 ------- .../Editor}/markdownHandler.js | 0 .../Editor/styles/Editor.style.js} | 0 src/components/RichText/Parser/MdParser.js | 4 +- src/components/RichText/Parser/Parser.js | 10 +- .../Parser/__tests__/MdParser.spec.js | 2 +- .../RichText/Parser/__tests__/Parser.spec.js | 11 +- .../RichText/editor/markdownHandler.js | 133 ++++++++ .../RichText/editor/styles/Editor.style.js | 102 +++++++ src/components/RichText/index.js | 8 +- .../common => }/UserMention/UserList.js | 0 .../UserMention/UserMentionWrapper.js | 0 .../styles/UserMentionWrapper.style.js | 0 .../UserMention/useUserSearchResults.js | 0 25 files changed, 570 insertions(+), 483 deletions(-) delete mode 100644 src/components/Interpretations/common/RichTextEditor/RichTextEditor.js delete mode 100644 src/components/Interpretations/common/RichTextEditor/index.js delete mode 100644 src/components/RichText/Editor/convertCtrlKey.js rename src/components/{Interpretations/common/RichTextEditor => RichText/Editor}/markdownHandler.js (100%) rename src/components/{Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js => RichText/Editor/styles/Editor.style.js} (100%) create mode 100644 src/components/RichText/editor/markdownHandler.js create mode 100644 src/components/RichText/editor/styles/Editor.style.js rename src/components/{Interpretations/common => }/UserMention/UserList.js (100%) rename src/components/{Interpretations/common => }/UserMention/UserMentionWrapper.js (100%) rename src/components/{Interpretations/common => }/UserMention/styles/UserMentionWrapper.style.js (100%) rename src/components/{Interpretations/common => }/UserMention/useUserSearchResults.js (100%) diff --git a/i18n/en.pot b/i18n/en.pot index bfd246658..cf6fb236e 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-09-27T14:15:13.876Z\n" -"PO-Revision-Date: 2023-09-27T14:15:13.876Z\n" +"POT-Creation-Date: 2023-06-26T13:41:45.359Z\n" +"PO-Revision-Date: 2023-06-26T13:41:45.359Z\n" msgid "view only" msgstr "view only" @@ -415,39 +415,6 @@ msgstr "Could not update interpretation" msgid "Enter interpretation text" msgstr "Enter interpretation text" -msgid "Bold text" -msgstr "Bold text" - -msgid "Italic text" -msgstr "Italic text" - -msgid "Link to a URL" -msgstr "Link to a URL" - -msgid "Mention a user" -msgstr "Mention a user" - -msgid "Add emoji" -msgstr "Add emoji" - -msgid "Preview" -msgstr "Preview" - -msgid "Back to write mode" -msgstr "Back to write mode" - -msgid "Too many results. Try refining the search." -msgstr "Too many results. Try refining the search." - -msgid "Search for a user" -msgstr "Search for a user" - -msgid "Searching for \"{{- searchText}}\"" -msgstr "Searching for \"{{- searchText}}\"" - -msgid "No results found" -msgstr "No results found" - msgid "Not available offline" msgstr "Not available offline" @@ -870,6 +837,27 @@ msgstr "Financial Years" msgid "Years" msgstr "Years" +msgid "Bold text" +msgstr "Bold text" + +msgid "Italic text" +msgstr "Italic text" + +msgid "Link to a URL" +msgstr "Link to a URL" + +msgid "Mention a user" +msgstr "Mention a user" + +msgid "Add emoji" +msgstr "Add emoji" + +msgid "Preview" +msgstr "Preview" + +msgid "Back to write mode" +msgstr "Back to write mode" + msgid "Interpretations and details" msgstr "Interpretations and details" @@ -900,6 +888,18 @@ msgstr "Could not load translations" msgid "Retry" msgstr "Retry" +msgid "Too many results. Try refining the search." +msgstr "Too many results. Try refining the search." + +msgid "Search for a user" +msgstr "Search for a user" + +msgid "Searching for \"{{- searchText}}\"" +msgstr "Searching for \"{{- searchText}}\"" + +msgid "No results found" +msgstr "No results found" + msgid "Series" msgstr "Series" diff --git a/src/components/Interpretations/InterpretationModal/CommentAddForm.js b/src/components/Interpretations/InterpretationModal/CommentAddForm.js index 9cae2d4a7..0b8f98498 100644 --- a/src/components/Interpretations/InterpretationModal/CommentAddForm.js +++ b/src/components/Interpretations/InterpretationModal/CommentAddForm.js @@ -3,8 +3,8 @@ import i18n from '@dhis2/d2-i18n' import { Button } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useRef, useState } from 'react' +import { RichTextEditor } from '../../RichText/index.js' import { - RichTextEditor, MessageEditorContainer, MessageButtonStrip, MessageInput, diff --git a/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js b/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js index ea9d3812e..dc90d2175 100644 --- a/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js +++ b/src/components/Interpretations/InterpretationModal/CommentUpdateForm.js @@ -3,11 +3,8 @@ import i18n from '@dhis2/d2-i18n' import { Button, spacers, colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState, useRef } from 'react' -import { - MessageEditorContainer, - RichTextEditor, - MessageButtonStrip, -} from '../common/index.js' +import { RichTextEditor } from '../../RichText/index.js' +import { MessageEditorContainer, MessageButtonStrip } from '../common/index.js' export const CommentUpdateForm = ({ interpretationId, diff --git a/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js b/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js index 2b7e9d02c..0f8650e87 100644 --- a/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js +++ b/src/components/Interpretations/InterpretationsUnit/InterpretationForm.js @@ -3,11 +3,8 @@ import i18n from '@dhis2/d2-i18n' import { Button, Input } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useRef, useState } from 'react' -import { - RichTextEditor, - MessageEditorContainer, - MessageButtonStrip, -} from '../common/index.js' +import { RichTextEditor } from '../../RichText/index.js' +import { MessageEditorContainer, MessageButtonStrip } from '../common/index.js' export const InterpretationForm = ({ type, diff --git a/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js b/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js index cf900fdf1..9891d7052 100644 --- a/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +++ b/src/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js @@ -3,9 +3,9 @@ import i18n from '@dhis2/d2-i18n' import { Button, spacers, colors } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState } from 'react' +import { RichTextEditor } from '../../../RichText/index.js' import { MessageEditorContainer, - RichTextEditor, MessageButtonStrip, InterpretationSharingLink, } from '../index.js' diff --git a/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js b/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js deleted file mode 100644 index cd0b66b98..000000000 --- a/src/components/Interpretations/common/RichTextEditor/RichTextEditor.js +++ /dev/null @@ -1,277 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { - Button, - Popover, - Tooltip, - Field, - IconAt24, - IconFaceAdd24, - IconLink24, - IconTextBold24, - IconTextItalic24, - colors, -} from '@dhis2/ui' -import PropTypes from 'prop-types' -import React, { forwardRef, useRef, useEffect, useState } from 'react' -import { RichTextParser } from '../../../RichText/index.js' -import { UserMentionWrapper } from '../UserMention/UserMentionWrapper.js' -import { - convertCtrlKey, - insertMarkdown, - emojis, - EMOJI_SMILEY_FACE, - EMOJI_SAD_FACE, - EMOJI_THUMBS_UP, - EMOJI_THUMBS_DOWN, - BOLD, - ITALIC, - LINK, - MENTION, -} from './markdownHandler.js' -import { - mainClasses, - toolbarClasses, - tooltipAnchorClasses, - emojisPopoverClasses, -} from './styles/RichTextEditor.style.js' - -const EmojisPopover = ({ onInsertMarkdown, onClose, reference }) => ( - -

    -
  • onInsertMarkdown(EMOJI_SMILEY_FACE)}> - {emojis[EMOJI_SMILEY_FACE]} -
  • -
  • onInsertMarkdown(EMOJI_SAD_FACE)}> - {emojis[EMOJI_SAD_FACE]} -
  • -
  • onInsertMarkdown(EMOJI_THUMBS_UP)}> - {emojis[EMOJI_THUMBS_UP]} -
  • -
  • onInsertMarkdown(EMOJI_THUMBS_DOWN)}> - {emojis[EMOJI_THUMBS_DOWN]} -
  • -
- - -) - -EmojisPopover.propTypes = { - onClose: PropTypes.func.isRequired, - onInsertMarkdown: PropTypes.func.isRequired, - reference: PropTypes.object, -} - -const IconButtonWithTooltip = ({ tooltipContent, disabled, icon, onClick }) => ( - <> - - {({ ref, onMouseOver, onMouseOut }) => ( - - -
- - ) : ( -
- -
- )} - - - - ) -} - -Toolbar.propTypes = { - previewButtonDisabled: PropTypes.bool.isRequired, - previewMode: PropTypes.bool.isRequired, - onInsertMarkdown: PropTypes.func.isRequired, - onTogglePreview: PropTypes.func.isRequired, - disabled: PropTypes.bool, -} - -export const RichTextEditor = forwardRef( - ( - { value, disabled, inputPlaceholder, onChange, errorText, helpText }, - externalRef - ) => { - const [previewMode, setPreviewMode] = useState(false) - const internalRef = useRef() - const textareaRef = externalRef || internalRef - - useEffect(() => textareaRef.current?.focus(), [textareaRef]) - - return ( -
- { - insertMarkdown( - markdown, - textareaRef.current, - (text, caretPos) => { - onChange(text) - textareaRef.current.focus() - textareaRef.current.selectionEnd = caretPos - } - ) - - if (markdown === MENTION) { - textareaRef.current.dispatchEvent( - new KeyboardEvent('keydown', { - key: '@', - bubbles: true, - }) - ) - } - }} - onTogglePreview={() => setPreviewMode(!previewMode)} - previewMode={previewMode} - previewButtonDisabled={!value} - disabled={disabled} - /> - {previewMode ? ( -
- {value} -
- ) : ( - - -