├── .browserslistrc ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── fitty-share.png ├── fitty.gif ├── logo.png └── oswald.woff2 ├── dist ├── fitty.min.js └── fitty.module.js ├── header.svg ├── index.d.ts ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── fitty.js └── test.html /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | > 0.5% 3 | ie >= 10 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test.html linguist-vendored 2 | index.html linguist-documentation 3 | readme.md linguist-documentation 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.buymeacoffee.com/rikschennink'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | npm-debug.log 4 | .npmrc -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | assets/ 2 | src/ 3 | .idea/ 4 | .gitattributes 5 | .babelrc 6 | gulpfile.babel.js 7 | npm-debug.log 8 | *.html 9 | .npmrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/**/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "svelteSortOrder": "scripts-markup-styles", 3 | "trailingComma": "es5", 4 | "tabWidth": 4, 5 | "printWidth": 100, 6 | "singleQuote": true, 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 Rik Schennink - All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fitty, Snugly text resizing 2 | 3 | Scales up (or down) text so it fits perfectly to its parent container. 4 | 5 | Ideal for flexible and responsive websites. 6 | 7 | **[Visit PQINA.nl for other useful Web Components](https://pqina.nl/)** 8 | 9 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/rikschennink/fitty/blob/gh-pages/LICENSE) 10 | [![npm version](https://badge.fury.io/js/fitty.svg)](https://badge.fury.io/js/fitty) 11 | ![npm](https://img.shields.io/npm/dt/fitty) 12 | 13 | --- 14 | 15 | [](https://www.buymeacoffee.com/rikschennink/) 16 | 17 | [Buy me a Coffee](https://www.buymeacoffee.com/rikschennink/) / [Dev updates on Twitter](https://twitter.com/rikschennink/) 18 | 19 | --- 20 | 21 | ## Features 22 | 23 | - No dependencies 24 | - Easy setup 25 | - Optimal performance by grouping DOM read and write operations 26 | - Works with WebFonts (see example below) 27 | - Min and max font sizes 28 | - Support for MultiLine 29 | - Auto update when viewport changes 30 | - Monitors element subtree and updates accordingly 31 | 32 | ## Installation 33 | 34 | Install from npm: 35 | 36 | ``` 37 | npm install fitty --save 38 | ``` 39 | 40 | Or download `dist/fitty.min.js` and include the script on your page like shown below. 41 | 42 | ## Usage 43 | 44 | Run fitty like shown below and pass an element reference or a querySelector. For best performance include the script just before the closing `` element. 45 | 46 | ```html 47 |
Hello World
48 | 49 | 50 | 53 | ``` 54 | 55 | The following options are available to pass to the `fitty` method. 56 | 57 | | Option | Default | Description | 58 | | ------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 59 | | `minSize` | `16` | The minimum font size in pixels | 60 | | `maxSize` | `512` | The maximum font size in pixels | 61 | | `multiLine` | `true` | Wrap lines when using minimum font size. | 62 | | `observeMutations` | `MutationObserverInit` | Rescale when element contents is altered. Is set to false when `MutationObserver` is not supported. Pass a custom MutationObserverInit config to optimize monitoring based on your project. By default contains the MutationObserverInit configuration below or `false` based on browser support. | 63 | 64 | Default MutationObserverInit configuration: 65 | 66 | ```javascript 67 | { 68 | subtree: true, 69 | childList: true, 70 | characterData: true 71 | } 72 | ``` 73 | 74 | You can pass custom arguments like this: 75 | 76 | ```javascript 77 | fitty('#my-element', { 78 | minSize: 12, 79 | maxSize: 300, 80 | }); 81 | ``` 82 | 83 | The `fitty` function returns a single or multiple Fitty instances depending on how it's called. If you pass a query selector it will return an array of Fitty instances, if you pass a single element reference you'll receive back a single Fitty instance. 84 | 85 | | Method | Description | 86 | | --------------- | ---------------------------------------------------------------------------------- | 87 | | `fit(options)` | Force a redraw of the current fitty element | 88 | | `freeze()` | No longer update this fitty on changes | 89 | | `unfreeze()` | Resume updates to this fitty | 90 | | `unsubscribe()` | Remove the fitty element from the redraw loop and restore it to its original state | 91 | 92 | | Properties | Description | 93 | | ---------- | -------------------------------- | 94 | | `element` | Reference to the related element | 95 | 96 | ```javascript 97 | var fitties = fitty('.fit'); 98 | 99 | // get element reference of first fitty 100 | var myFittyElement = fitties[0].element; 101 | 102 | // force refit 103 | fitties[0].fit(); 104 | 105 | // force synchronous refit 106 | fitties[0].fit({ sync: true }); 107 | 108 | // stop updating this fitty and restore to original state 109 | fitties[0].unsubscribe(); 110 | ``` 111 | 112 | Fitty dispatches an event named `"fit"` when a fitty is fitted. 113 | 114 | | Event | Description | 115 | | ------- | --------------------------------------------------------------- | 116 | | `"fit"` | Fired when the element has been fitted to the parent container. | 117 | 118 | The `detail` property of the event contains an object which exposes the font size `oldValue` the `newValue` and the `scaleFactor`. 119 | 120 | ```js 121 | myFittyElement.addEventListener('fit', function (e) { 122 | // log the detail property to the console 123 | console.log(e.detail); 124 | }); 125 | ``` 126 | 127 | The `fitty` function itself also exposes some static options and methods: 128 | 129 | | Option | Default | Description | 130 | | -------------------------- | ------- | --------------------------------------------------------------------------------------------------------- | 131 | | `fitty.observeWindow` | `true` | Listen to the "resize" and "orientationchange" event on the window object and update fitties accordingly. | 132 | | `fitty.observeWindowDelay` | `100` | Redraw debounce delay in milliseconds for when above events are triggered. | 133 | 134 | | Method | Description | 135 | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 136 | | `fitty.fitAll(options)` | Refits all fitty instances to match their parent containers. Essentially a request to redraw all fitties. The `options` object is passed to fitty instance `fit()` method. | 137 | 138 | ## Performance 139 | 140 | For optimal performance add a CSS selector to your stylesheet that sets the elements that will be resized to have `white-space:nowrap` and `display:inline-block`. If not, Fitty will detect this and will have to restyle the elements automatically, resulting in a slight performance penalty. 141 | 142 | Suppose all elements that you apply fitty to are assigned the `fit` class name, add the following CSS selector to your stylesheet: 143 | 144 | ```css 145 | .fit { 146 | display: inline-block; 147 | white-space: nowrap; 148 | } 149 | ``` 150 | 151 | Should you only want to do this when JavaScript is available, add the following to the `` of your web page. 152 | 153 | ```html 154 | 157 | ``` 158 | 159 | And change the CSS selector to: 160 | 161 | ```css 162 | .js .fit { 163 | display: inline-block; 164 | white-space: nowrap; 165 | } 166 | ``` 167 | 168 | ## Do Not Upscale Text 169 | 170 | Fitty calculates the difference in width between the text container and its parent container. If you use CSS to set the width of the text container to be equal to the parent container it won't scale the text. 171 | 172 | This could be achieved by forcing the text container to be a block level element with `display: block !important` or setting its width to 100% with `width: 100%`. 173 | 174 | ## Custom Fonts 175 | 176 | Fitty does not concern itself with custom fonts. But it will be important to redraw Fitty text after a custom font has loaded (as previous measurements are probably incorrect at that point). 177 | 178 | If you need to use fitty on browsers that don't have [CSS Font Loading](http://caniuse.com/#feat=font-loading) support (Edge and Internet Explorer) you can use the fantastic [FontFaceObserver by Bram Stein](https://github.com/bramstein/fontfaceobserver) to detect when your custom fonts have loaded. 179 | 180 | See an example custom font implementation below. This assumes fitty has already been called on all elements with class name `fit`. 181 | 182 | ```html 183 | 189 | 240 | ``` 241 | 242 | ## Notes 243 | 244 | - Will not work if the fitty element is not part of the DOM. 245 | 246 | - If the parent element of the fitty element has horizontal padding the width calculation will be incorrect. You can fix this by wrapping the fitty element in another element. 247 | 248 | ```html 249 | 250 |
251 |

