Skip to content

Commit fc8110e

Browse files
Mingzemergify[bot]
andauthored
feat(draftjs): Extract functions into utils for other libs to reuse (#2106)
* feat(draftjs): Extract functions into utils for other libs to reuse * feat(draftjs): Move tests * feat(draftjs): Address comments * feat(draftjs): Address comments Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 71b6eb4 commit fc8110e

File tree

5 files changed

+223
-194
lines changed

5 files changed

+223
-194
lines changed

src/components/form-elements/draft-js-mention-selector/DraftJSMentionSelectorCore.js

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
import * as React from 'react';
33
import { FormattedMessage } from 'react-intl';
44
import classNames from 'classnames';
5-
import { EditorState, Modifier } from 'draft-js';
5+
import { EditorState } from 'draft-js';
66

77
import DatalistItem from '../../datalist-item';
88
import DraftJSEditor from '../../draft-js-editor';
99
import SelectorDropdown from '../../selector-dropdown';
10+
import { addMention, defaultMentionTriggers, getActiveMentionForEditorState } from './utils';
1011

1112
import messages from './messages';
1213

1314
import type { SelectorItems } from '../../../common/types/core';
15+
import type { Mention } from './utils';
1416

1517
import './MentionSelector.scss';
1618

@@ -56,7 +58,7 @@ type Props = {
5658
};
5759

5860
type State = {
59-
activeMention: Object | null,
61+
activeMention: Mention | null,
6062
isFocused: boolean,
6163
mentionPattern: RegExp,
6264
};
@@ -67,7 +69,7 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
6769
contacts: [],
6870
isDisabled: false,
6971
isRequired: false,
70-
mentionTriggers: ['@', '@', '﹫'],
72+
mentionTriggers: defaultMentionTriggers,
7173
selectorRow: <DefaultSelectorRow />,
7274
startMentionMessage: <DefaultStartMentionMessage />,
7375
};
@@ -110,42 +112,7 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
110112
getActiveMentionForEditorState(editorState: EditorState) {
111113
const { mentionPattern } = this.state;
112114

113-
const contentState = editorState.getCurrentContent();
114-
const selectionState = editorState.getSelection();
115-
116-
const startKey = selectionState.getStartKey();
117-
const activeBlock = contentState.getBlockForKey(startKey);
118-
119-
const cursorPosition = selectionState.getStartOffset();
120-
121-
let result = null;
122-
123-
// Break the active block into entity ranges.
124-
activeBlock.findEntityRanges(
125-
character => character.getEntity() === null,
126-
(start, end) => {
127-
// Find the active range (is the cursor inside this range?)
128-
if (start <= cursorPosition && cursorPosition <= end) {
129-
// Determine if the active range contains a mention.
130-
const activeRangeText = activeBlock.getText().substr(start, cursorPosition - start);
131-
const mentionMatch = activeRangeText.match(mentionPattern);
132-
133-
if (mentionMatch) {
134-
result = {
135-
blockID: startKey,
136-
mentionString: mentionMatch[2],
137-
mentionTrigger: mentionMatch[1],
138-
start: start + mentionMatch.index,
139-
end: cursorPosition,
140-
};
141-
}
142-
}
143-
144-
return null;
145-
},
146-
);
147-
148-
return result;
115+
return getActiveMentionForEditorState(editorState, mentionPattern);
149116
}
150117

