@dhis2/analytics 26.6.14 → 26.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/build/cjs/components/AboutAOUnit/AboutAOUnit.js +5 -3
  2. package/build/cjs/components/Interpretations/InterpretationModal/CommentAddForm.js +5 -4
  3. package/build/cjs/components/Interpretations/InterpretationModal/CommentUpdateForm.js +4 -3
  4. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationForm.js +5 -4
  5. package/build/cjs/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +5 -4
  6. package/build/cjs/components/Interpretations/common/Message/Message.js +2 -2
  7. package/build/cjs/components/Interpretations/common/Message/MessageEditorContainer.js +5 -5
  8. package/build/cjs/components/Interpretations/common/index.js +0 -11
  9. package/build/cjs/components/{Interpretations/common/RichTextEditor/RichTextEditor.js → RichText/Editor/Editor.js} +76 -53
  10. package/build/cjs/components/RichText/Editor/__tests__/Editor.spec.js +38 -0
  11. package/build/cjs/components/RichText/Editor/__tests__/convertCtrlKey.spec.js +204 -0
  12. package/build/cjs/components/{Interpretations/common/RichTextEditor → RichText/Editor}/markdownHandler.js +12 -6
  13. package/build/cjs/components/{Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js → RichText/Editor/styles/Editor.style.js} +2 -2
  14. package/build/cjs/components/RichText/Parser/MdParser.js +106 -0
  15. package/build/cjs/components/RichText/Parser/Parser.js +35 -0
  16. package/build/cjs/components/RichText/Parser/__tests__/MdParser.spec.js +42 -0
  17. package/build/cjs/components/RichText/Parser/__tests__/Parser.spec.js +41 -0
  18. package/build/cjs/components/RichText/index.js +26 -0
  19. package/build/cjs/components/{Interpretations/common/UserMention → UserMention}/UserMentionWrapper.js +19 -9
  20. package/build/cjs/components/{Interpretations/common/UserMention → UserMention}/styles/UserMentionWrapper.style.js +2 -2
  21. package/build/cjs/components/{Interpretations/common/UserMention → UserMention}/useUserSearchResults.js +2 -2
  22. package/build/cjs/index.js +58 -46
  23. package/build/cjs/locales/en/translations.json +11 -11
  24. package/build/cjs/locales/ne/translations.json +15 -15
  25. package/build/es/components/AboutAOUnit/AboutAOUnit.js +5 -3
  26. package/build/es/components/Interpretations/InterpretationModal/CommentAddForm.js +2 -1
  27. package/build/es/components/Interpretations/InterpretationModal/CommentUpdateForm.js +2 -1
  28. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationForm.js +3 -2
  29. package/build/es/components/Interpretations/common/Interpretation/InterpretationUpdateForm.js +2 -1
  30. package/build/es/components/Interpretations/common/Message/Message.js +1 -1
  31. package/build/es/components/Interpretations/common/Message/MessageEditorContainer.js +5 -5
  32. package/build/es/components/Interpretations/common/index.js +0 -1
  33. package/build/es/components/{Interpretations/common/RichTextEditor/RichTextEditor.js → RichText/Editor/Editor.js} +51 -28
  34. package/build/es/components/RichText/Editor/__tests__/Editor.spec.js +35 -0
  35. package/build/es/components/RichText/Editor/__tests__/convertCtrlKey.spec.js +202 -0
  36. package/build/es/components/{Interpretations/common/RichTextEditor → RichText/Editor}/markdownHandler.js +12 -6
  37. package/build/es/components/{Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js → RichText/Editor/styles/Editor.style.js} +2 -2
  38. package/build/es/components/RichText/Parser/MdParser.js +98 -0
  39. package/build/es/components/RichText/Parser/Parser.js +25 -0
  40. package/build/es/components/RichText/Parser/__tests__/MdParser.spec.js +40 -0
  41. package/build/es/components/RichText/Parser/__tests__/Parser.spec.js +38 -0
  42. package/build/es/components/RichText/index.js +3 -0
  43. package/build/es/components/{Interpretations/common/UserMention → UserMention}/UserMentionWrapper.js +19 -8
  44. package/build/es/components/UserMention/styles/UserMentionWrapper.style.js +16 -0
  45. package/build/es/components/{Interpretations/common/UserMention → UserMention}/useUserSearchResults.js +2 -2
  46. package/build/es/index.js +1 -0
  47. package/build/es/locales/en/translations.json +11 -11
  48. package/build/es/locales/ne/translations.json +15 -15
  49. package/package.json +2 -2
  50. package/build/cjs/components/Interpretations/common/RichTextEditor/index.js +0 -12
  51. package/build/es/components/Interpretations/common/RichTextEditor/index.js +0 -1
  52. package/build/es/components/Interpretations/common/UserMention/styles/UserMentionWrapper.style.js +0 -16
  53. /package/build/cjs/components/{Interpretations/common/UserMention → UserMention}/UserList.js +0 -0
  54. /package/build/es/components/{Interpretations/common/UserMention → UserMention}/UserList.js +0 -0
@@ -1,12 +1,13 @@
1
1
  import _JSXStyle from "styled-jsx/style";
2
2
  import i18n from '@dhis2/d2-i18n';
