UNPKG

@eccenca/gui-elements

Version:

GUI elements based on other libraries, usable in React application, written in Typescript.

466 lines 25.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeAutocompleteField = exports.OVERWRITTEN_KEYS = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const react_1 = __importStar(require("react")); const core_1 = require("@blueprintjs/core"); const lodash_1 = require("lodash"); const constants_1 = require("../../configuration/constants"); const __1 = require("./../../"); const markText_1 = require("./extensions/markText"); const AutoSuggestionList_1 = require("./AutoSuggestionList"); //custom components const ExtendedCodeEditor_1 = __importDefault(require("./ExtendedCodeEditor")); const EXTRA_VERTICAL_PADDING = 10; var OVERWRITTEN_KEYS; (function (OVERWRITTEN_KEYS) { OVERWRITTEN_KEYS["ArrowUp"] = "ArrowUp"; OVERWRITTEN_KEYS["ArrowDown"] = "ArrowDown"; OVERWRITTEN_KEYS["Enter"] = "Enter"; OVERWRITTEN_KEYS["Tab"] = "Tab"; OVERWRITTEN_KEYS["Escape"] = "Escape"; })(OVERWRITTEN_KEYS || (exports.OVERWRITTEN_KEYS = OVERWRITTEN_KEYS = {})); /** * Input component that allows partial, fine-grained auto-completion, i.e. of sub-strings of the input string. * This is comparable to a one line code editor. * * Example usage: input of a path string offering auto-completion for each single part of the path. */ const CodeAutocompleteField = ({ className, label, initialValue, onChange, fetchSuggestions, checkInput, validationErrorText = "Invalid value", clearIconText = "Clear", onFocusChange, id, onInputChecked, leftElement, rightElement, useTabForCompletions = false, placeholder, showScrollBar = true, autoCompletionRequestDelay = 1000, validationRequestDelay = 200, mode, multiline = false, reInitOnInitialValueChange = false, height, readOnly, outerDivAttributes, }) => { var _a, _b, _c; const value = react_1.default.useRef(initialValue); const cursorPosition = react_1.default.useRef(0); const dropdownXYoffset = react_1.default.useRef({ x: 0, y: 0 }); const [shouldShowDropdown, setShouldShowDropdown] = react_1.default.useState(false); const [suggestions, setSuggestions] = react_1.default.useState([]); const [suggestionsPending, setSuggestionsPending] = react_1.default.useState(false); const suggestionRequestData = react_1.default.useRef({ requestId: undefined }); const [pathValidationPending, setPathValidationPending] = react_1.default.useState(false); const validationRequestData = react_1.default.useRef({ requestId: undefined }); const errorMarkers = react_1.default.useRef([]); const [validationResponse, setValidationResponse] = (0, react_1.useState)(undefined); const [suggestionResponse, setSuggestionResponse] = (0, react_1.useState)(undefined); // The element that should be used for replacement highlighting const [highlightedElement, setHighlightedElement] = (0, react_1.useState)(undefined); const [cm, setCM] = react_1.default.useState(); const currentCm = react_1.default.useRef(); currentCm.current = cm; const isFocused = react_1.default.useRef(false); const autoSuggestionDivRef = react_1.default.useRef(null); /** Mutable editor state, since this needs to be current in scope of the SingleLineEditorComponent. */ const [editorState] = react_1.default.useState({ index: 0, suggestions: [], dropdownShown: false }); /** This is for the AutoSuggestionList component in order to re-render. */ const [focusedIndex, setFocusedIndex] = react_1.default.useState(0); const selectedTextRanges = react_1.default.useRef([]); const pathIsValid = (_a = validationResponse === null || validationResponse === void 0 ? void 0 : validationResponse.valid) !== null && _a !== void 0 ? _a : true; react_1.default.useEffect(() => { var _a; if (reInitOnInitialValueChange && initialValue != null && currentCm.current) { dispatch({ changes: { from: 0, to: (_a = currentCm.current.state) === null || _a === void 0 ? void 0 : _a.doc.length, insert: initialValue }, }); // Validate initial value change checkValuePathValidity(initialValue); } }, [initialValue, reInitOnInitialValueChange]); react_1.default.useEffect(() => { if (currentCm.current) { // Validate initial value checkValuePathValidity(initialValue); } }, [!!currentCm.current]); const setCurrentIndex = (newIndex) => { editorState.index = newIndex; setFocusedIndex(newIndex); }; const currentIndex = () => editorState.index; react_1.default.useEffect(() => { editorState.cm = cm; }, [cm, editorState]); const dispatch = (typeof ((_b = editorState === null || editorState === void 0 ? void 0 : editorState.cm) === null || _b === void 0 ? void 0 : _b.dispatch) === "function" ? (_c = editorState === null || editorState === void 0 ? void 0 : editorState.cm) === null || _c === void 0 ? void 0 : _c.dispatch : () => { }); react_1.default.useEffect(() => { editorState.dropdownShown = shouldShowDropdown; }, [shouldShowDropdown, editorState]); // Handle replacement highlighting (0, react_1.useEffect)(() => { var _a; if (highlightedElement && cm) { const { from, length } = highlightedElement; if (length > 0 && selectedTextRanges.current.length === 0) { const to = from + length; const { toOffset, fromOffset } = getOffsetRange(cm, from, to); (0, markText_1.markText)({ view: cm, from: fromOffset, to: toOffset, className: `${constants_1.CLASSPREFIX}-autosuggestion__text--highlighted`, }); return () => (0, markText_1.removeMarkFromText)({ view: cm, from, to }); } } else { if (cm) { (0, markText_1.removeMarkFromText)({ view: cm, from: 0, to: (_a = cm.state) === null || _a === void 0 ? void 0 : _a.doc.length }); } } return; }, [highlightedElement, selectedTextRanges, cm]); //handle linting react_1.default.useEffect(() => { const parseError = validationResponse === null || validationResponse === void 0 ? void 0 : validationResponse.parseError; if (cm) { const clearCurrentErrorMarker = () => { if (errorMarkers.current.length) { const [from, to] = errorMarkers.current; (0, markText_1.removeMarkFromText)({ view: cm, from, to }); errorMarkers.current = []; } }; if (parseError) { const { message, start, end } = parseError; clearCurrentErrorMarker(); const { from, to } = (0, markText_1.markText)({ view: cm, from: start, to: end, className: `${constants_1.CLASSPREFIX}-autosuggestion__text--highlighted-error`, title: message, }); errorMarkers.current = [from, to]; } else { clearCurrentErrorMarker(); } } const isValid = (validationResponse === null || validationResponse === void 0 ? void 0 : validationResponse.valid) === undefined || validationResponse.valid; onInputChecked === null || onInputChecked === void 0 ? void 0 : onInputChecked(isValid); }, [validationResponse === null || validationResponse === void 0 ? void 0 : validationResponse.valid, validationResponse === null || validationResponse === void 0 ? void 0 : validationResponse.parseError, cm, onInputChecked]); /** generate suggestions and also populate the replacement indexes dict */ react_1.default.useEffect(() => { var _a, _b, _c, _d; let newSuggestions = []; if (((_a = suggestionResponse === null || suggestionResponse === void 0 ? void 0 : suggestionResponse.replacementResults) === null || _a === void 0 ? void 0 : _a.length) === 1 && !((_c = (_b = suggestionResponse === null || suggestionResponse === void 0 ? void 0 : suggestionResponse.replacementResults[0]) === null || _b === void 0 ? void 0 : _b.replacements) === null || _c === void 0 ? void 0 : _c.length)) { setShouldShowDropdown(false); } if ((_d = suggestionResponse === null || suggestionResponse === void 0 ? void 0 : suggestionResponse.replacementResults) === null || _d === void 0 ? void 0 : _d.length) { suggestionResponse.replacementResults.forEach(({ replacements, replacementInterval: { from, length }, extractedQuery }) => { const replacementsWithMetaData = replacements.map((r) => (Object.assign(Object.assign({}, r), { query: extractedQuery, from, length }))); newSuggestions = [...newSuggestions, ...replacementsWithMetaData]; }); editorState.suggestions = newSuggestions; setSuggestions(newSuggestions); } else { editorState.suggestions = []; setSuggestions([]); } setCurrentIndex(0); }, [suggestionResponse, editorState]); const getOffsetRange = (cm, from, to) => { if (!cm) return { fromOffset: 0, toOffset: 0 }; const cursor = cm.state.selection.main.head; const cursorLine = cm.state.doc.lineAt(cursor).number; const offsetFromFirstLine = cm.state.doc.line(cursorLine).from; //all characters including line breaks const fromOffset = offsetFromFirstLine + from; const toOffset = offsetFromFirstLine + to; return { fromOffset, toOffset }; }; const inputActionsDisplayed = react_1.default.useCallback((node) => { if (!node) return; const width = node.offsetWidth; const slCodeEditor = node.parentElement.getElementsByClassName(`${constants_1.CLASSPREFIX}-singlelinecodeeditor`); if (slCodeEditor.length > 0) { slCodeEditor[0].style.paddingRight = `${width}px`; } }, []); const asyncCheckInput = (0, react_1.useMemo)(() => (inputString) => __awaiter(void 0, void 0, void 0, function* () { if (!checkInput || inputString !== value.current || validationRequestData.current.requestId === inputString) { return; } validationRequestData.current.requestId = inputString; setPathValidationPending(true); try { const result = yield checkInput(inputString); setValidationResponse(result); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { setValidationResponse(undefined); // TODO: Error handling } finally { setPathValidationPending(false); } }), [checkInput]); const checkValuePathValidity = (0, react_1.useMemo)(() => (0, lodash_1.debounce)((inputString) => asyncCheckInput(inputString), validationRequestDelay), [asyncCheckInput, validationRequestDelay]); const asyncHandleEditorInputChange = (0, react_1.useMemo)(() => (inputString, cursorPosition) => __awaiter(void 0, void 0, void 0, function* () { const requestId = `${inputString} ${cursorPosition}`; if (requestId === suggestionRequestData.current.requestId || !(editorState === null || editorState === void 0 ? void 0 : editorState.cm)) { return; } suggestionRequestData.current.requestId = requestId; setSuggestionsPending(true); try { const cursor = editorState === null || editorState === void 0 ? void 0 : editorState.cm.state.selection.main.head; ///actual cursor position const cursorLine = editorState === null || editorState === void 0 ? void 0 : editorState.cm.state.doc.lineAt(cursor !== null && cursor !== void 0 ? cursor : 0).number; if (cursorLine) { const result = yield fetchSuggestions(inputString.split("\n")[cursorLine - 1], //line starts from 1 cursorPosition); if (value.current === inputString) { setSuggestionResponse(result); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { setSuggestionResponse(undefined); // TODO: Error handling } finally { setSuggestionsPending(false); } }), [fetchSuggestions, cm]); const handleEditorInputChange = (0, react_1.useMemo)(() => (0, lodash_1.debounce)((inputString, cursorPosition) => asyncHandleEditorInputChange(inputString, cursorPosition), autoCompletionRequestDelay), [asyncHandleEditorInputChange, autoCompletionRequestDelay]); const handleChange = react_1.default.useMemo(() => { return (val) => { value.current = val; checkValuePathValidity.cancel(); checkValuePathValidity(value.current); onChange(val); }; }, [onChange, checkValuePathValidity]); const handleCursorChange = (cursor, coords, scrollinfo, view) => { //cursor here is offset from line 1, autosuggestion works with cursor per-line. // derived cursor is current cursor position - start of line of cursor const cursorLine = view.state.doc.lineAt(cursor).number; const offsetFromFirstLine = view.state.doc.line(cursorLine).from; //; cursorPosition.current = cursor - offsetFromFirstLine; // cursor change is fired after onChange, so we put the auto-complete logic here //get value at line if (isFocused.current) { setShouldShowDropdown(true); handleEditorInputChange.cancel(); handleEditorInputChange(value.current, cursorPosition.current); } setTimeout(() => { dropdownXYoffset.current = { x: Math.min(coords.left, Math.max(coords.left - (scrollinfo === null || scrollinfo === void 0 ? void 0 : scrollinfo.scrollLeft), 0)), y: multiline ? Math.min(coords.bottom, Math.max(coords.bottom - (scrollinfo === null || scrollinfo === void 0 ? void 0 : scrollinfo.scrollTop), 0)) + EXTRA_VERTICAL_PADDING : 0, }; }, 1); }; const handleInputEditorKeyPress = (event) => { const overWrittenKeys = Object.values(OVERWRITTEN_KEYS); if (overWrittenKeys.includes(event.key) && (useTabForCompletions || event.key !== OVERWRITTEN_KEYS.Tab)) { //don't prevent when enter should create new line (multiline config) and dropdown isn't shown const allowDefaultEnterKeyPressBehavior = multiline && !editorState.suggestions.length; const overwrittenKey = OVERWRITTEN_KEYS[event.key]; if (!allowDefaultEnterKeyPressBehavior) { event.preventDefault(); makeDropDownRespondToKeyPress(overwrittenKey); return true; //prevent new line } makeDropDownRespondToKeyPress(overwrittenKey); return false; // allow new line if enter } return true; }; const closeDropDown = () => { setHighlightedElement(undefined); setShouldShowDropdown(false); }; const handleDropdownChange = (selectedSuggestion) => { var _a; if (selectedSuggestion && editorState.cm) { const { from, length, value } = selectedSuggestion; // const cursor = editorState.editorInstance.getCursor(); const cursor = (_a = editorState.cm) === null || _a === void 0 ? void 0 : _a.state.selection.main.head; const to = from + length; const { fromOffset, toOffset } = getOffsetRange(editorState.cm, from, to); editorState.cm.dispatch({ changes: { from: fromOffset, to: toOffset, insert: value, }, }); closeDropDown(); const cursorLine = editorState.cm.state.doc.lineAt(cursor).number; const newCursorPos = editorState.cm.state.doc.line(cursorLine).from + (from + value.length); editorState.cm.dispatch({ selection: { anchor: newCursorPos } }); editorState.cm.focus(); } }; const handleInputEditorClear = () => { dispatch({ changes: { from: 0, to: cm === null || cm === void 0 ? void 0 : cm.state.doc.length, insert: "" }, }); cursorPosition.current = 0; handleChange(""); cm === null || cm === void 0 ? void 0 : cm.focus(); }; const handleInputFocus = (focusState) => { onFocusChange === null || onFocusChange === void 0 ? void 0 : onFocusChange(focusState); if (focusState) { setShouldShowDropdown(true); } else { closeDropDown(); } if (!isFocused.current && focusState) { // Just got focus // Clear suggestions and repeat suggestion request, something else might have changed while this component was not focused setSuggestions([]); suggestionRequestData.current.requestId = undefined; isFocused.current = focusState; handleEditorInputChange.cancel(); handleEditorInputChange(value.current, cursorPosition.current); } else { isFocused.current = focusState; } }; const handleInputMouseDown = react_1.default.useCallback((editor) => { var _a, _b; const cursor = (_a = editorState.cm) === null || _a === void 0 ? void 0 : _a.state.selection.main.head; const currentLine = (_b = editorState.cm) === null || _b === void 0 ? void 0 : _b.state.doc.lineAt(cursor !== null && cursor !== void 0 ? cursor : 0).number; const clickedLine = editor === null || editor === void 0 ? void 0 : editor.state.doc.lineAt(cursor !== null && cursor !== void 0 ? cursor : 0).number; //Clicking on a different line other than the current line //where the dropdown already suggests should close the dropdown if (currentLine !== clickedLine) { closeDropDown(); editorState.suggestions = []; setSuggestions([]); } }, []); //keyboard handlers const handleArrowDown = () => { const lastSuggestionIndex = editorState.suggestions.length - 1; setCurrentIndex(currentIndex() === lastSuggestionIndex ? 0 : currentIndex() + 1); }; const handleArrowUp = () => { const lastSuggestionIndex = editorState.suggestions.length - 1; setCurrentIndex(currentIndex() === 0 ? lastSuggestionIndex : currentIndex() - 1); }; const handleEnterPressed = () => { handleDropdownChange(editorState.suggestions[currentIndex()]); setCurrentIndex(0); }; const handleTabPressed = () => { handleDropdownChange(editorState.suggestions[currentIndex()]); }; const handleEscapePressed = () => { closeDropDown(); editorState.suggestions = []; setSuggestions([]); }; const makeDropDownRespondToKeyPress = (keyPressedFromInput) => { // React state unknown if (editorState.dropdownShown) { switch (keyPressedFromInput) { case OVERWRITTEN_KEYS.ArrowUp: handleArrowUp(); break; case OVERWRITTEN_KEYS.ArrowDown: handleArrowDown(); break; case OVERWRITTEN_KEYS.Enter: handleEnterPressed(); break; case OVERWRITTEN_KEYS.Tab: handleTabPressed(); break; case OVERWRITTEN_KEYS.Escape: handleEscapePressed(); break; default: //do nothing closeDropDown(); } } }; const handleItemHighlighting = react_1.default.useCallback((item) => { setHighlightedElement(item); }, []); const onSelection = react_1.default.useMemo(() => (ranges) => { selectedTextRanges.current = ranges; }, []); const codeEditor = react_1.default.useMemo(() => { return (react_1.default.createElement(ExtendedCodeEditor_1.default, { mode: mode, setCM: setCM, onChange: handleChange, onCursorChange: handleCursorChange, initialValue: initialValue, onFocusChange: handleInputFocus, onKeyDown: handleInputEditorKeyPress, enableTab: useTabForCompletions, placeholder: placeholder, onSelection: onSelection, showScrollBar: showScrollBar, multiline: multiline, onMouseDown: handleInputMouseDown, height: height, readOnly: readOnly })); }, [ mode, setCM, handleChange, initialValue, useTabForCompletions, placeholder, showScrollBar, multiline, handleInputMouseDown, readOnly, ]); const hasError = !!value.current && !pathIsValid && !pathValidationPending; const autoSuggestionInput = (react_1.default.createElement("div", Object.assign({ id: id, ref: autoSuggestionDivRef, className: `${constants_1.CLASSPREFIX}-codeautocompletefield ${constants_1.CLASSPREFIX}-autosuggestion` + (className ? ` ${className}` : "") }, outerDivAttributes), react_1.default.createElement("div", { className: ` ${constants_1.CLASSPREFIX}-autosuggestion__inputfield ${core_1.Classes.INPUT_GROUP} ${core_1.Classes.FILL} ${hasError ? core_1.Classes.INTENT_DANGER : ""}` }, react_1.default.createElement(__1.ContextOverlay, { minimal: true, fill: true, isOpen: shouldShowDropdown, placement: "bottom-start", modifiers: { flip: { enabled: false } }, openOnTargetFocus: false, autoFocus: false, content: react_1.default.createElement(AutoSuggestionList_1.AutoSuggestionList, { id: id + "__dropdown", offsetValues: dropdownXYoffset.current, loading: suggestionsPending, options: suggestions, isOpen: !suggestionsPending && shouldShowDropdown, onItemSelectionChange: handleDropdownChange, currentlyFocusedIndex: focusedIndex, itemToHighlight: handleItemHighlighting }) }, codeEditor), !!value.current && (react_1.default.createElement("span", { className: core_1.Classes.INPUT_ACTION, ref: inputActionsDisplayed }, react_1.default.createElement(__1.IconButton, { "data-test-id": "value-path-clear-btn", name: "operation-clear", text: clearIconText, disabled: readOnly, onClick: handleInputEditorClear })))))); const withRightElement = rightElement || leftElement ? (react_1.default.createElement(__1.Toolbar, { noWrap: true }, leftElement && react_1.default.createElement(__1.ToolbarSection, null, leftElement), react_1.default.createElement(__1.ToolbarSection, { canGrow: true, canShrink: true }, react_1.default.createElement("div", { style: { minWidth: "100%", maxWidth: "100%" } }, autoSuggestionInput)), rightElement && react_1.default.createElement(__1.ToolbarSection, null, rightElement))) : (autoSuggestionInput); return label ? (react_1.default.createElement(__1.FieldItem, { labelProps: { text: (react_1.default.createElement(react_1.default.Fragment, null, label, "\u00A0", (pathValidationPending || suggestionsPending) && (react_1.default.createElement(__1.Spinner, { size: "tiny", position: "inline" })))), }, intent: hasError ? "danger" : undefined, messageText: hasError ? validationErrorText : undefined }, withRightElement)) : (withRightElement); }; exports.CodeAutocompleteField = CodeAutocompleteField; exports.default = exports.CodeAutocompleteField; //# sourceMappingURL=AutoSuggestion.js.map