I'm a wonderful heading

252 |
253 | ``` 254 | 255 | ```html 256 | 257 |
258 |

I'm a wonderful heading

259 |
260 | ``` 261 | 262 | ## Tested 263 | 264 | - Modern browsers 265 | - IE 10+ 266 | 267 | Note that IE will require CustomEvent polyfill: 268 | https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill 269 | 270 | IE10 will require a polyfill for `Object.assign`: 271 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 272 | 273 | ## Versioning 274 | 275 | Versioning follows [Semver](http://semver.org). 276 | 277 | ## License 278 | 279 | MIT 280 | -------------------------------------------------------------------------------- /assets/fitty-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikschennink/fitty/310295262598fc88dfc330c169cb55d20f316b6b/assets/fitty-share.png -------------------------------------------------------------------------------- /assets/fitty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikschennink/fitty/310295262598fc88dfc330c169cb55d20f316b6b/assets/fitty.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikschennink/fitty/310295262598fc88dfc330c169cb55d20f316b6b/assets/logo.png -------------------------------------------------------------------------------- /assets/oswald.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikschennink/fitty/310295262598fc88dfc330c169cb55d20f316b6b/assets/oswald.woff2 -------------------------------------------------------------------------------- /dist/fitty.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fitty v2.4.2 - Snugly resizes text to fit its parent container 3 | * Copyright (c) 2023 Rik Schennink (https://pqina.nl/) 4 | */ 5 | 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).fitty=t()}(this,(function(){"use strict";return function(e){if(e){var t=function(e){return[].slice.call(e)},n=0,i=1,r=2,o=3,l=[],u=null,a="requestAnimationFrame"in e?function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{sync:!1};e.cancelAnimationFrame(u);var n=function(){return s(l.filter((function(e){return e.dirty&&e.active})))};if(t.sync)return n();u=e.requestAnimationFrame(n)}:function(){},c=function(e){return function(t){l.forEach((function(t){return t.dirty=e})),a(t)}},s=function(e){e.filter((function(e){return!e.styleComputed})).forEach((function(e){e.styleComputed=y(e)})),e.filter(m).forEach(v);var t=e.filter(p);t.forEach(d),t.forEach((function(e){v(e),f(e)})),t.forEach(S)},f=function(e){return e.dirty=n},d=function(e){e.availableWidth=e.element.parentNode.clientWidth,e.currentWidth=e.element.scrollWidth,e.previousFontSize=e.currentFontSize,e.currentFontSize=Math.min(Math.max(e.minSize,e.availableWidth/e.currentWidth*e.previousFontSize),e.maxSize),e.whiteSpace=e.multiLine&&e.currentFontSize===e.minSize?"normal":"nowrap"},p=function(e){return e.dirty!==r||e.dirty===r&&e.element.parentNode.clientWidth!==e.availableWidth},y=function(t){var n=e.getComputedStyle(t.element,null);return t.currentFontSize=parseFloat(n.getPropertyValue("font-size")),t.display=n.getPropertyValue("display"),t.whiteSpace=n.getPropertyValue("white-space"),!0},m=function(e){var t=!1;return!e.preStyleTestCompleted&&(/inline-/.test(e.display)||(t=!0,e.display="inline-block"),"nowrap"!==e.whiteSpace&&(t=!0,e.whiteSpace="nowrap"),e.preStyleTestCompleted=!0,t)},v=function(e){e.element.style.whiteSpace=e.whiteSpace,e.element.style.display=e.display,e.element.style.fontSize=e.currentFontSize+"px"},S=function(e){e.element.dispatchEvent(new CustomEvent("fit",{detail:{oldValue:e.previousFontSize,newValue:e.currentFontSize,scaleFactor:e.currentFontSize/e.previousFontSize}}))},h=function(e,t){return function(n){e.dirty=t,e.active&&a(n)}},b=function(e){return function(){l=l.filter((function(t){return t.element!==e.element})),e.observeMutations&&e.observer.disconnect(),e.element.style.whiteSpace=e.originalStyle.whiteSpace,e.element.style.display=e.originalStyle.display,e.element.style.fontSize=e.originalStyle.fontSize}},w=function(e){return function(){e.active||(e.active=!0,a())}},z=function(e){return function(){return e.active=!1}},g=function(e){e.observeMutations&&(e.observer=new MutationObserver(h(e,i)),e.observer.observe(e.element,e.observeMutations))},F={minSize:16,maxSize:512,multiLine:!0,observeMutations:"MutationObserver"in e&&{subtree:!0,childList:!0,characterData:!0}},W=null,E=function(){e.clearTimeout(W),W=e.setTimeout(c(r),C.observeWindowDelay)},M=["resize","orientationchange"];return Object.defineProperty(C,"observeWindow",{set:function(t){var n="".concat(t?"add":"remove","EventListener");M.forEach((function(t){e[n](t,E)}))}}),C.observeWindow=!0,C.observeWindowDelay=100,C.fitAll=c(o),C}function x(e,t){var n=Object.assign({},F,t),i=e.map((function(e){var t=Object.assign({},n,{element:e,active:!0});return function(e){e.originalStyle={whiteSpace:e.element.style.whiteSpace,display:e.element.style.display,fontSize:e.element.style.fontSize},g(e),e.newbie=!0,e.dirty=!0,l.push(e)}(t),{element:e,fit:h(t,o),unfreeze:w(t),freeze:z(t),unsubscribe:b(t)}}));return a(),i}function C(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"string"==typeof e?x(t(document.querySelectorAll(e)),n):x([e],n)[0]}}("undefined"==typeof window?null:window)})); 7 | -------------------------------------------------------------------------------- /dist/fitty.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fitty v2.4.2 - Snugly resizes text to fit its parent container 3 | * Copyright (c) 2023 Rik Schennink (https://pqina.nl/) 4 | */ 5 | 6 | var e=function(e){if(e){var t=function(e){return[].slice.call(e)},n=0,i=1,r=2,o=3,a=[],l=null,u="requestAnimationFrame"in e?function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{sync:!1};e.cancelAnimationFrame(l);var n=function(){return s(a.filter((function(e){return e.dirty&&e.active})))};if(t.sync)return n();l=e.requestAnimationFrame(n)}:function(){},c=function(e){return function(t){a.forEach((function(t){return t.dirty=e})),u(t)}},s=function(e){e.filter((function(e){return!e.styleComputed})).forEach((function(e){e.styleComputed=m(e)})),e.filter(y).forEach(v);var t=e.filter(p);t.forEach(d),t.forEach((function(e){v(e),f(e)})),t.forEach(S)},f=function(e){return e.dirty=n},d=function(e){e.availableWidth=e.element.parentNode.clientWidth,e.currentWidth=e.element.scrollWidth,e.previousFontSize=e.currentFontSize,e.currentFontSize=Math.min(Math.max(e.minSize,e.availableWidth/e.currentWidth*e.previousFontSize),e.maxSize),e.whiteSpace=e.multiLine&&e.currentFontSize===e.minSize?"normal":"nowrap"},p=function(e){return e.dirty!==r||e.dirty===r&&e.element.parentNode.clientWidth!==e.availableWidth},m=function(t){var n=e.getComputedStyle(t.element,null);return t.currentFontSize=parseFloat(n.getPropertyValue("font-size")),t.display=n.getPropertyValue("display"),t.whiteSpace=n.getPropertyValue("white-space"),!0},y=function(e){var t=!1;return!e.preStyleTestCompleted&&(/inline-/.test(e.display)||(t=!0,e.display="inline-block"),"nowrap"!==e.whiteSpace&&(t=!0,e.whiteSpace="nowrap"),e.preStyleTestCompleted=!0,t)},v=function(e){e.element.style.whiteSpace=e.whiteSpace,e.element.style.display=e.display,e.element.style.fontSize=e.currentFontSize+"px"},S=function(e){e.element.dispatchEvent(new CustomEvent("fit",{detail:{oldValue:e.previousFontSize,newValue:e.currentFontSize,scaleFactor:e.currentFontSize/e.previousFontSize}}))},h=function(e,t){return function(n){e.dirty=t,e.active&&u(n)}},w=function(e){return function(){a=a.filter((function(t){return t.element!==e.element})),e.observeMutations&&e.observer.disconnect(),e.element.style.whiteSpace=e.originalStyle.whiteSpace,e.element.style.display=e.originalStyle.display,e.element.style.fontSize=e.originalStyle.fontSize}},b=function(e){return function(){e.active||(e.active=!0,u())}},z=function(e){return function(){return e.active=!1}},F=function(e){e.observeMutations&&(e.observer=new MutationObserver(h(e,i)),e.observer.observe(e.element,e.observeMutations))},g={minSize:16,maxSize:512,multiLine:!0,observeMutations:"MutationObserver"in e&&{subtree:!0,childList:!0,characterData:!0}},W=null,E=function(){e.clearTimeout(W),W=e.setTimeout(c(r),x.observeWindowDelay)},M=["resize","orientationchange"];return Object.defineProperty(x,"observeWindow",{set:function(t){var n="".concat(t?"add":"remove","EventListener");M.forEach((function(t){e[n](t,E)}))}}),x.observeWindow=!0,x.observeWindowDelay=100,x.fitAll=c(o),x}function C(e,t){var n=Object.assign({},g,t),i=e.map((function(e){var t=Object.assign({},n,{element:e,active:!0});return function(e){e.originalStyle={whiteSpace:e.element.style.whiteSpace,display:e.element.style.display,fontSize:e.element.style.fontSize},F(e),e.newbie=!0,e.dirty=!0,a.push(e)}(t),{element:e,fit:h(t,o),unfreeze:b(t),freeze:z(t),unsubscribe:w(t)}}));return u(),i}function x(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"string"==typeof e?C(t(document.querySelectorAll(e)),n):C([e],n)[0]}}("undefined"==typeof window?null:window);export default e; 7 | -------------------------------------------------------------------------------- /header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 70 |
71 | 72 |
73 |

Hi! It's me Rik! 👋

74 | 75 |

I'm the dev behind Fitty.

76 | 77 |

Love Fitty? Buy me a Coffee ☕️ to support its development.

78 | 79 |

Thanks! 🙏

80 | 81 |

PS. I love a good cup of coffee but donations are secretly spent on LEGOs for my kids.

82 |
83 | 84 | 85 |
86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'fitty' { 2 | export interface FittyOptions { 3 | minSize?: number; 4 | maxSize?: number; 5 | multiLine?: boolean; 6 | observeMutations?: MutationObserverInit; 7 | } 8 | 9 | export interface FitOptions { 10 | sync?: boolean; 11 | } 12 | 13 | export interface FittyInstance { 14 | element: HTMLElement; 15 | fit: (options?: FitOptions) => void; 16 | freeze: () => void; 17 | unfreeze: () => void; 18 | unsubscribe: () => void; 19 | } 20 | 21 | function fitty(el: HTMLElement, options?: FittyOptions): FittyInstance; 22 | function fitty(el: string, options?: FittyOptions): FittyInstance[]; 23 | 24 | declare namespace fitty { 25 | let observeWindow: boolean; 26 | let observeWindowDelay: number; 27 | const fitAll: (options?: FittyOptions) => void; 28 | } 29 | 30 | export default fitty; 31 | } 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fitty 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 231 | 232 | 233 | 234 | 235 |
236 | 237 |
238 | 239 |
240 |

Fitty

241 |
242 | 243 |

Snugly resizes text to fit its parent container.

244 | 245 |

~ 1 kcal, ehh, kB gzipped

246 | 247 |
248 | 249 | Examples 250 | 251 | Get it on GitHub 252 | 253 |
254 | 255 |
256 | 257 |
258 | 259 |
260 | 261 |

Examples

262 | 263 |

The examples below can edited live and will auto update.

264 | 265 |
266 |

The wizard quickly jinxed the gnomes before they vaporized.

267 |
268 | 269 |
270 |

Wizard

271 |
272 | 273 |

Click button below to load a custom font to the fitties above.

274 | 275 | 276 | 277 |

The fields above use contenteditable, this is useful for demo purposes but also a bit buggy, in production I would expect Fitty to be mostly used on headers and taglines.

278 | 279 |
280 | To the Docs! 281 |
282 | 283 |
284 | 285 |
286 | 287 |
288 | 289 | 290 | 291 | 292 | 300 | 301 | 302 | 303 | 427 | 428 | 429 | 430 | 486 | 487 | 488 | 489 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fitty", 3 | "version": "2.4.2", 4 | "description": "Snugly resizes text to fit its parent container", 5 | "keywords": [ 6 | "fit", 7 | "responsive", 8 | "text", 9 | "font", 10 | "rescale", 11 | "resize" 12 | ], 13 | "main": "dist/fitty.min.js", 14 | "module": "dist/fitty.module.js", 15 | "types": "index.d.ts", 16 | "author": "Rik Schennink (https://pqina.nl/)", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@babel/preset-env": "^7.14.7", 20 | "@rollup/plugin-babel": "^5.3.0", 21 | "rollup-plugin-license": "^2.5.0", 22 | "rollup-plugin-livereload": "^2.0.5", 23 | "rollup-plugin-serve": "^1.1.0", 24 | "rollup-plugin-terser": "^7.0.2" 25 | }, 26 | "scripts": { 27 | "dev": "npx rollup -c -w", 28 | "build": "npx rollup -c" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/rikschennink/fitty.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/rikschennink/fitty/issues" 36 | }, 37 | "homepage": "https://github.com/rikschennink/fitty" 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import license from 'rollup-plugin-license'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import serve from 'rollup-plugin-serve'; 5 | import livereload from 'rollup-plugin-livereload'; 6 | 7 | const isProduction = !process.env.ROLLUP_WATCH; 8 | 9 | const pkg = require('./package.json'); 10 | const banner = ` 11 | ${pkg.name} v${pkg.version} - ${pkg.description} 12 | Copyright (c) ${new Date().getFullYear()} ${pkg.author} 13 | `; 14 | 15 | const config = { 16 | watch: { 17 | clearScreen: false, 18 | }, 19 | 20 | input: 'src/fitty.js', 21 | 22 | output: [ 23 | { 24 | format: 'esm', 25 | sourcemap: false, 26 | plugins: isProduction ? [terser()] : [], 27 | file: 'dist/fitty.module.js', 28 | }, 29 | { 30 | sourcemap: false, 31 | format: 'umd', 32 | name: 'fitty', 33 | plugins: isProduction ? [terser()] : [], 34 | file: 'dist/fitty.min.js', 35 | }, 36 | ], 37 | plugins: [ 38 | babel({ 39 | babelHelpers: 'bundled', 40 | presets: ['@babel/preset-env'], 41 | }), 42 | isProduction && license({ banner }), 43 | !isProduction && serve(), 44 | !isProduction && 45 | livereload({ 46 | watch: 'dist', 47 | }), 48 | ], 49 | }; 50 | 51 | export default config; 52 | -------------------------------------------------------------------------------- /src/fitty.js: -------------------------------------------------------------------------------- 1 | export default ((w) => { 2 | // no window, early exit 3 | if (!w) return; 4 | 5 | // node list to array helper method 6 | const toArray = (nl) => [].slice.call(nl); 7 | 8 | // states 9 | const DrawState = { 10 | IDLE: 0, 11 | DIRTY_CONTENT: 1, 12 | DIRTY_LAYOUT: 2, 13 | DIRTY: 3, 14 | }; 15 | 16 | // all active fitty elements 17 | let fitties = []; 18 | 19 | // group all redraw calls till next frame, we cancel each frame request when a new one comes in. If no support for request animation frame, this is an empty function and supports for fitty stops. 20 | let redrawFrame = null; 21 | const requestRedraw = 22 | 'requestAnimationFrame' in w 23 | ? (options = { sync: false }) => { 24 | w.cancelAnimationFrame(redrawFrame); 25 | 26 | const redrawFn = () => redraw(fitties.filter((f) => f.dirty && f.active)); 27 | 28 | if (options.sync) return redrawFn(); 29 | 30 | redrawFrame = w.requestAnimationFrame(redrawFn); 31 | } 32 | : () => {}; 33 | 34 | // sets all fitties to dirty so they are redrawn on the next redraw loop, then calls redraw 35 | const redrawAll = (type) => (options) => { 36 | fitties.forEach((f) => (f.dirty = type)); 37 | requestRedraw(options); 38 | }; 39 | 40 | // redraws fitties so they nicely fit their parent container 41 | const redraw = (fitties) => { 42 | // getting info from the DOM at this point should not trigger a reflow, let's gather as much intel as possible before triggering a reflow 43 | 44 | // check if styles of all fitties have been computed 45 | fitties 46 | .filter((f) => !f.styleComputed) 47 | .forEach((f) => { 48 | f.styleComputed = computeStyle(f); 49 | }); 50 | 51 | // restyle elements that require pre-styling, this triggers a reflow, please try to prevent by adding CSS rules (see docs) 52 | fitties.filter(shouldPreStyle).forEach(applyStyle); 53 | 54 | // we now determine which fitties should be redrawn 55 | const fittiesToRedraw = fitties.filter(shouldRedraw); 56 | 57 | // we calculate final styles for these fitties 58 | fittiesToRedraw.forEach(calculateStyles); 59 | 60 | // now we apply the calculated styles from our previous loop 61 | fittiesToRedraw.forEach((f) => { 62 | applyStyle(f); 63 | markAsClean(f); 64 | }); 65 | 66 | // now we dispatch events for all restyled fitties 67 | fittiesToRedraw.forEach(dispatchFitEvent); 68 | }; 69 | 70 | const markAsClean = (f) => (f.dirty = DrawState.IDLE); 71 | 72 | const calculateStyles = (f) => { 73 | // get available width from parent node 74 | f.availableWidth = f.element.parentNode.clientWidth; 75 | 76 | // the space our target element uses 77 | f.currentWidth = f.element.scrollWidth; 78 | 79 | // remember current font size 80 | f.previousFontSize = f.currentFontSize; 81 | 82 | // let's calculate the new font size 83 | f.currentFontSize = Math.min( 84 | Math.max(f.minSize, (f.availableWidth / f.currentWidth) * f.previousFontSize), 85 | f.maxSize 86 | ); 87 | 88 | // if allows wrapping, only wrap when at minimum font size (otherwise would break container) 89 | f.whiteSpace = f.multiLine && f.currentFontSize === f.minSize ? 'normal' : 'nowrap'; 90 | }; 91 | 92 | // should always redraw if is not dirty layout, if is dirty layout, only redraw if size has changed 93 | const shouldRedraw = (f) => 94 | f.dirty !== DrawState.DIRTY_LAYOUT || 95 | (f.dirty === DrawState.DIRTY_LAYOUT && 96 | f.element.parentNode.clientWidth !== f.availableWidth); 97 | 98 | // every fitty element is tested for invalid styles 99 | const computeStyle = (f) => { 100 | // get style properties 101 | const style = w.getComputedStyle(f.element, null); 102 | 103 | // get current font size in pixels (if we already calculated it, use the calculated version) 104 | f.currentFontSize = parseFloat(style.getPropertyValue('font-size')); 105 | 106 | // get display type and wrap mode 107 | f.display = style.getPropertyValue('display'); 108 | f.whiteSpace = style.getPropertyValue('white-space'); 109 | 110 | // styles computed 111 | return true; 112 | }; 113 | 114 | // determines if this fitty requires initial styling, can be prevented by applying correct styles through CSS 115 | const shouldPreStyle = (f) => { 116 | let preStyle = false; 117 | 118 | // if we already tested for prestyling we don't have to do it again 119 | if (f.preStyleTestCompleted) return false; 120 | 121 | // should have an inline style, if not, apply 122 | if (!/inline-/.test(f.display)) { 123 | preStyle = true; 124 | f.display = 'inline-block'; 125 | } 126 | 127 | // to correctly calculate dimensions the element should have whiteSpace set to nowrap 128 | if (f.whiteSpace !== 'nowrap') { 129 | preStyle = true; 130 | f.whiteSpace = 'nowrap'; 131 | } 132 | 133 | // we don't have to do this twice 134 | f.preStyleTestCompleted = true; 135 | 136 | return preStyle; 137 | }; 138 | 139 | // apply styles to single fitty 140 | const applyStyle = (f) => { 141 | f.element.style.whiteSpace = f.whiteSpace; 142 | f.element.style.display = f.display; 143 | f.element.style.fontSize = f.currentFontSize + 'px'; 144 | }; 145 | 146 | // dispatch a fit event on a fitty 147 | const dispatchFitEvent = (f) => { 148 | f.element.dispatchEvent( 149 | new CustomEvent('fit', { 150 | detail: { 151 | oldValue: f.previousFontSize, 152 | newValue: f.currentFontSize, 153 | scaleFactor: f.currentFontSize / f.previousFontSize, 154 | }, 155 | }) 156 | ); 157 | }; 158 | 159 | // fit method, marks the fitty as dirty and requests a redraw (this will also redraw any other fitty marked as dirty) 160 | const fit = (f, type) => (options) => { 161 | f.dirty = type; 162 | if (!f.active) return; 163 | requestRedraw(options); 164 | }; 165 | 166 | const init = (f) => { 167 | // save some of the original CSS properties before we change them 168 | f.originalStyle = { 169 | whiteSpace: f.element.style.whiteSpace, 170 | display: f.element.style.display, 171 | fontSize: f.element.style.fontSize, 172 | }; 173 | 174 | // should we observe DOM mutations 175 | observeMutations(f); 176 | 177 | // this is a new fitty so we need to validate if it's styles are in order 178 | f.newbie = true; 179 | 180 | // because it's a new fitty it should also be dirty, we want it to redraw on the first loop 181 | f.dirty = true; 182 | 183 | // we want to be able to update this fitty 184 | fitties.push(f); 185 | }; 186 | 187 | const destroy = (f) => () => { 188 | // remove from fitties array 189 | fitties = fitties.filter((_) => _.element !== f.element); 190 | 191 | // stop observing DOM 192 | if (f.observeMutations) f.observer.disconnect(); 193 | 194 | // reset the CSS properties we changes 195 | f.element.style.whiteSpace = f.originalStyle.whiteSpace; 196 | f.element.style.display = f.originalStyle.display; 197 | f.element.style.fontSize = f.originalStyle.fontSize; 198 | }; 199 | 200 | // add a new fitty, does not redraw said fitty 201 | const subscribe = (f) => () => { 202 | if (f.active) return; 203 | f.active = true; 204 | requestRedraw(); 205 | }; 206 | 207 | // remove an existing fitty 208 | const unsubscribe = (f) => () => (f.active = false); 209 | 210 | const observeMutations = (f) => { 211 | // no observing? 212 | if (!f.observeMutations) return; 213 | 214 | // start observing mutations 215 | f.observer = new MutationObserver(fit(f, DrawState.DIRTY_CONTENT)); 216 | 217 | // start observing 218 | f.observer.observe(f.element, f.observeMutations); 219 | }; 220 | 221 | // default mutation observer settings 222 | const mutationObserverDefaultSetting = { 223 | subtree: true, 224 | childList: true, 225 | characterData: true, 226 | }; 227 | 228 | // default fitty options 229 | const defaultOptions = { 230 | minSize: 16, 231 | maxSize: 512, 232 | multiLine: true, 233 | observeMutations: 'MutationObserver' in w ? mutationObserverDefaultSetting : false, 234 | }; 235 | 236 | // array of elements in, fitty instances out 237 | function fittyCreate(elements, options) { 238 | // set options object 239 | const fittyOptions = Object.assign( 240 | {}, 241 | 242 | // expand default options 243 | defaultOptions, 244 | 245 | // override with custom options 246 | options 247 | ); 248 | 249 | // create fitties 250 | const publicFitties = elements.map((element) => { 251 | // create fitty instance 252 | const f = Object.assign({}, fittyOptions, { 253 | // internal options for this fitty 254 | element, 255 | active: true, 256 | }); 257 | 258 | // initialise this fitty 259 | init(f); 260 | 261 | // expose API 262 | return { 263 | element, 264 | fit: fit(f, DrawState.DIRTY), 265 | unfreeze: subscribe(f), 266 | freeze: unsubscribe(f), 267 | unsubscribe: destroy(f), 268 | }; 269 | }); 270 | 271 | // call redraw on newly initiated fitties 272 | requestRedraw(); 273 | 274 | // expose fitties 275 | return publicFitties; 276 | } 277 | 278 | // fitty creation function 279 | function fitty(target, options = {}) { 280 | // if target is a string 281 | return typeof target === 'string' 282 | ? // treat it as a querySelector 283 | fittyCreate(toArray(document.querySelectorAll(target)), options) 284 | : // create single fitty 285 | fittyCreate([target], options)[0]; 286 | } 287 | 288 | // handles viewport changes, redraws all fitties, but only does so after a timeout 289 | let resizeDebounce = null; 290 | const onWindowResized = () => { 291 | w.clearTimeout(resizeDebounce); 292 | resizeDebounce = w.setTimeout(redrawAll(DrawState.DIRTY_LAYOUT), fitty.observeWindowDelay); 293 | }; 294 | 295 | // define observe window property, so when we set it to true or false events are automatically added and removed 296 | const events = ['resize', 'orientationchange']; 297 | Object.defineProperty(fitty, 'observeWindow', { 298 | set: (enabled) => { 299 | const method = `${enabled ? 'add' : 'remove'}EventListener`; 300 | events.forEach((e) => { 301 | w[method](e, onWindowResized); 302 | }); 303 | }, 304 | }); 305 | 306 | // fitty global properties (by setting observeWindow to true the events above get added) 307 | fitty.observeWindow = true; 308 | fitty.observeWindowDelay = 100; 309 | 310 | // public fit all method, will force redraw no matter what 311 | fitty.fitAll = redrawAll(DrawState.DIRTY); 312 | 313 | // export our fitty function, we don't want to keep it to our selves 314 | return fitty; 315 | })(typeof window === 'undefined' ? null : window); 316 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Fitty Test Page 9 | 10 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 |

Integer

33 |

Lorem ipsum dolor sit.

34 |

consectetur adipiscing elit.

35 |

aecenas elementum purus nec felis pellentesque, eget volutpat mauris vestibulum.

36 |
37 | 38 |
39 |

Integer

40 |

Lorem ipsum dolor sit.

41 |

consectetur adipiscing elit.

42 |

aecenas elementum purus nec felis pellentesque, eget volutpat mauris vestibulum.

43 |
44 | 45 |
46 |

Integer

47 |

Lorem ipsum dolor sit.

48 |

consectetur adipiscing elit.

49 |

aecenas elementum purus nec felis pellentesque, eget volutpat mauris vestibulum.

50 |
51 | 52 |
53 |

Integer

54 |

Lorem ipsum dolor sit.

55 |

consectetur adipiscing elit.

56 |

aecenas elementum purus nec felis pellentesque, eget volutpat mauris vestibulum.

57 |
58 | 59 | 60 |
61 | 62 | 63 | 99 | 100 | 101 | --------------------------------------------------------------------------------