Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,15 @@ export default App;
dataProvider: (
token: string
) => Promise<Array<Object | string>> | Array<Object | string>,
allowWhitespace?: boolean,
component: ReactClass<*>
|},
}
```

- **dataProvider** is called after each keystroke to get data what the suggestion list should display (array or promise resolving array)
- **component** is the component for render the item in suggestion list. It has `selected` and `entity` props provided by React Textarea Autocomplete
- **allowWhitespace** (Optional; defaults to false) Set this to true if you want to provide autocomplete for words (tokens) containing whitespace
- **output** (Optional for string based item. If the item is an object this method is *required*) This function defines text which will be placed into textarea after the user makes a selection.

You can also specify the behavior of caret if you return object `{text: "item", caretPosition: "start"}` the caret will be before the word once the user confirms his selection. Other possible value are "next", "end" and number, which is absolute number in contex of textarea (0 is equal position before the first char). Defaults to "next" which is space after the injected word.
Expand Down
16 changes: 7 additions & 9 deletions __tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import ReactTextareaAutocomplete from '../src';
import Item from '../src/Item';

// eslint-disable-next-line
const SmileItemComponent = ({ entity: { label, text } }) => <div> {label} </div>;
const SmileItemComponent = ({ entity: { label, text } }) => (
<div> {label} </div>
);

const Loading = () => <div>Loading...</div>;

Expand Down Expand Up @@ -118,9 +120,7 @@ describe('object-based items', () => {
});

it('text in textarea should be changed', () => {
expect(rta.find('textarea').node.value).toBe(
'___happy_face___ some test :a'
);
expect(rta.find('textarea').node.value).toBe('some test ___happy_face___ ');
});
});

Expand Down Expand Up @@ -201,7 +201,7 @@ describe('string-based items w/o output fn', () => {
});

it('text in textarea should be changed', () => {
expect(rta.find('textarea').node.value).toBe(':happy_face: some test :a');
expect(rta.find('textarea').node.value).toBe('some test :happy_face: ');
});
});

Expand Down Expand Up @@ -282,7 +282,7 @@ describe('string-based items with output fn', () => {
});

it('text in textarea should be changed', () => {
expect(rta.find('textarea').node.value).toBe('__happy_face__ some test :a');
expect(rta.find('textarea').node.value).toBe('some test __happy_face__ ');
});
});

Expand Down Expand Up @@ -440,8 +440,6 @@ describe('object-based items with keys', () => {
});

it('text in textarea should be changed', () => {
expect(rta.find('textarea').node.value).toBe(
'___happy_face___ some test :a'
);
expect(rta.find('textarea').node.value).toBe('some test ___happy_face___ ');
});
});
31 changes: 30 additions & 1 deletion cypress/integration/textarea.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/**
Helper function for a repeating of commands

e.g : cy
.get('.rta__textarea')
.type(`${repeat('{backspace}', 13)} again {downarrow}{enter}`);
*/
function repeat(string, times = 1) {
let result = '';
let round = times;
while (round--) {
result += string;
}

return result;
}

describe('React Textarea Autocomplete', () => {
it('server is reachable', () => {
cy.visit('http://localhost:8080');
Expand Down Expand Up @@ -29,7 +46,7 @@ describe('React Textarea Autocomplete', () => {
it('special character like [, ( should be also possible to use as trigger char', () => {
cy
.get('.rta__textarea')
.type('This is test [{downarrow}{enter}')
.type('This is test [{enter}')
.should('have.value', 'This is test @');
});

Expand Down Expand Up @@ -186,5 +203,17 @@ describe('React Textarea Autocomplete', () => {

cy.get('.rta__autocomplete').should('not.be.visible');
});

it('should allows tokens with eventual whitespace', () => {
cy.get('.rta__textarea').type('This is test [another charact');
cy.get('[data-test="actualToken"]').contains('another charact');
cy.get('.rta__textarea').type('{esc} and', { force: true });
cy
.get('.rta__textarea')
.type(`${repeat('{backspace}', 13)} again {downarrow}{enter}`, {
force: true,
});
cy.get('.rta__textarea').should('have.value', 'This is test /');
});
});
});
19 changes: 18 additions & 1 deletion example/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class App extends React.Component {
movePopupAsYouType: false,
text: '',
optionsCaret: 'start',
actualTokenInProvider: '',
};

_handleOptionsCaretEnd = () => {
Expand Down Expand Up @@ -102,6 +103,7 @@ class App extends React.Component {
caretPosition,
clickoutsideOption,
movePopupAsYouType,
actualTokenInProvider,
text,
} = this.state;

Expand Down Expand Up @@ -170,6 +172,10 @@ class App extends React.Component {
<button data-test="getCaretPosition" onClick={this._getCaretPosition}>
getCaretPosition();
</button>
<div>
Actual token in "[" provider:{' '}
<span data-test="actualToken">{actualTokenInProvider}</span>
</div>

<ReactTextareaAutocomplete
className="one"
Expand Down Expand Up @@ -222,8 +228,19 @@ class App extends React.Component {
},
// test of special character
'[': {
dataProvider: () => [{ name: 'alt', char: '@' }],
dataProvider: token => {
/**
Let's pass token to state to easily test it in Cypress
We going to test that we get also whitespace because this trigger has set "allowWhitespace"
*/
this.setState({ actualTokenInProvider: token });
return [
{ name: 'alt', char: '@' },
{ name: 'another character', char: '/' },
];
},
component: Item,
allowWhitespace: true,
output: {
start: this._outputCaretStart,
end: this._outputCaretEnd,
Expand Down
58 changes: 40 additions & 18 deletions src/Textarea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const errorMessage = (message: string) =>
message
} Check the documentation or create issue if you think it's bug. https://github.com/webscopeio/react-textarea-autocomplete/issues`
);

