From e797cf0490efb3b81d8ab3f7ab3c0d2dd031bac3 Mon Sep 17 00:00:00 2001 From: Jakub Benes Date: Sat, 7 Apr 2018 12:03:39 +0200 Subject: [PATCH 1/5] feat(trigger): initial solution to allow whitespace in token --- cypress/integration/textarea.js | 2 +- example/App.jsx | 9 +++++- src/Textarea.jsx | 54 ++++++++++++++++++++++----------- src/types.js | 1 + 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/cypress/integration/textarea.js b/cypress/integration/textarea.js index cf63d13..a01d5a0 100644 --- a/cypress/integration/textarea.js +++ b/cypress/integration/textarea.js @@ -29,7 +29,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 @'); }); diff --git a/example/App.jsx b/example/App.jsx index 4360786..d16f69f 100644 --- a/example/App.jsx +++ b/example/App.jsx @@ -222,8 +222,15 @@ class App extends React.Component { }, // test of special character '[': { - dataProvider: () => [{ name: 'alt', char: '@' }], + dataProvider: token => { + console.log(token); + return [ + { name: 'alt', char: '@' }, + { name: 'another character', char: '/' }, + ]; + }, component: Item, + allowWhitespace: true, output: { start: this._outputCaretStart, end: this._outputCaretEnd, diff --git a/src/Textarea.jsx b/src/Textarea.jsx index 6aec44d..f2048e4 100644 --- a/src/Textarea.jsx +++ b/src/Textarea.jsx @@ -99,7 +99,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; @@ -125,24 +125,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 @@ -309,6 +302,9 @@ class ReactTextareaAutocomplete extends React.Component< * Close autocomplete, also clean up trigger (to avoid slow promises) */ _closeAutocomplete = () => { + if (!this.state.currentTrigger) { + return; + } this.setState({ data: null, dataLoading: false, @@ -381,18 +377,38 @@ 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) && + ((this.state.currentTrigger && + !trigger[this.state.currentTrigger].allowWhitespace) || + !this.state.currentTrigger) + ) { this._closeAutocomplete(); return; } + 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 = @@ -473,6 +489,8 @@ class ReactTextareaAutocomplete extends React.Component< tokenRegExp: RegExp; + tokenRegExpWhitespace: RegExp; + render() { const { loadingComponent: Loader, diff --git a/src/types.js b/src/types.js index 95ccc19..a025a7d 100644 --- a/src/types.js +++ b/src/types.js @@ -51,6 +51,7 @@ export type ListState = { export type settingType = {| component: React$StatelessFunctionalComponent<*>, dataProvider: dataProviderType, + allowWhitespace?: boolean, output?: (Object | string, ?string) => textToReplaceType | string, |}; From dca227f57ef22c23ba550590f6b217a4845d4546 Mon Sep 17 00:00:00 2001 From: Jakub Benes Date: Sun, 8 Apr 2018 12:56:36 +0200 Subject: [PATCH 2/5] test: added test for allowWhitespace --- cypress/integration/textarea.js | 29 +++++++++++++++++++++++++++++ example/App.jsx | 12 +++++++++++- src/Textarea.jsx | 7 ++++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/cypress/integration/textarea.js b/cypress/integration/textarea.js index a01d5a0..e5790fc 100644 --- a/cypress/integration/textarea.js +++ b/cypress/integration/textarea.js @@ -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'); @@ -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 /'); + }); }); }); diff --git a/example/App.jsx b/example/App.jsx index d16f69f..87da800 100644 --- a/example/App.jsx +++ b/example/App.jsx @@ -30,6 +30,7 @@ class App extends React.Component { movePopupAsYouType: false, text: '', optionsCaret: 'start', + actualTokenInProvider: '', }; _handleOptionsCaretEnd = () => { @@ -102,6 +103,7 @@ class App extends React.Component { caretPosition, clickoutsideOption, movePopupAsYouType, + actualTokenInProvider, text, } = this.state; @@ -170,6 +172,10 @@ class App extends React.Component { +
+ Actual token in "[" provider:{' '} + {actualTokenInProvider} +
{ - console.log(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: '/' }, diff --git a/src/Textarea.jsx b/src/Textarea.jsx index f2048e4..2c8de92 100644 --- a/src/Textarea.jsx +++ b/src/Textarea.jsx @@ -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 @@ -262,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, @@ -302,9 +306,6 @@ class ReactTextareaAutocomplete extends React.Component< * Close autocomplete, also clean up trigger (to avoid slow promises) */ _closeAutocomplete = () => { - if (!this.state.currentTrigger) { - return; - } this.setState({ data: null, dataLoading: false, From 059825fd16c2132bbcacd2b54f9d9cc99ec81169 Mon Sep 17 00:00:00 2001 From: Jakub Benes Date: Sun, 8 Apr 2018 13:01:44 +0200 Subject: [PATCH 3/5] test: fixed unit test --- __tests__/index.spec.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/__tests__/index.spec.js b/__tests__/index.spec.js index 18065ab..89ac0c8 100644 --- a/__tests__/index.spec.js +++ b/__tests__/index.spec.js @@ -4,7 +4,9 @@ import ReactTextareaAutocomplete from '../src'; import Item from '../src/Item'; // eslint-disable-next-line -const SmileItemComponent = ({ entity: { label, text } }) =>
{label}
; +const SmileItemComponent = ({ entity: { label, text } }) => ( +
{label}
+); const Loading = () =>
Loading...
; @@ -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___ '); }); }); @@ -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: '); }); }); @@ -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__ '); }); }); @@ -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___ '); }); }); From e4314820fe45bb90a170dd1e41273080a66f8e7e Mon Sep 17 00:00:00 2001 From: Jakub Benes Date: Sun, 8 Apr 2018 13:08:30 +0200 Subject: [PATCH 4/5] docs: added allowWhitespace --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8fc7eef..9bfad04 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ export default App; dataProvider: ( token: string ) => Promise> | Array, + allowWhitespace?: boolean, component: ReactClass<*> |}, } @@ -138,6 +139,7 @@ export default App; - **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. From 48c3985141074bdcf2f674791ab9243778061ae2 Mon Sep 17 00:00:00 2001 From: Jakub Benes Date: Sun, 8 Apr 2018 13:39:14 +0200 Subject: [PATCH 5/5] refactor: clean the codebase and add some coments to whitespace --- src/Textarea.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Textarea.jsx b/src/Textarea.jsx index 2c8de92..2206d86 100644 --- a/src/Textarea.jsx +++ b/src/Textarea.jsx @@ -387,6 +387,7 @@ class ReactTextareaAutocomplete extends React.Component< */ if ( (!lastToken || lastToken.length <= minChar) && + // check if our current trigger disallows whitespace ((this.state.currentTrigger && !trigger[this.state.currentTrigger].allowWhitespace) || !this.state.currentTrigger) @@ -395,6 +396,10 @@ class ReactTextareaAutocomplete extends React.Component< 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 @@ -490,8 +495,6 @@ class ReactTextareaAutocomplete extends React.Component< tokenRegExp: RegExp; - tokenRegExpWhitespace: RegExp; - render() { const { loadingComponent: Loader,