UNPKG

@point-api/dropdown-react

Version:

HOC to add a Point API autocomplete dropdown

219 lines 9.93 kB
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