@eccenca/gui-elements
Version:
GUI elements based on other libraries, usable in React application, written in Typescript.
466 lines • 25.8 kB
JavaScript
"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