├── .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 | [](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 | );
--------------------------------------------------------------------------------