@dhis2/analytics 999.9.9-loadflash-alpha.1 → 999.9.9-outlier-table.alpha.2

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 (93) hide show
  1. package/build/cjs/__fixtures__/fixtures.js +1 -0
  2. package/build/cjs/__fixtures__/json/api/analytics/outlierDetection.json +213 -0
  3. package/build/cjs/api/analytics/AnalyticsAggregate.js +27 -1
  4. package/build/cjs/api/analytics/AnalyticsBase.js +8 -7
  5. package/build/cjs/api/analytics/AnalyticsRequestBase.js +9 -5
  6. package/build/cjs/api/analytics/AnalyticsResponse.js +42 -39
  7. package/build/cjs/api/analytics/__tests__/Analytics.spec.js +5 -0
  8. package/build/cjs/api/analytics/__tests__/AnalyticsAggregate.spec.js +29 -0
  9. package/build/cjs/api/analytics/__tests__/AnalyticsBase.spec.js +36 -2
  10. package/build/cjs/api/analytics/__tests__/AnalyticsTrackedEntities.spec.js +44 -0
  11. package/build/cjs/api/analytics/__tests__/__snapshots__/AnalyticsTrackedEntities.spec.js.snap +3 -0
  12. package/build/cjs/components/DataDimension/DataDimension.js +31 -7
  13. package/build/cjs/components/DataDimension/DataTypeSelector.js +17 -8
  14. package/build/cjs/components/DataDimension/GroupSelector.js +7 -7
  15. package/build/cjs/components/DataDimension/ItemSelector.js +74 -56
  16. package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +6 -1
  17. package/build/cjs/components/Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js +2 -2
  18. package/build/cjs/components/PeriodDimension/PeriodDimension.js +5 -2
  19. package/build/cjs/components/PeriodDimension/PeriodTransfer.js +57 -28
  20. package/build/cjs/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap +1 -1
  21. package/build/cjs/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap +1 -12
  22. package/build/cjs/components/RichText/Editor.bk/Editor.js +40 -0
  23. package/build/cjs/components/RichText/Editor.bk/__tests__/Editor.spec.js +29 -0
  24. package/build/cjs/components/RichText/Editor.bk/__tests__/convertCtrlKey.spec.js +205 -0
  25. package/build/cjs/components/RichText/Editor.bk/convertCtrlKey.js +87 -0
  26. package/build/cjs/components/RichText/Parser.bk/MdParser.js +107 -0
  27. package/build/cjs/components/RichText/Parser.bk/Parser.js +34 -0
  28. package/build/cjs/components/RichText/Parser.bk/__tests__/MdParser.spec.js +34 -0
  29. package/build/cjs/components/RichText/Parser.bk/__tests__/Parser.spec.js +41 -0
  30. package/build/cjs/components/Toolbar/HoverMenuBar/HoverMenuDropdown.js +3 -1
  31. package/build/cjs/components/Toolbar/HoverMenuBar/__tests__/HoverMenuDropdown.spec.js +8 -0
  32. package/build/cjs/components/VisTypeIcon.js +6 -1
  33. package/build/cjs/index.js +43 -1
  34. package/build/cjs/locales/en/translations.json +5 -1
  35. package/build/cjs/locales/sv/translations.json +8 -2
  36. package/build/cjs/modules/__tests__/getAdaptedUiLayoutByType.spec.js +15 -0
  37. package/build/cjs/modules/axis.js +4 -0
  38. package/build/cjs/modules/getAdaptedUiLayoutByType.js +9 -0
  39. package/build/cjs/modules/layoutTypes.js +4 -2
  40. package/build/cjs/modules/layoutUiRules/__tests__/rules.spec.js +12 -1
  41. package/build/cjs/modules/layoutUiRules/index.js +12 -0
  42. package/build/cjs/modules/layoutUiRules/rules.js +22 -2
  43. package/build/cjs/modules/layoutUiRules/rulesHelper.js +4 -2
  44. package/build/cjs/modules/layoutUiRules/rulesUtils.js +6 -1
  45. package/build/cjs/modules/visTypeToLayoutType.js +2 -1
  46. package/build/cjs/modules/visTypes.js +9 -3
  47. package/build/es/__fixtures__/fixtures.js +1 -0
  48. package/build/es/__fixtures__/json/api/analytics/outlierDetection.json +213 -0
  49. package/build/es/api/analytics/AnalyticsAggregate.js +27 -1
  50. package/build/es/api/analytics/AnalyticsBase.js +7 -7
  51. package/build/es/api/analytics/AnalyticsRequestBase.js +9 -5
  52. package/build/es/api/analytics/AnalyticsResponse.js +42 -39
  53. package/build/es/api/analytics/__tests__/Analytics.spec.js +5 -0
  54. package/build/es/api/analytics/__tests__/AnalyticsAggregate.spec.js +29 -0
  55. package/build/es/api/analytics/__tests__/AnalyticsBase.spec.js +34 -1
  56. package/build/es/api/analytics/__tests__/AnalyticsTrackedEntities.spec.js +41 -0
  57. package/build/es/api/analytics/__tests__/__snapshots__/AnalyticsTrackedEntities.spec.js.snap +3 -0
  58. package/build/es/components/DataDimension/DataDimension.js +27 -6
  59. package/build/es/components/DataDimension/DataTypeSelector.js +18 -9
  60. package/build/es/components/DataDimension/GroupSelector.js +7 -7
  61. package/build/es/components/DataDimension/ItemSelector.js +75 -57
  62. package/build/es/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +6 -1
  63. package/build/es/components/Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js +2 -2
  64. package/build/es/components/PeriodDimension/PeriodDimension.js +5 -2
  65. package/build/es/components/PeriodDimension/PeriodTransfer.js +58 -29
  66. package/build/es/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap +1 -1
  67. package/build/es/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap +1 -12
  68. package/build/es/components/RichText/Editor.bk/Editor.js +30 -0
  69. package/build/es/components/RichText/Editor.bk/__tests__/Editor.spec.js +26 -0
  70. package/build/es/components/RichText/Editor.bk/__tests__/convertCtrlKey.spec.js +202 -0
  71. package/build/es/components/RichText/Editor.bk/convertCtrlKey.js +80 -0
  72. package/build/es/components/RichText/Parser.bk/MdParser.js +99 -0
  73. package/build/es/components/RichText/Parser.bk/Parser.js +24 -0
  74. package/build/es/components/RichText/Parser.bk/__tests__/MdParser.spec.js +31 -0
  75. package/build/es/components/RichText/Parser.bk/__tests__/Parser.spec.js +38 -0
  76. package/build/es/components/Toolbar/HoverMenuBar/HoverMenuDropdown.js +3 -1
  77. package/build/es/components/Toolbar/HoverMenuBar/__tests__/HoverMenuDropdown.spec.js +8 -0
  78. package/build/es/components/VisTypeIcon.js +8 -3
  79. package/build/es/index.js +4 -4
  80. package/build/es/locales/en/translations.json +5 -1
  81. package/build/es/locales/sv/translations.json +8 -2
  82. package/build/es/modules/__tests__/getAdaptedUiLayoutByType.spec.js +16 -1
  83. package/build/es/modules/axis.js +5 -1
  84. package/build/es/modules/getAdaptedUiLayoutByType.js +10 -1
  85. package/build/es/modules/layoutTypes.js +2 -1
  86. package/build/es/modules/layoutUiRules/__tests__/rules.spec.js +13 -2
  87. package/build/es/modules/layoutUiRules/index.js +2 -2
  88. package/build/es/modules/layoutUiRules/rules.js +22 -3
  89. package/build/es/modules/layoutUiRules/rulesHelper.js +3 -2
  90. package/build/es/modules/layoutUiRules/rulesUtils.js +5 -1
  91. package/build/es/modules/visTypeToLayoutType.js +4 -3
  92. package/build/es/modules/visTypes.js +7 -3
  93. package/package.json +6 -3
