@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.
- package/build/cjs/__fixtures__/fixtures.js +1 -0
- package/build/cjs/__fixtures__/json/api/analytics/outlierDetection.json +213 -0
- package/build/cjs/api/analytics/AnalyticsAggregate.js +27 -1
- package/build/cjs/api/analytics/AnalyticsBase.js +8 -7
- package/build/cjs/api/analytics/AnalyticsRequestBase.js +9 -5
- package/build/cjs/api/analytics/AnalyticsResponse.js +42 -39
- package/build/cjs/api/analytics/__tests__/Analytics.spec.js +5 -0
- package/build/cjs/api/analytics/__tests__/AnalyticsAggregate.spec.js +29 -0
- package/build/cjs/api/analytics/__tests__/AnalyticsBase.spec.js +36 -2
- package/build/cjs/api/analytics/__tests__/AnalyticsTrackedEntities.spec.js +44 -0
- package/build/cjs/api/analytics/__tests__/__snapshots__/AnalyticsTrackedEntities.spec.js.snap +3 -0
- package/build/cjs/components/DataDimension/DataDimension.js +31 -7
- package/build/cjs/components/DataDimension/DataTypeSelector.js +17 -8
- package/build/cjs/components/DataDimension/GroupSelector.js +7 -7
- package/build/cjs/components/DataDimension/ItemSelector.js +74 -56
- package/build/cjs/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +6 -1
- package/build/cjs/components/Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js +2 -2
- package/build/cjs/components/PeriodDimension/PeriodDimension.js +5 -2
- package/build/cjs/components/PeriodDimension/PeriodTransfer.js +57 -28
- package/build/cjs/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap +1 -1
- package/build/cjs/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap +1 -12
- package/build/cjs/components/RichText/Editor.bk/Editor.js +40 -0
- package/build/cjs/components/RichText/Editor.bk/__tests__/Editor.spec.js +29 -0
- package/build/cjs/components/RichText/Editor.bk/__tests__/convertCtrlKey.spec.js +205 -0
- package/build/cjs/components/RichText/Editor.bk/convertCtrlKey.js +87 -0
- package/build/cjs/components/RichText/Parser.bk/MdParser.js +107 -0
- package/build/cjs/components/RichText/Parser.bk/Parser.js +34 -0
- package/build/cjs/components/RichText/Parser.bk/__tests__/MdParser.spec.js +34 -0
- package/build/cjs/components/RichText/Parser.bk/__tests__/Parser.spec.js +41 -0
- package/build/cjs/components/Toolbar/HoverMenuBar/HoverMenuDropdown.js +3 -1
- package/build/cjs/components/Toolbar/HoverMenuBar/__tests__/HoverMenuDropdown.spec.js +8 -0
- package/build/cjs/components/VisTypeIcon.js +6 -1
- package/build/cjs/index.js +43 -1
- package/build/cjs/locales/en/translations.json +5 -1
- package/build/cjs/locales/sv/translations.json +8 -2
- package/build/cjs/modules/__tests__/getAdaptedUiLayoutByType.spec.js +15 -0
- package/build/cjs/modules/axis.js +4 -0
- package/build/cjs/modules/getAdaptedUiLayoutByType.js +9 -0
- package/build/cjs/modules/layoutTypes.js +4 -2
- package/build/cjs/modules/layoutUiRules/__tests__/rules.spec.js +12 -1
- package/build/cjs/modules/layoutUiRules/index.js +12 -0
- package/build/cjs/modules/layoutUiRules/rules.js +22 -2
- package/build/cjs/modules/layoutUiRules/rulesHelper.js +4 -2
- package/build/cjs/modules/layoutUiRules/rulesUtils.js +6 -1
- package/build/cjs/modules/visTypeToLayoutType.js +2 -1
- package/build/cjs/modules/visTypes.js +9 -3
- package/build/es/__fixtures__/fixtures.js +1 -0
- package/build/es/__fixtures__/json/api/analytics/outlierDetection.json +213 -0
- package/build/es/api/analytics/AnalyticsAggregate.js +27 -1
- package/build/es/api/analytics/AnalyticsBase.js +7 -7
- package/build/es/api/analytics/AnalyticsRequestBase.js +9 -5
- package/build/es/api/analytics/AnalyticsResponse.js +42 -39
- package/build/es/api/analytics/__tests__/Analytics.spec.js +5 -0
- package/build/es/api/analytics/__tests__/AnalyticsAggregate.spec.js +29 -0
- package/build/es/api/analytics/__tests__/AnalyticsBase.spec.js +34 -1
- package/build/es/api/analytics/__tests__/AnalyticsTrackedEntities.spec.js +41 -0
- package/build/es/api/analytics/__tests__/__snapshots__/AnalyticsTrackedEntities.spec.js.snap +3 -0
- package/build/es/components/DataDimension/DataDimension.js +27 -6
- package/build/es/components/DataDimension/DataTypeSelector.js +18 -9
- package/build/es/components/DataDimension/GroupSelector.js +7 -7
- package/build/es/components/DataDimension/ItemSelector.js +75 -57
- package/build/es/components/Interpretations/InterpretationsUnit/InterpretationsUnit.js +6 -1
- package/build/es/components/Interpretations/common/RichTextEditor/styles/RichTextEditor.style.js +2 -2
- package/build/es/components/PeriodDimension/PeriodDimension.js +5 -2
- package/build/es/components/PeriodDimension/PeriodTransfer.js +58 -29
- package/build/es/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap +1 -1
- package/build/es/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap +1 -12
- package/build/es/components/RichText/Editor.bk/Editor.js +30 -0
- package/build/es/components/RichText/Editor.bk/__tests__/Editor.spec.js +26 -0
- package/build/es/components/RichText/Editor.bk/__tests__/convertCtrlKey.spec.js +202 -0
- package/build/es/components/RichText/Editor.bk/convertCtrlKey.js +80 -0
- package/build/es/components/RichText/Parser.bk/MdParser.js +99 -0
- package/build/es/components/RichText/Parser.bk/Parser.js +24 -0
- package/build/es/components/RichText/Parser.bk/__tests__/MdParser.spec.js +31 -0
- package/build/es/components/RichText/Parser.bk/__tests__/Parser.spec.js +38 -0
- package/build/es/components/Toolbar/HoverMenuBar/HoverMenuDropdown.js +3 -1
- package/build/es/components/Toolbar/HoverMenuBar/__tests__/HoverMenuDropdown.spec.js +8 -0
- package/build/es/components/VisTypeIcon.js +8 -3
- package/build/es/index.js +4 -4
- package/build/es/locales/en/translations.json +5 -1
- package/build/es/locales/sv/translations.json +8 -2
- package/build/es/modules/__tests__/getAdaptedUiLayoutByType.spec.js +16 -1
- package/build/es/modules/axis.js +5 -1
- package/build/es/modules/getAdaptedUiLayoutByType.js +10 -1
- package/build/es/modules/layoutTypes.js +2 -1
- package/build/es/modules/layoutUiRules/__tests__/rules.spec.js +13 -2
- package/build/es/modules/layoutUiRules/index.js +2 -2
- package/build/es/modules/layoutUiRules/rules.js +22 -3
- package/build/es/modules/layoutUiRules/rulesHelper.js +3 -2
- package/build/es/modules/layoutUiRules/rulesUtils.js +5 -1
- package/build/es/modules/visTypeToLayoutType.js +4 -3
- package/build/es/modules/visTypes.js +7 -3
- 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
|
|
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
|
-
|
|
40
|
+
selectedItems,
|
|
22
41
|
rightFooter,
|
|
23
42
|
excludedPeriodTypes,
|
|
24
|
-
periodsSettings
|
|
25
|
-
|
|
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:
|
|
126
|
+
onChange: _ref3 => {
|
|
109
127
|
let {
|
|
110
128
|
selected
|
|
111
|
-
} =
|
|
112
|
-
const formattedItems = selected.map(id =>
|
|
113
|
-
id
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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:
|
|
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:
|
|
147
|
+
rightHeader: /*#__PURE__*/React.createElement(RightHeader, {
|
|
148
|
+
infoText: infoBoxMessage
|
|
149
|
+
}),
|
|
127
150
|
rightFooter: rightFooter,
|
|
128
|
-
options: [...allPeriods, ...
|
|
151
|
+
options: [...allPeriods, ...selectedItems].map(_ref4 => {
|
|
129
152
|
let {
|
|
130
153
|
id,
|
|
131
154
|
name
|
|
132
|
-
} =
|
|
155
|
+
} = _ref4;
|
|
133
156
|
return {
|
|
134
157
|
label: name,
|
|
135
158
|
value: id
|
|
136
159
|
};
|
|
137
160
|
}),
|
|
138
|
-
renderOption: props => /*#__PURE__*/React.createElement(TransferOption
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/build/es/components/PeriodDimension/__tests__/__snapshots__/PeriodDimension.spec.js.snap
CHANGED
|
@@ -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
|
`;
|
package/build/es/components/PeriodDimension/__tests__/__snapshots__/PeriodSelector.spec.js.snap
CHANGED
|
@@ -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;
|