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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ or there is UMD build available. [Check out this pen as example](https://codepen
| movePopupAsYouType | boolean | When it's true the textarea will move along with a caret as a user continues to type. Defaults to false. |
| boundariesElement | string \| HTMLElement | Element which should prevent autocomplete to overflow. Defaults to _body_. |
| textAreaComponent | React.Component \| {component: React.Component, ref: string} | What component use for as textarea. Default is `textarea`. (You can combine this with [react-autosize-textarea](https://github.com/buildo/react-autosize-textarea) for instance) |
| renderToBody | boolean | When set to `true` the autocomplete will be rendered at the end of the `<body>`. Default is `false`. |
| renderToBody | boolean | When set to `true` the autocomplete will be rendered at the end of the `<body>`. Default is `false`. |
| onItemSelected | ({currentTrigger: string, item: string \| Object}) => void | Callback get called everytime item is selected |
| style | Style Object | Style's of textarea |
| listStyle | Style Object | Styles of list's wrapper |
| itemStyle | Style Object | Styles of item's wrapper |
Expand Down
12 changes: 12 additions & 0 deletions cypress/integration/textarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ describe("React Textarea Autocomplete", () => {
cy.get('[data-test="minChar"]').clear({ force: true });
});

it("onSelectItem should return correct item and trigger", () => {
cy.get(".rta__textarea").type(":ro{uparrow}{uparrow}{enter}");
cy.window().then(async win => {
const shouldSelectItem = {
currentTrigger: ":",
item: { name: "rofl", char: "🤣" }
};

expect(win.__lastSelectedItem).to.deep.equal(shouldSelectItem);
});
});

it("should have place caret before outputted word", () => {
/**
* This is probably Cypress bug (1.0.2)
Expand Down
4 changes: 4 additions & 0 deletions example/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ class App extends React.Component {
}}
movePopupAsYouType={movePopupAsYouType}
onCaretPositionChange={this._onCaretPositionChangeHandle}
onItemSelected={info => {
// save selected item to window; use it later in E2E tests
window.__lastSelectedItem = info;
}}
minChar={minChar}
value={text}
onChange={this._onChangeHandle}
Expand Down
45 changes: 30 additions & 15 deletions src/List.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export default class List extends React.Component<ListProps, ListState> {
selectedItem: null
};

cachedIdOfItems: Map<Object | string, string> = new Map();

componentDidMount() {
this.listeners.push(
Listeners.add([KEY_CODES.DOWN, KEY_CODES.UP], this.scroll),
Expand Down Expand Up @@ -60,26 +62,39 @@ export default class List extends React.Component<ListProps, ListState> {
};

getId = (item: Object | string): string => {
if (this.cachedIdOfItems.has(item)) {
// $FlowFixMe
return this.cachedIdOfItems.get(item);
}

const textToReplace = this.props.getTextToReplace(item);

if (textToReplace) {
if (textToReplace.key) {
return textToReplace.key;
const computeId = (): string => {
if (textToReplace) {
if (textToReplace.key) {
return textToReplace.key;
}

if (typeof item === "string" || !item.key) {
return textToReplace.text;
}
}

if (typeof item === "string" || !item.key) {
return textToReplace.text;
if (!item.key) {
throw new Error(
`Item ${JSON.stringify(item)} has to have defined "key" property`
);
}
}

if (!item.key) {
throw new Error(
`Item ${JSON.stringify(item)} has to have defined "key" property`
);
}
// $FlowFixMe
return item.key;
};

const id = computeId();

this.cachedIdOfItems.set(item, id);

// $FlowFixMe
return item.key;
return id;
};

props: ListProps;
Expand All @@ -93,9 +108,9 @@ export default class List extends React.Component<ListProps, ListState> {
modifyText = (value: Object | string) => {
if (!value) return;

const { onSelect, getTextToReplace } = this.props;
const { onSelect } = this.props;

onSelect(getTextToReplace(value));
onSelect(value);
};

selectItem = (item: Object | string, keyboard: boolean = false) => {
Expand Down
55 changes: 33 additions & 22 deletions src/Textarea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint react/no-multi-comp: 0 */

import React from "react";
import ReactDOM from 'react-dom'
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import getCaretCoordinates from "textarea-caret";
import CustomEvent from "custom-event";
Expand All @@ -18,7 +18,6 @@ import type {
caretPositionType,
outputType,
triggerType,
textToReplaceType,
settingType
} from "./types";

Expand Down Expand Up @@ -151,7 +150,7 @@ class Autocomplete extends React.Component<AutocompleteProps> {
unusedClasses.push(POSITION_CONFIGURATION.Y.TOP);
}

if(this.props.renderToBody) {
if (this.props.renderToBody) {
topPosition += textareaBounds.top;
leftPosition += textareaBounds.left;
}
Expand Down Expand Up @@ -181,7 +180,9 @@ class Autocomplete extends React.Component<AutocompleteProps> {
</div>
);

return renderToBody && body !== null ? ReactDOM.createPortal(autocompleteContainer, body) : autocompleteContainer;
return renderToBody && body !== null
? ReactDOM.createPortal(autocompleteContainer, body)
: autocompleteContainer;
}
}

Expand Down Expand Up @@ -350,16 +351,34 @@ class ReactTextareaAutocomplete extends React.Component<
this.lastTrigger = this.getCaretPosition() - 1;
};

_onSelect = (newToken: textToReplaceType) => {
_onSelect = (item: Object | string) => {
const { selectionEnd, currentTrigger, value: textareaValue } = this.state;
const { trigger } = this.props;
const { trigger, onItemSelected } = this.props;

if (!currentTrigger) return;

const getTextToReplaceForCurrentTrigger = this._getTextToReplace(
currentTrigger
);

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

const newToken = getTextToReplaceForCurrentTrigger(item);

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

if (!currentTrigger) return;
if (onItemSelected) {
onItemSelected({
currentTrigger,
item
});
}

const computeCaretPosition = (
position: caretPositionType,
Expand Down Expand Up @@ -441,13 +460,7 @@ class ReactTextareaAutocomplete extends React.Component<
);
};

_getTextToReplace = ({
actualToken,
currentTrigger
}: {|
actualToken: string,
currentTrigger: string
|}): ?outputType => {
_getTextToReplace = (currentTrigger: string): ?outputType => {
const triggerSettings = this.props.trigger[currentTrigger];

if (!currentTrigger || !triggerSettings) return null;
Expand All @@ -471,7 +484,7 @@ class ReactTextareaAutocomplete extends React.Component<
throw new Error(
`Output functor should return string or object in shape {text: string, caretPosition: string | number}.\nGot "${String(
textToReplace
)}". Check the implementation for trigger "${currentTrigger}" and its token "${actualToken}"\n\nSee https://github.com/webscopeio/react-textarea-autocomplete#trigger-type for more informations.\n`
)}". Check the implementation for trigger "${currentTrigger}"\n\nSee https://github.com/webscopeio/react-textarea-autocomplete#trigger-type for more information.\n`
);
}

Expand All @@ -486,13 +499,13 @@ class ReactTextareaAutocomplete extends React.Component<

if (!textToReplace.text) {
throw new Error(
`Output "text" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}" and its token "${actualToken}"\n`
`Output "text" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}"\n`
);
}

if (!textToReplace.caretPosition) {
throw new Error(
`Output "caretPosition" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}" and its token "${actualToken}"\n`
`Output "caretPosition" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger "${currentTrigger}"\n`
);
}

Expand Down Expand Up @@ -656,7 +669,8 @@ class ReactTextareaAutocomplete extends React.Component<
"dropdownClassName",
"movePopupAsYouType",
"textAreaComponent",
"renderToBody"
"renderToBody",
"onItemSelected"
];

// eslint-disable-next-line
Expand Down Expand Up @@ -819,10 +833,7 @@ class ReactTextareaAutocomplete extends React.Component<

this.escListenerInit();

const textToReplace = this._getTextToReplace({
actualToken,
currentTrigger
});
const textToReplace = this._getTextToReplace(currentTrigger);

this.setState(
{
Expand Down
3 changes: 2 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type ListProps = {
itemStyle: ?Object,
className: ?string,
itemClassName: ?string,
onSelect: textToReplaceType => void,
onSelect: (Object | string) => void,
dropdownScroll: HTMLDivElement => void
};

Expand Down Expand Up @@ -78,6 +78,7 @@ export type TextareaProps = {
| boolean
| ((container: HTMLDivElement, item: HTMLDivElement) => void),
closeOnClickOutside?: boolean,
onItemSelected?: ({ currentTrigger: string, item: Object | string }) => void,
movePopupAsYouType?: boolean,
boundariesElement: string | HTMLElement,
minChar: number,
Expand Down