UNPKG

@yukimoto/use-clamp-text

Version:

This package provide a custome hook that can limit the text in the container in specified line number.

223 lines (222 loc) 10.2 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useClampText = void 0; /** * This hooks is adapted from * 1. https://github.com/drenther/use-clamp-text/blob/main/src/index.ts * 2. https://github.com/zoltantothcom/react-clamp-lines/blob/master/src/index.js */ var react_1 = require("react"); var useDebounce_1 = require("./useDebounce"); var useOnWindowResize_1 = require("./useOnWindowResize"); var getHeight = function (element) { var style = window.getComputedStyle(element); var paddingHeight = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom); var innerHeight = element.getBoundingClientRect().height; return innerHeight - paddingHeight; }; var useClampText = function (_a) { var _b = _a.originalText, originalText = _b === void 0 ? '' : _b, _c = _a.lines, lines = _c === void 0 ? 2 : _c, _d = _a.debounceTime, debounceTime = _d === void 0 ? 300 : _d, _e = _a.minSpaceCharNum, minSpaceCharNum = _e === void 0 ? 0 : _e, _f = _a.unitSpaceChar, unitSpaceChar = _f === void 0 ? '.' : _f, _g = _a.endSpaceNumber, endSpaceNumber = _g === void 0 ? 0 : _g; var longEnoughToClampRef = (0, react_1.useRef)(false); var maxHeightRef = (0, react_1.useRef)(0); var wrapperContainerRef = (0, react_1.useRef)(null); var textContainerRef = (0, react_1.useRef)(null); var addOnsContainerRef = (0, react_1.useRef)(null); var initializedRef = (0, react_1.useRef)(false); var _h = (0, react_1.useState)({ clamped: true, /** * The '.' is chosen to be the initial clampedText so as to measure the lineHeight, * since one char must be rendered in a single line */ // clampedText: '.', isClampLinesApplied: false, }), _j = _h[0], clamped = _j.clamped, clampedText = _j.clampedText, isClampLinesApplied = _j.isClampLinesApplied, setClampState = _h[1]; var initializeHeight = (0, react_1.useCallback)(function () { /** * This initializedRef is needed only in development. * Since from React 18 ahead, the development server will use each `effect` * twice even for those without dependencies. This will potentially cause * the calculation of single line height incorrect. */ if (!initializedRef.current) { var wrapperContainer = wrapperContainerRef.current; if (!wrapperContainer) return; var lineHeight = getHeight(wrapperContainer) + 1; maxHeightRef.current = lineHeight * lines + 1; } initializedRef.current = true; }, [lines]); var setLongEnoughToClamp = (0, react_1.useCallback)(function () { var wrapperContainer = wrapperContainerRef.current; var textContainer = textContainerRef.current; var addOnsContainer = addOnsContainerRef.current; if (!wrapperContainer || !textContainer) return; // longEnoughToClamp should exclude addons var addOnsContainerStyle = ''; if (addOnsContainer) { addOnsContainerStyle = addOnsContainer.getAttribute('style') || ''; addOnsContainer.setAttribute('style', 'display: none;'); } textContainer.innerText = originalText; var fullClientHeight = getHeight(wrapperContainer); var maxHeight = maxHeightRef.current; longEnoughToClampRef.current = fullClientHeight > maxHeight; if (addOnsContainer) { addOnsContainer.setAttribute('style', addOnsContainerStyle); } }, [originalText]); /** * When did mount, * 1. initialize line height * 2. check whether longEnoughToClamp */ (0, react_1.useLayoutEffect)(function () { initializeHeight(); setLongEnoughToClamp(); }, [initializeHeight, setLongEnoughToClamp]); var clampLines = (0, react_1.useCallback)(function () { var textContainer = textContainerRef.current; var wrapperContainer = wrapperContainerRef.current; var addOnsContainer = addOnsContainerRef.current; if (!textContainer || !wrapperContainer) return; var maxHeight = maxHeightRef.current; var fitText = originalText; if (clamped) { // clamping should exclude addons var addOnsContainerStyle = ''; if (addOnsContainer) { addOnsContainerStyle = addOnsContainer.getAttribute('style') || ''; addOnsContainer.setAttribute('style', 'display: none;'); } var start_1 = 0; var middle_1 = 0; var end_1 = originalText.length; var moveMarkers = function () { var clientHeight = getHeight(wrapperContainer); if (clientHeight <= maxHeight) { start_1 = middle_1 + 1; } else { end_1 = middle_1 - 1; } }; while (start_1 <= end_1) { middle_1 = Math.floor((start_1 + end_1) / 2); textContainer.innerText = originalText.slice(0, middle_1); moveMarkers(); } fitText = originalText.slice(0, Math.max(middle_1, 0)); if (addOnsContainer) { addOnsContainer.setAttribute('style', addOnsContainerStyle); } } textContainer.innerText = fitText; /** * The below setClampState will trigger re-render and hence the * render of addons next to the clamped text and then the * adjustSpace layout effect. */ setClampState(function (prev) { return (__assign(__assign({}, prev), { clampedText: fitText, isClampLinesApplied: true })); }); }, [clamped, originalText]); (0, react_1.useLayoutEffect)(function () { clampLines(); }, [clampLines]); var adjustSpace = (0, react_1.useCallback)(function () { if (!clamped || !isClampLinesApplied) return; var textContainer = textContainerRef.current; var wrapperContainer = wrapperContainerRef.current; if (!textContainer || !wrapperContainer) return; var maxHeight = maxHeightRef.current; var clientHeight = getHeight(wrapperContainer); if (clientHeight > maxHeight && longEnoughToClampRef.current) { /** * After the potential render of addons, triggered by the setClampeState * in the clampLines, need to make sure that the wrapperContainer * is still within maxHeight. If not, shrink. */ var currentText = textContainer.innerText; // native javascript loop for efficiency // tslint:disable-next-line no-var-keyword for (var x = 0; x < currentText.length; x++) { // tslint:disable-next-line no-shadowed-variable var clientHeight_1 = getHeight(wrapperContainer); if (clientHeight_1 <= maxHeight || textContainer.innerText.length === 0) { break; } else { textContainer.innerText = textContainer.innerText.slice(0, -1); } } } /** * Apply space and progressively shink. * If the desired space makes the wrapperContainer on the brink of maxHeight, * the desired result is obtained. */ if (endSpaceNumber && longEnoughToClampRef.current) { var currentText = textContainer.innerText; var endSpace = unitSpaceChar.repeat(endSpaceNumber); if (minSpaceCharNum) { textContainer.innerText = textContainer.innerText.slice(0, -minSpaceCharNum); } // tslint:disable-next-line no-var-keyword for (var x = 0; x < currentText.length - minSpaceCharNum; x++) { // add space textContainer.innerText = textContainer.innerText + endSpace; // check // tslint:disable-next-line no-shadowed-variable var clientHeight_2 = getHeight(wrapperContainer); if (clientHeight_2 <= maxHeight || textContainer.innerText.length === 0) { // remove space textContainer.innerText = textContainer.innerText.slice(0, -endSpace.length); break; } else { // remove space and shrink textContainer.innerText = textContainer.innerText.slice(0, -endSpace.length - 1); } } } setClampState(function (prev) { return (__assign(__assign({}, prev), { clampedText: textContainer.innerText, isClampLinesApplied: false })); }); }, [clamped, isClampLinesApplied, endSpaceNumber, unitSpaceChar, minSpaceCharNum]); (0, react_1.useLayoutEffect)(function () { adjustSpace(); }, [adjustSpace]); // on window resize var resizeDebuncedRefresh = (0, useDebounce_1.useDebounce)(function () { setLongEnoughToClamp(); clampLines(); }, debounceTime); (0, useOnWindowResize_1.useOnWindowResize)(resizeDebuncedRefresh); var toggleClamp = function () { return setClampState(function (prev) { return (__assign(__assign({}, prev), { clamped: !prev.clamped })); }); }; return { longEnoughToClamp: longEnoughToClampRef.current, clamped: clamped, clampedText: clampedText, toggleClamp: toggleClamp, wrapperContainerRef: wrapperContainerRef, textContainerRef: textContainerRef, addOnsContainerRef: addOnsContainerRef, }; }; exports.useClampText = useClampText;