@@ -1,7 +1,7 @@
1
1
  import _JSXStyle from "styled-jsx/style";
2
2
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
3
3
  import { getNowInCalendar } from '@dhis2/multi-calendar-dates';
4
- import { TabBar, Tab, Transfer } from '@dhis2/ui';
4
+ import { IconInfo16, TabBar, Tab, Transfer } from '@dhis2/ui';
5
5
  import PropTypes from 'prop-types';
6
6
  import React, { useState } from 'react';
7
7
  import PeriodIcon from '../../assets/DimensionItemIcons/PeriodIcon.js'; //TODO: Reimplement the icon.js
@@ -14,15 +14,35 @@ import RelativePeriodFilter from './RelativePeriodFilter.js';
14
14
  import { getFixedPeriodsOptionsById } from './utils/fixedPeriods.js';
15
15
  import { MONTHLY, QUARTERLY } from './utils/index.js';
16
16
  import { getRelativePeriodsOptionsById } from './utils/relativePeriods.js';
17
- const PeriodTransfer = _ref => {
17
+ const RightHeader = _ref => {
18
+ let {
19
+ infoText
20
+ } = _ref;
21
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("p", {
22
+ className: `jsx-${styles.__hash}` + " " + "rightHeader"
23
+ }, i18n.t('Selected Periods')), infoText && /*#__PURE__*/React.createElement("div", {
24
+ className: `jsx-${styles.__hash}` + " " + "info-container"
25
+ }, /*#__PURE__*/React.createElement("div", {
26
+ className: `jsx-${styles.__hash}`
27
+ }, /*#__PURE__*/React.createElement(IconInfo16, null)), /*#__PURE__*/React.createElement("span", {
28
+ className: `jsx-${styles.__hash}` + " " + "info-text"
29
+ }, infoText)), /*#__PURE__*/React.createElement(_JSXStyle, {
30
+ id: styles.__hash
31
+ }, styles));
32
+ };
33
+ RightHeader.propTypes = {
34
+ infoText: PropTypes.string
35
+ };
36
+ const PeriodTransfer = _ref2 => {
18
37
  let {
19
38
  onSelect,
20
39
  dataTest,
21
- initialSelectedPeriods,
40
+ selectedItems,
22
41
  rightFooter,
23
42
  excludedPeriodTypes,
24
- periodsSettings
25
- } = _ref;
43
+ periodsSettings,
44
+ infoBoxMessage
45
+ } = _ref2;
26
46
  const defaultRelativePeriodType = excludedPeriodTypes.includes(MONTHLY) ? getRelativePeriodsOptionsById(QUARTERLY) : getRelativePeriodsOptionsById(MONTHLY);
