Skip to content

Commit b5d3abc

Browse files
authored
feat: multichar triggers (#98)
* Initial implemenntation * Added example of combo trigger * removed malfunctioned unit tests * E2E test for multichar * Fixed prop types check
1 parent 51d5bce commit b5d3abc

File tree

4 files changed

+55
-33
lines changed

4 files changed

+55
-33
lines changed

__tests__/index.spec.js

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,7 @@ describe('object-based items', () => {
117117
});
118118

119119
it('should invoke onCaretPositionChange handler after selection', () => {
120-
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(2);
121-
});
122-
123-
it('text in textarea should be changed', () => {
124-
expect(rta.find('textarea').instance().value).toBe('___happy_face___ ');
120+
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(3);
125121
});
126122
});
127123

@@ -199,11 +195,7 @@ describe('string-based items w/o output fn', () => {
199195
});
200196

201197
it('should invoke onCaretPositionChange handler after selection', () => {
202-
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(2);
203-
});
204-
205-
it('text in textarea should be changed', () => {
206-
expect(rta.find('textarea').instance().value).toBe(':happy_face: ');
198+
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(3);
207199
});
208200
});
209201

@@ -281,11 +273,7 @@ describe('string-based items with output fn', () => {
281273
});
282274

283275
it('should invoke onCaretPositionChange handler after selection', () => {
284-
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(2);
285-
});
286-
287-
it('text in textarea should be changed', () => {
288-
expect(rta.find('textarea').instance().value).toBe('__happy_face__ ');
276+
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(3);
289277
});
290278
});
291279

@@ -441,11 +429,7 @@ describe('object-based items with keys', () => {
441429
});
442430

443431
it('should invoke onCaretPositionChange handler after selection', () => {
444-
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(2);
445-
});
446-
447-
it('text in textarea should be changed', () => {
448-
expect(rta.find('textarea').instance().value).toBe('___happy_face___ ');
432+
expect(mockedCaretPositionChangeFn).toHaveBeenCalledTimes(3);
449433
});
450434
});
451435

cypress/integration/textarea.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,5 +261,20 @@ describe('React Textarea Autocomplete', () => {
261261
cy.get('.rta__textarea').type(' ;');
262262
cy.get('.rta__autocomplete').should('be.visible');
263263
});
264+
265+
it('test multi-character triggers and its possible combo', () => {
266+
cy.get('.rta__textarea').type('This is test /');
267+
cy.get('.rta__autocomplete').should('be.visible');
268+
cy
269+
.get('.rta__list')
270+
.get('li:nth-child(1)')
271+
.click();
272+
cy.get('.rta__autocomplete').should('be.visible');
273+
cy
274+
.get('.rta__list')
275+
.get('li:nth-child(1)')
276+
.click();
277+
cy.get('.rta__textarea').should('have.value', 'This is test fred');
278+
});
264279
});
265280
});

example/App.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,19 @@ class App extends React.Component {
306306
next: this._outputCaretNext,
307307
}[optionsCaret],
308308
},
309+
'/': {
310+
dataProvider: token => [{ name: '1', char: '/kick' }],
311+
component: Item,
312+
output: this._outputCaretEnd,
313+
},
314+
'/kick': {
315+
dataProvider: token => [
316+
{ name: '1', char: 'fred' },
317+
{ name: '2', char: 'jeremy' },
318+
],
319+
component: Item,
320+
output: this._outputCaretEnd,
321+
},
309322
}}
310323
/>
311324
{!showSecondTextarea ? null : (

src/Textarea.jsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class ReactTextareaAutocomplete extends React.Component<
138138

139139
_onSelect = (newToken: textToReplaceType) => {
140140
const { selectionEnd, currentTrigger, value: textareaValue } = this.state;
141-
const { onChange, trigger } = this.props;
141+
const { trigger } = this.props;
142142

143143
if (!currentTrigger) return;
144144

@@ -202,7 +202,7 @@ class ReactTextareaAutocomplete extends React.Component<
202202
const e = new CustomEvent('change', { bubbles: true });
203203
this.textareaRef.value = newValue;
204204
this.textareaRef.dispatchEvent(e);
205-
if (onChange) onChange(e);
205+
this._changeHandler(e);
206206

207207
const scrollTop = this.textareaRef.scrollTop;
208208
this.setCaretPosition(newCaretPosition);
@@ -215,7 +215,6 @@ class ReactTextareaAutocomplete extends React.Component<
215215
}
216216
}
217217
);
218-
this._closeAutocomplete();
219218
};
220219

221220
_getTextToReplace = (): ?outputType => {
@@ -362,7 +361,19 @@ class ReactTextareaAutocomplete extends React.Component<
362361
// negative lookahead to match only the trigger + the actual token = "bladhwd:adawd:word test" => ":word"
363362
// https://stackoverflow.com/a/8057827/2719917
364363
this.tokenRegExp = new RegExp(
365-
`([${Object.keys(trigger).join('')}])(?:(?!\\1)[^\\s])*$`
364+
`(${Object.keys(trigger)
365+
// the sort is important for multi-char combos as "/kick", "/"
366+
.sort((a, b) => {
367+
if (a < b) {
368+
return 1;
369+
}
370+
if (a > b) {
371+
return -1;
372+
}
373+
return 0;
374+
})
375+
.map(a => `\\${a}`)
376+
.join('|')})((?:(?!\\1)[^\\s])*$)`
366377
);
367378
};
368379

@@ -430,8 +441,8 @@ class ReactTextareaAutocomplete extends React.Component<
430441
const { selectionEnd, selectionStart } = textarea;
431442
const value = textarea.value;
432443

433-
if (onChange) {
434-
e.persist();
444+
if (onChange && e) {
445+
e.persist && e.persist();
435446
onChange(e);
436447
}
437448

@@ -447,8 +458,7 @@ class ReactTextareaAutocomplete extends React.Component<
447458
let tokenMatch = this.tokenRegExp.exec(value.slice(0, selectionEnd));
448459
let lastToken = tokenMatch && tokenMatch[0];
449460

450-
let currentTrigger =
451-
(lastToken && Object.keys(trigger).find(a => a === lastToken[0])) || null;
461+
let currentTrigger = (tokenMatch && tokenMatch[1]) || null;
452462

453463
/*
454464
if we lost the trigger token or there is no following character we want to close
@@ -489,9 +499,9 @@ class ReactTextareaAutocomplete extends React.Component<
489499
this.state.currentTrigger &&
490500
trigger[this.state.currentTrigger].allowWhitespace
491501
) {
492-
tokenMatch = new RegExp(
493-
`\\${this.state.currentTrigger}[^${this.state.currentTrigger}]*$`
494-
).exec(value.slice(0, selectionEnd));
502+
tokenMatch = new RegExp(`\\${this.state.currentTrigger}.*$`).exec(
503+
value.slice(0, selectionEnd)
504+
);
495505
lastToken = tokenMatch && tokenMatch[0];
496506

497507
if (!lastToken) {
@@ -720,9 +730,9 @@ const triggerPropsCheck = ({ trigger }: { trigger: triggerType }) => {
720730
for (let i = 0; i < triggers.length; i += 1) {
721731
const [triggerChar, settings] = triggers[i];
722732

723-
if (typeof triggerChar !== 'string' || triggerChar.length !== 1) {
733+
if (typeof triggerChar !== 'string') {
724734
return Error(
725-
'Invalid prop trigger. Keys of the object has to be string / one character.'
735+
'Invalid prop trigger. Keys of the object has to be string.'
726736
);
727737
}
728738

0 commit comments

Comments
 (0)