151118
/**
@@ -238,49 +205,8 @@ class DraftJSMentionSelector extends React.Component<Props, State> {
238205
addMention(mention: Object) {
239206
const { activeMention } = this.state;
240207
const { editorState } = this.props;
241-
const { start, end } = activeMention || {};
242-
243-
const { id, name } = mention;
244-
245-
const contentState = editorState.getCurrentContent();
246-
const selectionState = editorState.getSelection();
247-
248-
const preInsertionSelectionState = selectionState.merge({
249-
anchorOffset: start,
250-
focusOffset: end,
251-
});
252-
253-
const textToInsert = `@${name}`;
254208

255-
const contentStateWithEntity = contentState.createEntity('MENTION', 'IMMUTABLE', { id });
256-
257-
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
258-
259-
const contentStateWithLink = Modifier.replaceText(
260-
contentState,
261-
preInsertionSelectionState,
262-
textToInsert,
263-
null,
264-
entityKey,
265-
);
266-
267-
const spaceOffset = preInsertionSelectionState.getStartOffset() + textToInsert.length;
268-
const selectionStateForAddingSpace = preInsertionSelectionState.merge({
269-
anchorOffset: spaceOffset,
270-
focusOffset: spaceOffset,
271-
});
272-
273-
const contentStateWithLinkAndExtraSpace = Modifier.insertText(
274-
contentStateWithLink,
275-
selectionStateForAddingSpace,
276-
' ',
277-
);
278-
279-
const editorStateWithLink = EditorState.push(
280-
editorState,
281-
contentStateWithLinkAndExtraSpace,
282-
'change-block-type',
283-
);
209+
const editorStateWithLink = addMention(editorState, activeMention, mention);
284210

285211
this.setState(
286212
{

src/components/form-elements/draft-js-mention-selector/__tests__/DraftJSMentionSelectorCore.test.js

Lines changed: 17 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,14 @@
11
import React from 'react';
22
import { mount, shallow } from 'enzyme';
3-
import { ContentState, EditorState } from 'draft-js';
3+
import { EditorState } from 'draft-js';
44
import sinon from 'sinon';
55

6+
import * as utils from '../utils';
67
import DraftJSEditor from '../../../draft-js-editor';
78
import DraftJSMentionSelector from '../DraftJSMentionSelectorCore';
89

910
const sandbox = sinon.sandbox.create();
1011

11-
const noMentionEditorState = EditorState.createWithContent(ContentState.createFromText('No mention here'));
12-
const oneMentionEditorState = EditorState.createWithContent(ContentState.createFromText('Hey @foo'));
13-
const twoMentionEditorState = EditorState.createWithContent(ContentState.createFromText('Hi @foo, meet @bar'));
14-
15-
const oneMentionSelectionState = oneMentionEditorState.getSelection().merge({
16-
anchorOffset: 8,
17-
focusOffset: 8,
18-
});
19-
20-
const twoMentionSelectionState = twoMentionEditorState.getSelection().merge({
21-
anchorOffset: 18,
22-
focusOffset: 18,
23-
});
24-
25-
const twoMentionSelectionStateCursorInside = twoMentionEditorState.getSelection().merge({
26-
anchorOffset: 17,
27-
focusOffset: 17,
28-
});
29-
30-
const oneMentionExpectedMention = {
31-
mentionString: 'foo',
32-
mentionTrigger: '@',
33-
start: 4,
34-
end: 8,
35-
};
36-
37-
const twoMentionExpectedMention = {
38-
mentionString: 'bar',
39-
mentionTrigger: '@',
40-
start: 14,
41-
end: 18,
42-
};
43-
44-
const twoMentionCursorInsideExpectedMention = {
45-
mentionString: 'ba',
46-
mentionTrigger: '@',
47-
start: 14,
48-
end: 17,
49-
};
50-
5112
describe('components/form-elements/draft-js-mention-selector/DraftJSMentionSelector', () => {
5213
afterEach(() => {
5314
sandbox.verifyAndRestore();
@@ -161,71 +122,16 @@ describe('components/form-elements/draft-js-mention-selector/DraftJSMentionSelec
161122
});
162123

163124
describe('getActiveMentionForEditorState()', () => {
164-
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
165-
166-
const instance = wrapper.instance();
167-
168-
// TESTS
169-
[
170-
// empty input
171-
{
172-
editorState: EditorState.createEmpty(),
173-
},
174-
// input has ne mention
175-
{
176-
editorState: noMentionEditorState,
177-
},
178-
].forEach(({ editorState }) => {
179-
test('should return null', () => {
180-
expect(instance.getActiveMentionForEditorState(editorState)).toBeNull();
181-
});
182-
});
183-
184-
[
185-
// one string beginning with "@"
186-
{
187-
editorState: oneMentionEditorState,
188-
selectionState: oneMentionSelectionState,
189-
expected: oneMentionExpectedMention,
190-
},
191-
// two strings beginning with "@"
192-
{
193-
editorState: twoMentionEditorState,
194-
selectionState: twoMentionSelectionState,
195-
expected: twoMentionExpectedMention,
196-
},
197-
// two strings beginning "@", cursor inside one
198-
{
199-
editorState: twoMentionEditorState,
200-
selectionState: twoMentionSelectionStateCursorInside,
201-
expected: twoMentionCursorInsideExpectedMention,
202-
},
203-
].forEach(({ editorState, selectionState, expected }) => {
204-
test('should return null when cursor is not over a mention', () => {
205-
const selectionStateAtBeginning = editorState.getSelection().merge({
206-
anchorOffset: 0,
207-
focusOffset: 0,
208-
});
209-
210-
const editorStateWithForcedSelection = EditorState.acceptSelection(
211-
editorState,
212-
selectionStateAtBeginning,
213-
);
214-
215-
const result = instance.getActiveMentionForEditorState(editorStateWithForcedSelection);
125+
test('should call getActiveMentionForEditorState from utils', () => {
126+
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
216127

217-
expect(result).toBeNull();
218-
});
128+
wrapper.setState({ mentionPattern: 'testPattern' });
219129

220-
test('should return the selected mention when it is selected', () => {
221-
const editorStateWithForcedSelection = EditorState.acceptSelection(editorState, selectionState);
130+
const getMentionStub = sandbox.stub(utils, 'getActiveMentionForEditorState');
222131

223-
const result = instance.getActiveMentionForEditorState(editorStateWithForcedSelection);
132+
wrapper.instance().getActiveMentionForEditorState('testState');
224133

225-
Object.keys(expected).forEach(key => {
226-
expect(result[key]).toEqual(expected[key]);
227-
});
228-
});
134+
expect(getMentionStub.calledWith('testState', 'testPattern')).toBe(true);
229135
});
230136
});
231137

@@ -368,24 +274,22 @@ describe('components/form-elements/draft-js-mention-selector/DraftJSMentionSelec
368274
});
369275

370276
describe('addMention()', () => {
371-
test('should null out activeMention and call handleChange with updated string (plus space) when called', () => {
372-
const mention = { id: 1, name: 'Fool Name' };
373-
374-
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} editorState={oneMentionEditorState} />);
277+
test('should call addMention from utils', () => {
278+
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} editorState="testState" />);
375279

376280
wrapper.setState({
377-
activeMention: oneMentionExpectedMention,
281+
activeMention: 'testActiveMention',
378282
});
379283

380284
const instance = wrapper.instance();
381-
const handleChangeMock = sandbox.mock(instance).expects('handleChange');
285+
const addMentionStub = sandbox.stub(utils, 'addMention').returns('testReturn');
382286
const setStateSpy = sandbox.spy(instance, 'setState');
287+
const handleChangeStub = sandbox.stub(instance, 'handleChange');
383288

384-
instance.addMention(mention);
289+
instance.addMention('testMention');
290+
expect(addMentionStub.calledWith('testState', 'testActiveMention', 'testMention')).toBe(true);
385291
expect(setStateSpy.calledWith({ activeMention: null })).toBe(true);
386-
387-
const editorStateCall = handleChangeMock.firstCall.args[0];
388-
expect(editorStateCall.getCurrentContent().getPlainText()).toEqual('Hey @Fool Name ');
292+
expect(handleChangeStub.calledWith('testReturn')).toBe(true);
389293
});
390294
});
391295

@@ -400,7 +304,7 @@ describe('components/form-elements/draft-js-mention-selector/DraftJSMentionSelec
400304
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} contacts={[contact]} />);
401305

402306
wrapper.setState({
403-
activeMention: oneMentionExpectedMention,
307+
activeMention: {},
404308
});
405309

406310
wrapper.setProps({ contacts: [] });
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { ContentState, EditorState } from 'draft-js';
2+
import { addMention, getActiveMentionForEditorState } from '../utils';
3+
4+
const noMentionEditorState = EditorState.createWithContent(ContentState.createFromText('No mention here'));
5+
const oneMentionEditorState = EditorState.createWithContent(ContentState.createFromText('Hey @foo'));
6+
const twoMentionEditorState = EditorState.createWithContent(ContentState.createFromText('Hi @foo, meet @bar'));
7+
// one string beginning with "@"
8+
const oneMentionSelectionState = oneMentionEditorState.getSelection().merge({
9+
anchorOffset: 8,
10+
focusOffset: 8,
11+
});
12+
// two strings beginning with "@"
13+
const twoMentionSelectionState = twoMentionEditorState.getSelection().merge({
14+
anchorOffset: 18,
15+
focusOffset: 18,
16+
});
17+
// two strings beginning "@", cursor inside one
18+
const twoMentionSelectionStateCursorInside = twoMentionEditorState.getSelection().merge({
19+
anchorOffset: 17,
20+
focusOffset: 17,
21+
});
22+
const oneMentionExpectedMention = {
23+
mentionString: 'foo',
24+
mentionTrigger: '@',
25+
start: 4,
26+
end: 8,
27+
};
28+
const twoMentionExpectedMention = {
29+
mentionString: 'bar',
30+
mentionTrigger: '@',
31+
start: 14,
32+
end: 18,
33+
};
34+
const twoMentionCursorInsideExpectedMention = {
35+
mentionString: 'ba',
36+
mentionTrigger: '@',
37+
start: 14,
38+
end: 17,
39+
};
40+
41+
describe('components/form-elements/draft-js-mention-selector/utils', () => {
42+
describe('getActiveMentionForEditorState()', () => {
43+
test.each`
44+
input | editorState
45+
${'input is empty'} | ${EditorState.createEmpty()}
46+
${'input has no mention'} | ${noMentionEditorState}
47+
`('should return null if $input', ({ editorState }) => {
48+
expect(getActiveMentionForEditorState(editorState)).toBeNull();
49+
});
50+
51+
test('should return null when cursor is not over a mention', () => {
52+
const editorState = oneMentionEditorState;
53+
const selectionStateAtBeginning = editorState.getSelection().merge({
54+
anchorOffset: 0,
55+
focusOffset: 0,
56+
});
57+
58+
const editorStateWithForcedSelection = EditorState.acceptSelection(editorState, selectionStateAtBeginning);
59+
60+
const result = getActiveMentionForEditorState(editorStateWithForcedSelection);
61+
62+
expect(result).toBeNull();
63+
});
64+
65+
test.each`
66+
editorState | selectionState | expected
67+
${oneMentionEditorState} | ${oneMentionSelectionState} | ${oneMentionExpectedMention}
68+
${twoMentionEditorState} | ${twoMentionSelectionState} | ${twoMentionExpectedMention}
69+
${twoMentionEditorState} | ${twoMentionSelectionStateCursorInside} | ${twoMentionCursorInsideExpectedMention}
70+
`('should return the selected mention when it is selected', ({ editorState, selectionState, expected }) => {
71+
const editorStateWithForcedSelection = EditorState.acceptSelection(editorState, selectionState);
72+
73+
const result = getActiveMentionForEditorState(editorStateWithForcedSelection);
74+
75+
Object.keys(expected).forEach(key => {
76+
expect(result[key]).toEqual(expected[key]);
77+
});
78+
});
79+
});
80+
81+
describe('addMention()', () => {
82+
test('should return updated string (plus space)', () => {
83+
const mention = { id: 1, name: 'Fool Name' };
84+
85+
const editorStateWithLink = addMention(oneMentionEditorState, oneMentionExpectedMention, mention);
86+
87+
expect(editorStateWithLink.getCurrentContent().getPlainText()).toEqual('Hey @Fool Name ');
88+
});
89+
});
90+
});

0 commit comments

Comments
 (0)