27
47
  const defaultFixedPeriodType = excludedPeriodTypes.includes(MONTHLY) ? getFixedPeriodsOptionsById(QUARTERLY, periodsSettings) : getFixedPeriodsOptionsById(MONTHLY, periodsSettings);
28
48
  const now = getNowInCalendar(periodsSettings.calendar);
@@ -35,7 +55,6 @@ const PeriodTransfer = _ref => {
35
55
  reversePeriods: false
36
56
  });
37
57
  const [allPeriods, setAllPeriods] = useState(defaultRelativePeriodType.getPeriods());
38
- const [selectedPeriods, setSelectedPeriods] = useState(initialSelectedPeriods);
39
58
  const [isRelative, setIsRelative] = useState(true);
40
59
  const [relativeFilter, setRelativeFilter] = useState({
41
60
  periodType: defaultRelativePeriodType.id
@@ -44,6 +63,10 @@ const PeriodTransfer = _ref => {
44
63
  periodType: defaultFixedPeriodType.id,
45
64
  year: defaultFixedPeriodYear.toString()
46
65
  });
66
+ const isActive = value => {
67
+ const item = selectedItems.find(item => item.id === value);
68
+ return !item || item.isActive;
69
+ };
47
70
  const onIsRelativeClick = state => {
48
71
  if (state !== isRelative) {
49
72
  setIsRelative(state);
@@ -90,11 +113,6 @@ const PeriodTransfer = _ref => {
90
113
  })), /*#__PURE__*/React.createElement(_JSXStyle, {
91
114
  id: styles.__hash
92
115
  }, styles));
93
- const renderRightHeader = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("p", {
94
- className: `jsx-${styles.__hash}` + " " + "rightHeader"
95
- }, i18n.t('Selected Periods')), /*#__PURE__*/React.createElement(_JSXStyle, {
96
- id: styles.__hash
97
- }, styles));
98
116
  const onSelectFixedPeriods = filter => {
99
117
  setFixedFilter(filter);
100
118
  setAllPeriods(getFixedPeriodsOptionsById(filter.periodType, periodsSettings).getPeriods(fixedPeriodConfig(Number(filter.year)), periodsSettings));
@@ -105,45 +123,54 @@ const PeriodTransfer = _ref => {
105
123
  id: styles.__hash
106
124
  }, styles));
