UNPKG

@sitecore/sc-contenthub-blok

Version:

A UI library for [Sitecore Content Hub](https://doc.sitecore.com/ch/).

421 lines 19.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlokSearchBox = exports.truncateString = exports.DEFAULT_SEARCH_BOX_HEIGHT = exports.SEARCH_BOX_MULTI_LINE_MAX_HEIGHT = exports.SEARCH_BOX_MAX_WIDTH = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const css_1 = require("@emotion/css"); const react_1 = require("react"); const Foundations_1 = require("../../Foundations"); const utils_1 = require("../../Foundations/utils"); const BlokIcon_1 = require("../BlokIcon/BlokIcon"); const BlokIconButton_1 = require("../BlokIconButton/BlokIconButton"); const BlokTooltip_1 = require("../BlokTooltip/BlokTooltip"); exports.SEARCH_BOX_MAX_WIDTH = 500; exports.SEARCH_BOX_MULTI_LINE_MAX_HEIGHT = 130; exports.DEFAULT_SEARCH_BOX_HEIGHT = "36px"; const classes = { scrolled: (0, css_1.css)({ backgroundColor: "white", boxShadow: Foundations_1.shadowSm, outline: "none !important", }), searchBoxActive: (0, css_1.css)({ backgroundColor: "white", boxShadow: `inset 0 0 0 1px ${Foundations_1.blackAlpha[400]}`, outline: "none !important", "&:hover": { backgroundColor: "white", boxShadow: `inset 0 0 0 1px ${Foundations_1.blackAlpha[500]}`, }, }), searchBoxInDropdown: (0, css_1.css)({ margin: "0 0.5rem 0.5rem 0.5rem", position: "sticky !important", top: 0, zIndex: 100, backgroundColor: "transparent", }), gutterBottom: (0, css_1.css)({ marginBottom: Foundations_1.baseSpacing * 2, }), searchBoxOuterWrapper: (0, css_1.css)({ position: "relative", minHeight: "41px", maxWidth: "100%", width: "100%", }), withMaxWidth: (0, css_1.css)({ width: `${exports.SEARCH_BOX_MAX_WIDTH}px`, maxWidth: "100%" }), searchBoxInnerWrapper: (0, css_1.css)({ display: "flex", flexDirection: "row", }), searchBoxWrapper: (0, css_1.css)({ borderRadius: `${Foundations_1.borderRadiusMd} !important`, width: "100%", maxWidth: "100%", zIndex: 10, border: `1px solid ${Foundations_1.inputBorderColor}`, backgroundColor: "white", "&:hover": { backgroundColor: "white", border: `1px solid ${Foundations_1.inputBorderHoveColor}`, color: Foundations_1.textColorDefault, }, "&:focus, &:focus-visible, &:focus-within": { backgroundColor: "white !important", border: `1px solid ${Foundations_1.focusColor} !important`, color: Foundations_1.textColorDefault, outline: 0, boxShadow: "0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 1px 2px 0px rgba(0, 0, 0, 0.06)", }, }), aiSearchBoxWrapper: (0, css_1.css)({ "&:hover": { background: `linear-gradient(white, white) padding-box, linear-gradient(to top right, ${Foundations_1.aiGradientColor1[500]}, ${Foundations_1.aiGradientColor2[500]}) border-box`, border: `1px solid transparent !important`, }, "&:focus, &:focus-visible, &:focus-within": { background: `linear-gradient(white, white) padding-box, linear-gradient(to top right, ${Foundations_1.aiGradientColor1[600]}, ${Foundations_1.aiGradientColor2[600]}) border-box`, border: `1px solid transparent !important`, }, }), searchBoxFixedHeight: (0, css_1.css)({ height: exports.DEFAULT_SEARCH_BOX_HEIGHT, }), searchBox: (0, css_1.css)({ padding: "9.5px 0", margin: "0 6px", border: "none", outline: "none", lineHeight: 1.3, width: "100%", minHeight: exports.DEFAULT_SEARCH_BOX_HEIGHT, resize: "none", overflow: "hidden", color: Foundations_1.textColorDefault, fontSize: Foundations_1.fontSizeBase, fontFamily: Foundations_1.fontFamilyBase, boxShadow: "none !important", "&::placeholder": { color: Foundations_1.textColorDisabled, }, }), searchBoxSingleLine: (0, css_1.css)({ whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", }), absolute: (0, css_1.css)({ position: "absolute" }), insideDropdown: (0, css_1.css)({ "&:focus, &:focus-visible": { "& ~[class*=textHider]": { display: "block !important", }, }, }), disabled: (0, css_1.css)({ cursor: "not-allowed", pointerEvents: "none", "*": { cursor: "not-allowed", pointerEvents: "none", }, }), button: (0, css_1.css)({ margin: "2px 3.5px", }), aiIcon: (0, css_1.css)({ margin: "6.5px 8px 0 8px", width: 24, height: 24, }), searchIconImage: (0, css_1.css)({ width: "22px", height: "22px" }), counter: (0, css_1.css)({ fontSize: Foundations_1.fontSizeSmall, color: Foundations_1.semanticTokens.subtleText.default, whiteSpace: "nowrap", marginTop: "8px", }), hidden: (0, css_1.css)({ visibility: "hidden", pointerEvents: "none" }), }; const stickySearchBoxWrapper = (stickyTopPosition, stickySearchBoxBackground) => { return (0, css_1.css)({ position: "sticky", top: stickyTopPosition, marginTop: Foundations_1.baseSpacing, marginBottom: Foundations_1.baseSpacing * 2, zIndex: 2, borderRadius: `${Foundations_1.borderRadiusMd}px !important`, backgroundColor: stickySearchBoxBackground, maxWidth: "100%", "&:before": { content: "''", display: "block", background: stickySearchBoxBackground, zIndex: -1, opacity: 1, position: "absolute", top: "0", bottom: "0", left: "-16px", right: "-16px", }, "&:after": { content: "''", display: "block", height: "24px", background: `linear-gradient(to bottom, ${stickySearchBoxBackground} 0%,rgba(255,255,255,0) 100%)`, opacity: 1, position: "absolute", bottom: "-24px", left: "-16px", right: "-16px", zIndex: -1, }, }); }; // Helper function to adjust the height of the textarea. const adjustTextareaHeight = (textareaElement, containerElement, shouldApplyHeightLimit) => { textareaElement.style.height = exports.DEFAULT_SEARCH_BOX_HEIGHT; if (shouldApplyHeightLimit) { containerElement.style.width = `${containerElement.clientWidth}px`; } else { containerElement.style.removeProperty("width"); } const newHeight = shouldApplyHeightLimit ? Math.min(textareaElement.scrollHeight - 1, exports.SEARCH_BOX_MULTI_LINE_MAX_HEIGHT) : textareaElement.scrollHeight - 1; textareaElement.style.height = `${newHeight}px`; textareaElement.style.overflow = newHeight === exports.SEARCH_BOX_MULTI_LINE_MAX_HEIGHT ? "auto" : "hidden"; }; // Helper function to reset the textarea styles. const resetTextareaStyles = (textarea, wrapper) => { textarea.style.height = exports.DEFAULT_SEARCH_BOX_HEIGHT; textarea.style.overflow = "hidden"; wrapper.style.removeProperty("width"); }; const truncateString = (str, maxChars, maxWords) => { let result = str; const words = result.trim().split(/\s+/); if (maxWords && words.length > maxWords) { result = `${words.slice(0, maxWords).join(" ")} `; } if (maxChars && result.length > maxChars) { result = result.slice(0, maxChars); } return result; }; exports.truncateString = truncateString; const BlokSearchBox = ({ ref, onSearch, onClearSearch, onEnterPress, value: externalValue, className, placeholder, cancelOnEscape = true, isSticky = false, stickyTopPosition = "0", isInsideDropdown = false, isDisabled = false, hasGutterBottom = true, stickySearchBoxBackground = Foundations_1.bodyBackgroundColor, shouldShowScrolledStatus = false, clearButtonClassName, allowKeyPressPropagation = false, handleKeyPress, onPaste, maxNumberOfCharacters, maxNumberOfWords, clearMessage = "Clear", searchMessage = "Search", allowMultipleLines = false, hasLimitedWidth = true, expandOverContent = true, slots, searchIcon = "m-icon-magnify", searchIconImage, applyAiStyles = false, handlePastedText, showCounter = false, ignoreLimitationsRegex, transformTypedText, }) => { const [value, setValue] = (0, react_1.useState)(externalValue); const [isActive, setIsActive] = (0, react_1.useState)(false); const searchBoxRef = (0, react_1.useRef)(null); const searchBoxWrapperRef = (0, react_1.useRef)(null); const [allowMultiLine, setAllowMultiLine] = (0, react_1.useState)(allowMultipleLines && !expandOverContent); const [isFocused, setIsFocused] = (0, react_1.useState)(false); // Call search callback when typing into the searchBox. const onChange = (0, react_1.useCallback)((e) => { const searchText = typeof transformTypedText === "function" ? transformTypedText(e.target.value) : e.target.value; const shouldIgnoreLimitations = !ignoreLimitationsRegex ? false : !!searchText.match(ignoreLimitationsRegex); const searchValue = shouldIgnoreLimitations ? searchText : (0, exports.truncateString)(searchText, maxNumberOfCharacters, maxNumberOfWords); setValue(searchValue); onSearch?.(searchValue); }, [ ignoreLimitationsRegex, maxNumberOfCharacters, maxNumberOfWords, onSearch, transformTypedText, ]); const processAndInsertPastedText = (0, react_1.useCallback)((pastedText, currentValue, cursorPos, selectionEnd) => { let textToInsert = pastedText; if (typeof transformTypedText === "function") { textToInsert = transformTypedText(pastedText); } if (handlePastedText && typeof handlePastedText === "function") { textToInsert = handlePastedText(pastedText, value); } const textBeforeCursor = currentValue.substring(0, cursorPos); const textAfterCursor = currentValue.substring(selectionEnd); const fullText = textBeforeCursor + textToInsert + textAfterCursor; const shouldIgnoreLimitations = !ignoreLimitationsRegex ? false : !!fullText.match(ignoreLimitationsRegex); return shouldIgnoreLimitations ? fullText : (0, exports.truncateString)(fullText, maxNumberOfCharacters, maxNumberOfWords); }, [ handlePastedText, ignoreLimitationsRegex, maxNumberOfCharacters, maxNumberOfWords, transformTypedText, value, ]); const handlePaste = (0, react_1.useCallback)((e) => { e.preventDefault(); // Get pasted text from clipboard const clipboardData = e.clipboardData; const pastedText = clipboardData.getData("text"); // Get current input value and cursor position const currentValue = e.currentTarget.value; const cursorPos = e.currentTarget.selectionStart || 0; const selectionEnd = e.currentTarget.selectionEnd || cursorPos; const newValue = processAndInsertPastedText(pastedText, currentValue, cursorPos, selectionEnd); // Update the input value setValue(newValue); // Call onSearch with the new value onSearch?.(newValue); // If there's a custom onPaste handler, call it if (onPaste) { onPaste(e); } }, [onPaste, onSearch, processAndInsertPastedText]); // Clear search value and call search callback. const clearSearch = (0, react_1.useCallback)(() => { setValue(""); setIsActive(false); onClearSearch?.(); }, [onClearSearch]); const onKeyPress = (0, react_1.useCallback)((e) => { if (!allowKeyPressPropagation) { e.stopPropagation(); } // Run enter press callback when enter key is pressed if (e.key === "Enter") { // Allow newline with Shift+Enter if multiple lines are enabled if (e.shiftKey && allowMultipleLines) { return; } e.preventDefault(); if ((0, utils_1.isFunction)(onEnterPress)) { onEnterPress(); searchBoxRef?.current?.blur(); setIsActive(true); } } // Clear the search when the escape key is pressed if (e.key === "Escape" && cancelOnEscape) { clearSearch(); } if (e.key === "ArrowDown" && isInsideDropdown) { // Focus the next item in the dropdown e.currentTarget.parentElement?.parentElement ?.nextElementSibling?.focus(); } // Pass the event down to the handleKeyPress method (suggestions). if ((0, utils_1.isFunction)(handleKeyPress)) { handleKeyPress(e); } }, [ allowKeyPressPropagation, allowMultipleLines, cancelOnEscape, clearSearch, handleKeyPress, isInsideDropdown, onEnterPress, ]); // When the searchBox is used inside the select menu there are some actions to do when pressing certain keys const checkKeyDown = (0, react_1.useCallback)((e) => { e.stopPropagation(); if (e.key === "ArrowDown") { // Focus the next item in the dropdown e.currentTarget.nextElementSibling?.focus(); } if (e.key === "Enter") { // Focus the searchBox ref?.current?.focus(); } }, [ref]); const searchButton = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(BlokTooltip_1.BlokTooltip, { title: searchMessage, children: (0, jsx_runtime_1.jsx)(BlokIconButton_1.BlokIconButton, { id: "searchbox-search", onClick: () => { if ((0, utils_1.isFunction)(onEnterPress)) { onEnterPress(); } }, size: "small", className: (0, css_1.cx)(classes.button), colorScheme: applyAiStyles && value ? "ai" : "neutral", children: searchIconImage ? ((0, jsx_runtime_1.jsx)("img", { alt: "search-icon", src: searchIconImage, className: classes.searchIconImage })) : ((0, jsx_runtime_1.jsx)(BlokIcon_1.BlokIcon, { icon: searchIcon })) }, "search") })), [ searchMessage, applyAiStyles, value, searchIconImage, searchIcon, onEnterPress, ]); const handleFocus = (0, react_1.useCallback)(() => { setIsFocused(true); if (!isInsideDropdown && allowMultipleLines) { setAllowMultiLine(true); } }, [allowMultipleLines, isInsideDropdown]); const handleBlur = (0, react_1.useCallback)(() => { setIsFocused(false); if (expandOverContent) { setAllowMultiLine(false); } }, [expandOverContent]); const searchInput = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)("textarea", { placeholder: placeholder || searchMessage, onChange: onChange, onBlur: handleBlur, onFocus: handleFocus, className: (0, css_1.cx)(classes.searchBox, expandOverContent ? classes.searchBoxFixedHeight : null, isActive ? classes.searchBoxActive : null, shouldShowScrolledStatus ? classes.scrolled : null, !allowMultiLine ? classes.searchBoxSingleLine : null, isInsideDropdown ? classes.insideDropdown : null, "custom-scrollbars"), "data-testid": "searchbox", ref: ref ?? searchBoxRef, value: value, tabIndex: 0, onKeyDown: onKeyPress, autoComplete: "off", disabled: isDisabled, onPaste: handlePaste, role: "search", spellCheck: true })), [ allowMultiLine, handleBlur, handleFocus, handlePaste, isActive, isDisabled, isInsideDropdown, onChange, onKeyPress, expandOverContent, placeholder, ref, searchMessage, shouldShowScrolledStatus, value, ]); const clearButton = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(BlokTooltip_1.BlokTooltip, { title: clearMessage, children: (0, jsx_runtime_1.jsx)(BlokIconButton_1.BlokIconButton, { id: "clear-select-search", onClick: clearSearch, className: (0, css_1.cx)(classes.button, clearButtonClassName, value === "" ? classes.hidden : undefined), size: "small", children: (0, jsx_runtime_1.jsx)(BlokIcon_1.BlokIcon, { icon: "m-icon-close" }) }, "clear") })), [clearButtonClassName, clearSearch, clearMessage, value]); (0, react_1.useEffect)(() => { if (externalValue === "") { setValue(""); setIsActive(false); } else { setValue(externalValue); } }, [externalValue]); // Adjust the height of the textarea based on its content. (0, react_1.useEffect)(() => { if (searchBoxRef.current && searchBoxWrapperRef.current) { if (allowMultiLine) { adjustTextareaHeight(searchBoxRef.current, searchBoxWrapperRef.current, expandOverContent); } else { resetTextareaStyles(searchBoxRef.current, searchBoxWrapperRef.current); } } }, [value, allowMultiLine, expandOverContent]); const counter = (0, react_1.useMemo)(() => { if (showCounter && maxNumberOfCharacters && isFocused) { return ((0, jsx_runtime_1.jsx)("div", { className: (0, css_1.cx)(classes.counter), children: `${value?.length ?? 0} / ${maxNumberOfCharacters}` })); } return null; }, [isFocused, maxNumberOfCharacters, showCounter, value?.length]); const searchBoxContent = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsxs)("div", { className: classes.searchBoxInnerWrapper, children: [applyAiStyles && ((0, jsx_runtime_1.jsx)(BlokIcon_1.BlokIcon, { icon: "star-four-points", classes: [classes.aiIcon], iconColor: "neutral", iconColorShade: 600 })), !applyAiStyles && searchButton, slots?.slot1, searchInput, counter, clearButton, applyAiStyles && searchButton] })), [ applyAiStyles, clearButton, counter, searchButton, searchInput, slots?.slot1, ]); return isInsideDropdown ? ((0, jsx_runtime_1.jsx)("li", { onKeyDown: checkKeyDown, onMouseUp: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), role: "menuitem", tabIndex: 0, className: (0, css_1.cx)(classes.searchBoxInDropdown), children: (0, jsx_runtime_1.jsx)("div", { role: "search", ref: searchBoxWrapperRef, className: (0, css_1.cx)(classes.searchBoxWrapper, classes.withMaxWidth, hasGutterBottom ? classes.gutterBottom : null), children: searchBoxContent }) })) : ((0, jsx_runtime_1.jsx)("div", { className: (0, css_1.cx)(classes.searchBoxOuterWrapper, isSticky ? stickySearchBoxWrapper(stickyTopPosition, stickySearchBoxBackground) : null, hasLimitedWidth ? classes.withMaxWidth : null, hasGutterBottom ? classes.gutterBottom : null, className), ref: searchBoxWrapperRef, children: (0, jsx_runtime_1.jsxs)("div", { className: (0, css_1.cx)(classes.searchBoxWrapper, applyAiStyles ? classes.aiSearchBoxWrapper : null, allowMultiLine && !isSticky && expandOverContent ? classes.absolute : null, isDisabled ? classes.disabled : null, isActive ? "searchBoxActive" : undefined), role: "search", children: [searchBoxContent, (0, jsx_runtime_1.jsx)("div", { "data-testid": "slot2", children: slots?.slot2 })] }) })); }; exports.BlokSearchBox = BlokSearchBox; //# sourceMappingURL=BlokSearchBox.js.map