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