@mui/x-charts
Version:
The community edition of MUI X Charts components.
213 lines (204 loc) • 6.99 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.batchMeasureStrings = batchMeasureStrings;
exports.clearStringMeasurementCache = clearStringMeasurementCache;
exports.getStringSize = void 0;
exports.getStyleString = getStyleString;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
// DOM utils adapted from
// https://github.com/recharts/recharts/blob/master/src/util/DOMUtils.ts
function isSsr() {
return typeof window === 'undefined';
}
const stringCache = new Map();
function clearStringMeasurementCache() {
stringCache.clear();
}
const MAX_CACHE_NUM = 2000;
const PIXEL_STYLES = new Set(['minWidth', 'maxWidth', 'width', 'minHeight', 'maxHeight', 'height', 'top', 'left', 'fontSize', 'padding', 'margin', 'paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom', 'marginLeft', 'marginRight', 'marginTop', 'marginBottom']);
/**
* Convert number value to pixel value for certain CSS properties
* @param name CSS property name
* @param value
* @returns add 'px' for distance properties
*/
function convertPixelValue(name, value) {
if (PIXEL_STYLES.has(name) && value === +value) {
return `${value}px`;
}
return value;
}
/**
* Converts camelcase to dash-case
* @param text camelcase css property
*/
const AZ = /([A-Z])/g;
function camelCaseToDashCase(text) {
return String(text).replace(AZ, match => `-${match.toLowerCase()}`);
}
/**
* Converts a style object into a string to be used as a cache key
* @param style React style object
* @returns CSS styling string
*/
function getStyleString(style) {
let result = '';
for (const key in style) {
if (Object.hasOwn(style, key)) {
const k = key;
const value = style[k];
if (value === undefined) {
continue;
}
result += `${camelCaseToDashCase(k)}:${convertPixelValue(k, value)};`;
}
}
return result;
}
/**
*
* @param text The string to estimate
* @param style The style applied
* @returns width and height of the text
*/
const getStringSize = (text, style = {}) => {
if (text === undefined || text === null || isSsr()) {
return {
width: 0,
height: 0
};
}
const str = String(text);
const styleString = getStyleString(style);
const cacheKey = `${str}-${styleString}`;
const size = stringCache.get(cacheKey);
if (size) {
return size;
}
try {
const measurementSpanContainer = getMeasurementContainer();
const measurementElem = document.createElementNS('http://www.w3.org/2000/svg', 'text');
// Need to use CSS Object Model (CSSOM) to be able to comply with Content Security Policy (CSP)
// https://en.wikipedia.org/wiki/Content_Security_Policy
Object.keys(style).map(styleKey => {
measurementElem.style[camelCaseToDashCase(styleKey)] = convertPixelValue(styleKey, style[styleKey]);
return styleKey;
});
measurementElem.textContent = str;
measurementSpanContainer.replaceChildren(measurementElem);
const result = measureSVGTextElement(measurementElem);
stringCache.set(cacheKey, result);
if (stringCache.size + 1 > MAX_CACHE_NUM) {
stringCache.clear();
}
if (process.env.NODE_ENV === 'test') {
// In test environment, we clean the measurement span immediately
measurementSpanContainer.replaceChildren();
}
return result;
} catch {
return {
width: 0,
height: 0
};
}
};
exports.getStringSize = getStringSize;
function batchMeasureStrings(texts, style = {}) {
if (isSsr()) {
return new Map(Array.from(texts).map(text => [text, {
width: 0,
height: 0
}]));
}
const sizeMap = new Map();
const textToMeasure = [];
const styleString = getStyleString(style);
for (const text of texts) {
const cacheKey = `${text}-${styleString}`;
const size = stringCache.get(cacheKey);
if (size) {
sizeMap.set(text, size);
} else {
textToMeasure.push(text);
}
}
const measurementContainer = getMeasurementContainer();
// Need to use CSS Object Model (CSSOM) to be able to comply with Content Security Policy (CSP)
// https://en.wikipedia.org/wiki/Content_Security_Policy
const measurementSpanStyle = (0, _extends2.default)({}, style);
Object.keys(measurementSpanStyle).map(styleKey => {
measurementContainer.style[camelCaseToDashCase(styleKey)] = convertPixelValue(styleKey, measurementSpanStyle[styleKey]);
return styleKey;
});
const measurementElements = [];
for (const string of textToMeasure) {
const measurementElem = document.createElementNS('http://www.w3.org/2000/svg', 'text');
measurementElem.textContent = `${string}`;
measurementElements.push(measurementElem);
}
measurementContainer.replaceChildren(...measurementElements);
for (let i = 0; i < textToMeasure.length; i += 1) {
const text = textToMeasure[i];
const measurementElem = measurementContainer.children[i];
const result = measureSVGTextElement(measurementElem);
const cacheKey = `${text}-${styleString}`;
stringCache.set(cacheKey, result);
sizeMap.set(text, result);
}
if (stringCache.size + 1 > MAX_CACHE_NUM) {
stringCache.clear();
}
if (process.env.NODE_ENV === 'test') {
// In test environment, we clean the measurement span immediately
measurementContainer.replaceChildren();
}
return sizeMap;
}
/**
* Measures an SVG text element using getBBox() with fallback to getBoundingClientRect()
* @param element SVG text element to measure
* @returns width and height of the text element
*/
function measureSVGTextElement(element) {
// getBBox() is more reliable across browsers for SVG elements
try {
const result = element.getBBox();
return {
width: result.width,
height: result.height
};
} catch {
// Fallback to getBoundingClientRect if getBBox fails
// This can happen in tests
const result = element.getBoundingClientRect();
return {
width: result.width,
height: result.height
};
}
}
let measurementContainer = null;
/**
* Get (or create) a hidden span element to measure text size.
*/
function getMeasurementContainer() {
if (measurementContainer === null) {
measurementContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
measurementContainer.setAttribute('aria-hidden', 'true');
measurementContainer.style.position = 'absolute';
measurementContainer.style.top = '-20000px';
measurementContainer.style.left = '0';
measurementContainer.style.padding = '0';
measurementContainer.style.margin = '0';
measurementContainer.style.border = 'none';
measurementContainer.style.pointerEvents = 'none';
measurementContainer.style.visibility = 'hidden';
measurementContainer.style.contain = 'strict';
document.body.appendChild(measurementContainer);
}
return measurementContainer;
}