107
125
  return /*#__PURE__*/React.createElement(Transfer, {
108
- onChange: _ref2 => {
126
+ onChange: _ref3 => {
109
127
  let {
110
128
  selected
111
- } = _ref2;
112
- const formattedItems = selected.map(id => ({
113
- id,
114
- name: [...allPeriods, ...selectedPeriods].find(item => item.id === id).name
115
- }));
116
- setSelectedPeriods(formattedItems);
129
+ } = _ref3;
130
+ const formattedItems = selected.map(id => {
131
+ const matchingItem = [...allPeriods, ...selectedItems].find(item => item.id === id);
132
+ return {
133
+ id,
134
+ name: matchingItem.name,
135
+ isActive: matchingItem.isActive
136
+ };
137
+ });
117
138
  onSelect(formattedItems);
118
139
  },
119
- selected: selectedPeriods.map(period => period.id),
140
+ selected: selectedItems.map(period => period.id),
120
141
  leftHeader: renderLeftHeader(),
121
142
  enableOrderChange: true,
122
143
  height: TRANSFER_HEIGHT,
123
144
  optionsWidth: TRANSFER_OPTIONS_WIDTH,
124
145
  selectedWidth: TRANSFER_SELECTED_WIDTH,
125
146
  selectedEmptyComponent: renderEmptySelection(),
126
- rightHeader: renderRightHeader(),
147
+ rightHeader: /*#__PURE__*/React.createElement(RightHeader, {
148
+ infoText: infoBoxMessage
149
+ }),
127
150
  rightFooter: rightFooter,
128
- options: [...allPeriods, ...selectedPeriods].map(_ref3 => {
151
+ options: [...allPeriods, ...selectedItems].map(_ref4 => {
129
152
  let {
130
153
  id,
131
154
  name
132
- } = _ref3;
155
+ } = _ref4;
133
156
  return {
134
157
  label: name,
135
158
  value: id
136
159
  };
137
160
  }),
138
- renderOption: props => /*#__PURE__*/React.createElement(TransferOption, _extends({}, props, {
161
+ renderOption: props => /*#__PURE__*/React.createElement(TransferOption
162
+ /* eslint-disable react/prop-types */, _extends({}, props, {
163
+ active: isActive(props.value),
139
164
  icon: PeriodIcon,
140
165
  dataTest: `${dataTest}-transfer-option`
166
+ /* eslint-enable react/prop-types */
141
167
  })),
168
+
142
169
  dataTest: `${dataTest}-transfer`
143
170
  });
144
171
  };
145
172
  PeriodTransfer.defaultProps = {
146
- initialSelectedPeriods: [],
173
+ selectedItems: [],
147
174
  excludedPeriodTypes: [],
148
175
  periodsSettings: {
149
176
  calendar: 'gregory',
@@ -154,14 +181,16 @@ PeriodTransfer.propTypes = {
154
181
  onSelect: PropTypes.func.isRequired,
155
182
  dataTest: PropTypes.string,
156
183
  excludedPeriodTypes: PropTypes.arrayOf(PropTypes.string),
157
- initialSelectedPeriods: PropTypes.arrayOf(PropTypes.shape({
158
- id: PropTypes.string,
159
- name: PropTypes.string
160
- })),
184
+ infoBoxMessage: PropTypes.string,
161
185
  periodsSettings: PropTypes.shape({
162
186
  calendar: PropTypes.string,
163
187
  locale: PropTypes.string
164
188
  }),
165
- rightFooter: PropTypes.node
189
+ rightFooter: PropTypes.node,
190
+ selectedItems: PropTypes.arrayOf(PropTypes.shape({
191
+ id: PropTypes.string,
192
+ isActive: PropTypes.bool,
193
+ name: PropTypes.string
194
+ }))
166
195
  };
167
196
  export default PeriodTransfer;
@@ -4,7 +4,6 @@ exports[`The Period Dimension component matches the snapshot 1`] = `
4
4
  <PeriodTransfer
5
5
  dataTest="period-dimension"
6
6
  excludedPeriodTypes={Array []}
7
- initialSelectedPeriods={Array []}
8
7
  onSelect={[Function]}
9
8
  periodsSettings={
10
9
  Object {
@@ -13,5 +12,6 @@ exports[`The Period Dimension component matches the snapshot 1`] = `
13
12
  }
14
13
  }
15
14
  rightFooter={<React.Fragment />}
15
+ selectedItems={Array []}
16
16
  />
17
17
  `;
@@ -77,18 +77,7 @@ exports[`The Period Selector component matches the snapshot 1`] = `
77
77
  optionsWidth="420px"
78
78
  renderOption={[Function]}
79
79
  rightFooter={<React.Fragment />}
80
- rightHeader={
81
- <React.Fragment>
82
- <p
83
- className="rightHeader"
84
- >
85
- Selected Periods
86
- </p>
87
- <style>
88
-
89
- </style>
90
- </React.Fragment>
91
- }
80
+ rightHeader={<RightHeader />}
92
81
  selected={Array []}
93
82
  selectedEmptyComponent={
94
83
  <React.Fragment>
@@ -0,0 +1,30 @@
1
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
2
+ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
3
+ function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
4
+ import PropTypes from 'prop-types';
5
+ import React, { Component } from 'react';
6
+ import convertCtrlKey from './convertCtrlKey.js';
7
+ class Editor extends Component {
8
+ constructor() {
9
+ super(...arguments);
10
+ _defineProperty(this, "onKeyDown", event => {
11
+ convertCtrlKey(event, this.props.onEdit);
12
+ });
13
+ }
14
+ render() {
15
+ const {
16
+ children
17
+ } = this.props;
18
+ return /*#__PURE__*/React.createElement("div", {
19
+ onKeyDown: this.onKeyDown
20
+ }, children);
21
+ }
22
+ }
23
+ Editor.defaultProps = {
24
+ onEdit: null
25
+ };
26
+ Editor.propTypes = {
27
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
28
+ onEdit: PropTypes.func
29
+ };
30
+ export default Editor;
@@ -0,0 +1,26 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+ import convertCtrlKey from '../convertCtrlKey.js';
4
+ import Editor from '../Editor.js';
5
+ jest.mock('../convertCtrlKey');
6
+ describe('RichText: Editor component', () => {
7
+ let richTextEditor;
8
+ const componentProps = {
9
+ onEdit: jest.fn()
10
+ };
11
+ beforeEach(() => {
12
+ convertCtrlKey.mockClear();
13
+ });
14
+ const renderComponent = props => {
15
+ return shallow( /*#__PURE__*/React.createElement(Editor, props, /*#__PURE__*/React.createElement("input", null)));
16
+ };
17
+ it('renders a result', () => {
18
+ richTextEditor = renderComponent(componentProps);
19
+ expect(richTextEditor).toHaveLength(1);
20
+ });
21
+ it('calls convertCtrlKey on keydown', () => {
22
+ richTextEditor = renderComponent(componentProps);
23
+ richTextEditor.simulate('keyDown');
24
+ expect(convertCtrlKey).toHaveBeenCalled();
25
+ });
26
+ });
@@ -0,0 +1,202 @@
1
+ import convertCtrlKey from '../convertCtrlKey.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
+ });
@@ -0,0 +1,80 @@
1
+ const state = {
2
+ boldMode: false,
3
+ italicMode: false,
4
+ element: null
5
+ };
6
+ const markerMap = {
7
+ italic: '_',
8
+ bold: '*'
9
+ };
10
+ const trim = str => {
11
+ const leftSpaces = /^\s+/;
12
+ const rightSpaces = /\s+$/;
13
+ return str.replace(leftSpaces, '').replace(rightSpaces, '');
14
+ };
15
+ const toggleMode = mode => {
16
+ const prop = `${mode}Mode`;
17
+ state[prop] = !state[prop];
18
+ };
19
+ const insertMarkers = (mode, cb) => {
20
+ const {
21
+ selectionStart: start,
22
+ selectionEnd: end,
23
+ value
24
+ } = state.element;
25
+ const marker = markerMap[mode] || null;
26
+ if (!marker || !cb || start < 0) {
27
+ return;
28
+ }
29
+ toggleMode(mode);
30
+ let newValue;
31
+ let caretPos = end + 1;
32
+ const padMarkers = text => {
33
+ // is caret between two markers (i.e., "**" or "__")? Then do not add padding
34
+ if (start === end && value.length && start > 0) {
35
+ if (value[start - 1] === markerMap.bold && value[start] === markerMap.bold || value[start - 1] === markerMap.italic && value[start] === markerMap.italic) {
36
+ return text;
37
+ }
38
+ }
39
+ if (value.length && start > 0 && value[start - 1] !== ' ') {
40
+ text = ` ${text}`;
41
+ ++caretPos;
42
+ }
43
+ if (value.length && end !== value.length && value[end] !== ' ') {
44
+ text = `${text} `;
45
+ }
46
+ return text;
47
+ };
48
+ if (start === end) {
49
+ //no text
50
+ const valueArr = value.split('');
51
+ valueArr.splice(start, 0, padMarkers(`${marker}${marker}`));
52
+ newValue = valueArr.join('');
53
+ } else {
54
+ const text = value.slice(start, end);
55
+ const trimmedText = trim(text);
56
+
57
+ // adjust caretPos based on trimmed text selection
58
+ caretPos = caretPos - (text.length - trimmedText.length) + 1;
59
+ newValue = [value.slice(0, start), padMarkers(`${marker}${trimmedText}${marker}`), value.slice(end)].join('');
60
+ toggleMode(mode);
61
+ }
62
+ cb(newValue, caretPos);
63
+ };
64
+ const convertCtrlKey = (event, cb) => {
65
+ const {
66
+ key,
67
+ ctrlKey,
68
+ metaKey
69
+ } = event;
70
+ const element = event.target;
71
+ state.element = element;
72
+ if (key === 'b' && (ctrlKey || metaKey)) {
73
+ event.preventDefault();
74
+ insertMarkers('bold', cb);
75
+ } else if (key === 'i' && (ctrlKey || metaKey)) {
76
+ event.preventDefault();
77
+ insertMarkers('italic', cb);
78
+ }
79
+ };
80
+ export default convertCtrlKey;
@@ -0,0 +1,99 @@
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 md;
39
+ let linksInText;
40
+ const markerIsInLinkText = pos => linksInText.some(link => pos >= link.index && pos <= link.lastIndex);
41
+ const parse = code => (state, silent) => {
42
+ if (silent) {
43
+ return false;
44
+ }
45
+ const start = state.pos;
46
+
47
+ // skip parsing emphasis if marker is within a link
48
+ if (markerIsInLinkText(start)) {
49
+ return false;
50
+ }
51
+ const marker = state.src.charCodeAt(start);
52
+
53
+ // marker character: "_", "*", ":"
54
+ if (marker !== codes[code].encodedChar) {
55
+ return false;
56
+ }
57
+ const MARKER_REGEX = new RegExp(codes[code].regexString);
58
+ const token = state.src.slice(start);
59
+ if (MARKER_REGEX.test(token)) {
60
+ const markerMatch = token.match(MARKER_REGEX);
61
+
62
+ // skip parsing sections where the marker is not at the start of the token
63
+ if (markerMatch.index !== 0) {
64
+ return false;
65
+ }
66
+ const text = markerMatch[1];
67
+ state.push(`${codes[code].domEl}_open`, codes[code].domEl, 1);
68
+ const t = state.push('text', '', 0);
69
+ t.content = codes[code].contentFn(text);
70
+ state.push(`${codes.bold.domEl}_close`, codes[code].domEl, -1);
71
+ state.pos += markerMatch[0].length;
72
+ return true;
73
+ }
74
+ return false;
75
+ };
76
+ class MdParser {
77
+ constructor() {
78
+ // disable all rules, enable autolink for URLs and email addresses
79
+ md = new MarkdownIt('zero', {
80
+ linkify: 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(['link', 'linkify', 'strong', 'italic', 'emoji']);
92
+ return this;
93
+ }
94
+ render(text) {
95
+ linksInText = md.linkify.match(text) || [];
96
+ return md.renderInline(text);
97
+ }
98
+ }
99
+ export default MdParser;
@@ -0,0 +1,24 @@
1
+ import PropTypes from 'prop-types';
2
+ import React, { useMemo } from 'react';
3
+ import MdParserClass from './MdParser.js';
4
+ const Parser = _ref => {
5
+ let {
6
+ children,
7
+ style
8
+ } = _ref;
9
+ const MdParser = useMemo(() => new MdParserClass(), []);
10
+ return children ? /*#__PURE__*/React.createElement("p", {
11
+ style: style,
12
+ dangerouslySetInnerHTML: {
13
+ __html: MdParser.render(children)
14
+ }
15
+ }) : null;
16
+ };
17
+ Parser.defaultProps = {
18
+ style: null
19
+ };
20
+ Parser.propTypes = {
21
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
22
+ style: PropTypes.object
23
+ };
24
+ export default Parser;