3
- import { Parser as RichTextParser } from '@dhis2/d2-ui-rich-text';
4
- import { Button, Popover, Tooltip, Field, IconAt24, IconFaceAdd24, IconLink24, IconTextBold24, IconTextItalic24, colors } from '@dhis2/ui';
3
+ import { Button, Popover, Tooltip, Help, IconAt24, IconFaceAdd24, IconLink24, IconTextBold24, IconTextItalic24, colors } from '@dhis2/ui';
4
+ import cx from 'classnames';
5
5
  import PropTypes from 'prop-types';
6
6
  import React, { forwardRef, useRef, useEffect, useState } from 'react';
7
- import { UserMentionWrapper } from '../UserMention/UserMentionWrapper.js';
7
+ import { UserMentionWrapper } from '../../UserMention/UserMentionWrapper.js';
8
+ import { Parser } from '../Parser/Parser.js';
8
9
  import { convertCtrlKey, insertMarkdown, emojis, EMOJI_SMILEY_FACE, EMOJI_SAD_FACE, EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN, BOLD, ITALIC, LINK, MENTION } from './markdownHandler.js';
9
- import { mainClasses, toolbarClasses, tooltipAnchorClasses, emojisPopoverClasses } from './styles/RichTextEditor.style.js';
10
+ import { mainClasses, toolbarClasses, tooltipAnchorClasses, emojisPopoverClasses } from './styles/Editor.style.js';
10
11
  const EmojisPopover = _ref => {
11
12
  let {
12
13
  onInsertMarkdown,
@@ -21,16 +22,16 @@ const EmojisPopover = _ref => {
21
22
  }, /*#__PURE__*/React.createElement("li", {
22
23
  onClick: () => onInsertMarkdown(EMOJI_SMILEY_FACE),
23
24
  className: `jsx-${emojisPopoverClasses.__hash}`
24
- }, /*#__PURE__*/React.createElement(RichTextParser, null, emojis[EMOJI_SMILEY_FACE])), /*#__PURE__*/React.createElement("li", {
25
+ }, /*#__PURE__*/React.createElement(Parser, null, emojis[EMOJI_SMILEY_FACE])), /*#__PURE__*/React.createElement("li", {
25
26
  onClick: () => onInsertMarkdown(EMOJI_SAD_FACE),
26
27
  className: `jsx-${emojisPopoverClasses.__hash}`
27
- }, /*#__PURE__*/React.createElement(RichTextParser, null, emojis[EMOJI_SAD_FACE])), /*#__PURE__*/React.createElement("li", {
28
+ }, /*#__PURE__*/React.createElement(Parser, null, emojis[EMOJI_SAD_FACE])), /*#__PURE__*/React.createElement("li", {
28
29
  onClick: () => onInsertMarkdown(EMOJI_THUMBS_UP),
29
30
  className: `jsx-${emojisPopoverClasses.__hash}`
30
- }, /*#__PURE__*/React.createElement(RichTextParser, null, emojis[EMOJI_THUMBS_UP])), /*#__PURE__*/React.createElement("li", {
31
+ }, /*#__PURE__*/React.createElement(Parser, null, emojis[EMOJI_THUMBS_UP])), /*#__PURE__*/React.createElement("li", {
31
32
  onClick: () => onInsertMarkdown(EMOJI_THUMBS_DOWN),
32
33
  className: `jsx-${emojisPopoverClasses.__hash}`
33
- }, /*#__PURE__*/React.createElement(RichTextParser, null, emojis[EMOJI_THUMBS_DOWN]))), /*#__PURE__*/React.createElement(_JSXStyle, {
34
+ }, /*#__PURE__*/React.createElement(Parser, null, emojis[EMOJI_THUMBS_DOWN]))), /*#__PURE__*/React.createElement(_JSXStyle, {
34
35
  id: emojisPopoverClasses.__hash
35
36
  }, emojisPopoverClasses));
36
37
  };
@@ -167,31 +168,45 @@ Toolbar.propTypes = {
167
168
  onTogglePreview: PropTypes.func.isRequired,
168
169
  disabled: PropTypes.bool
169
170
  };