class ReactTextareaAutocomplete extends React.Component<
TextareaProps,
TextareaState
Expand Down Expand Up @@ -99,7 +100,7 @@ class ReactTextareaAutocomplete extends React.Component<

_onSelect = (newToken: textToReplaceType) => {
const { selectionEnd, currentTrigger, value: textareaValue } = this.state;
const { onChange } = this.props;
const { onChange, trigger } = this.props;

if (!currentTrigger) return;

Expand All @@ -125,24 +126,17 @@ class ReactTextareaAutocomplete extends React.Component<
}
};

let offsetToEndOfToken = 0;
while (
textareaValue[selectionEnd + offsetToEndOfToken] &&
/\S/.test(textareaValue[selectionEnd + offsetToEndOfToken])
) {
offsetToEndOfToken += 1;
}

const textToModify = textareaValue.slice(
0,
selectionEnd + offsetToEndOfToken
);
const textToModify = textareaValue.slice(0, selectionEnd);

const startOfTokenPosition = textToModify.search(
/**
* It's important to enscape the currentTrigger char for chars like [, (,...
* It's important to escape the currentTrigger char for chars like [, (,...
*/
new RegExp(`\\${currentTrigger}\\S*$`)
new RegExp(
`\\${currentTrigger}${
trigger[currentTrigger].allowWhitespace ? '.' : '\\S'
}*$`
)
);

// we add space after emoji is selected if a caret position is next
Expand Down Expand Up @@ -269,6 +263,9 @@ class ReactTextareaAutocomplete extends React.Component<
throw new Error('Component should be defined!');
}

// throw away if we resolved old trigger
if (currentTrigger !== this.state.currentTrigger) return;

this.setState({
dataLoading: false,
data,
Expand Down Expand Up @@ -381,18 +378,43 @@ class ReactTextareaAutocomplete extends React.Component<
value,
});

const tokenMatch = this.tokenRegExp.exec(value.slice(0, selectionEnd));
const lastToken = tokenMatch && tokenMatch[0];
let tokenMatch = this.tokenRegExp.exec(value.slice(0, selectionEnd));
let lastToken = tokenMatch && tokenMatch[0];

/*
if we lost the trigger token or there is no following character we want to close
the autocomplete
*/
if (!lastToken || lastToken.length <= minChar) {
if (
(!lastToken || lastToken.length <= minChar) &&
// check if our current trigger disallows whitespace
((this.state.currentTrigger &&
!trigger[this.state.currentTrigger].allowWhitespace) ||
!this.state.currentTrigger)
) {
this._closeAutocomplete();
return;
}

/**
If our current trigger allows whitespace
get the correct token for DataProvider, so we need to construct new RegExp
*/
if (
this.state.currentTrigger &&
trigger[this.state.currentTrigger].allowWhitespace
) {
tokenMatch = new RegExp(
`\\${this.state.currentTrigger}[^${this.state.currentTrigger}]*$`
).exec(value.slice(0, selectionEnd));
lastToken = tokenMatch && tokenMatch[0];

if (!lastToken) {
this._closeAutocomplete();
return;
}
}

const triggerChars = Object.keys(trigger);

const currentTrigger =
Expand Down
1 change: 1 addition & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type ListState = {
export type settingType = {|
component: React$StatelessFunctionalComponent<*>,
dataProvider: dataProviderType,
allowWhitespace?: boolean,
output?: (Object | string, ?string) => textToReplaceType | string,
|};

Expand Down