├── .eslintrc ├── .githooks ├── pre-commit └── pre-push ├── tests └── tests.test.ts ├── .github ├── dependabot.yaml └── workflows │ └── ci.yml ├── tsconfig.build.json ├── jest.config.json ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── LICENSE ├── webpack.config.ts ├── demo.html ├── dist └── fit-html-text.min.js ├── package.json ├── .gitignore ├── src └── lib.ts ├── tsconfig.json └── eslint.config.mjs /.eslintrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run lint -------------------------------------------------------------------------------- /tests/tests.test.ts: -------------------------------------------------------------------------------- 1 | describe("text", () => { 2 | it("ok", () => { 3 | // TODO 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build:webpack 4 | if ! git diff --exit-code dist; then 5 | git add dist && git commit --no-verify -m "generate dist files" 6 | fi -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*" 5 | ], 6 | "exclude": [ 7 | "**/*.spec.ts", 8 | "**/*.test.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionsToTreatAsEsm": [ 3 | ".ts" 4 | ], 5 | "transform": { 6 | "^.+\\.[jt]sx?$": [ 7 | "ts-jest", 8 | { 9 | "useESM": true 10 | } 11 | ] 12 | }, 13 | "moduleNameMapper": { 14 | "^(\\.\\.?\\/.+)\\.jsx?$": "$1" 15 | } 16 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node-terminal", 6 | "name": "test", 7 | "request": "launch", 8 | "command": "npm test" 9 | }, 10 | { 11 | "type": "node", 12 | "name": "test:inspect-brk", 13 | "request": "attach", 14 | "port": 9230 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "label": "Build", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | } 12 | }, 13 | { 14 | "type": "npm", 15 | "script": "test", 16 | "label": "Test", 17 | "group": { 18 | "kind": "test", 19 | "isDefault": true 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fit-html-text 2 | ========== 3 | 4 | [![CI](https://github.com/magiclen/fit-html-text/actions/workflows/ci.yml/badge.svg)](https://github.com/magiclen/fit-html-text/actions/workflows/ci.yml) 5 | 6 | Fit text into its surrounding container. 7 | 8 | ## Usage 9 | 10 | ```typescript 11 | import { fitText } from "fit-html-text"; 12 | 13 | const element = document.getElementById("myElement"); 14 | 15 | fitText(element, { 16 | fontMinSize: 8, 17 | fontMaxSize: 72, 18 | containerMaxWidth: 300, 19 | containerMaxHeight: 300, 20 | multipleLines: false, 21 | }); 22 | ``` 23 | 24 | ## Usage for Browsers 25 | 26 | [Source](demo.html) 27 | 28 | [Demo Page](https://rawcdn.githack.com/magiclen/fit-html-text/master/demo.html) 29 | 30 | ## License 31 | 32 | [MIT](LICENSE) -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | - macos-latest 13 | - windows-latest 14 | node-version: 15 | - 20.x 16 | - 22.x 17 | name: Use ${{ matrix.node-version }} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - uses: pnpm/action-setup@v4 25 | with: 26 | version: latest 27 | - run: pnpm install 28 | - run: npm run build --if-present 29 | - run: npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 magiclen.org (Ron Li) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import TerserPlugin from "terser-webpack-plugin"; 2 | import type { Configuration } from "webpack"; 3 | 4 | const config: Configuration = { 5 | entry: "./src/lib.ts", 6 | output: { 7 | clean: true, 8 | filename: "fit-html-text.min.js", 9 | library: "FitHtmlText", 10 | libraryTarget: "umd", 11 | }, 12 | plugins: [], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/iu, 17 | use: [ 18 | { 19 | loader: "babel-loader", 20 | options: { presets: ["@babel/preset-env", "@babel/preset-typescript"] }, 21 | }, 22 | ], 23 | }, 24 | { 25 | test: /\.js$/iu, 26 | use: [ 27 | { 28 | loader: "babel-loader", 29 | options: { presets: ["@babel/preset-env"] }, 30 | }, 31 | ], 32 | }, 33 | ], 34 | }, 35 | resolve: { extensionAlias: { ".js": [".ts", ".js"] } }, 36 | optimization: { 37 | minimizer: [ 38 | new TerserPlugin({ 39 | extractComments: false, 40 | terserOptions: { format: { comments: false } }, 41 | }), 42 | ], 43 | }, 44 | }; 45 | 46 | export default config; 47 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | fit-html-text 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | 33 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /dist/fit-html-text.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitHtmlText=t():e.FitHtmlText=t()}(self,(()=>(()=>{"use strict";var e={d:(t,i)=>{for(var n in i)e.o(i,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{fitText:()=>i});var i=function(e,t){if(void 0===t&&(t={}),void 0!==t.fontMinSize&&t.fontMinSize>0&&t.fontMinSize!==1/0||(t.fontMinSize=8),void 0!==t.fontMaxSize&&t.fontMaxSize>0||(t.fontMaxSize=1/0),void 0===t.containerMaxWidth||!(t.containerMaxWidth>0)||t.containerMaxWidth===1/0){var i=e.getBoundingClientRect();t.containerMaxWidth=i.width}void 0!==t.containerMaxHeight&&t.containerMaxHeight>0||(t.containerMaxHeight=1/0),void 0===t.multipleLines&&(t.multipleLines=!1);var n=window.getComputedStyle(e),o=document.createElement("_measure");o.style.position="absolute",o.style.top="-9999px",o.style.left="-".concat(t.containerMaxWidth,"px"),o.style.visibility="hidden",o.style.border=n.getPropertyValue("border"),o.style.boxSizing=n.getPropertyValue("box-sizing"),o.style.padding=n.getPropertyValue("padding"),o.style.fontFamily=n.getPropertyValue("font-family"),o.style.letterSpacing=n.getPropertyValue("letter-spacing"),e instanceof HTMLInputElement?o.innerText=e.value:o.innerHTML=e.innerHTML,document.body.appendChild(o);var a=t.fontMinSize+1,r=t.fontMinSize;if(t.multipleLines){o.style.display="block",o.style.width="".concat(t.containerMaxWidth,"px"),o.style.wordBreak=n.getPropertyValue("word-break");for(var l=0,d=Number.MIN_SAFE_INTEGER;a<=t.fontMaxSize;){o.style.fontSize="".concat(a,"px");var f=o.getBoundingClientRect();if(f.height>t.containerMaxHeight)break;if(f.height===d){if(3===(l+=1))break}else l=0;d=f.height,r=a,a+=1}}else for(var c=0,h=Number.MIN_SAFE_INTEGER,p=Number.MIN_SAFE_INTEGER;a<=t.fontMaxSize;){o.style.fontSize="".concat(a,"px");var y=o.getBoundingClientRect();if(y.width>t.containerMaxWidth)break;if(y.height>t.containerMaxHeight)break;if(y.width===h){if(3===(c+=1))break}else c=0;var s=y.width-h,g=y.height-p;h=y.width,p=y.height,r=a,a+=Math.max(1,Math.floor(Math.min((t.containerMaxWidth-y.width)/(s+3),(t.containerMaxHeight-y.height)/(g+3))))}document.body.removeChild(o),e.style.fontSize="".concat(r,"px")};return t})())); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fit-html-text", 3 | "version": "0.3.0", 4 | "description": "Fit text into its surrounding container.", 5 | "type": "module", 6 | "exports": "./lib/lib.js", 7 | "types": "./lib/lib.d.ts", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "files": [ 12 | "lib" 13 | ], 14 | "scripts": { 15 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 16 | "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage", 17 | "test:inspect-brk": "node --experimental-vm-modules --inspect-brk=0.0.0.0:9230 node_modules/jest/bin/jest.js --testTimeout 0 --runInBand", 18 | "clean": "rimraf lib", 19 | "build": "npm run clean && tsc -p tsconfig.build.json", 20 | "build:watch": "npm run build -- -w", 21 | "build:webpack": "webpack --mode production", 22 | "build:src": "node build.js", 23 | "lint": "eslint src tests", 24 | "lint:fix": "npm run lint -- --fix", 25 | "prepare": "git config core.hooksPath .githooks || exit 0", 26 | "prepack": "npm run build", 27 | "prepublishOnly": "npm run lint && npm run test" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/magiclen/fit-html-text.git" 32 | }, 33 | "keywords": [ 34 | "html", 35 | "text", 36 | "font-size" 37 | ], 38 | "author": "Magic Len", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/magiclen/fit-html-text/issues" 42 | }, 43 | "homepage": "https://magiclen.org/fit-html-text/", 44 | "devDependencies": { 45 | "@babel/core": "^7.25.8", 46 | "@babel/preset-env": "^7.25.8", 47 | "@babel/preset-typescript": "^7.25.7", 48 | "@babel/register": "^7.25.7", 49 | "@eslint/js": "^9.13.0", 50 | "@stylistic/eslint-plugin": "^2.9.0", 51 | "@types/eslint__js": "^8.42.3", 52 | "@types/jest": "^29.5.13", 53 | "babel-loader": "^9.2.1", 54 | "eslint": "^9.13.0", 55 | "eslint-import-resolver-typescript": "^3.6.3", 56 | "eslint-plugin-import": "^2.31.0", 57 | "globals": "^15.11.0", 58 | "jest": "^29.7.0", 59 | "rimraf": "^6.0.1", 60 | "terser-webpack-plugin": "^5.3.10", 61 | "ts-jest": "^29.2.5", 62 | "typescript": "~5.6.3", 63 | "typescript-eslint": "^8.10.0", 64 | "webpack": "^5.95.0", 65 | "webpack-cli": "^5.1.4" 66 | } 67 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | 3 | ### Intellij+all ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # AWS User-specific 15 | .idea/**/aws.xml 16 | 17 | # Generated files 18 | .idea/**/contentModel.xml 19 | 20 | # Sensitive or high-churn files 21 | .idea/**/dataSources/ 22 | .idea/**/dataSources.ids 23 | .idea/**/dataSources.local.xml 24 | .idea/**/sqlDataSources.xml 25 | .idea/**/dynamic.xml 26 | .idea/**/uiDesigner.xml 27 | .idea/**/dbnavigator.xml 28 | 29 | # Gradle 30 | .idea/**/gradle.xml 31 | .idea/**/libraries 32 | 33 | # Gradle and Maven with auto-import 34 | # When using Gradle or Maven with auto-import, you should exclude module files, 35 | # since they will be recreated, and may cause churn. Uncomment if using 36 | # auto-import. 37 | # .idea/artifacts 38 | # .idea/compiler.xml 39 | # .idea/jarRepositories.xml 40 | # .idea/modules.xml 41 | # .idea/*.iml 42 | # .idea/modules 43 | # *.iml 44 | # *.ipr 45 | 46 | # CMake 47 | cmake-build-*/ 48 | 49 | # Mongo Explorer plugin 50 | .idea/**/mongoSettings.xml 51 | 52 | # File-based project format 53 | *.iws 54 | 55 | # IntelliJ 56 | out/ 57 | 58 | # mpeltonen/sbt-idea plugin 59 | .idea_modules/ 60 | 61 | # JIRA plugin 62 | atlassian-ide-plugin.xml 63 | 64 | # Cursive Clojure plugin 65 | .idea/replstate.xml 66 | 67 | # SonarLint plugin 68 | .idea/sonarlint/ 69 | 70 | # Crashlytics plugin (for Android Studio and IntelliJ) 71 | com_crashlytics_export_strings.xml 72 | crashlytics.properties 73 | crashlytics-build.properties 74 | fabric.properties 75 | 76 | # Editor-based Rest Client 77 | .idea/httpRequests 78 | 79 | # Android studio 3.1+ serialized cache file 80 | .idea/caches/build_file_checksums.ser 81 | 82 | ### Intellij+all Patch ### 83 | # Ignore everything but code style settings and run configurations 84 | # that are supposed to be shared within teams. 85 | 86 | .idea/* 87 | 88 | !.idea/codeStyles 89 | !.idea/runConfigurations 90 | 91 | ### Node ### 92 | # Logs 93 | logs 94 | *.log 95 | npm-debug.log* 96 | yarn-debug.log* 97 | yarn-error.log* 98 | lerna-debug.log* 99 | .pnpm-debug.log* 100 | 101 | # Diagnostic reports (https://nodejs.org/api/report.html) 102 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 103 | 104 | # Runtime data 105 | pids 106 | *.pid 107 | *.seed 108 | *.pid.lock 109 | 110 | # Directory for instrumented libs generated by jscoverage/JSCover 111 | lib-cov 112 | 113 | # Coverage directory used by tools like istanbul 114 | coverage 115 | *.lcov 116 | 117 | # nyc test coverage 118 | .nyc_output 119 | 120 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 121 | .grunt 122 | 123 | # Bower dependency directory (https://bower.io/) 124 | bower_components 125 | 126 | # node-waf configuration 127 | .lock-wscript 128 | 129 | # Compiled binary addons (https://nodejs.org/api/addons.html) 130 | build/ 131 | 132 | # Dependency directories 133 | node_modules/ 134 | jspm_packages/ 135 | 136 | # Snowpack dependency directory (https://snowpack.dev/) 137 | web_modules/ 138 | 139 | # TypeScript cache 140 | *.tsbuildinfo 141 | 142 | # Optional npm cache directory 143 | .npm 144 | 145 | # Optional eslint cache 146 | .eslintcache 147 | 148 | # Optional stylelint cache 149 | .stylelintcache 150 | 151 | # Microbundle cache 152 | .rpt2_cache/ 153 | .rts2_cache_cjs/ 154 | .rts2_cache_es/ 155 | .rts2_cache_umd/ 156 | 157 | # Optional REPL history 158 | .node_repl_history 159 | 160 | # Output of 'npm pack' 161 | *.tgz 162 | 163 | # Yarn Integrity file 164 | .yarn-integrity 165 | 166 | # dotenv environment variable files 167 | .env 168 | .env.development.local 169 | .env.test.local 170 | .env.production.local 171 | .env.local 172 | 173 | # parcel-bundler cache (https://parceljs.org/) 174 | .cache 175 | .parcel-cache 176 | 177 | # Next.js build output 178 | .next 179 | out 180 | 181 | # Nuxt.js build / generate output 182 | .nuxt 183 | dist 184 | 185 | # Gatsby files 186 | .cache/ 187 | # Comment in the public line in if your project uses Gatsby and not Next.js 188 | # https://nextjs.org/blog/next-9-1#public-directory-support 189 | # public 190 | 191 | # vuepress build output 192 | .vuepress/dist 193 | 194 | # vuepress v2.x temp and cache directory 195 | .temp 196 | 197 | # Docusaurus cache and generated files 198 | .docusaurus 199 | 200 | # Serverless directories 201 | .serverless/ 202 | 203 | # FuseBox cache 204 | .fusebox/ 205 | 206 | # DynamoDB Local files 207 | .dynamodb/ 208 | 209 | # TernJS port file 210 | .tern-port 211 | 212 | # Stores VSCode versions used for testing VSCode extensions 213 | .vscode-test 214 | 215 | # yarn v2 216 | .yarn/cache 217 | .yarn/unplugged 218 | .yarn/build-state.yml 219 | .yarn/install-state.gz 220 | .pnp.* 221 | 222 | ### Node Patch ### 223 | # Serverless Webpack directories 224 | .webpack/ 225 | 226 | # Optional stylelint cache 227 | 228 | # SvelteKit build / generate output 229 | .svelte-kit 230 | 231 | ### Vim ### 232 | # Swap 233 | [._]*.s[a-v][a-z] 234 | !*.svg # comment out if you don't need vector files 235 | [._]*.sw[a-p] 236 | [._]s[a-rt-v][a-z] 237 | [._]ss[a-gi-z] 238 | [._]sw[a-p] 239 | 240 | # Session 241 | Session.vim 242 | Sessionx.vim 243 | 244 | # Temporary 245 | .netrwhist 246 | *~ 247 | # Auto-generated tag files 248 | tags 249 | # Persistent undo 250 | [._]*.un~ 251 | 252 | ### VisualStudioCode ### 253 | .vscode/* 254 | !.vscode/settings.json 255 | !.vscode/tasks.json 256 | !.vscode/launch.json 257 | !.vscode/extensions.json 258 | !.vscode/*.code-snippets 259 | 260 | # Local History for Visual Studio Code 261 | .history/ 262 | 263 | # Built Visual Studio Code Extensions 264 | *.vsix 265 | 266 | ### VisualStudioCode Patch ### 267 | # Ignore all local history of files 268 | .history 269 | .ionide -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | export interface FitTextOptions { 2 | /** 3 | * The minimum font size, in pixels. 4 | * 5 | * @default 8 6 | */ 7 | fontMinSize?: number; 8 | /** 9 | * The maximum font size, in pixels. 10 | * 11 | * Set to `Infinity` means unlimited. 12 | * 13 | * @default Infinity 14 | */ 15 | fontMaxSize?: number; 16 | /** 17 | * The maximum width for text, in pixels. 18 | * 19 | * @default element.getBoundingClientRect().width 20 | */ 21 | containerMaxWidth?: number; 22 | /** 23 | * The maximum height for text, in pixels. 24 | * 25 | * If `multipleLines` is set to `true`, this option should also be set to a reasonable integer. 26 | * 27 | * Set to `Infinity` means unlimited. 28 | * 29 | * @default Infinity 30 | */ 31 | containerMaxHeight?: number; 32 | /** 33 | * Whether to allow multiple lines. 34 | * 35 | * If the text allows line break or multiple lines, this option should be set to `true`. 36 | * 37 | * @default false 38 | */ 39 | multipleLines?: boolean; 40 | } 41 | 42 | export const fitText = (element: HTMLElement, options?: FitTextOptions): void => { 43 | if (typeof options === "undefined") { 44 | options = {}; 45 | } 46 | 47 | if ( 48 | typeof options.fontMinSize === "undefined" 49 | || !(options.fontMinSize > 0) || options.fontMinSize === Infinity 50 | ) { 51 | options.fontMinSize = 8; 52 | } 53 | 54 | if ( 55 | typeof options.fontMaxSize === "undefined" || !(options.fontMaxSize > 0) 56 | ) { 57 | options.fontMaxSize = Infinity; 58 | } 59 | 60 | if ( 61 | typeof options.containerMaxWidth === "undefined" 62 | || !(options.containerMaxWidth > 0) 63 | || options.containerMaxWidth === Infinity 64 | ) { 65 | const elementRect = element.getBoundingClientRect(); 66 | 67 | options.containerMaxWidth = elementRect.width; 68 | } 69 | 70 | if ( 71 | typeof options.containerMaxHeight === "undefined" 72 | || !(options.containerMaxHeight > 0) 73 | ) { 74 | options.containerMaxHeight = Infinity; 75 | } 76 | 77 | if (typeof options.multipleLines === "undefined") { 78 | options.multipleLines = false; 79 | } 80 | 81 | const style = window.getComputedStyle(element); 82 | 83 | const meassureNode = document.createElement("_measure"); 84 | 85 | meassureNode.style.position = "absolute"; 86 | meassureNode.style.top = "-9999px"; 87 | meassureNode.style.left = `-${options.containerMaxWidth}px`; 88 | meassureNode.style.visibility = "hidden"; 89 | 90 | meassureNode.style.border = style.getPropertyValue("border"); 91 | meassureNode.style.boxSizing = style.getPropertyValue("box-sizing"); 92 | meassureNode.style.padding = style.getPropertyValue("padding"); 93 | meassureNode.style.fontFamily = style.getPropertyValue("font-family"); 94 | meassureNode.style.letterSpacing = style.getPropertyValue("letter-spacing"); 95 | 96 | if (element instanceof HTMLInputElement) { 97 | meassureNode.innerText = element.value; 98 | } else { 99 | meassureNode.innerHTML = element.innerHTML; 100 | } 101 | 102 | document.body.appendChild(meassureNode); 103 | 104 | let fontSize = options.fontMinSize + 1; 105 | let lastFontSize = options.fontMinSize; 106 | 107 | if (options.multipleLines) { 108 | meassureNode.style.display = "block"; 109 | meassureNode.style.width = `${options.containerMaxWidth}px`; 110 | 111 | meassureNode.style.wordBreak = style.getPropertyValue("word-break"); 112 | 113 | // this counter is used to avoid infinite looping 114 | let lastClientHeightSameCounter = 0; 115 | let lastClientHeight = Number.MIN_SAFE_INTEGER; 116 | 117 | while (fontSize <= options.fontMaxSize) { 118 | meassureNode.style.fontSize = `${fontSize}px`; 119 | 120 | const rect = meassureNode.getBoundingClientRect(); 121 | 122 | if (rect.height > options.containerMaxHeight) { 123 | break; 124 | } 125 | 126 | if (rect.height === lastClientHeight) { 127 | lastClientHeightSameCounter += 1; 128 | 129 | if (lastClientHeightSameCounter === 3) { 130 | break; 131 | } 132 | } else { 133 | lastClientHeightSameCounter = 0; 134 | } 135 | 136 | lastClientHeight = rect.height; 137 | 138 | lastFontSize = fontSize; 139 | 140 | fontSize += 1; 141 | } 142 | } else { 143 | // this counter is used to avoid infinite looping 144 | let lastClientWidthSameCounter = 0; 145 | let lastClientWidth = Number.MIN_SAFE_INTEGER; 146 | let lastClientHeight = Number.MIN_SAFE_INTEGER; 147 | 148 | while (fontSize <= options.fontMaxSize) { 149 | meassureNode.style.fontSize = `${fontSize}px`; 150 | 151 | const rect = meassureNode.getBoundingClientRect(); 152 | 153 | if (rect.width > options.containerMaxWidth) { 154 | break; 155 | } 156 | 157 | if (rect.height > options.containerMaxHeight) { 158 | break; 159 | } 160 | 161 | if (rect.width === lastClientWidth) { 162 | lastClientWidthSameCounter += 1; 163 | 164 | if (lastClientWidthSameCounter === 3) { 165 | break; 166 | } 167 | } else { 168 | lastClientWidthSameCounter = 0; 169 | } 170 | 171 | const diffWidth = rect.width - lastClientWidth; 172 | const diffHeight = rect.height - lastClientHeight; 173 | 174 | lastClientWidth = rect.width; 175 | lastClientHeight = rect.height; 176 | 177 | lastFontSize = fontSize; 178 | 179 | const step = Math.max( 180 | 1, 181 | Math.floor( 182 | Math.min( 183 | (options.containerMaxWidth - rect.width) 184 | / (diffWidth + 3), 185 | (options.containerMaxHeight - rect.height) 186 | / (diffHeight + 3), 187 | ), 188 | ), 189 | ); 190 | 191 | fontSize += step; 192 | } 193 | } 194 | 195 | document.body.removeChild(meassureNode); 196 | 197 | element.style.fontSize = `${lastFontSize}px`; 198 | }; 199 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2023", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "Node16", /* Specify what module code is generated. */ 29 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./lib", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import stylistic from "@stylistic/eslint-plugin"; 3 | 4 | import * as importPlugin from "eslint-plugin-import"; 5 | import globals from "globals"; 6 | import tseslint from "typescript-eslint"; 7 | 8 | export default tseslint.config( 9 | eslint.configs.recommended, 10 | ...tseslint.configs.strictTypeChecked, 11 | ...tseslint.configs.stylisticTypeChecked, 12 | { 13 | languageOptions: { 14 | ecmaVersion: 2023, 15 | sourceType: "module", 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node, 19 | }, 20 | parserOptions: { 21 | projectService: true, 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | rules: { 26 | // These rules relate to possible logic errors in code: 27 | 28 | "array-callback-return": "error", 29 | "no-constructor-return": "error", 30 | "no-inner-declarations": ["error", "both"], 31 | "no-promise-executor-return": "error", 32 | "no-self-compare": "error", 33 | "no-template-curly-in-string": "warn", 34 | "no-unmodified-loop-condition": "warn", 35 | "no-unreachable-loop": "error", 36 | "require-atomic-updates": "error", 37 | 38 | // These rules suggest alternate ways of doing things: 39 | 40 | "accessor-pairs": "error", 41 | "arrow-body-style": ["error", "as-needed"], 42 | "block-scoped-var": "error", 43 | 44 | "camelcase": ["error", { 45 | properties: "never", 46 | }], 47 | 48 | "consistent-this": ["error", "self"], 49 | "curly": "error", 50 | "default-case-last": "error", 51 | 52 | "eqeqeq": "error", 53 | "func-names": ["error", "as-needed"], 54 | "func-style": ["error", "expression"], 55 | 56 | "grouped-accessor-pairs": "error", 57 | "guard-for-in": "error", 58 | 59 | "logical-assignment-operators": ["error", "always", { 60 | enforceForIfStatements: true 61 | }], 62 | 63 | "new-cap": ["error", { 64 | newIsCap: true, 65 | capIsNew: true, 66 | }], 67 | 68 | "no-bitwise": ["error", { 69 | int32Hint: true, 70 | }], 71 | 72 | "no-caller": "error", 73 | "no-div-regex": "error", 74 | "no-eq-null": "error", 75 | "no-eval": "error", 76 | "no-extend-native": "error", 77 | "no-extra-bind": "error", 78 | "no-extra-label": "error", 79 | "no-implicit-coercion": "error", 80 | "no-implicit-globals": "error", 81 | "no-invalid-this": "error", 82 | "no-iterator": "error", 83 | "no-label-var": "error", 84 | "no-lone-blocks": "error", 85 | "no-lonely-if": "error", 86 | "no-multi-assign": "error", 87 | "no-nested-ternary": "error", 88 | "no-new": "error", 89 | "no-new-func": "error", 90 | "no-new-wrappers": "error", 91 | "no-object-constructor": "error", 92 | "no-octal-escape": "error", 93 | 94 | "no-plusplus": ["error", { 95 | allowForLoopAfterthoughts: true, 96 | }], 97 | 98 | "no-proto": "error", 99 | "no-return-assign": "error", 100 | "no-sequences": "error", 101 | "no-unneeded-ternary": "error", 102 | "one-var": ["error", "never"], 103 | "operator-assignment": "error", 104 | "prefer-arrow-callback": "error", 105 | "prefer-const": "error", 106 | "prefer-exponentiation-operator": "error", 107 | "prefer-numeric-literals": "error", 108 | "prefer-object-has-own": "error", 109 | "prefer-object-spread": "error", 110 | "prefer-regex-literals": "error", 111 | "prefer-rest-params": "error", 112 | "prefer-spread": "error", 113 | "prefer-template": "error", 114 | "radix": ["error", "as-needed"], 115 | "require-unicode-regexp": "error", 116 | 117 | "sort-imports": ["error", { 118 | ignoreDeclarationSort: true, 119 | }], 120 | 121 | "symbol-description": "error", 122 | "vars-on-top": "error", 123 | 124 | // These rules care about how the code looks rather than how it executes: 125 | 126 | "unicode-bom": "error", 127 | } 128 | }, 129 | { 130 | rules: { 131 | // override 132 | "@typescript-eslint/no-require-imports": ["error", { 133 | allowAsImport: true, 134 | }], 135 | 136 | "@typescript-eslint/no-unused-vars": ["error", { 137 | args: "all", 138 | argsIgnorePattern: "^_", 139 | caughtErrors: "all", 140 | caughtErrorsIgnorePattern: "^_", 141 | destructuredArrayIgnorePattern: "^_", 142 | varsIgnorePattern: "^_", 143 | ignoreRestSiblings: true, 144 | }], 145 | 146 | "@typescript-eslint/restrict-template-expressions": ["error", { 147 | allowBoolean: true, 148 | allowNullish: true, 149 | allowNumber: true, 150 | allowRegExp: true, 151 | }], 152 | 153 | // switch on 154 | 155 | "@typescript-eslint/consistent-type-exports": "error", 156 | 157 | "@typescript-eslint/consistent-type-imports": ["error", { 158 | disallowTypeAnnotations: false, 159 | fixStyle: "separate-type-imports", 160 | prefer: "type-imports", 161 | }], 162 | 163 | "@typescript-eslint/default-param-last": "error", 164 | "@typescript-eslint/explicit-function-return-type": "error", 165 | 166 | "@typescript-eslint/explicit-member-accessibility": ["error", { 167 | accessibility: "no-public", 168 | }], 169 | 170 | "@typescript-eslint/explicit-module-boundary-types": "error", 171 | "@typescript-eslint/method-signature-style": "error", 172 | "@typescript-eslint/no-import-type-side-effects": "error", 173 | "@typescript-eslint/no-loop-func": "error", 174 | "@typescript-eslint/no-unnecessary-parameter-property-assignment": "error", 175 | "@typescript-eslint/no-unnecessary-qualifier": "error", 176 | "@typescript-eslint/no-use-before-define": "error", 177 | "@typescript-eslint/no-useless-empty-export": "error", 178 | }, 179 | }, 180 | stylistic.configs['recommended-flat'], 181 | { 182 | rules: { 183 | // override 184 | 185 | "@stylistic/arrow-parens": ["error", "always"], 186 | "@stylistic/brace-style": ["error", "1tbs", {}], 187 | 188 | "@stylistic/indent": ["error", 4, { 189 | SwitchCase: 1, 190 | }], 191 | 192 | "@stylistic/indent-binary-ops": ["error", 4], 193 | "@stylistic/lines-between-class-members": "off", 194 | "@stylistic/member-delimiter-style": ["error", {}], 195 | "@stylistic/multiline-ternary": ["error", "never"], 196 | 197 | "@stylistic/no-extra-parens": ["error", "all", { 198 | nestedBinaryExpressions: false, 199 | }], 200 | 201 | "@stylistic/no-mixed-operators": ["error", {}], 202 | 203 | "@stylistic/no-multiple-empty-lines": ["error", { 204 | max: 2, 205 | maxEOF: 0, 206 | maxBOF: 0, 207 | }], 208 | 209 | "@stylistic/no-trailing-spaces": ["error", { 210 | skipBlankLines: true, 211 | }], 212 | 213 | "@stylistic/object-curly-spacing": ["error", "always", { 214 | arraysInObjects: true, 215 | objectsInObjects: true, 216 | }], 217 | 218 | "@stylistic/quote-props": ["error", "as-needed"], 219 | "@stylistic/quotes": ["error", "double", {}], 220 | "@stylistic/semi": ["error", "always"], 221 | 222 | "@stylistic/semi-spacing": ["error", { 223 | before: false, 224 | after: false, 225 | }], 226 | 227 | "@stylistic/spaced-comment": ["error", "always", {}], 228 | "@stylistic/wrap-iife": ["error", "outside", {}], 229 | "@stylistic/yield-star-spacing": ["error", "after"], 230 | 231 | // switch on 232 | 233 | "@stylistic/array-bracket-newline": ["error", { 234 | minItems: 4, 235 | multiline: true, 236 | }], 237 | 238 | "@stylistic/array-element-newline": ["error", "consistent"], 239 | "@stylistic/function-call-argument-newline": ["error", "consistent"], 240 | "@stylistic/function-call-spacing": "error", 241 | "@stylistic/function-paren-newline": ["error", "multiline-arguments"], 242 | 243 | "@stylistic/generator-star-spacing": ["error", { 244 | before: false, 245 | after: true, 246 | }], 247 | 248 | "@stylistic/implicit-arrow-linebreak": "error", 249 | "@stylistic/jsx-quotes": ["error", "prefer-double"], 250 | "@stylistic/linebreak-style": "error", 251 | 252 | "@stylistic/newline-per-chained-call": ["error", { 253 | ignoreChainWithDepth: 3, 254 | }], 255 | 256 | "@stylistic/no-extra-semi": "error", 257 | "@stylistic/nonblock-statement-body-position": "error", 258 | 259 | "@stylistic/object-curly-newline": ["error", { 260 | "ObjectExpression": { "multiline": true, "minProperties": 4 }, 261 | "ObjectPattern": { "multiline": true, "minProperties": 4 }, 262 | "ImportDeclaration": { "multiline": true, "minProperties": 4 }, 263 | "ExportDeclaration": { "multiline": true, "minProperties": 4 } 264 | }], 265 | 266 | "@stylistic/object-property-newline": ["error", { 267 | allowAllPropertiesOnSameLine: true, 268 | }], 269 | 270 | "@stylistic/semi-style": "error", 271 | "@stylistic/switch-colon-spacing": "error", 272 | "@stylistic/wrap-regex": "error", 273 | } 274 | }, 275 | importPlugin.flatConfigs.recommended, 276 | importPlugin.flatConfigs.typescript, 277 | { 278 | settings: { 279 | "import/resolver": { 280 | "typescript": true, 281 | "node": true, 282 | } 283 | }, 284 | rules: { 285 | // override 286 | 287 | // switch on 288 | 289 | "import/no-empty-named-blocks": "error", 290 | 291 | "import/no-extraneous-dependencies": ["error", { 292 | devDependencies: [ 293 | "tests/**/*", 294 | "**/*.test.ts", 295 | "**/*.spec.ts", 296 | "webpack.config.ts", 297 | ], 298 | }], 299 | 300 | "import/no-mutable-exports": "error", 301 | 302 | "import/no-unused-modules": ["warn", { 303 | unusedExports: true, 304 | src: [ 305 | "src/**/*", 306 | ], 307 | ignoreExports: [ 308 | "src/lib.ts", 309 | ], 310 | }], 311 | 312 | "import/no-absolute-path": "error", 313 | "import/no-cycle": "error", 314 | "import/no-self-import": "error", 315 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"], 316 | "import/first": "error", 317 | "import/newline-after-import": "error", 318 | "import/no-named-default": "error", 319 | "import/no-namespace": "warn", 320 | 321 | "import/order": ["error", { 322 | "newlines-between": "always-and-inside-groups", 323 | alphabetize: { 324 | order: "asc", 325 | }, 326 | warnOnUnassignedImports: true, 327 | }], 328 | } 329 | }, 330 | { ignores: ["eslint.config.mjs", "dist/*", "lib/*"], }, 331 | ); --------------------------------------------------------------------------------