@sitecore/sc-contenthub-blok
Version:
A UI library for [Sitecore Content Hub](https://doc.sitecore.com/ch/).
421 lines • 19.8 kB
JavaScript
;
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