├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── index.html ├── main.js ├── screenshot.gif └── style.css ├── index.js └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 5 9 | }, 10 | "rules": { 11 | "indent": [2, 4], 12 | "semi": [2, "always"], 13 | "comma-dangle": [2, "always-multiline"], 14 | "space-before-function-paren": [2, { "anonymous": "always", "named": "never" }] 15 | }, 16 | "globals": { 17 | "define": true, 18 | "FlexText": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dolphin Wood 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flex-text 2 | 3 | [![npm](https://img.shields.io/npm/v/flex-text.svg?style=flat-square)](https://www.npmjs.com/package/flex-text) 4 | [![npm](https://img.shields.io/npm/dt/flex-text.svg?style=flat-square)](https://www.npmjs.com/package/flex-text) 5 | [![npm](https://img.shields.io/npm/l/flex-text.svg?style=flat-square)](https://www.npmjs.com/package/flex-text) 6 | 7 | Mastering font-size like flexbox for **IE 8+**: 8 | 9 | ``` 10 | font-size A:B:C:D = 1:2:1:1 11 | ``` 12 | 13 | ![screenshot](https://raw.githubusercontent.com/idiotWu/flex-text/master/demo/screenshot.gif) 14 | 15 | ## Demo 16 | 17 | [http://idiotwu.github.io/flex-text/](http://idiotwu.github.io/flex-text/) 18 | 19 | ## Install 20 | 21 | ``` 22 | npm install flex-text 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```javascript 28 | import FlexText from 'flex-text'; 29 | 30 | const flexText = new FlexText({ 31 | container: document.querySelector('.container'), 32 | spacing: 0, 33 | items: [{ 34 | elem: document.querySelector('.first'), 35 | flex: 1, 36 | }, { 37 | elem: document.querySelector('.second'), 38 | flex: 2, 39 | }, ...] 40 | }); 41 | ``` 42 | 43 | ## Important Notes 44 | 45 | ### Unwanted white space: 46 | 47 | You may get white spaces around flex items when they are layouted as `inline-block`, here's a little trick to it: 48 | 49 | ```css 50 | .container { 51 | letter-spacing: -0.31em; 52 | } 53 | 54 | .item { 55 | letter-spacing: normal; 56 | } 57 | ``` 58 | 59 | ### Canvas vs Legacy Element 60 | 61 | This plugin does text measuring with `` element. As a result, the newly created `` element must be inserted into document so that we can measure boundings. Text measuring with canvas is easier and will calculate at a higher performace. However, using legacy elements keeps us away from incompatibility :) 62 | 63 | That's also the reason why I wrote it in es5 flavor. 64 | 65 | ## APIs 66 | 67 | ### new FlexText() 68 | 69 | ```ts 70 | type FlexItem = { 71 | elem: Element, 72 | flex: number, // flex ratio, likes css `flex-grow` property 73 | } 74 | 75 | type FlexTextOptions = { 76 | container?: Element, 77 | spacing?: number, 78 | items?: FlexItem[], 79 | } 80 | 81 | class FlexText { 82 | constructor(options?: FlexTextOptions); 83 | 84 | update(): void; 85 | setSpacing(value: number): void; 86 | attachTo(container: Element): void; 87 | addItem(elem: Element, flex: number = 1): void; 88 | removeItem(elem: Element): void; 89 | clear(): void; 90 | alloc(): Array<{ 91 | elem: Element, 92 | fontSize: number, 93 | }>; 94 | } 95 | ``` 96 | 97 | Construct new instance with supported options: 98 | 99 | | Field | Type | Description | 100 | | ----------- | ------------ | ---------------------- | 101 | | `container` | `Element` | The element that contains all flex items. You can set container later by calling `instance.attachTo()`. | 102 | | `spacing` | `number` | White space between each item. You can also modify spacing by calling `instance.setSpacing()`. | 103 | | `items` | `FlexItem[]` | A list of all flex items inside. You can also add single flex item by calling `instance.addItem()`. | 104 | 105 | ### instance.update() 106 | 107 | ```ts 108 | instance.update(): void 109 | ``` 110 | 111 | Updates DOM layout at next frame. 112 | 113 | ### instance.setSpacing() 114 | 115 | ```ts 116 | instance.setSpacing(value: number): void 117 | ``` 118 | 119 | Changes white space between flex items. 120 | 121 | ### instance.attachTo() 122 | 123 | ```ts 124 | instance.attachTo(container: Element): void 125 | ``` 126 | 127 | Sets the container element. 128 | 129 | ### instance.addItem() 130 | 131 | ```ts 132 | instance.addItem(elem: Element, flex: number = 1): void 133 | ``` 134 | 135 | Adds single flex item. 136 | 137 | ### instance.removeItem() 138 | 139 | ```ts 140 | instance.removeItem(elem: Element): void 141 | ``` 142 | 143 | Removes specified item from list. 144 | 145 | ### instance.clear() 146 | 147 | ```ts 148 | instance.clear(): void 149 | ``` 150 | 151 | Removes all flex items. 152 | 153 | ### instance.alloc() 154 | 155 | ```ts 156 | instance.alloc(): Array<{ 157 | elem: Element, 158 | fontSize: number, 159 | }> 160 | ``` 161 | 162 | Calculates font sizes for all flex items. This method **WILL NOT** update DOM layout. 163 | 164 | ## License 165 | 166 | MIT. 167 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flexible Text 7 | 8 | 9 | 10 | 11 |

Flexible Text

12 |
13 |
14 | 100 15 | 16 |
17 |
18 | 0 19 | 20 |
21 |
22 | 2.33 23 | 24 |
25 |
26 | 1:2:1:1 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | $ 35 | 2 36 | . 37 | 33 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var width = document.querySelector('#width'); 3 | var dollar = document.querySelector('#dollar'); 4 | var spacing = document.querySelector('#spacing'); 5 | var container = document.querySelector('.dollar'); 6 | 7 | var widthVal = document.querySelector('#width-val'); 8 | var dollarVal = document.querySelector('#dollar-val'); 9 | var spacingVal = document.querySelector('#spacing-val'); 10 | var ratioVal = document.querySelector('#ratio-val'); 11 | 12 | var intPart = document.querySelector('.integer'); 13 | var floatPart = document.querySelector('.float'); 14 | var dotPart = document.querySelector('.dot'); 15 | 16 | var flexText = new FlexText({ 17 | container: container, 18 | items: [ 19 | { 20 | elem: document.querySelector('.symbol'), 21 | flex: 1, 22 | }, 23 | { 24 | elem: intPart, 25 | flex: 2, 26 | }, 27 | { 28 | elem: dotPart, 29 | flex: 1, 30 | }, 31 | { 32 | elem: floatPart, 33 | flex: 1, 34 | }, 35 | ], 36 | }); 37 | 38 | function forEach(arr, fn) { 39 | if (typeof arr.forEach === 'function') { 40 | return arr.forEach(fn); 41 | } 42 | 43 | for (var i = 0, max = arr.length; i < max; i++) { 44 | fn(arr[i], i, arr); 45 | } 46 | } 47 | 48 | function map(arr, fn) { 49 | if (typeof arr.map === 'function') { 50 | return arr.map(fn); 51 | } 52 | 53 | var res = []; 54 | 55 | for (var i = 0, max = arr.length; i < max; i++) { 56 | res.push(fn(arr[i], i, arr)); 57 | } 58 | 59 | return res; 60 | } 61 | 62 | function attachEvent(elem, type, handler) { 63 | if (typeof elem.addEventListener === 'function') { 64 | return elem.addEventListener(type, handler); 65 | } 66 | 67 | elem.attachEvent('on' + type, handler); 68 | } 69 | 70 | forEach([ 71 | 'input', 72 | 'change', 73 | ], function (type) { 74 | attachEvent(width, type, function () { 75 | var value = width.value; 76 | container.style.width = value + 'px'; 77 | widthVal.textContent = widthVal.innerText = value; 78 | flexText.update(); 79 | }); 80 | attachEvent(spacing, type, function () { 81 | var value = spacing.value; 82 | spacingVal.textContent = spacingVal.innerText = value; 83 | flexText.setSpacing(value); 84 | flexText.update(); 85 | }); 86 | attachEvent(dollar, type, function () { 87 | var value = dollar.value; 88 | var p = value.split('.'); 89 | dollarVal.textContent = dollarVal.innerText = value; 90 | intPart.textContent = intPart.innerText = p[0] || ''; 91 | floatPart.textContent = floatPart.innerText = p[1] || ''; 92 | dotPart.textContent = dotPart.innerText = p[1] ? '.' : ''; 93 | flexText.update(); 94 | }); 95 | }); 96 | 97 | var ratios = document.querySelectorAll('.flex-ratio'); 98 | 99 | forEach(ratios, function (el) { 100 | forEach([ 101 | 'input', 102 | 'change', 103 | ], function (type) { 104 | attachEvent(el, type, function () { 105 | var vals = map(ratios, function (e, i) { 106 | flexText.items[i].flex = parseFloat(e.value); 107 | 108 | return e.value; 109 | }); 110 | 111 | ratioVal.textContent = ratioVal.innerText = vals.join(':'); 112 | 113 | flexText.update(); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /demo/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiotWu/flex-text/2fb95a620749fa29a1e6f93557e6231da0ec5dde/demo/screenshot.gif -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | .dollar { 2 | color: #da2b50; 3 | width: 100px; 4 | border: 1px solid #abc; 5 | letter-spacing: -0.31em; 6 | white-space: nowrap; 7 | display: -webkit-box; 8 | display: -webkit-flex; 9 | display: -ms-flexbox; 10 | display: flex; 11 | -webkit-box-align: start; 12 | -webkit-align-items: flex-start; 13 | -ms-flex-align: start; 14 | align-items: flex-start; 15 | font-family: Helvetica, Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuan Yi Micro Hei", sans-serif; 16 | } 17 | .dollar > span { 18 | display: inline-block; 19 | vertical-align: top; 20 | outline: 1px solid; 21 | letter-spacing: normal; 22 | } 23 | 24 | .integer { 25 | font-size: 2em; 26 | } 27 | 28 | [data-desc] { 29 | display: inline-block; 30 | width: 10em; 31 | } 32 | [data-desc]::before { 33 | content: attr(data-desc) ":"; 34 | width: 10em; 35 | } 36 | 37 | #ctrl { 38 | margin-bottom: 1em; 39 | } 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define([], factory); 5 | } else if (typeof module === 'object' && module.exports) { 6 | // Node. Does not work with strict CommonJS, but 7 | // only CommonJS-like environments that support module.exports, 8 | // like Node. 9 | module.exports = factory(); 10 | } else { 11 | // Browser globals (root is window) 12 | root.FlexText = factory(); 13 | } 14 | }(this, function () { 15 | 'use strict'; 16 | 17 | // init measuring element 18 | var span = document.createElement('span'); 19 | span.style.visibility = 'hidden'; 20 | span.style.position = 'fixed'; 21 | span.style.top = '99999999px'; 22 | 23 | document.body.appendChild(span); 24 | 25 | var BASE_FONT_SIZE = 100; 26 | 27 | function async(fn) { 28 | if (typeof window.requestAnimationFrame === 'function') { 29 | requestAnimationFrame(fn); 30 | } else { 31 | setTimeout(fn, 0); 32 | } 33 | } 34 | 35 | function checkElem(elem, name) { 36 | if (!elem || elem.nodeType !== 1) { 37 | throw new TypeError('expect `' + (name || 'elem') + '` to be an Element, but got ' + typeof elem); 38 | } 39 | 40 | return true; 41 | } 42 | 43 | function getStyle(el, prop) { 44 | if (typeof window.getComputedStyle === 'function') { 45 | return getComputedStyle(el)[prop]; 46 | } 47 | 48 | prop = prop.replace(/-(\w)/g, function (m, $1) { 49 | return $1.toUpperCase(); 50 | }); 51 | 52 | return el.currentStyle[prop]; 53 | } 54 | 55 | function getWidth(el) { 56 | var bound = el.getBoundingClientRect(); 57 | 58 | return bound.width || (bound.right - bound.left); 59 | }; 60 | 61 | function forEach(arr, fn) { 62 | if (typeof arr.forEach === 'function') { 63 | return arr.forEach(fn); 64 | } 65 | 66 | for (var i = 0, max = arr.length; i < max; i++) { 67 | fn(arr[i], i, arr); 68 | } 69 | } 70 | 71 | function map(arr, fn) { 72 | if (typeof arr.map === 'function') { 73 | return arr.map(fn); 74 | } 75 | 76 | var res = []; 77 | 78 | for (var i = 0, max = arr.length; i < max; i++) { 79 | res.push(fn(arr[i], i, arr)); 80 | } 81 | 82 | return res; 83 | } 84 | 85 | function FlexText(options) { 86 | options = options || {}; 87 | 88 | this.items = []; 89 | 90 | this.setSpacing(options.spacing); 91 | 92 | if (options.items && options.items.length) { 93 | var self = this; 94 | 95 | forEach(options.items, function (v) { 96 | if (v && v.elem) { 97 | self.addItem(v.elem, v.flex); 98 | } 99 | }); 100 | } 101 | 102 | if (options.container) { 103 | this.attachTo(options.container); 104 | } 105 | } 106 | 107 | FlexText.prototype.attachTo = function attachTo(container) { 108 | checkElem(container, 'container'); 109 | 110 | this.container = container; 111 | this.update(); 112 | }; 113 | 114 | FlexText.prototype.setSpacing = function setSpacing(val) { 115 | this.spacing = parseFloat(val) || 0; 116 | }; 117 | 118 | FlexText.prototype.addItem = function addItem(elem, flex) { 119 | checkElem(elem, 'elem'); 120 | 121 | flex = flex || 1; 122 | 123 | if (flex <= 0) { 124 | throw new Error('expect flex to be greater than 0, but got ' + flex); 125 | } 126 | 127 | this.items.push({ 128 | elem: elem, 129 | flex: flex, 130 | }); 131 | }; 132 | 133 | FlexText.prototype.removeItem = function removeItem(elem) { 134 | checkElem(elem, 'elem'); 135 | 136 | var items = this.items; 137 | 138 | for (var i = 0, max = items.length; i < max; i++) { 139 | if (elem === items[i].elem) { 140 | return items.splice(i, 1); 141 | } 142 | } 143 | }; 144 | 145 | FlexText.prototype.clear = function clear() { 146 | this.items.length = 0; 147 | }; 148 | 149 | FlexText.prototype.alloc = function alloc() { 150 | var items = this.items; 151 | var spacing = this.spacing; 152 | var container = this.container; 153 | 154 | var totalSpace = getWidth(container); 155 | 156 | var widths = []; 157 | var totalWidth = 0; 158 | 159 | var whiteSpaceCount = items.length - 1; 160 | 161 | forEach(items, function (item) { 162 | var elem = item.elem; 163 | var flex = item.flex; 164 | 165 | var text = elem.textContent || elem.innerText; 166 | var fontSize = BASE_FONT_SIZE * flex; 167 | 168 | if (!text && whiteSpaceCount > 0) { 169 | whiteSpaceCount--; 170 | } 171 | 172 | span.style.fontWeight = getStyle(elem, 'font-weight'); 173 | span.style.fontFamily = getStyle(elem, 'font-family'); 174 | span.style.fontSize = fontSize + 'px'; 175 | 176 | span.textContent = span.innerText = text; 177 | 178 | var width = getWidth(span); 179 | 180 | widths.push(width); 181 | totalWidth += width; 182 | }); 183 | 184 | totalSpace -= parseFloat(spacing) * whiteSpaceCount; 185 | 186 | return map(widths, function (w, i) { 187 | var item = items[i]; 188 | 189 | var fontSize = BASE_FONT_SIZE * item.flex; 190 | var targetWidth = (w / totalWidth) * totalSpace; 191 | 192 | return { 193 | elem: item.elem, 194 | fontSize: Math.max(0, fontSize / (w / targetWidth)), 195 | }; 196 | }); 197 | }; 198 | 199 | FlexText.prototype.render = function render() { 200 | var spacing = this.spacing; 201 | 202 | var result = this.alloc(); 203 | 204 | forEach(result, function (item, idx) { 205 | var elem = item.elem; 206 | var fontSize = item.fontSize; 207 | 208 | elem.style.fontSize = Math.floor(fontSize) + 'px'; 209 | 210 | if (idx > 0) { 211 | elem.style.marginLeft = spacing + 'px'; 212 | } 213 | }); 214 | }; 215 | 216 | FlexText.prototype.update = function update() { 217 | var self = this; 218 | 219 | async(function () { 220 | self.render(); 221 | }); 222 | }; 223 | 224 | return FlexText; 225 | })); 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flex-text", 3 | "version": "1.3.0", 4 | "description": "Mastering font-size like flexbox!", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint ." 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/idiotWu/flex-text.git" 12 | }, 13 | "keywords": [ 14 | "responsive", 15 | "font", 16 | "flexbox" 17 | ], 18 | "author": "Dolphin Wood ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/idiotWu/flex-text/issues" 22 | }, 23 | "homepage": "https://github.com/idiotWu/flex-text#readme", 24 | "devDependencies": { 25 | "eslint": "^3.6.0", 26 | "eslint-config-standard": "^6.0.1", 27 | "eslint-plugin-promise": "^2.0.1", 28 | "eslint-plugin-standard": "^2.0.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------