@point-api/dropdown-react
Version:
HOC to add a Point API autocomplete dropdown
219 lines • 9.93 kB
JavaScript
import * as React from "react";
import ReactDOM from "react-dom";
import Dropdown from "./Dropdown";
import ContentEditableAdapter from "./ContentEditableAdapter";
import TextAreaAdapter from "./TextAreaAdapter";
import { matchAll, NAME_REGEX, CONTENT_REGEX, getOrigin } from "./Utils";
import showAlert from "./Alert";
/**
* Attach a Point dropdown to a given Editable component
* @param Editable - A component containing a ContentEditable, TextArea, or Input(type=text) element
* @param pointApi - An instance of Point API wrapper object
* @param options - Additional options to pass to the dropdown
* @returns An Autocomplete component containing the editable with the attached dropdown
*/
function addDropdown(Editable, pointApi, { dropdownClass = "point-dropdown", tabCompletion = true, searchType = "standard", storeInteractions = false, } = {}) {
var _a;
return _a = class AutoComplete extends React.Component {
constructor(props) {
super(props);
this.initSession = async () => {
this.session = await pointApi.initAutocompleteSessionAsync(searchType);
};
/**
* Set state after receiving data from Point API event
*/
this.setStateAfterRequest = (query, response, matchType) => {
if (!this.mounted) {
return;
}
this.setState({
query,
currentResponseId: response ? response.responseId : null,
suggestedSnippets: response ? response.snippets : [],
matchType
});
};
/**
* Replace sentence text and update dropdown position
* @param snippet - selected snippet
*/
this.onSnippetSelect = (snippet) => {
const { currentResponseId, matchType } = this.state;
if (!snippet || !this.adapter || !matchType) {
return;
}
const regex = matchType === "name" ? NAME_REGEX : CONTENT_REGEX;
const match = this.searchCurrentSentence(regex);
if (!match) {
return;
}
const content = snippet.content;
if (storeInteractions && currentResponseId) {
this.storeChosenSnippetInteraction(currentResponseId, snippet);
}
this.adapter.replaceText(match, content);
this.updateSuggestedSnippets();
};
this.onSnippetDelete = async (snippetId) => {
showAlert("info", "Deleting a snippet, please wait...");
const response = await this.pointApi.snippets.delete(snippetId);
if (response) {
showAlert("success", "Snippet successfully deleted!");
this.updateDropdown();
}
else {
showAlert("error", "Error occurred while deleting a snippet");
}
};
/**
* Update the dropdown position and refresh top snippets
*/
this.updateDropdown = () => {
if (this.dropdown.current) {
this.dropdown.current.updatePos();
}
this.updateSuggestedSnippets();
};
this.state = {
suggestedSnippets: [],
currentResponseId: null,
query: null,
matchType: undefined
};
this.dropdown = React.createRef();
this.adapter = null;
this.pointApi = pointApi;
this.mounted = false;
this.initSession();
}
componentDidMount() {
this.mounted = true;
this.createAdapter();
}
componentWillUnmount() {
this.mounted = false;
}
createAdapter() {
const thisNode = ReactDOM.findDOMNode(this);
if (!thisNode || !(thisNode instanceof HTMLElement))
return;
const editableNode = thisNode.children[0];
if (editableNode.nodeName === "DIV") {
if (editableNode.children[1]
&& editableNode.children[1].children[1]
&& editableNode.children[1].children[1].nodeName === "TEXTAREA") {
// workaround for Material UI TextArea
this.adapter = new TextAreaAdapter(editableNode.children[1].children[1]);
}
else {
this.adapter = new ContentEditableAdapter(editableNode);
}
}
else if (editableNode.nodeName === "TEXTAREA" ||
editableNode.nodeName === "INPUT") {
this.adapter = new TextAreaAdapter(editableNode);
}
else {
throw new Error('HOC must be called with component immediatley wrapping a <div/>, <textarea/>, or <input type="text">');
}
editableNode.addEventListener("keyup", this.updateDropdown);
}
/**
* Get the text of the current sentence a user's cursor is in
* @returns the current sentence as a regex match and type of match
*/
searchCurrentSentence(regex) {
if (!this.adapter) {
return;
}
const cursorIndex = this.adapter.getCursorIndex();
if (!cursorIndex) {
return;
}
const text = this.adapter.text;
const matches = matchAll(regex, text);
const chosenMatch = matches.filter(match => cursorIndex >= match.index &&
cursorIndex <= match.index + match[0].length)[0];
return chosenMatch;
}
/**
* Query the PointApi to refresh the list of top snippets
*/
async updateSuggestedSnippets() {
if (!this.adapter || !this.mounted) {
return;
}
const cursorIndex = this.adapter.getCursorIndex();
if (cursorIndex === null) {
return;
}
let match = this.searchCurrentSentence(NAME_REGEX);
const regexType = match ? "name" : "content";
if (!match) {
match = this.searchCurrentSentence(CONTENT_REGEX);
}
const query = match && match[0].slice(0, cursorIndex - match.index);
if (!query) {
this.setState({
query: null,
currentResponseId: null,
suggestedSnippets: []
});
return;
}
if (regexType === "name") {
let trigger = query;
if (trigger.startsWith(":")) {
if (trigger.length === 1) {
trigger = "";
}
else {
trigger = trigger.slice(1);
}
}
else if (trigger.startsWith(" :")) {
if (trigger.length === 1) {
trigger = "";
}
else {
trigger = trigger.slice(2);
}
}
const response = await this.session.queryByName(trigger);
this.setStateAfterRequest(trigger, response, regexType);
}
else {
if (query.length < 3) {
this.setState({
query: null,
currentResponseId: null,
suggestedSnippets: []
});
return;
}
const response = await this.session.queryByContent(query);
this.setStateAfterRequest(query, response, regexType);
}
}
storeChosenSnippetInteraction(responseId, snippet) {
const origin = getOrigin();
if (responseId) {
this.session.feedback(responseId, snippet, origin);
}
}
render() {
const { suggestedSnippets, query } = this.state;
return (React.createElement("div", null,
React.createElement(Editable, Object.assign({}, this.props)),
this.adapter && (React.createElement(Dropdown, { dropdownClass: dropdownClass, innerRef: this.dropdown, editable: this.adapter, snippets: suggestedSnippets, query: query, onSnippetSelect: this.onSnippetSelect, onSnippetDelete: this.onSnippetDelete, searchType: searchType, tabCompletion: tabCompletion }))));
}
},
_a.displayName = `WithSubscription(${getDisplayName(Editable)})`,
_a;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}
export default addDropdown;
//# sourceMappingURL=AutoComplete.js.map