UNPKG

@chayns-components/core

Version:

A set of beautiful React components for developing your own applications with chayns.

469 lines (458 loc) • 21.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _chaynsApi = require("chayns-api"); var _react = require("motion/react"); var _react2 = _interopRequireWildcard(require("react")); var _reactDom = require("react-dom"); var _comboBox = require("../../types/comboBox"); var _calculate = require("../../utils/calculate"); var _environment = require("../../utils/environment"); var _AreaContextProvider = require("../area-provider/AreaContextProvider"); var _Icon = _interopRequireDefault(require("../icon/Icon")); var _ComboBoxItem = _interopRequireDefault(require("./combobox-item/ComboBoxItem")); var _ComboBox = require("./ComboBox.styles"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const ComboBox = ({ bodyWidth, direction = _comboBox.ComboBoxDirection.RIGHT, isDisabled = false, lists, maxHeight = '280px', onSelect, placeholder, prefix, container, selectedItem, shouldShowBigImage, shouldShowClearIcon, shouldShowRoundImage, onInputFocus, shouldUseFullWidth = false, onInputChange, shouldUseCurrentItemWidth = false, onInputBlur, inputValue }) => { const [internalSelectedItem, setInternalSelectedItem] = (0, _react2.useState)(); const [isAnimating, setIsAnimating] = (0, _react2.useState)(false); const [minWidth, setMinWidth] = (0, _react2.useState)(undefined); const [bodyMinWidth, setBodyMinWidth] = (0, _react2.useState)(0); const [focusedIndex, setFocusedIndex] = (0, _react2.useState)(null); const [overflowY, setOverflowY] = (0, _react2.useState)('hidden'); const [translateX, setTranslateX] = (0, _react2.useState)('0px'); const [translateY, setTranslateY] = (0, _react2.useState)('0px'); const [portal, setPortal] = (0, _react2.useState)(); const [internalCoordinates, setInternalCoordinates] = (0, _react2.useState)({ x: 0, y: 0 }); const [newContainer, setNewContainer] = (0, _react2.useState)(container ?? null); const [shouldUseTopAlignment, setShouldUseTopAlignment] = (0, _react2.useState)(false); const isInputFocused = (0, _react2.useRef)(false); const styledComboBoxElementRef = (0, _react2.useRef)(null); const contentRef = (0, _react2.useRef)(null); const functions = (0, _chaynsApi.useFunctions)(); const values = (0, _chaynsApi.useValues)(); const { browser } = (0, _chaynsApi.useDevice)(); const isTouch = (0, _environment.getIsTouch)(); const areaProvider = (0, _react2.useContext)(_AreaContextProvider.AreaContext); const shouldChangeColor = (0, _react2.useMemo)(() => areaProvider.shouldChangeColor ?? false, [areaProvider.shouldChangeColor]); const shouldDisableActions = (0, _react2.useMemo)(() => { if (!selectedItem) { return false; } const combinedLists = lists.flatMap(list => list.list); return combinedLists.length === 1 && combinedLists.some(item => item.value === selectedItem.value); }, [lists, selectedItem]); const contentHeight = (0, _react2.useMemo)(() => { const flatItems = lists.flatMap(list => list.list); let result = flatItems.length * 36; if (lists.length > 1) { result += lists.length * 36; } // ToDo: Implement a better solution to also work with percentage values or other units if (maxHeight.toString().includes('px')) { const maxHeightValue = parseInt(maxHeight.toString().replace('px', ''), 10); if (maxHeightValue < result) { result = maxHeightValue; } } return result; }, [lists, maxHeight]); (0, _react2.useEffect)(() => { if (styledComboBoxElementRef.current && !container) { const el = styledComboBoxElementRef.current; const element = el.closest('.dialog-inner') || el.closest('.page-provider') || el.closest('body'); setNewContainer(element); } }, [container]); (0, _react2.useEffect)(() => { if (container instanceof Element) { setNewContainer(container); } }, [container]); const handleInputFocus = (0, _react2.useCallback)(event => { isInputFocused.current = true; onInputFocus === null || onInputFocus === void 0 || onInputFocus(event); }, [onInputFocus]); const handleInputBlur = (0, _react2.useCallback)(event => { isInputFocused.current = false; onInputBlur === null || onInputBlur === void 0 || onInputBlur(event); }, [onInputBlur]); const handleClick = (0, _react2.useCallback)(event => { if (styledComboBoxElementRef.current && !styledComboBoxElementRef.current.contains(event.target) && contentRef.current && !contentRef.current.contains(event.target)) { setIsAnimating(false); } }, [styledComboBoxElementRef]); const handleOpen = (0, _react2.useCallback)(() => { if (shouldDisableActions) { return; } if (styledComboBoxElementRef.current && newContainer) { const { left: comboBoxLeft, top: comboBoxTop, height } = styledComboBoxElementRef.current.getBoundingClientRect(); const { left, top, height: containerHeight } = newContainer.getBoundingClientRect(); const x = comboBoxLeft - left + newContainer.scrollLeft; const y = comboBoxTop - top + newContainer.scrollTop; let useTopAlignment = [_comboBox.ComboBoxDirection.TOP, _comboBox.ComboBoxDirection.TOP_LEFT, _comboBox.ComboBoxDirection.TOP_RIGHT].includes(direction); const hasBottomAlignment = [_comboBox.ComboBoxDirection.BOTTOM, _comboBox.ComboBoxDirection.BOTTOM_LEFT, _comboBox.ComboBoxDirection.BOTTOM_RIGHT].includes(direction); if (!hasBottomAlignment && y + height + contentHeight > containerHeight) { useTopAlignment = true; setShouldUseTopAlignment(true); } else { setShouldUseTopAlignment(false); } setInternalCoordinates({ x, y: useTopAlignment ? y : y + height }); setIsAnimating(true); } }, [shouldDisableActions, newContainer, contentHeight, direction]); const handleClose = (0, _react2.useCallback)(() => { setIsAnimating(false); }, []); /** * This function adds an event listener to the document to close the combobox when the user clicks outside of it */ (0, _react2.useEffect)(() => { document.addEventListener('click', handleClick); return () => { document.removeEventListener('click', handleClick); }; }, [handleClick, styledComboBoxElementRef]); /** * This function sets the selected item */ const handleSetSelectedItem = (0, _react2.useCallback)(itemToSelect => { if (typeof onSelect === 'function') { const onSelectResult = onSelect(itemToSelect); if (onSelectResult === false) { return; } if (onSelectResult instanceof Promise) { void onSelectResult.then(shouldPreventSelection => { if (shouldPreventSelection) return; setInternalSelectedItem(itemToSelect); setIsAnimating(false); }); return; } } setInternalSelectedItem(itemToSelect); setIsAnimating(false); }, [onSelect]); const handleClear = (0, _react2.useCallback)(event => { event.preventDefault(); event.stopPropagation(); handleSetSelectedItem(undefined); }, [handleSetSelectedItem]); (0, _react2.useEffect)(() => { const currentContent = contentRef.current; if (portal && isAnimating && currentContent) { const scrollHeight = currentContent.scrollHeight ?? 0; const maxHeightInPixels = (0, _calculate.getMaxHeightInPixels)(maxHeight, styledComboBoxElementRef.current ?? document.body); setOverflowY(scrollHeight > maxHeightInPixels ? 'scroll' : 'hidden'); } }, [isAnimating, maxHeight, portal]); (0, _react2.useEffect)(() => { const handleKeyDown = e => { if (!isAnimating) { return; } if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { var _contentRef$current; e.preventDefault(); const children = (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 ? void 0 : _contentRef$current.children; if (children && children.length > 0) { const newIndex = focusedIndex !== null ? (focusedIndex + (e.key === 'ArrowUp' ? -1 : 1) + children.length) % children.length : 0; if (focusedIndex !== null) { const prevElement = children[focusedIndex]; prevElement.tabIndex = -1; } setFocusedIndex(newIndex); const newElement = children[newIndex]; newElement.tabIndex = 0; newElement.focus(); } } else if (e.key === 'Enter' && focusedIndex !== null) { var _contentRef$current2; const element = (_contentRef$current2 = contentRef.current) === null || _contentRef$current2 === void 0 ? void 0 : _contentRef$current2.children[focusedIndex]; if (!element) { return; } const { id } = element; let newSelectedItem; lists.some(list => { newSelectedItem = list.list.find(({ value }) => String(value) === id.replace('combobox-item__', '')); return !!newSelectedItem; }); if (!newSelectedItem) { return; } handleSetSelectedItem(newSelectedItem); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [focusedIndex, handleSetSelectedItem, isAnimating, lists]); /** * This function calculates the greatest width */ (0, _react2.useEffect)(() => { var _styledComboBoxElemen; const allItems = lists.flatMap(list => list.list); const hasImage = [selectedItem, ...allItems].some(item => item === null || item === void 0 ? void 0 : item.imageUrl); const hasIcon = [selectedItem, ...allItems].some(item => item === null || item === void 0 ? void 0 : item.icons); const parentWidth = ((_styledComboBoxElemen = styledComboBoxElementRef.current) === null || _styledComboBoxElemen === void 0 || (_styledComboBoxElemen = _styledComboBoxElemen.parentElement) === null || _styledComboBoxElemen === void 0 ? void 0 : _styledComboBoxElemen.getBoundingClientRect().width) ?? 0; const paddingWidth = 20 + 2 + 40 + 40; // padding + border + arrow icon + optional clear icon const imageWidth = hasImage ? 32 : 0; // image width + gap if images present const iconWidth = hasIcon ? 40 : 0; // icon width + gap if icons present let prefixWidth = 0; if (prefix) { const prefixTextWidth = (0, _calculate.calculateContentWidth)([{ text: prefix, value: 'prefix' }], functions, values) + 5; prefixWidth = Math.max(prefixTextWidth, 32); } const baseWidth = (0, _calculate.calculateContentWidth)([...allItems, { text: placeholder, value: 'placeholder' }, ...(selectedItem ? [selectedItem] : [])], functions, values); const calculatedWidth = baseWidth + paddingWidth + imageWidth + iconWidth + prefixWidth; let tmpMinWidth = calculatedWidth; let tmpBodyMinWidth = calculatedWidth; // Full width settings if (shouldUseFullWidth) { tmpMinWidth = parentWidth; tmpBodyMinWidth = parentWidth < calculatedWidth - 20 ? calculatedWidth - 20 : parentWidth; } // Current item width settings else if (shouldUseCurrentItemWidth && internalSelectedItem) { const itemWidth = (0, _calculate.calculateContentWidth)([internalSelectedItem], functions, values) + paddingWidth + imageWidth + iconWidth + prefixWidth; tmpMinWidth = itemWidth; tmpBodyMinWidth = itemWidth < calculatedWidth - 20 ? calculatedWidth - 20 : itemWidth; } if (tmpMinWidth > parentWidth) { tmpMinWidth = parentWidth; } if (tmpBodyMinWidth > parentWidth) { tmpBodyMinWidth = parentWidth; } setMinWidth(tmpMinWidth); setBodyMinWidth(shouldUseCurrentItemWidth ? tmpMinWidth : tmpBodyMinWidth); }, [lists, placeholder, shouldUseFullWidth, shouldUseCurrentItemWidth, internalSelectedItem, prefix, selectedItem, functions, values]); /** * This function sets the external selected item */ (0, _react2.useEffect)(() => { setIsAnimating(false); setInternalSelectedItem(selectedItem); }, [selectedItem]); (0, _react2.useEffect)(() => { if ([_comboBox.ComboBoxDirection.BOTTOM_LEFT, _comboBox.ComboBoxDirection.TOP_LEFT, _comboBox.ComboBoxDirection.LEFT].includes(direction) && typeof bodyWidth === 'number' && typeof minWidth === 'number') { const difference = minWidth - bodyWidth; setTranslateX(`${difference}px`); } else { setTranslateX('0px'); } }, [bodyWidth, direction, minWidth]); const placeholderImageUrl = (0, _react2.useMemo)(() => { if (selectedItem) { return selectedItem.imageUrl; } if (internalSelectedItem) { return internalSelectedItem.imageUrl; } return undefined; }, [internalSelectedItem, selectedItem]); const placeholderIcon = (0, _react2.useMemo)(() => { if (selectedItem) { return selectedItem.icons; } if (internalSelectedItem) { return internalSelectedItem.icons; } return undefined; }, [internalSelectedItem, selectedItem]); /** * This function resets the placeholder */ const placeholderText = (0, _react2.useMemo)(() => { let text = placeholder; if (selectedItem) { text = selectedItem.text; } else if (internalSelectedItem) { text = internalSelectedItem.text; } return text; }, [internalSelectedItem, placeholder, selectedItem]); const shouldShowRoundPlaceholderImage = (0, _react2.useMemo)(() => { const selectedItemList = lists.find(list => list.list.some(({ value }) => value === ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.value) ?? (internalSelectedItem === null || internalSelectedItem === void 0 ? void 0 : internalSelectedItem.value)))); return (selectedItemList === null || selectedItemList === void 0 ? void 0 : selectedItemList.shouldShowRoundImage) ?? shouldShowRoundImage; }, [internalSelectedItem === null || internalSelectedItem === void 0 ? void 0 : internalSelectedItem.value, lists, selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.value, shouldShowRoundImage]); /** * This function opens the content of the combobox */ const handleHeaderClick = (0, _react2.useCallback)(() => { if (!isDisabled && !isInputFocused.current) { if (isAnimating) { handleClose(); } else { handleOpen(); } } }, [handleClose, handleOpen, isAnimating, isDisabled]); const comboBoxGroups = (0, _react2.useMemo)(() => lists.map(list => /*#__PURE__*/_react2.default.createElement("div", { key: list.groupName ?? 'default-group' }, list.groupName && lists.length > 1 && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxTopic, null, list.groupName), list.list.map(item => /*#__PURE__*/ // ToDo: Cleanup this - item should be given as a prop to avoid full spreading _react2.default.createElement(_ComboBoxItem.default, { icons: item.icons, id: item.value, imageBackground: item.imageBackground, imageUrl: item.imageUrl, isDisabled: item.isDisabled, isSelected: selectedItem ? item.value === selectedItem.value : false, key: item.value, onSelect: handleSetSelectedItem, rightElement: item.rightElement, shouldShowBigImage: shouldShowBigImage, shouldShowRoundImage: list.shouldShowRoundImage ?? shouldShowRoundImage, subtext: item.subtext, suffixElement: item.suffixElement, text: item.text, value: item.value, textStyles: item.textStyles })))), [handleSetSelectedItem, lists, selectedItem, shouldShowBigImage, shouldShowRoundImage]); const bodyStyles = (0, _react2.useMemo)(() => ({ left: internalCoordinates.x, top: internalCoordinates.y }), [internalCoordinates.x, internalCoordinates.y]); (0, _react2.useEffect)(() => { const useTopAlignment = shouldUseTopAlignment || [_comboBox.ComboBoxDirection.TOP, _comboBox.ComboBoxDirection.TOP_LEFT, _comboBox.ComboBoxDirection.TOP_RIGHT].includes(direction); if (useTopAlignment) { setTranslateY('-100%'); } else { setTranslateY('0px'); } }, [direction, shouldUseTopAlignment]); (0, _react2.useEffect)(() => { if (!newContainer) { return; } setPortal(() => /*#__PURE__*/(0, _reactDom.createPortal)(/*#__PURE__*/_react2.default.createElement(_react.AnimatePresence, { initial: false }, isAnimating && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledMotionComboBoxBody, { $browser: browser === null || browser === void 0 ? void 0 : browser.name, animate: { height: 'fit-content', opacity: 1 }, $overflowY: overflowY, $translateX: translateX, $translateY: translateY, initial: { height: 0, opacity: 0 }, exit: { height: 0, opacity: 0 }, $maxHeight: maxHeight, $minWidth: bodyWidth ?? bodyMinWidth, style: bodyStyles, $direction: direction, $shouldUseCurrentItemWidth: shouldUseCurrentItemWidth, transition: { duration: 0.2 }, tabIndex: 0, ref: contentRef }, comboBoxGroups)), newContainer)); }, [bodyWidth, bodyMinWidth, bodyStyles, browser === null || browser === void 0 ? void 0 : browser.name, comboBoxGroups, newContainer, direction, isAnimating, maxHeight, minWidth, overflowY, shouldUseCurrentItemWidth, translateX, translateY]); return (0, _react2.useMemo)(() => /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBox, { ref: styledComboBoxElementRef, $minWidth: minWidth, $shouldUseFullWidth: shouldUseFullWidth, $shouldUseCurrentItemWidth: shouldUseCurrentItemWidth }, /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxHeader, { $direction: direction, onClick: handleHeaderClick, $isOpen: isAnimating, $isTouch: isTouch, $isDisabled: isDisabled, $shouldChangeColor: shouldChangeColor, $shouldShowBigImage: shouldShowBigImage }, /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPrefixAndPlaceholderWrapper, null, prefix && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPrefix, null, prefix), /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPlaceholder, { $shouldReduceOpacity: !selectedItem && !internalSelectedItem }, placeholderImageUrl && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPlaceholderImage, { src: placeholderImageUrl, $shouldShowBigImage: shouldShowBigImage, $shouldShowRoundImage: shouldShowRoundPlaceholderImage }), placeholderIcon && /*#__PURE__*/_react2.default.createElement(_Icon.default, { icons: placeholderIcon }), typeof inputValue === 'string' ? /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxInput, { disabled: isDisabled, value: inputValue, onChange: onInputChange, onBlur: handleInputBlur, onFocus: handleInputFocus, placeholder: placeholderText }) : /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxPlaceholderText, null, placeholderText), internalSelectedItem && internalSelectedItem.suffixElement && internalSelectedItem.suffixElement)), shouldShowClearIcon && internalSelectedItem && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxClearIconWrapper, { onClick: handleClear }, /*#__PURE__*/_react2.default.createElement(_Icon.default, { icons: ['fa fa-times'] })), !shouldDisableActions && /*#__PURE__*/_react2.default.createElement(_ComboBox.StyledComboBoxIconWrapper, { $shouldShowBorderLeft: shouldShowClearIcon === true && internalSelectedItem !== undefined }, /*#__PURE__*/_react2.default.createElement(_Icon.default, { icons: ['fa fa-chevron-down'] }))), portal), [minWidth, shouldUseFullWidth, shouldUseCurrentItemWidth, direction, handleHeaderClick, isAnimating, isTouch, isDisabled, shouldChangeColor, shouldShowBigImage, prefix, selectedItem, internalSelectedItem, placeholderImageUrl, shouldShowRoundPlaceholderImage, placeholderIcon, inputValue, onInputChange, handleInputBlur, handleInputFocus, placeholderText, shouldShowClearIcon, handleClear, shouldDisableActions, portal]); }; ComboBox.displayName = 'ComboBox'; var _default = exports.default = ComboBox; //# sourceMappingURL=ComboBox.js.map