UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

205 lines 8.92 kB
"use strict"; /** * * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.useTruncateWithButton = useTruncateWithButton; const base_1 = require("@neo4j-ndl/base"); const react_1 = require("react"); const hooks_1 = require("../hooks"); const text_overflow_utils_1 = require("./text-overflow-utils"); // Cache for computed styles to avoid repeated getComputedStyle calls const styleCache = new WeakMap(); // Helper function to copy relevant styles for measurement const copyStylesForMeasurement = (measureElement, sourceElement) => { // Use cached styles if available let sourceStyles = styleCache.get(sourceElement); if (sourceStyles === undefined) { sourceStyles = window.getComputedStyle(sourceElement); styleCache.set(sourceElement, sourceStyles); } measureElement.style.position = 'absolute'; measureElement.style.visibility = 'hidden'; measureElement.style.top = '-9999px'; measureElement.style.left = '-9999px'; measureElement.style.width = sourceStyles.width; measureElement.style.fontFamily = sourceStyles.fontFamily; measureElement.style.fontSize = sourceStyles.fontSize; measureElement.style.fontWeight = sourceStyles.fontWeight; measureElement.style.lineHeight = sourceStyles.lineHeight; measureElement.style.letterSpacing = sourceStyles.letterSpacing; measureElement.style.wordSpacing = sourceStyles.wordSpacing; measureElement.style.overflow = 'hidden'; measureElement.style.wordBreak = 'break-word'; }; // Helper function to find the optimal truncation point using optimized binary search const findTruncationPoint = (content, allText, measureElement, maxHeight, button, buttonType) => { let low = 0; let high = allText.length; let bestFitContent = null; // Pre-calculate button text to avoid repeated string operations const buttonText = typeof button === 'string' ? button : 'View more'; const buttonMargin = buttonType !== 'ellipsis' ? base_1.tokens.space[4] : '0'; // Create reusable DOM elements to avoid repeated creation const tempSpan = document.createElement('span'); const btnSpan = document.createElement('span'); btnSpan.style.display = 'inline-block'; btnSpan.style.marginLeft = buttonMargin; while (low <= high) { const mid = Math.floor((low + high) / 2); const testText = allText.slice(0, mid); measureElement.innerHTML = ''; // Reuse span elements tempSpan.textContent = buttonType === 'ellipsis' ? testText : `${testText}\u2026`; measureElement.appendChild(tempSpan); btnSpan.textContent = buttonText; measureElement.appendChild(btnSpan); if (measureElement.scrollHeight <= maxHeight) { bestFitContent = (0, text_overflow_utils_1.truncateReactNodeByCharacters)(content, mid, buttonType !== 'ellipsis').truncatedContent; low = mid + 1; } else { high = mid - 1; } } return bestFitContent; }; function useTruncateWithButton({ content, lineClamp, button, onExpandedToggle, maxCharacters, buttonType, closeTooltip, isDisabled, }) { const containerRef = (0, react_1.useRef)(null); const measureRef = (0, react_1.useRef)(null); const [isExpanded, setIsExpanded] = (0, react_1.useState)(false); const [isTruncated, setIsTruncated] = (0, react_1.useState)(false); const [truncatedContent, setTruncatedContent] = (0, react_1.useState)(content); // Memoize text content to avoid repeated traversals const textContent = (0, react_1.useMemo)(() => (0, text_overflow_utils_1.getTextContent)(content), [content]); const toggleExpand = (0, react_1.useCallback)(() => { if (isDisabled === true) { return; } const shouldExpand = !isExpanded; setIsExpanded(shouldExpand); if (shouldExpand) { onExpandedToggle === null || onExpandedToggle === void 0 ? void 0 : onExpandedToggle(true); closeTooltip === null || closeTooltip === void 0 ? void 0 : closeTooltip(); } else { onExpandedToggle === null || onExpandedToggle === void 0 ? void 0 : onExpandedToggle(false); } }, [isExpanded, onExpandedToggle, closeTooltip, isDisabled]); const recalculateTruncation = (0, react_1.useCallback)(() => { // If disabled, don't perform any calculations if (isDisabled === true) { setIsTruncated(false); setTruncatedContent(content); return; } // Character-based truncation takes precedence if (typeof maxCharacters === 'number') { const result = (0, text_overflow_utils_1.truncateReactNodeByCharacters)(content, maxCharacters); setIsTruncated(result.needsTruncation); setTruncatedContent(result.needsTruncation ? result.truncatedContent : content); return; } // Fallback to line-based truncation if (!measureRef.current || !containerRef.current) { return; } const measure = measureRef.current; const container = containerRef.current; copyStylesForMeasurement(measure, container); const containerStyles = window.getComputedStyle(container); const lineHeight = parseFloat(containerStyles.lineHeight); const maxHeight = lineHeight * lineClamp; const allText = textContent; // Early exit if no text content if (allText.length === 0) { setIsTruncated(false); setTruncatedContent(content); return; } // First, check if truncation is needed at all measure.innerHTML = ''; const fullTextSpan = document.createElement('span'); fullTextSpan.textContent = allText; measure.appendChild(fullTextSpan); const isTruncationNeeded = measure.scrollHeight > maxHeight; if (!isTruncationNeeded) { setIsTruncated(false); setTruncatedContent(content); return; } // Binary search for truncation point const bestFitContent = findTruncationPoint(content, allText, measure, maxHeight, button, buttonType); setIsTruncated(true); setTruncatedContent(bestFitContent); }, [ content, button, lineClamp, maxCharacters, buttonType, isDisabled, textContent, ]); // Create debounced version of recalculateTruncation for resize events const debouncedRecalculateTruncation = (0, hooks_1.useDebounceCallback)(recalculateTruncation, 100); // Initial calculation and resize handling (0, react_1.useLayoutEffect)(() => { // If disabled, don't set up any observers or event listeners if (isDisabled === true) { return; } recalculateTruncation(); // Set up resize observer for container size changes const container = containerRef.current; if (!container) { return; } const resizeObserver = new ResizeObserver(() => { debouncedRecalculateTruncation(); }); resizeObserver.observe(container); // Also observe parent element to catch container size changes if (container.parentElement) { resizeObserver.observe(container.parentElement); } // Handle window resize events const handleWindowResize = () => { debouncedRecalculateTruncation(); }; window.addEventListener('resize', handleWindowResize); return () => { resizeObserver.disconnect(); window.removeEventListener('resize', handleWindowResize); }; }, [recalculateTruncation, debouncedRecalculateTruncation, isDisabled]); // Compute the final content to render based on expanded state const contentToRender = isExpanded ? content : truncatedContent; return { containerRef, contentToRender, isExpanded, isTruncated, measureRef, toggleExpand, }; } //# sourceMappingURL=use-truncate-with-button.js.map