UNPKG

@ai-stack/payloadcms

Version:

<p align="center"> <img alt="Payload AI Plugin" src="assets/payload-ai-intro.gif" width="100%" /> </p>

437 lines (436 loc) 16.8 kB
/** * Credit: Yury Dymov * Github: https://github.com/yury-dymov/react-autocomplete-input * * Modified to only be use for PromptEditorField */ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import * as inputSelection from 'get-input-selection'; import isEqual from 'lodash.isequal'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import scrollIntoView from 'scroll-into-view-if-needed'; import getCaretCoordinates from 'textarea-caret'; import styles from './AutocompleteTextArea.module.scss'; const KEY_UP = 38; const KEY_DOWN = 40; const KEY_RETURN = 13; const KEY_ENTER = 14; const KEY_ESCAPE = 27; const KEY_TAB = 9; const OPTION_LIST_MIN_WIDTH = 100; export const AutocompleteTextField = (props)=>{ const { changeOnSelect = (trigger, slug)=>trigger + slug, defaultValue = '', disabled = false, matchAny = false, maxOptions = 10, minChars = 0, offsetX = 0, offsetY = 0, onBlur = (e)=>{}, onChange = (e)=>{}, onKeyDown = (e)=>{}, onRequestOptions = (e)=>{}, onSelect = (e)=>{}, options = [], passThroughEnter = false, passThroughTab = true, regex = '^[A-Za-z0-9\\-_]+$', requestOnlyIfNoOptions = true, spaceRemovers = [ ',', '.', '!', '?' ], spacer = ' ', trigger = '@', triggerCaseInsensitive = false, triggerMatchWholeWord = false, value: propValue = null, ...rest } = props; const [helperVisible, setHelperVisible] = useState(false); const [left, setLeft] = useState(0); const [top, setTop] = useState(0); const [triggerChar, setTriggerChar] = useState(null); const [matchLength, setMatchLength] = useState(0); const [matchStart, setMatchStart] = useState(0); const [selection, setSelection] = useState(0); const [value, setValue] = useState(null); const [caret, setCaret] = useState(0); const [currentOptions, setCurrentOptions] = useState([]); const recentValue = useRef(defaultValue); const enableSpaceRemovers = useRef(false); const inputRef = useRef(null); const currentRef = useRef(null); const parentRef = useRef(null); useEffect(()=>{ window.addEventListener('resize', handleResize); window.addEventListener('scroll', handleResize); return ()=>{ window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleResize); }; }, []); useEffect(()=>{ if (!isEqual(options, currentOptions)) { updateHelper(recentValue.current, caret, options); } }, [ options, caret ]); useEffect(()=>{ if (helperVisible && currentRef.current) { scrollIntoView(currentRef.current, { boundary: parentRef.current, scrollMode: 'if-needed' }); } }, [ helperVisible, selection ]); const handleResize = useCallback(()=>{ setHelperVisible(false); }, []); const isTrigger = useCallback((triggerStr, str, i)=>{ if (!triggerStr || !triggerStr.length) { return true; } if (triggerMatchWholeWord && i > 0 && str.charAt(i - 1).match(/\w/)) { return false; } if (str.substr(i, triggerStr.length) === triggerStr || triggerCaseInsensitive && str.substr(i, triggerStr.length).toLowerCase() === triggerStr.toLowerCase()) { return true; } return false; }, [ triggerMatchWholeWord, triggerCaseInsensitive ]); const arrayTriggerMatch = useCallback((triggers, re)=>{ return triggers.map((trigger)=>({ triggerLength: trigger.length, triggerMatch: trigger.match(re), triggerStr: trigger })); }, []); const getMatch = useCallback((str, caret, providedOptions)=>{ const re = new RegExp(regex); const triggers = Array.isArray(trigger) ? trigger : [ trigger ]; triggers.sort(); const providedOptionsObject = Array.isArray(providedOptions) ? triggers.reduce((acc, triggerStr)=>({ ...acc, [triggerStr]: providedOptions }), {}) : providedOptions; const triggersMatch = arrayTriggerMatch(triggers, re); let slugData = null; for(let triggersIndex = 0; triggersIndex < triggersMatch.length; triggersIndex++){ const { triggerLength, triggerMatch, triggerStr } = triggersMatch[triggersIndex]; for(let i = caret - 1; i >= 0; --i){ const substr = str.substring(i, caret); const match = substr.match(re); let matchStart = -1; if (triggerLength > 0) { const triggerIdx = triggerMatch ? i : i - triggerLength + 1; if (triggerIdx < 0) { break; } if (isTrigger(triggerStr, str, triggerIdx)) { matchStart = triggerIdx + triggerLength; } if (!match && matchStart < 0) { break; } } else { if (match && i > 0) { continue; } matchStart = i === 0 && match ? 0 : i + 1; if (caret - matchStart === 0) { break; } } if (matchStart >= 0) { const triggerOptions = providedOptionsObject[triggerStr]; if (triggerOptions == null) { continue; } const matchedSlug = str.substring(matchStart, caret); const filteredOptions = triggerOptions.filter((slug)=>{ const idx = slug.toLowerCase().indexOf(matchedSlug.toLowerCase()); return idx !== -1 && (matchAny || idx === 0); }); const currTrigger = triggerStr; const matchLength = matchedSlug.length; slugData = { matchLength, matchStart, options: filteredOptions, trigger: currTrigger }; } } } return slugData; }, [ regex, trigger, arrayTriggerMatch, isTrigger, matchAny ]); const updateCaretPosition = useCallback((newCaret)=>{ requestAnimationFrame(()=>{ setCaret(newCaret); inputSelection.default.setCaretPosition(inputRef.current, newCaret); }); }, []); const updateHelper = useCallback((str, caretPos, helperOptions)=>{ const slug = getMatch(str, caretPos, helperOptions); if (slug) { const caretCoordinates = getCaretCoordinates(inputRef.current, caretPos); const rect = inputRef.current.getBoundingClientRect(); const newTop = caretCoordinates.top + rect.top - inputRef.current.scrollTop; const newLeft = Math.min(caretCoordinates.left + rect.left - inputRef.current.scrollLeft, window.innerWidth - OPTION_LIST_MIN_WIDTH); if (slug.matchLength >= minChars && (slug.options.length > 1 || slug.options.length === 1 && (slug.options[0].length !== slug.matchLength || slug.options[0].length === 1))) { setHelperVisible(true); setTop(newTop); setLeft(newLeft); setTriggerChar(slug.trigger); setMatchLength(slug.matchLength); setMatchStart(slug.matchStart); setCurrentOptions(slug.options); } else { if (!requestOnlyIfNoOptions || !slug.options.length) { onRequestOptions(str.substr(slug.matchStart, slug.matchLength)); } resetHelper(); } updateCaretPosition(caretPos); } else { resetHelper(); } }, [ getMatch, minChars, requestOnlyIfNoOptions, onRequestOptions, updateCaretPosition ]); const resetHelper = useCallback(()=>{ setHelperVisible(false); setSelection(0); }, []); const handleChange = useCallback((e)=>{ const str = e.target.value; const caretPos = inputSelection.default.default(e.target).end; if (!str.length) { setHelperVisible(false); } recentValue.current = str; if (!str.length || !caretPos) { return onChange(e.target.value); } // Space removers logic if (enableSpaceRemovers.current && spaceRemovers.length && str.length > 2 && spacer.length) { for(let i = 0; i < Math.max(recentValue.current.length, str.length); ++i){ if (recentValue.current[i] !== str[i]) { if (i >= 2 && str[i - 1] === spacer && spaceRemovers.indexOf(str[i - 2]) === -1 && spaceRemovers.indexOf(str[i]) !== -1 && getMatch(str.substring(0, i - 2), caretPos - 3, options)) { const newValue = `${str.slice(0, i - 1)}${str.slice(i, i + 1)}${str.slice(i - 1, i)}${str.slice(i + 1)}`; updateCaretPosition(i + 1); inputRef.current.value = newValue; if (!propValue) { setValue(newValue); } return onChange(newValue); } break; } } enableSpaceRemovers.current = false; } updateHelper(str, caretPos, options); if (!propValue) { setValue(e.target.value); } return onChange(e.target.value); }, [ onChange, propValue, spaceRemovers, spacer, options, updateCaretPosition, updateHelper, getMatch ]); const handleBlur = useCallback((e)=>{ resetHelper(); onBlur(e); }, [ onBlur, resetHelper ]); const handleSelection = useCallback((idx)=>{ const slug = currentOptions[idx]; const value = recentValue.current; const part1 = triggerChar.length === 0 ? '' : value.substring(0, matchStart - triggerChar.length); const part2 = value.substring(matchStart + matchLength); const event = { target: inputRef.current }; const changedStr = changeOnSelect(triggerChar, slug); event.target.value = `${part1}${changedStr}${spacer}${part2}`; handleChange(event); onSelect(event.target.value); resetHelper(); const advanceCaretDistance = part1.length + changedStr.length + (spacer ? spacer.length : 1); updateCaretPosition(advanceCaretDistance); enableSpaceRemovers.current = true; }, [ currentOptions, triggerChar, matchStart, matchLength, changeOnSelect, spacer, handleChange, onSelect, resetHelper, updateCaretPosition ]); const handleKeyDown = useCallback((event)=>{ const optionsCount = maxOptions > 0 ? Math.min(currentOptions.length, maxOptions) : currentOptions.length; if (helperVisible) { switch(event.keyCode){ case KEY_ESCAPE: event.preventDefault(); resetHelper(); break; case KEY_UP: event.preventDefault(); if (optionsCount > 0) { setSelection((prevSelection)=>Math.max(0, optionsCount + prevSelection - 1) % optionsCount); } break; case KEY_DOWN: event.preventDefault(); if (optionsCount > 0) { setSelection((prevSelection)=>(prevSelection + 1) % optionsCount); } break; case KEY_ENTER: case KEY_RETURN: if (!passThroughEnter) { event.preventDefault(); } handleSelection(selection); break; case KEY_TAB: if (!passThroughTab) { event.preventDefault(); } handleSelection(selection); break; default: onKeyDown(event); break; } } else { onKeyDown(event); } }, [ helperVisible, currentOptions, maxOptions, passThroughEnter, passThroughTab, selection, onKeyDown, resetHelper, handleSelection ]); const renderAutocompleteList = useCallback(()=>{ if (!helperVisible || currentOptions.length === 0) { return null; } if (selection >= currentOptions.length) { setSelection(0); return null; } const optionNumber = maxOptions === 0 ? currentOptions.length : maxOptions; const helperOptions = currentOptions.slice(0, optionNumber).map((val, idx)=>{ let [helper, value] = val.split(' '); if (!value) { helper = undefined; value = val; } const renderHighlightedText = (text)=>{ const highlightStart = text.toLowerCase().indexOf(recentValue.current.substr(matchStart, matchLength).toLowerCase()); const highlightedText = text.substr(highlightStart, matchLength); if (!val.startsWith(text)) { return text; } return /*#__PURE__*/ _jsxs(React.Fragment, { children: [ text.slice(0, highlightStart), /*#__PURE__*/ _jsx("strong", { children: highlightedText }), text.slice(highlightStart + matchLength) ] }); }; return /*#__PURE__*/ _jsxs("li", { className: idx === selection ? styles.active : null, onClick: ()=>{ handleSelection(idx); }, onMouseDown: (e)=>{ e.preventDefault(); }, onMouseEnter: ()=>{ setSelection(idx); }, ref: idx === selection ? currentRef : null, role: "presentation", children: [ helper && /*#__PURE__*/ _jsx("code", { className: styles.helper, children: renderHighlightedText(helper) }), renderHighlightedText(value) ] }, val); }); const maxWidth = window.innerWidth - left - offsetX - 5; const maxHeight = window.innerHeight - top - offsetY - 5; return /*#__PURE__*/ _jsx("ul", { className: styles.autocompleteInput + ' ' + 'popup__content', ref: parentRef, style: { left: left + offsetX, maxHeight, maxWidth, opacity: 'initial', pointerEvents: 'initial', position: 'fixed', top: top + offsetY, visibility: 'initial' }, children: helperOptions }); }, [ helperVisible, currentOptions, selection, maxOptions, left, top, offsetX, offsetY, matchStart, matchLength, handleSelection ]); return /*#__PURE__*/ _jsxs("div", { className: "popup", children: [ /*#__PURE__*/ _jsx("textarea", { className: "textarea-outer", disabled: disabled, onBlur: handleBlur, onChange: handleChange, onKeyDown: handleKeyDown, ref: inputRef, rows: 6, style: { overflow: 'auto' }, value: propValue !== null ? propValue : value || defaultValue, ...rest }), renderAutocompleteList() ] }); }; //# sourceMappingURL=AutocompleteTextArea.js.map