170
- export const RichTextEditor = /*#__PURE__*/forwardRef((_ref5, externalRef) => {
171
+ export const Editor = /*#__PURE__*/forwardRef((_ref5, externalRef) => {
171
172
  let {
172
173
  value,
173
174
  disabled,
174
175
  inputPlaceholder,
175
176
  onChange,
176
177
  errorText,
177
- helpText
178
+ helpText,
179
+ initialFocus,
180
+ resizable
178
181
  } = _ref5;
179
182
  const [previewMode, setPreviewMode] = useState(false);
180
183
  const internalRef = useRef();
181
184
  const textareaRef = externalRef || internalRef;
185
+ const caretPosRef = useRef(undefined);
186
+ const insertMarkdownCallback = (text, caretPos) => {
187
+ caretPosRef.current = caretPos;
188
+ onChange(text);
189
+ textareaRef.current.focus();
190
+ };
182
191
  useEffect(() => {
183
- var _textareaRef$current;
184
- return (_textareaRef$current = textareaRef.current) === null || _textareaRef$current === void 0 ? void 0 : _textareaRef$current.focus();
185
- }, [textareaRef]);
192
+ if (initialFocus) {
193
+ var _textareaRef$current;
194
+ (_textareaRef$current = textareaRef.current) === null || _textareaRef$current === void 0 ? void 0 : _textareaRef$current.focus();
195
+ }
196
+ }, [initialFocus, textareaRef]);
197
+ useEffect(() => {
198
+ if (caretPosRef.current) {
199
+ var _textareaRef$current2;
200
+ (_textareaRef$current2 = textareaRef.current) === null || _textareaRef$current2 === void 0 ? void 0 : _textareaRef$current2.setSelectionRange(caretPosRef.current, caretPosRef.current);
201
+ caretPosRef.current = undefined;
202
+ }
203
+ }, [value, textareaRef]);
186
204
  return /*#__PURE__*/React.createElement("div", {
205
+ "data-test": "@dhis2-analytics-richtexteditor",
187
206
  className: `jsx-${mainClasses.__hash}` + " " + "container"
188
207
  }, /*#__PURE__*/React.createElement(Toolbar, {
189
208
  onInsertMarkdown: markdown => {
190
- insertMarkdown(markdown, textareaRef.current, (text, caretPos) => {
191
- onChange(text);
192
- textareaRef.current.focus();
193
- textareaRef.current.selectionEnd = caretPos;
194
- });
209
+ insertMarkdown(markdown, textareaRef.current, insertMarkdownCallback);
195
210
  if (markdown === MENTION) {
196
211
  textareaRef.current.dispatchEvent(new KeyboardEvent('keydown', {
197
212
  key: '@',
@@ -205,10 +220,8 @@ export const RichTextEditor = /*#__PURE__*/forwardRef((_ref5, externalRef) => {
205
220
  disabled: disabled
206
221
  }), previewMode ? /*#__PURE__*/React.createElement("div", {
207
222
  className: `jsx-${mainClasses.__hash}` + " " + "preview"
208
- }, /*#__PURE__*/React.createElement(RichTextParser, null, value)) : /*#__PURE__*/React.createElement(Field, {
209
- error: !!errorText,
210
- validationText: errorText,
211
- helpText: helpText
223
+ }, /*#__PURE__*/React.createElement(Parser, null, value)) : /*#__PURE__*/React.createElement("div", {
224
+ className: `jsx-${mainClasses.__hash}` + " " + "edit"
212
225
  }, /*#__PURE__*/React.createElement(UserMentionWrapper, {
213
226
  onUserSelect: onChange,
214
227
  inputReference: textareaRef
@@ -218,18 +231,28 @@ export const RichTextEditor = /*#__PURE__*/forwardRef((_ref5, externalRef) => {
218
231
  disabled: disabled,
219
232
  value: value,
220
233
  onChange: event => onChange(event.target.value),
221
- onKeyDown: event => convertCtrlKey(event, onChange),
222
- className: `jsx-${mainClasses.__hash}` + " " + "textarea"
223
- }))), /*#__PURE__*/React.createElement(_JSXStyle, {
234
+ onKeyDown: event => convertCtrlKey(event, insertMarkdownCallback),
235
+ className: `jsx-${mainClasses.__hash}` + " " + (cx('textarea', {
236
+ resizable
237
+ }) || "")
238
+ })), errorText && /*#__PURE__*/React.createElement(Help, {
239
+ error: !!errorText
240
+ }, errorText), helpText && /*#__PURE__*/React.createElement(Help, null, helpText)), /*#__PURE__*/React.createElement(_JSXStyle, {
224
241
  id: mainClasses.__hash
225
242
  }, mainClasses));
226
243
  });
227
- RichTextEditor.displayName = 'RichTextEditor';
228
- RichTextEditor.propTypes = {
244
+ Editor.displayName = 'Editor';
245
+ Editor.defaultProps = {
246
+ initialFocus: true,
247
+ resizable: true
248
+ };
249
+ Editor.propTypes = {
229
250
  value: PropTypes.string.isRequired,
230
251
  onChange: PropTypes.func.isRequired,
231
252
  disabled: PropTypes.bool,
232
253
  errorText: PropTypes.string,
233
254
  helpText: PropTypes.string,
234
- inputPlaceholder: PropTypes.string
255
+ initialFocus: PropTypes.bool,
256
+ inputPlaceholder: PropTypes.string,
257
+ resizable: PropTypes.bool
235
258
  };
@@ -0,0 +1,35 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { Editor } from '../Editor.js';
5
+ const mockConvertCtrlKey = jest.fn();
6
+ jest.mock('../markdownHandler.js', () => ({
7
+ convertCtrlKey: () => mockConvertCtrlKey()
8
+ }));
9
+ jest.mock('../../../UserMention/UserMentionWrapper.js', () => ({
10
+ UserMentionWrapper: jest.fn(props => /*#__PURE__*/React.createElement(React.Fragment, null, props.children))
11
+ }));
12
+ describe('RichText: Editor component', () => {
13
+ const componentProps = {
14
+ value: '',
15
+ onChange: jest.fn()
16
+ };
17
+ beforeEach(() => {
18
+ mockConvertCtrlKey.mockClear();
19
+ });
20
+ const renderComponent = props => {
21
+ return render( /*#__PURE__*/React.createElement(Editor, props));
22
+ };
23
+ it('renders a result', () => {
24
+ renderComponent(componentProps);
25
+ expect(screen.getByTestId('@dhis2-analytics-richtexteditor')).toBeVisible();
26
+ });
27
+ it('calls convertCtrlKey on keydown', () => {
28
+ renderComponent(componentProps);
29
+ fireEvent.keyDown(screen.getByRole('textbox'), {
30
+ key: 'A',
31
+ code: 'keyA'
32
+ });
33
+ expect(mockConvertCtrlKey).toHaveBeenCalled();
34
+ });
35
+ });
@@ -0,0 +1,202 @@
1
+ import { convertCtrlKey } from '../markdownHandler.js';
2
+ describe('convertCtrlKey', () => {
3
+ it('does not trigger callback if no ctrl key', () => {
4
+ const cb = jest.fn();
5
+ const e = {
6
+ key: 'j',
7
+ preventDefault: () => {}
8
+ };
9
+ convertCtrlKey(e, cb);
10
+ expect(cb).not.toHaveBeenCalled();
11
+ });
12
+ describe('when ctrl key + "b" pressed', () => {
13
+ it('triggers callback with open/close markers and caret pos in between', () => {
14
+ const cb = jest.fn();
15
+ const e = {
16
+ key: 'b',
17
+ ctrlKey: true,
18
+ target: {
19
+ selectionStart: 0,
20
+ selectionEnd: 0,
21
+ value: 'rainbow dash'
22
+ },
23
+ preventDefault: () => {}
24
+ };
25
+ convertCtrlKey(e, cb);
26
+ expect(cb).toHaveBeenCalled();
27
+ expect(cb).toHaveBeenCalledWith('** rainbow dash', 1);
28
+ });
29
+ it('triggers callback with open/close markers and caret pos in between (end of text)', () => {
30
+ const cb = jest.fn();
31
+ const e = {
32
+ key: 'b',
33
+ ctrlKey: true,
34
+ target: {
35
+ selectionStart: 22,
36
+ selectionEnd: 22,
37
+ value: 'rainbow dash is purple'
38
+ },
39
+ preventDefault: () => {}
40
+ };
41
+ convertCtrlKey(e, cb);
42
+ expect(cb).toHaveBeenCalled();
43
+ expect(cb).toHaveBeenCalledWith('rainbow dash is purple **', 24);
44
+ });
45
+ it('triggers callback with open/close markers mid-text with surrounding spaces (1)', () => {
46
+ const cb = jest.fn();
47
+ const e = {
48
+ key: 'b',
49
+ metaKey: true,
50
+ target: {
51
+ selectionStart: 4,
52
+ // caret located just before "quick"
53
+ selectionEnd: 4,
54
+ value: 'the quick brown fox'
55
+ },
56
+ preventDefault: () => {}
57
+ };
58
+ convertCtrlKey(e, cb);
59
+ expect(cb).toHaveBeenCalled();
60
+ expect(cb).toHaveBeenCalledWith('the ** quick brown fox', 5);
61
+ });
62
+ it('triggers callback with open/close markers mid-text with surrounding spaces (2)', () => {
63
+ const cb = jest.fn();
64
+ const e = {
65
+ key: 'b',
66
+ metaKey: true,
67
+ target: {
68
+ selectionStart: 3,
69
+ // caret located just after "the"
70
+ selectionEnd: 3,
71
+ value: 'the quick brown fox'
72
+ },
73
+ preventDefault: () => {}
74
+ };
75
+ convertCtrlKey(e, cb);
76
+ expect(cb).toHaveBeenCalled();
77
+ expect(cb).toHaveBeenCalledWith('the ** quick brown fox', 5);
78
+ });
79
+ it('triggers callback with correct double markers and padding', () => {
80
+ const cb = jest.fn();
81
+ const e = {
82
+ key: 'b',
83
+ metaKey: true,
84
+ target: {
85
+ selectionStart: 9,
86
+ // between the underscores
87
+ selectionEnd: 9,
88
+ value: 'rainbow __'
89
+ },
90
+ preventDefault: () => {}
91
+ };
92
+ convertCtrlKey(e, cb);
93
+ expect(cb).toHaveBeenCalled();
94
+ expect(cb).toHaveBeenCalledWith('rainbow _**_', 10);
95
+ });
96
+ describe('selected text', () => {
97
+ it('triggers callback with open/close markers around text and caret pos after closing marker', () => {
98
+ const cb = jest.fn();
99
+ const e = {
100
+ key: 'b',
101
+ metaKey: true,
102
+ target: {
103
+ selectionStart: 5,
104
+ // "ow da" is selected
105
+ selectionEnd: 10,
106
+ value: 'rainbow dash is purple'
107
+ },
108
+ preventDefault: () => {}
109
+ };
110
+ convertCtrlKey(e, cb);
111
+ expect(cb).toHaveBeenCalled();
112
+ expect(cb).toHaveBeenCalledWith('rainb *ow da* sh is purple', 13);
113
+ });
114
+ it('triggers callback with open/close markers around text when starting at beginning of line', () => {
115
+ const cb = jest.fn();
116
+ const e = {
117
+ key: 'b',
118
+ metaKey: true,
119
+ target: {
120
+ selectionStart: 0,
121
+ // "rainbow" is selected
122
+ selectionEnd: 7,
123
+ value: 'rainbow dash is purple'
124
+ },
125
+ preventDefault: () => {}
126
+ };
127
+ convertCtrlKey(e, cb);
128
+ expect(cb).toHaveBeenCalled();
129
+ expect(cb).toHaveBeenCalledWith('*rainbow* dash is purple', 9);
130
+ });
131
+ it('triggers callback with open/close markers around text when ending at end of line', () => {
132
+ const cb = jest.fn();
133
+ const e = {
134
+ key: 'b',
135
+ metaKey: true,
136
+ target: {
137
+ selectionStart: 16,
138
+ // "purple" is selected
139
+ selectionEnd: 22,
140
+ value: 'rainbow dash is purple'
141
+ },
142
+ preventDefault: () => {}
143
+ };
144
+ convertCtrlKey(e, cb);
145
+ expect(cb).toHaveBeenCalled();
146
+ expect(cb).toHaveBeenCalledWith('rainbow dash is *purple*', 24);
147
+ });
148
+ it('triggers callback with open/close markers around word', () => {
149
+ const cb = jest.fn();
150
+ const e = {
151
+ key: 'b',
152
+ metaKey: true,
153
+ target: {
154
+ selectionStart: 8,
155
+ // "dash" is selected
156
+ selectionEnd: 12,
157
+ value: 'rainbow dash is purple'
158
+ },
159
+ preventDefault: () => {}
160
+ };
161
+ convertCtrlKey(e, cb);
162
+ expect(cb).toHaveBeenCalled();
163
+ expect(cb).toHaveBeenCalledWith('rainbow *dash* is purple', 14);
164
+ });
165
+ it('triggers callback with leading/trailing spaces trimmed from selection', () => {
166
+ const cb = jest.fn();
167
+ const e = {
168
+ key: 'b',
169
+ metaKey: true,
170
+ target: {
171
+ selectionStart: 8,
172
+ // " dash " is selected (note leading and trailing space)
173
+ selectionEnd: 13,
174
+ value: 'rainbow dash is purple'
175
+ },
176
+ preventDefault: () => {}
177
+ };
178
+ convertCtrlKey(e, cb);
179
+ expect(cb).toHaveBeenCalled();
180
+ expect(cb).toHaveBeenCalledWith('rainbow *dash* is purple', 14);
181
+ });
182
+ });
183
+ });
184
+ describe('when ctrl key + "i" pressed', () => {
185
+ it('triggers callback with open/close italics markers and caret pos in between', () => {
186
+ const cb = jest.fn();
187
+ const e = {
188
+ key: 'i',
189
+ ctrlKey: true,
190
+ target: {
191
+ selectionStart: 0,
192
+ selectionEnd: 0,
193
+ value: ''
194
+ },
195
+ preventDefault: () => {}
196
+ };
197
+ convertCtrlKey(e, cb);
198
+ expect(cb).toHaveBeenCalled();
199
+ expect(cb).toHaveBeenCalledWith('__', 1);
200
+ });
201
+ });
202
+ });
@@ -77,23 +77,29 @@ export const insertMarkdown = (markdown, target, cb) => {
77
77
  if (start === end) {
78
78
  //no text
79
79
  const valueArr = value.split('');
80
- let markdown = marker.prefix;
80
+ let markdownString = marker.prefix;
81
81
  if (marker.postfix) {
82
- markdown += marker.postfix;
82
+ markdownString += marker.postfix;
83
83
  }
84
- valueArr.splice(start, 0, padMarkers(markdown));
84
+ valueArr.splice(start, 0, padMarkers(markdownString));
85
85
  newValue = valueArr.join('');
86
+
87
+ // for smileys, put the caret after a space
88
+ if (Object.keys(emojis).includes(markdown)) {
89
+ newValue += ' ';
90
+ caretPos = caretPos + newValue.length - 1;
91
+ }
86
92
  } else {
87
93
  const text = value.slice(start, end);
88
94
  const trimmedText = trim(text); // TODO really needed?
89
95
 
90
96
  // adjust caretPos based on trimmed text selection
91
97
  caretPos = caretPos - (text.length - trimmedText.length) + 1;
92
- let markdown = `${marker.prefix}${trimmedText}`;
98
+ let markdownString = `${marker.prefix}${trimmedText}`;
93
99
  if (marker.postfix) {
94
- markdown += marker.postfix;
100
+ markdownString += marker.postfix;
95
101
  }
96
- newValue = [value.slice(0, start), padMarkers(markdown), value.slice(end)].join('');
102
+ newValue = [value.slice(0, start), padMarkers(markdownString), value.slice(end)].join('');
97
103
  }
98
104
  cb(newValue, caretPos);
99
105
  };
@@ -1,6 +1,6 @@
1
1
  import { colors, spacers, theme } from '@dhis2/ui';
2
- export const mainClasses = [".container.jsx-1273817287{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:100%;}", `.preview.jsx-1273817287{font-size:14px;line-height:19px;color:${colors.grey900};}`, `.textarea.jsx-1273817287{width:100%;box-sizing:border-box;padding:${spacers.dp8} ${spacers.dp12};color:${colors.grey900};background-color:${colors.white};border:1px solid ${colors.grey500};border-radius:3px;box-shadow:inset 0 0 0 1px rgba(102,113,123,0.15), inset 0 1px 2px 0 rgba(102,113,123,0.1);outline:0;font-size:14px;line-height:${spacers.dp16};-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;}`, `.textarea.jsx-1273817287:focus{outline:none;box-shadow:0 0 0 3px ${theme.focus};width:calc(100% - 3px);}`, `.textarea.jsx-1273817287:disabled{background-color:${colors.grey100};border-color:${colors.grey500};color:${theme.disabled};cursor:not-allowed;}`];
3
- mainClasses.__hash = "1273817287";
2
+ export const mainClasses = [".container.jsx-185829738{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:100%;height:100%;}", `.preview.jsx-185829738{padding:${spacers.dp8} ${spacers.dp12};font-size:14px;line-height:${spacers.dp16};color:${colors.grey900};overflow-y:auto;-webkit-scroll-behavior:smooth;-moz-scroll-behavior:smooth;-ms-scroll-behavior:smooth;scroll-behavior:smooth;}`, ".edit.jsx-185829738{width:100%;height:100%;-webkit-scroll-behavior:smooth;-moz-scroll-behavior:smooth;-ms-scroll-behavior:smooth;scroll-behavior:smooth;}", `.textarea.jsx-185829738{width:100%;height:100%;box-sizing:border-box;padding:${spacers.dp8} 15px;color:${colors.grey900};background-color:${colors.white};border:1px solid ${colors.grey500};border-radius:3px;box-shadow:inset 0 0 0 1px rgba(102,113,123,0.15), inset 0 1px 2px 0 rgba(102,113,123,0.1);outline:0;font-size:14px;line-height:${spacers.dp16};-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;resize:none;}`, ".textarea.resizable.jsx-185829738{resize:vertical;}", `.textarea.jsx-185829738:focus{outline:none;box-shadow:0 0 0 3px ${theme.focus};width:calc(100% - 6px);height:calc(100% - 3px);padding:${spacers.dp8} ${spacers.dp12};margin-left:3px;}`, `.textarea.jsx-185829738:disabled{background-color:${colors.grey100};border-color:${colors.grey500};color:${theme.disabled};cursor:not-allowed;}`];
3
+ mainClasses.__hash = "185829738";
4
4
  export const toolbarClasses = [`.toolbar.jsx-2267496677{background:${colors.grey050};border-radius:3px;border:1px solid ${colors.grey300};margin-bottom:${spacers.dp4};}`, `.actionsWrapper.jsx-2267496677{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:${spacers.dp4};-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:${spacers.dp4};}`, `.mainActions.jsx-2267496677{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:${spacers.dp4};margin-top:${spacers.dp2};}`, ".sideActions.jsx-2267496677{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;}", `.previewWrapper.jsx-2267496677{margin:${spacers.dp4};text-align:right;}`];
5
5
  toolbarClasses.__hash = "2267496677";
6
6
  export const tooltipAnchorClasses = [".tooltip.jsx-2182400256{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}"];
@@ -0,0 +1,98 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ const emojiDb = {
3
+ ':-)': '\u{1F642}',
4
+ ':)': '\u{1F642}',
5
+ ':-(': '\u{1F641}',
6
+ ':(': '\u{1F641}',
7
+ ':+1': '\u{1F44D}',
8
+ ':-1': '\u{1F44E}'
9
+ };
10
+ const codes = {
11
+ bold: {
12
+ name: 'bold',
13
+ char: '*',
14
+ domEl: 'strong',
15
+ encodedChar: 0x2a,
16
+ // see https://regex101.com/r/evswdV/8 for explanation of regexp
17
+ regexString: '\\B\\*((?!\\s)[^*]+(?:\\b|[^*\\s]))\\*\\B',
18
+ contentFn: val => val
19
+ },
20
+ italic: {
21
+ name: 'italic',
22
+ char: '_',
23
+ domEl: 'em',
24
+ encodedChar: 0x5f,
25
+ // see https://regex101.com/r/p6LpjK/6 for explanation of regexp
26
+ regexString: '\\b_((?!\\s)[^_]+(?:\\B|[^_\\s]))_\\b',
27
+ contentFn: val => val
28
+ },
29
+ emoji: {
30
+ name: 'emoji',
31
+ char: ':',
32
+ domEl: 'span',
33
+ encodedChar: 0x3a,
34
+ regexString: '^(:-\\)|:\\)|:\\(|:-\\(|:\\+1|:-1)',
35
+ contentFn: val => emojiDb[val]
36
+ }
37
+ };
38
+ let linksInText;
39
+ const markerIsInLinkText = pos => linksInText.some(link => pos >= link.index && pos <= link.lastIndex);
40
+ const parse = code => (state, silent) => {
41
+ if (silent) {
42
+ return false;
43
+ }
44
+ const start = state.pos;
45
+
46
+ // skip parsing emphasis if marker is within a link
47
+ if (markerIsInLinkText(start)) {
48
+ return false;
49
+ }
50
+ const marker = state.src.charCodeAt(start);
51
+
52
+ // marker character: "_", "*", ":"
53
+ if (marker !== codes[code].encodedChar) {
54
+ return false;
55
+ }
56
+ const MARKER_REGEX = new RegExp(codes[code].regexString);
57
+ const token = state.src.slice(start);
58
+ if (MARKER_REGEX.test(token)) {
59
+ const markerMatch = token.match(MARKER_REGEX);
60
+
61
+ // skip parsing sections where the marker is not at the start of the token
62
+ if (markerMatch.index !== 0) {
63
+ return false;
64
+ }
65
+ const text = markerMatch[1];
66
+ state.push(`${codes[code].domEl}_open`, codes[code].domEl, 1);
67
+ const t = state.push('text', '', 0);
68
+ t.content = codes[code].contentFn(text);
69
+ state.push(`${codes.bold.domEl}_close`, codes[code].domEl, -1);
70
+ state.pos += markerMatch[0].length;
71
+ return true;
72
+ }
73
+ return false;
74
+ };
75
+ export class MdParser {
76
+ constructor() {
77
+ // disable all rules, enable autolink for URLs and email addresses
78
+ const md = new MarkdownIt('zero', {
79
+ linkify: true,
80
+ breaks: true
81
+ });
82
+
83
+ // *bold* -> <strong>bold</strong>
84
+ md.inline.ruler.push('strong', parse(codes.bold.name));
85
+
86
+ // _italic_ -> <em>italic</em>
87
+ md.inline.ruler.push('italic', parse(codes.italic.name));
88
+
89
+ // :-) :) :-( :( :+1 :-1 -> <span>[unicode]</span>
90
+ md.inline.ruler.push('emoji', parse(codes.emoji.name));
91
+ md.enable(['heading', 'link', 'linkify', 'list', 'newline', 'strong', 'italic', 'emoji']);
92
+ this.md = md;
93
+ }
94
+ render(text) {
95
+ linksInText = this.md.linkify.match(text) || [];
96
+ return this.md.render(text);
97
+ }
98
+ }
@@ -0,0 +1,25 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { useMemo } from 'react';
3
+ import { MdParser } from './MdParser.js';
4
+ export const Parser = _ref => {
5
+ let {
6
+ children,
7
+ style
8
+ } = _ref;
9
+ const MdParserInstance = useMemo(() => new MdParser(), []);
10
+ return children ? /*#__PURE__*/React.createElement("div", {
11
+ style: {
12
+ ...style
13
+ },
14
+ dangerouslySetInnerHTML: {
15
+ __html: MdParserInstance.render(children)
16
+ }
17
+ }) : null;
18
+ };
19
+ Parser.defaultProps = {
20
+ style: null
21
+ };
22
+ Parser.propTypes = {
23
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
24
+ style: PropTypes.object
25
+ };
@@ -0,0 +1,40 @@
1
+ import { MdParser } from '../MdParser.js';
2
+ const Parser = new MdParser();
3
+ describe('MdParser class', () => {
4
+ it('converts text into HTML', () => {
5
+ const inlineTests = [['_italic_', '<em>italic</em>'], ['*bold*', '<strong>bold</strong>'], ['_ not italic because there is a space _', '_ not italic because there is a space _'], [':-)', '<span>\u{1F642}</span>'], [':)', '<span>\u{1F642}</span>'], [':-(', '<span>\u{1F641}</span>'], [':(', '<span>\u{1F641}</span>'], [':+1', '<span>\u{1F44D}</span>'], [':-1', '<span>\u{1F44E}</span>'], ['mixed _italic_ *bold* and :+1', 'mixed <em>italic</em> <strong>bold</strong> and <span>\u{1F44D}</span>'], ['_italic with * inside_', '<em>italic with * inside</em>'], ['*bold with _ inside*', '<strong>bold with _ inside</strong>'],
6
+ // italic marker followed by : should work
7
+ ['_italic_:', '<em>italic</em>:'], ['_italic_: some text, *bold*: some other text', '<em>italic</em>: some text, <strong>bold</strong>: some other text'],
8
+ // bold marker followed by : should work
9
+ ['*bold*:', '<strong>bold</strong>:'], ['*bold*: some text, _italic_: some other text', '<strong>bold</strong>: some text, <em>italic</em>: some other text'],
10
+ // italic marker inside an italic string not allowed
11
+ ['_italic with _ inside_', '_italic with _ inside_'],
12
+ // bold marker inside a bold string not allowed
13
+ ['*bold with * inside*', '*bold with * inside*'], ['_multiple_ italic in the _same line_', '<em>multiple</em> italic in the <em>same line</em>'],
14
+ // nested italic/bold combinations not allowed
15
+ ['_italic with *bold* inside_', '<em>italic with *bold* inside</em>'], ['*bold with _italic_ inside*', '<strong>bold with _italic_ inside</strong>'], ['text with : and :)', 'text with : and <span>\u{1F642}</span>'], ['(parenthesis and :))', '(parenthesis and <span>\u{1F642}</span>)'], [':((parenthesis:))', '<span>\u{1F641}</span>(parenthesis<span>\u{1F642}</span>)'], [':+1+1', '<span>\u{1F44D}</span>+1'], ['-1:-1', '-1<span>\u{1F44E}</span>'],
16
+ // links
17
+ ['example.com/path', '<a href="http://example.com/path">example.com/path</a>'],
18
+ // not recognized links with italic marker inside not converted
19
+ ['example_with_underscore.com/path', 'example_with_underscore.com/path'], ['example_with_underscore.com/path_with_underscore', 'example_with_underscore.com/path_with_underscore'],
20
+ // markers around non-recognized links
21
+ ['link example_with_underscore.com/path should _not_ be converted', 'link example_with_underscore.com/path should <em>not</em> be converted'], ['link example_with_underscore.com/path should *not* be converted', 'link example_with_underscore.com/path should <strong>not</strong> be converted'],
22
+ // italic marker inside links not converted
23
+ ['example.com/path_with_underscore', '<a href="http://example.com/path_with_underscore">example.com/path_with_underscore</a>'], ['_italic_ and *bold* with a example.com/link_with_underscore', '<em>italic</em> and <strong>bold</strong> with a <a href="http://example.com/link_with_underscore">example.com/link_with_underscore</a>'], ['example.com/path with *bold* after :)', '<a href="http://example.com/path">example.com/path</a> with <strong>bold</strong> after <span>\u{1F642}</span>'], ['_before_ example.com/path_with_underscore *after* :)', '<em>before</em> <a href="http://example.com/path_with_underscore">example.com/path_with_underscore</a> <strong>after</strong> <span>\u{1F642}</span>'],
24
+ // italic/bold markers right after non-word characters
25
+ ['_If % of ART retention rate after 12 months >90(%)_: Sustain the efforts.', '<em>If % of ART retention rate after 12 months &gt;90(%)</em>: Sustain the efforts.'], ['*If % of ART retention rate after 12 months >90(%)*: Sustain the efforts.', '<strong>If % of ART retention rate after 12 months &gt;90(%)</strong>: Sustain the efforts.']];
26
+ inlineTests.forEach(test => {
27
+ const renderedText = Parser.render(test[0]);
28
+ expect(renderedText).toEqual(`<p>${test[1]}</p>\n`);
29
+ });
30
+ const blockTests = [
31
+ // heading
32
+ ['# Heading 1', '<h1>Heading 1</h1>'], ['## Heading 2', '<h2>Heading 2</h2>'], ['### Heading 3', '<h3>Heading 3</h3>'], ['#### Heading 4', '<h4>Heading 4</h4>'], ['##### Heading 5', '<h5>Heading 5</h5>'], ['###### Heading 6', '<h6>Heading 6</h6>'], ['# *Bold head*', '<h1><strong>Bold head</strong></h1>'], ['## _Italic title_', '<h2><em>Italic title</em></h2>'], ['### *Bold* and _italic_ title', '<h3><strong>Bold</strong> and <em>italic</em> title</h3>'],
33
+ // lists
34
+ ['* first\n* second\n* third', '<ul>\n<li>first</li>\n<li>second</li>\n<li>third</li>\n</ul>'], ['1. one\n1. two\n1. three\n', '<ol>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ol>'], ['* *first*\n* second\n* _third_', '<ul>\n<li><strong>first</strong></li>\n<li>second</li>\n<li><em>third</em></li>\n</ul>']];
35
+ blockTests.forEach(test => {
36
+ const renderedText = Parser.render(test[0]);
37
+ expect(renderedText).toEqual(`${test[1]}\n`);
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,38 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+ import { Parser } from '../Parser.js';
4
+ jest.mock('../MdParser.js', () => ({
5
+ MdParser: jest.fn().mockImplementation(() => {
6
+ return {
7
+ render: () => 'converted text'
8
+ };
9
+ })
10
+ }));
11
+ describe('RichText: Parser component', () => {
12
+ let richTextParser;
13
+ const defaultProps = {
14
+ style: {
15
+ color: 'blue',
16
+ whiteSpace: 'pre-line'
17
+ }
18
+ };
19
+ const renderComponent = (props, text) => {
20
+ return shallow( /*#__PURE__*/React.createElement(Parser, props, text));
21
+ };
22
+ it('should have rendered a result', () => {
23
+ richTextParser = renderComponent({}, 'test');
24
+ expect(richTextParser).toHaveLength(1);
25
+ });
26
+ it('should have rendered a result with the style prop', () => {
27
+ richTextParser = renderComponent(defaultProps, 'test prop');
28
+ expect(richTextParser.props().style).toEqual(defaultProps.style);
29
+ });
30
+ it('should have rendered content', () => {
31
+ richTextParser = renderComponent({}, 'plain text');
32
+ expect(richTextParser.html()).toEqual('<div>converted text</div>');
33
+ });
34
+ it('should return null if no children is passed', () => {
35
+ richTextParser = renderComponent({}, undefined);
36
+ expect(richTextParser.html()).toBe(null);
37
+ });
38
+ });
@@ -0,0 +1,3 @@
1
+ export { Editor as RichTextEditor } from './Editor/Editor.js';
2
+ export { Parser as RichTextParser } from './Parser/Parser.js';
3
+ export { MdParser as RichTextMdParser } from './Parser/MdParser.js';