UNPKG

uicore-ts

Version:

UICore is a library to build native-like user interfaces using pure Typescript. No HTML is needed at all. Components are described as TS classes and all user interactions are handled explicitly. This library is strongly inspired by the UIKit framework tha

386 lines (385 loc) 14.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var UITextMeasurement_exports = {}; __export(UITextMeasurement_exports, { UITextMeasurement: () => UITextMeasurement }); module.exports = __toCommonJS(UITextMeasurement_exports); let _pretextPrepare = null; let _pretextLayout = null; try { const mod = require("@chenglou/pretext"); _pretextPrepare = mod.prepare; _pretextLayout = mod.layout; } catch (e) { } class UITextMeasurement { static generateStyleCacheKey(computed) { return [ computed.fontFamily, computed.fontSize, computed.fontWeight, computed.fontStyle, computed.fontVariant, computed.lineHeight, computed.letterSpacing, computed.wordSpacing, computed.textTransform, computed.whiteSpace, computed.wordBreak, computed.wordWrap, computed.paddingLeft, computed.paddingRight, computed.paddingTop, computed.paddingBottom, computed.borderLeftWidth, computed.borderRightWidth, computed.borderTopWidth, computed.borderBottomWidth, computed.boxSizing ].join("|"); } static getSemanticCacheKey(element) { const existingKey = this.elementToCacheKey.get(element); if (existingKey) { return existingKey; } const classList = Array.from(element.classList).sort().join(" "); const semanticKey = `${element.tagName.toLowerCase()}::${classList}`; if (this.globalStyleCache.has(semanticKey)) { this.elementToCacheKey.set(element, semanticKey); return semanticKey; } const styleCacheKey = this.generateStyleCacheKey(window.getComputedStyle(element)); this.elementToCacheKey.set(element, styleCacheKey); return styleCacheKey; } static getCanvasContext() { if (!this._context) { this._canvas = document.createElement("canvas"); this._context = this._canvas.getContext("2d"); } return this._context; } static getMeasurementElement() { if (!this.measurementElement) { this.measurementElement = document.createElement("div"); this.measurementElement.style.cssText = ` position: absolute; visibility: hidden; pointer-events: none; top: -9999px; left: -9999px; width: auto; height: auto; `; } return this.measurementElement; } static applyTextTransform(text, transform) { switch (transform) { case "uppercase": return text.toUpperCase(); case "lowercase": return text.toLowerCase(); case "capitalize": return text.replace(/\b\w/g, (c) => c.toUpperCase()); default: return text; } } static parseLineHeight(lineHeight, fontSize) { if (lineHeight === "normal") { return fontSize * 1.2; } if (lineHeight.endsWith("px")) { return parseFloat(lineHeight); } const numeric = parseFloat(lineHeight); return isNaN(numeric) ? fontSize * 1.2 : fontSize * numeric; } static isPlainText(content) { return !/<(?!\/?(b|i|em|strong|span|br)\b)[^>]+>/i.test(content); } static hasSimpleFormatting(content) { return /^[^<]*(?:<\/?(?:b|i|em|strong|span)(?:\s+style="[^"]*")?>[^<]*)*$/i.test(content); } static getPreparedText(text, font, whiteSpace) { const whiteSpaceOption = whiteSpace === "pre-wrap" ? "pre-wrap" : "normal"; const cacheKey = `${text}|${font}|${whiteSpaceOption}`; let prepared = this._preparedCache.get(cacheKey); if (!prepared) { prepared = _pretextPrepare(text, font, { whiteSpace: whiteSpaceOption }); this._preparedCache.set(cacheKey, prepared); } return prepared; } static calculatePlainTextSizeViaPretext(styles, text, constrainingWidth) { const paddingH = styles.paddingLeft + styles.paddingRight; const paddingV = styles.paddingTop + styles.paddingBottom; const transformedText = this.applyTextTransform(text, styles.textTransform); if (styles.whiteSpace === "nowrap" || styles.whiteSpace === "pre" || !constrainingWidth) { const prepared2 = this.getPreparedText(transformedText, styles.font, styles.whiteSpace); const result2 = _pretextLayout(prepared2, Infinity, styles.lineHeight); if (isNaN(result2.height)) { return { width: NaN, height: NaN }; } return { width: result2.height + paddingH, height: styles.lineHeight + paddingV }; } const availableWidth = constrainingWidth - paddingH; const prepared = this.getPreparedText(transformedText, styles.font, styles.whiteSpace); const result = _pretextLayout(prepared, availableWidth, styles.lineHeight); if (isNaN(result.height)) { return { width: NaN, height: NaN }; } return { width: constrainingWidth, height: result.height + paddingV }; } static measureTextWidth(text, font, letterSpacing = 0) { const ctx = this.getCanvasContext(); ctx.font = font; if (!ctx.font.includes(font.split(",")[0].split(" ").pop().trim())) { if (!this._fontsLoadingSet.has(font)) { this._fontsLoadingSet.add(font); document.fonts.load(font).then(() => { this._fontsLoadingSet.delete(font); }); } return NaN; } const baseWidth = ctx.measureText(text).width; return baseWidth + letterSpacing * text.length; } static wrapNormal(text, maxWidth, ctx) { const words = text.split(/\s+/).filter((w) => w.length > 0); const lines = []; let currentLine = ""; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; if (ctx.measureText(testLine).width > maxWidth && currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } return lines.length > 0 ? lines : [""]; } static wrapPreservingWhitespace(text, maxWidth, ctx) { const lines = []; let currentLine = ""; for (let i = 0; i < text.length; i++) { const testLine = currentLine + text[i]; if (ctx.measureText(testLine).width > maxWidth && currentLine) { lines.push(currentLine); currentLine = text[i]; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } return lines.length > 0 ? lines : [""]; } static wrapText(text, maxWidth, font, whiteSpace) { if (whiteSpace === "nowrap" || whiteSpace === "pre") { return [text]; } const ctx = this.getCanvasContext(); ctx.font = font; if (!ctx.font.includes(font.split(",")[0].split(" ").pop().trim())) { return null; } const lines = []; for (const paragraph of text.split("\n")) { if (whiteSpace === "pre-wrap") { lines.push(...this.wrapPreservingWhitespace(paragraph, maxWidth, ctx)); } else { lines.push(...this.wrapNormal(paragraph, maxWidth, ctx)); } } return lines; } static calculatePlainTextSizeViaCanvas(styles, text, constrainingWidth) { const paddingH = styles.paddingLeft + styles.paddingRight; const paddingV = styles.paddingTop + styles.paddingBottom; const transformedText = this.applyTextTransform(text, styles.textTransform); if (styles.whiteSpace === "nowrap" || styles.whiteSpace === "pre" || !constrainingWidth) { const width2 = this.measureTextWidth(transformedText, styles.font, styles.letterSpacing) + paddingH; const height2 = styles.lineHeight + paddingV; return { width: width2, height: height2 }; } const availableWidth = constrainingWidth - paddingH; const lines = this.wrapText(transformedText, availableWidth, styles.font, styles.whiteSpace); if (!lines) { return { width: NaN, height: NaN }; } const width = Math.max(...lines.map((l) => this.measureTextWidth(l, styles.font, styles.letterSpacing))) + paddingH; const height = lines.length * styles.lineHeight + paddingV; return { width, height }; } static calculatePlainTextSize(element, text, constrainingWidth, constrainingHeight, providedStyles) { const styles = this.getElementStyles(element, providedStyles); if (_pretextPrepare !== null && _pretextLayout !== null && styles.letterSpacing === 0) { return this.calculatePlainTextSizeViaPretext(styles, text, constrainingWidth); } return this.calculatePlainTextSizeViaCanvas(styles, text, constrainingWidth); } static measureWithDOM(element, content, constrainingWidth, constrainingHeight, providedStyles) { const measureEl = this.getMeasurementElement(); const styles = this.getElementStyles(element, providedStyles); measureEl.style.font = styles.font; measureEl.style.lineHeight = styles.lineHeight + "px"; measureEl.style.whiteSpace = styles.whiteSpace; measureEl.style.padding = `${styles.paddingTop}px ${styles.paddingRight}px ${styles.paddingBottom}px ${styles.paddingLeft}px`; measureEl.style.letterSpacing = styles.letterSpacing ? styles.letterSpacing + "px" : ""; measureEl.style.textTransform = styles.textTransform || ""; if (constrainingWidth) { measureEl.style.width = constrainingWidth + "px"; measureEl.style.maxWidth = constrainingWidth + "px"; } else { measureEl.style.width = "auto"; measureEl.style.maxWidth = "none"; } if (constrainingHeight) { measureEl.style.height = constrainingHeight + "px"; measureEl.style.maxHeight = constrainingHeight + "px"; } else { measureEl.style.height = "auto"; measureEl.style.maxHeight = "none"; } measureEl.innerHTML = content; if (!measureEl.parentElement) { document.body.appendChild(measureEl); } const rect = measureEl.getBoundingClientRect(); return { width: rect.width || measureEl.scrollWidth, height: rect.height || measureEl.scrollHeight }; } static getElementStyles(element, providedStyles) { if (providedStyles) { return providedStyles; } const cacheKey = this.getSemanticCacheKey(element); const cached = this.globalStyleCache.get(cacheKey); if (cached) { return cached; } const computed = window.getComputedStyle(element); const fontSize = parseFloat(computed.fontSize); const styles = { font: [ computed.fontStyle || "normal", computed.fontVariant || "normal", computed.fontWeight || "normal", computed.fontSize, computed.fontFamily || "sans-serif" ].join(" "), fontSize, lineHeight: this.parseLineHeight(computed.lineHeight, fontSize), whiteSpace: computed.whiteSpace, paddingLeft: parseFloat(computed.paddingLeft) || 0, paddingRight: parseFloat(computed.paddingRight) || 0, paddingTop: parseFloat(computed.paddingTop) || 0, paddingBottom: parseFloat(computed.paddingBottom) || 0, letterSpacing: parseFloat(computed.letterSpacing) || 0, textTransform: computed.textTransform || "none" }; this.globalStyleCache.set(cacheKey, styles); return styles; } static calculateTextSize(element, content, constrainingWidth, constrainingHeight, providedStyles) { if (!content || content.length === 0) { const styles = this.getElementStyles(element, providedStyles); return { width: styles.paddingLeft + styles.paddingRight, height: styles.paddingTop + styles.paddingBottom }; } const isPlain = this.isPlainText(content); const hasSimple = this.hasSimpleFormatting(content); if (isPlain) { return this.calculatePlainTextSize(element, content, constrainingWidth, constrainingHeight, providedStyles); } if (hasSimple) { const plainText = content.replace(/<[^>]+>/g, ""); return this.calculatePlainTextSize(element, plainText, constrainingWidth, constrainingHeight, providedStyles); } return this.measureWithDOM(element, content, constrainingWidth, constrainingHeight, providedStyles); } static clearCaches() { this._preparedCache.clear(); this.globalStyleCache.clear(); this.elementToCacheKey = /* @__PURE__ */ new WeakMap(); this._context = null; this._canvas = null; } static invalidateElement(element) { const cacheKey = this.elementToCacheKey.get(element); if (cacheKey) { this.globalStyleCache.delete(cacheKey); this.elementToCacheKey.delete(element); } this._preparedCache.clear(); } static invalidateClass(className) { for (const [key] of this.globalStyleCache.entries()) { if (key.includes(className)) { this.globalStyleCache.delete(key); } } this._preparedCache.clear(); } static prewarmCache(elements) { for (const el of elements) { this.getElementStyles(el); } } static cleanup() { var _a; if ((_a = this.measurementElement) == null ? void 0 : _a.parentElement) { document.body.removeChild(this.measurementElement); } this.measurementElement = null; this.clearCaches(); } } UITextMeasurement._preparedCache = /* @__PURE__ */ new Map(); UITextMeasurement.globalStyleCache = /* @__PURE__ */ new Map(); UITextMeasurement.elementToCacheKey = /* @__PURE__ */ new WeakMap(); UITextMeasurement._canvas = null; UITextMeasurement._context = null; UITextMeasurement.measurementElement = null; UITextMeasurement._fontsLoadingSet = /* @__PURE__ */ new Set(); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { UITextMeasurement }); //# sourceMappingURL=UITextMeasurement.js.map