UNPKG

@eccenca/gui-elements

Version:

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

312 lines 17.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 __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiSuggestField = MultiSuggestField; const react_1 = __importStar(require("react")); const select_1 = require("@blueprintjs/select"); const stringUtils_1 = require("../../common/utils/stringUtils"); const constants_1 = require("../../configuration/constants"); const index_1 = require("./../../index"); /** * Element behaves very similar to `SuggestField` but allows multiple selections. * Its value does not represent a string but a stack of objects. * * Example usage: input field for user created tags. * * Attention: there may be another `MultiSelect` component in future but this will be a re-implemented `Select` like element allowing multiple selections. */ function MultiSuggestField(_a) { var { items, selectedItems: externalSelectedItems, prePopulateWithItems, itemId, itemLabel, onSelection, contextOverlayProps, tagInputProps, inputProps, runOnQueryChange, fullWidth = true, noResultText = "No results.", newItemCreationText = "Add new item", newItemPostfix = " (new item)", disabled, createNewItemFromQuery, requestDelay = 0, clearQueryOnSelection = false, className, "data-test-id": dataTestId, "data-testid": dataTestid, wrapperProps, searchPredicate, limitHeightOpened, intent } = _a, otherMultiSelectProps = __rest(_a, ["items", "selectedItems", "prePopulateWithItems", "itemId", "itemLabel", "onSelection", "contextOverlayProps", "tagInputProps", "inputProps", "runOnQueryChange", "fullWidth", "noResultText", "newItemCreationText", "newItemPostfix", "disabled", "createNewItemFromQuery", "requestDelay", "clearQueryOnSelection", "className", "data-test-id", "data-testid", "wrapperProps", "searchPredicate", "limitHeightOpened", "intent"]); // Options created by a user const createdItems = (0, react_1.useRef)([]); // Options passed ouside (f.e. from the backend) const [externalItems, setExternalItems] = react_1.default.useState([...items]); // All options (created and passed) that match the query const [filteredItems, setFilteredItems] = react_1.default.useState([]); // All options (created and passed) selected by a user const [selectedItems, setSelectedItems] = react_1.default.useState(() => prePopulateWithItems ? [...items] : externalSelectedItems ? [...externalSelectedItems] : []); // Max height of the menu const [calculatedMaxHeight, setCalculatedMaxHeight] = react_1.default.useState(null); //currently focused element in popover list const [focusedItem, setFocusedItem] = react_1.default.useState(null); const [showSpinner, setShowSpinner] = react_1.default.useState(false); const inputRef = react_1.default.useRef(null); const requestState = (0, react_1.useRef)({}); /** Update external items when they change * e.g for auto-complete when query change */ react_1.default.useEffect(() => { setExternalItems(items); setFilteredItems([...items, ...createdItems.current]); }, [items.map((item) => itemId(item)).join("|")]); react_1.default.useEffect(() => { onSelection === null || onSelection === void 0 ? void 0 : onSelection({ newlySelected: selectedItems.slice(-1)[0], createdItems: createdItems.current, selectedItems, }); }, [ onSelection, selectedItems.map((item) => itemId(item)).join("|"), createdItems.current.map((item) => itemId(item)).join("|"), ]); /** * Update selected items if we get new selected items from outside */ react_1.default.useEffect(() => { if (!externalSelectedItems) { return; } setSelectedItems(externalSelectedItems); }, [externalSelectedItems === null || externalSelectedItems === void 0 ? void 0 : externalSelectedItems.map((item) => itemId(item)).join("|")]); react_1.default.useEffect(() => { const calculateMaxHeight = () => { if (inputRef.current) { // Get the height of the input target const inputTargetHeight = inputRef.current.getBoundingClientRect().height; // Calculate the menu dropdown by using the limited height reduced by the target height setCalculatedMaxHeight(`calc(${maxHeightToProcess}vh - ${inputTargetHeight}px)`); } }; const removeListener = () => { window.removeEventListener("resize", calculateMaxHeight); }; if (!limitHeightOpened || (typeof limitHeightOpened === "number" && limitHeightOpened > 100)) return removeListener; const maxHeightToProcess = typeof limitHeightOpened === "number" ? limitHeightOpened : 100; calculateMaxHeight(); window.addEventListener("resize", calculateMaxHeight); return removeListener; }, [limitHeightOpened, selectedItems]); /** * using the equality prop specified checks if an item has already been selected * @param matcher * @returns */ const itemHasBeenSelectedAlready = (matcher) => { return !!selectedItems.find((item) => itemId(item) === matcher); }; /** * removes already selected item from the selectedItems * @param matcher */ const removeItemSelection = (matcher) => { const filteredItems = selectedItems.filter((item) => itemId(item) !== matcher); setSelectedItems(filteredItems); }; const defaultFilterPredicate = (item, query) => { return itemLabel(item).toLowerCase().includes(query); }; /** * selects and deselects an item from selection list * if the item exists it removes it instead * @param item */ const onItemSelect = (item) => { var _a, _b; if (itemHasBeenSelectedAlready(itemId(item))) { removeItemSelection(itemId(item)); } else { setSelectedItems((items) => [...items, item]); } if (clearQueryOnSelection) { requestState.current.query = ""; (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); } else { (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.select(); } }; /** * search through item list using "label prop" and update the items popover * @param query */ const onQueryChange = (query) => { setFilteredItems([]); if (query.length && query !== requestState.current.query) { requestState.current.query = query; if (requestState.current.timeoutId) { clearTimeout(requestState.current.timeoutId); } const fn = () => __awaiter(this, void 0, void 0, function* () { setShowSpinner(true); setFilteredItems([]); const resultFromQuery = runOnQueryChange && (yield runOnQueryChange((0, stringUtils_1.removeExtraSpaces)(query))); if (requestState.current.query === query) { // Only use most recent request const outsideOptions = [...(resultFromQuery !== null && resultFromQuery !== void 0 ? resultFromQuery : externalItems)]; const filter = searchPredicate !== null && searchPredicate !== void 0 ? searchPredicate : defaultFilterPredicate; setFilteredItems([...outsideOptions, ...createdItems.current].filter((item) => filter(item, query.toLowerCase()))); setShowSpinner(false); } }); requestState.current.timeoutId = window.setTimeout(fn, requestDelay && requestDelay > 0 ? requestDelay : 0); } else if (!query.length) { // if the query is empty we need to show all options and reset current query requestState.current.query = ""; setFilteredItems(() => [...externalItems, ...createdItems.current]); } }; // Renders the entries of the (search) options list const optionRenderer = (label) => { return react_1.default.createElement(index_1.Highlighter, { label: label, searchValue: requestState.current.query }); }; /** * defines how an item in the item list is displayed */ const onItemRenderer = (item, { handleClick, modifiers }) => { if (!modifiers.matchesPredicate) { return null; } let label = itemLabel(item); if (createdItems.current.find((created) => itemId(created) === itemId(item))) { label += newItemPostfix; } return (react_1.default.createElement(index_1.MenuItem, { active: modifiers.active, key: itemId(item), icon: itemHasBeenSelectedAlready(itemId(item)) ? "state-checked" : "state-unchecked", onClick: handleClick, text: optionRenderer(label), shouldDismissPopover: false })); }; /** * clear all selected items in the multi-select input */ const handleClear = () => { requestState.current.query = ""; setSelectedItems([]); setFilteredItems([...externalItems, ...createdItems.current]); }; /** * remove a specific item from the multi-select input * @param label * @param index */ const removeTagFromSelectionViaIndex = (_label, index) => { setSelectedItems([...selectedItems.slice(0, index), ...selectedItems.slice(index + 1)]); }; /** * Utility function to create a new Item. createNewItemFromQuery is assumed to be defined! */ const createNewItem = (query) => { const newItem = createNewItemFromQuery(query); //set new items createdItems.current = [...createdItems.current, newItem]; setFilteredItems((items) => [...items, newItem]); requestState.current.query = ""; return newItem; }; /** * added functionality to create new item when there are no matching items on enter keypress * @param event */ const handleOnKeyUp = (event) => { var _a; if (event.key === "Enter" && !filteredItems.length && !!requestState.current.query && createNewItemFromQuery) { createNewItem(requestState.current.query); } (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }; /** * added functionality to either create new item * when there are no matching items or select an item on tab keypress * @param event */ const handleOnKeyDown = (event) => { if (event.key === "Tab" && !!requestState.current.query) { event.preventDefault(); if (focusedItem) { onItemSelect(focusedItem); } else { onItemSelect(createNewItem(requestState.current.query)); } requestState.current.query = ""; setTimeout(() => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }); } }; /** * create new item handler, displays the new item selector and creates a new item when selected * @param label ' * @param active * @param handleClick * @returns */ const newItemRenderer = (label, active, handleClick) => { if (!createNewItemFromQuery) return undefined; const clickHandler = (e) => { createNewItem(label); handleClick(e); }; return (react_1.default.createElement(index_1.MenuItem, { id: "new-item", icon: "item-add-artefact", active: active, key: label, onClick: clickHandler, text: react_1.default.createElement(index_1.OverflowText, null, `${newItemCreationText} '${label}'`) })); }; // Clear button and spinner are both shown as "right element" const clearButton = selectedItems.length > 0 ? (react_1.default.createElement(index_1.IconButton, { disabled: disabled, name: "operation-clear", "data-test-id": dataTestId ? dataTestId + "_clearance" : undefined, "data-testid": dataTestid ? dataTestid + "_clearance" : undefined, onClick: handleClear })) : undefined; const spinnerProps = showSpinner ? { rightElement: react_1.default.createElement(index_1.Spinner, { position: "inline", size: "tiny" }), } : {}; const contentMultiSelect = (react_1.default.createElement(select_1.MultiSelect, Object.assign({ placeholder: !otherMultiSelectProps.placeholder && createNewItemFromQuery ? "Search for item, or enter term to create new one..." : undefined }, otherMultiSelectProps, { query: requestState.current.query, onQueryChange: onQueryChange, items: filteredItems, onItemSelect: onItemSelect, itemRenderer: onItemRenderer, itemsEqual: (a, b) => itemId(a) === itemId(b), selectedItems: selectedItems, noResults: react_1.default.createElement(index_1.MenuItem, { disabled: true, text: noResultText }), tagRenderer: (item) => itemLabel(item), createNewItemRenderer: newItemRenderer, onActiveItemChange: (activeItem) => setFocusedItem(activeItem), fill: fullWidth, createNewItemFromQuery: createNewItemFromQuery, disabled: disabled, tagInputProps: Object.assign(Object.assign({ inputProps: Object.assign({ id: "item", autoComplete: "off", "data-test-id": dataTestId ? dataTestId + "_searchinput" : undefined, "data-testid": dataTestid ? dataTestid + "_searchinput" : undefined }, inputProps), className: `${constants_1.CLASSPREFIX}-multisuggestfield ${constants_1.CLASSPREFIX}-multiselect` + (className ? ` ${className}` : ""), fill: fullWidth, inputRef: inputRef, intent: intent, addOnBlur: true, onKeyDown: handleOnKeyDown, onKeyUp: handleOnKeyUp, onRemove: removeTagFromSelectionViaIndex, rightElement: (react_1.default.createElement(react_1.default.Fragment, null, clearButton !== null && clearButton !== void 0 ? clearButton : null, otherMultiSelectProps.openOnKeyDown !== true && (react_1.default.createElement(index_1.IconButton, { disabled: disabled, name: "toggler-caretdown", "data-test-id": dataTestId ? dataTestId + "_toggler" : undefined, "data-testid": dataTestid ? dataTestid + "_toggler" : undefined })))), tagProps: { minimal: true }, disabled }, tagInputProps), spinnerProps), popoverTargetProps: { className: `${constants_1.CLASSPREFIX}-multiselect__target`, }, popoverProps: Object.assign({ minimal: true, placement: "bottom-start", matchTargetWidth: fullWidth }, contextOverlayProps), popoverContentProps: { "data-test-id": dataTestId ? dataTestId + "_drowpdown" : undefined, "data-testid": dataTestid ? dataTestid + "_dropdown" : undefined, style: calculatedMaxHeight ? { "--eccgui-multisuggestfield-max-height": `${calculatedMaxHeight}`, } : undefined, } }))); return wrapperProps || dataTestId || dataTestid ? (react_1.default.createElement("div", Object.assign({ className: `${constants_1.CLASSPREFIX}-multiselect__wrapper` }, (wrapperProps !== null && wrapperProps !== void 0 ? wrapperProps : {}), { "data-test-id": dataTestId, "data-testid": dataTestid }), contentMultiSelect)) : (react_1.default.createElement(react_1.default.Fragment, null, contentMultiSelect)); } // we still return the Blueprint element here because it was already used like that /** * @deprecated (v26) use directly <MultiSuggestField<TYPE>> (`ofType` also returns the original BlueprintJS element, not ours!) */ MultiSuggestField.ofType = select_1.MultiSelect.ofType; exports.default = MultiSuggestField; //# sourceMappingURL=MultiSelect.js.map