react-text-row-count
Version:
React helper that adds a data-row-count attribute based on measured text rows.
117 lines (115 loc) • 4.12 kB
JavaScript
import React, { useRef, useCallback, useLayoutEffect, useEffect } from 'react';
// src/react/RowCount.tsx
function calculateRowCount(element) {
const computedStyle = window.getComputedStyle(element);
const lineHeight = computedStyle.lineHeight;
const height = element.scrollHeight;
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);
const borderTop = parseFloat(computedStyle.borderTopWidth);
const borderBottom = parseFloat(computedStyle.borderBottomWidth);
const effectiveHeight = height - paddingTop - paddingBottom - borderTop - borderBottom;
let lineHeightValue;
if (lineHeight === "normal") {
const fontSize = parseFloat(computedStyle.fontSize);
lineHeightValue = fontSize * 1.2;
} else {
lineHeightValue = parseFloat(lineHeight);
}
return Math.max(1, Math.round(effectiveHeight / lineHeightValue));
}
function setDataAttribute(node, value) {
node.setAttribute("data-row-count", String(value));
}
function dispatchRowCountEvent(node, rowCount) {
try {
node.dispatchEvent(
new CustomEvent("rowcountchanged", { detail: { rowCount } })
);
} catch (e) {
}
}
function RowCount({ children, onRowCountChanged }) {
const innerRef = useRef(null);
const rowCountRef = useRef(0);
const rafRef = useRef(null);
const mutationObserverRef = useRef(null);
const resizeObserverRef = useRef(null);
const rafeRefCleanup = () => {
if (rafRef.current != null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
const recalc = useCallback(() => {
if (!innerRef.current) return;
const count = calculateRowCount(innerRef.current);
const changed = rowCountRef.current !== count;
setDataAttribute(innerRef.current, count);
if (changed) {
rowCountRef.current = count;
onRowCountChanged == null ? void 0 : onRowCountChanged(count);
dispatchRowCountEvent(innerRef.current, count);
}
}, [onRowCountChanged]);
const scheduleRecalc = useCallback(() => {
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
rafeRefCleanup();
recalc();
});
}, [recalc]);
useLayoutEffect(() => {
scheduleRecalc();
return () => {
rafeRefCleanup();
};
}, [scheduleRecalc]);
useEffect(() => {
var _a;
const node = innerRef.current;
if (!node) return;
if (typeof ResizeObserver !== "undefined") {
const resizeObserver = new ResizeObserver(() => scheduleRecalc());
resizeObserver.observe(node);
resizeObserverRef.current = resizeObserver;
}
if (typeof MutationObserver !== "undefined") {
const mutationObserver = new MutationObserver(() => scheduleRecalc());
mutationObserver.observe(node, {
characterData: true,
subtree: true,
childList: true
});
mutationObserverRef.current = mutationObserver;
}
const onWindowResize = () => scheduleRecalc();
window.addEventListener("resize", onWindowResize);
const fonts = document.fonts;
const onFontsLoadingDone = () => scheduleRecalc();
if (fonts) {
fonts.ready.then(onFontsLoadingDone).catch(() => {
});
(_a = fonts.addEventListener) == null ? void 0 : _a.call(fonts, "loadingdone", onFontsLoadingDone);
}
scheduleRecalc();
return () => {
var _a2, _b, _c;
window.removeEventListener("resize", onWindowResize);
(_a2 = resizeObserverRef.current) == null ? void 0 : _a2.disconnect();
(_b = mutationObserverRef.current) == null ? void 0 : _b.disconnect();
(_c = fonts == null ? void 0 : fonts.removeEventListener) == null ? void 0 : _c.call(fonts, "loadingdone", onFontsLoadingDone);
};
}, [scheduleRecalc]);
const child = React.Children.only(children);
const attachRef = (node) => {
innerRef.current = node;
};
const cloned = React.cloneElement(child, {
ref: attachRef
});
return cloned;
}
export { RowCount as default };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map