@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
JavaScript
;
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;