├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── index.js ├── package.json ├── test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | /coverage 4 | /node_modules 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | 4 | services: 5 | - xvfb 6 | 7 | language: node_js 8 | 9 | node_js: 10 | - 'node' 11 | 12 | script: 13 | - yarn lint 14 | - yarn test && yarn coverage 15 | 16 | after_success: 17 | - yarn add --dev coveralls 18 | - cat coverage/lcov.info | yarn run coveralls 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.0 4 | 5 | - Remove `splitRegex` in favour of a `split` callback 6 | - Allow passing in a `setClassName` callback to set the class name on the inserted DOM element 7 | - Use modern syntax, namely `let`/`const` and default parameters 8 | 9 | ## 2.2.5 10 | 11 | - Bump dependencies 12 | 13 | ## 2.2.4 14 | 15 | - Add `rimraf`, `mkdirp` 16 | - Add `husky` and `lint-staged` 17 | - Bump dependencies 18 | 19 | ## 2.2.3 20 | 21 | - Only publish `index.js` 22 | 23 | ## 2.2.2 24 | 25 | - Add `standard` 26 | - Bump dependencies 27 | 28 | ## 2.2.1 29 | 30 | - Add `weight` script 31 | 32 | ## 2.2.0 33 | 34 | - Add support for passing in a regular expression to control how the contents of the element are wrapped 35 | 36 | ## 2.1.0 37 | 38 | - Add `aria-label` and `aria-hidden` attributes for accessibility 39 | 40 | ## 2.0.1 41 | 42 | - Add a [CodePen demo](https://codepen.io/anon/pen/WOxNqX) 43 | - Use [`prettier-standard`](https://github.com/sheerun/prettier-standard) 44 | 45 | ## 2.0.0 46 | 47 | - Prioritise the general case, where the `element` has a single child [text node](https://developer.mozilla.org/en-US/docs/Web/API/Text) 48 | - Update tooling: [`prettier`](https://github.com/prettier/prettier), [`tape`](https://github.com/substack/tape), [`yarn`](https://github.com/yarnpkg/yarn) 49 | - Drop [Bower](https://bower.io/) support 50 | 51 | ## 1.0.0 52 | 53 | - Initial release 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Lim Yuan Qing 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 | # charming [![npm Version](https://badgen.net/npm/v/charming)](https://www.npmjs.org/package/charming) [![Build Status](https://badgen.net/travis/yuanqing/charming?label=build)](https://travis-ci.org/yuanqing/charming) [![Coverage Status](https://badgen.net/coveralls/c/github/yuanqing/charming)](https://coveralls.io/r/yuanqing/charming) [![Bundle Size](https://badgen.net/bundlephobia/minzip/charming)](https://bundlephobia.com/result?p=charming) 2 | 3 | > [Lettering.js](https://github.com/davatron5000/Lettering.js) in vanilla JavaScript 4 | 5 | - Supports changing the tag name or class name of the inserted DOM elements 6 | - Supports controlling how the contents of the element are wrapped 7 | 8 | ## Usage 9 | 10 | > [**Editable demo (CodePen)**](https://codepen.io/lyuanqing/pen/YeYdrm) 11 | 12 | HTML: 13 | 14 | ```html 15 |

foo

16 | ``` 17 | 18 | JavaScript: 19 | 20 | ```js 21 | const charming = require('charming') 22 | 23 | const element = document.querySelector('h1') 24 | charming(element) 25 | ``` 26 | 27 | Boom: 28 | 29 | ```html 30 |

31 | 32 | 33 | 34 |

35 | ``` 36 | 37 | - Charming also works when the given element contains other (possibly nested) DOM elements; any character that is inside a [text node](https://developer.mozilla.org/en-US/docs/Web/API/Text) in the given element will be wrapped. 38 | - For accessibility, Charming adds an `aria-label` attribute on the given element and `aria-hidden` attributes on each of the inserted DOM elements. 39 | 40 | ## API 41 | 42 | ```js 43 | const charming = require('charming') 44 | ``` 45 | 46 | ### charming(element [, options]) 47 | 48 | - `element` is a DOM element 49 | - `options` is an optional configuration object 50 | 51 | Use `options.tagName` to change the tag name of the wrapper element: 52 | 53 | ```js 54 | charming(element, { 55 | tagName: 'b' 56 | }) 57 | ``` 58 | 59 | Use `options.setClassName` to change the class name on each wrapper element: 60 | 61 | ```js 62 | charming(element, { 63 | setClassName: function (index, letter) { 64 | return `index-${index} letter-${letter}` 65 | } 66 | }) 67 | ``` 68 | 69 | Use `options.split` to control how the contents of the element are wrapped: 70 | 71 | ```js 72 | charming(element, { 73 | split: function (string) { 74 | return string.split(/(\s+)/) 75 | }, 76 | setClassName: function (index) { 77 | return `word-${index}` 78 | } 79 | }) 80 | ``` 81 | 82 | ## Installation 83 | 84 | ```sh 85 | $ yarn add charming 86 | ``` 87 | 88 | ## License 89 | 90 | [MIT](LICENSE.md) 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function ( 2 | element, 3 | { 4 | tagName = 'span', 5 | split, 6 | setClassName = function (index) { 7 | return 'char' + index 8 | } 9 | } = {} 10 | ) { 11 | element.normalize() 12 | let index = 1 13 | function inject (element) { 14 | const parentNode = element.parentNode 15 | const nodeValue = element.nodeValue 16 | const array = split ? split(nodeValue) : nodeValue.split('') 17 | array.forEach(function (string) { 18 | const node = document.createElement(tagName) 19 | const className = setClassName(index++, string) 20 | if (className) { 21 | node.className = className 22 | } 23 | node.appendChild(document.createTextNode(string)) 24 | node.setAttribute('aria-hidden', 'true') 25 | parentNode.insertBefore(node, element) 26 | }) 27 | if (nodeValue.trim() !== '') { 28 | parentNode.setAttribute('aria-label', nodeValue) 29 | } 30 | parentNode.removeChild(element) 31 | } 32 | ;(function traverse (element) { 33 | // `element` is itself a text node 34 | if (element.nodeType === 3) { 35 | return inject(element) 36 | } 37 | // `element` has a single child text node 38 | const childNodes = Array.prototype.slice.call(element.childNodes) // static array of nodes 39 | const length = childNodes.length 40 | if (length === 1 && childNodes[0].nodeType === 3) { 41 | return inject(childNodes[0]) 42 | } 43 | // `element` has more than one child node 44 | childNodes.forEach(function (childNode) { 45 | traverse(childNode) 46 | }) 47 | })(element) 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charming", 3 | "version": "3.0.2", 4 | "description": "Lettering.js in vanilla JavaScript", 5 | "author": "Lim Yuan Qing", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/yuanqing/charming.git" 10 | }, 11 | "devDependencies": { 12 | "browserify": "^16.5.0", 13 | "browserify-istanbul": "^3.0.1", 14 | "gzip-size-cli": "^3.0.0", 15 | "husky": "^4.2.3", 16 | "lint-staged": "^10.0.8", 17 | "mkdirp": "^1.0.3", 18 | "nyc": "^15.0.0", 19 | "prettier-standard": "^16.2.1", 20 | "rimraf": "^3.0.2", 21 | "standard": "^14.3.1", 22 | "tape": "^4.13.2", 23 | "tape-istanbul": "^1.2.0", 24 | "tape-run": "^6.0.1", 25 | "terser": "^4.6.6" 26 | }, 27 | "scripts": { 28 | "clean": "rimraf '*.log' coverage .nyc_output", 29 | "coverage": "yarn run coverage-test && yarn run coverage-report", 30 | "coverage-test": "rimraf .nyc_output && mkdirp .nyc_output && browserify test.js --plugin tape-istanbul/plugin | tape-run | tape-istanbul --output .nyc_output/coverage.json", 31 | "coverage-report": "rimraf coverage && nyc report --reporter text --reporter html", 32 | "fix": "prettier-standard '*.js'", 33 | "lint": "standard '*.js'", 34 | "test": "browserify test.js | tape-run", 35 | "weight": "terser index.js --compress --mangle --toplevel | gzip-size" 36 | }, 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "lint-staged" 40 | } 41 | }, 42 | "lint-staged": { 43 | "*.js": [ 44 | "standard", 45 | "prettier-standard" 46 | ] 47 | }, 48 | "files": [ 49 | "index.js" 50 | ], 51 | "keywords": [ 52 | "kerning", 53 | "lettering", 54 | "letteringjs", 55 | "span", 56 | "typography" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const charming = require('./') 3 | 4 | function createElement (innerHTML) { 5 | const element = document.createElement('div') 6 | element.innerHTML = innerHTML || '' 7 | document.body.appendChild(element) 8 | return element 9 | } 10 | 11 | test('should not inject spans when `element` has no child nodes', function (t) { 12 | t.plan(2) 13 | const element = createElement() 14 | charming(element) 15 | t.equal(element.innerHTML, '') 16 | t.equal(element.getAttribute('aria-label'), null) 17 | }) 18 | 19 | test('should not inject spans when `element` has no child text nodes', function (t) { 20 | t.plan(2) 21 | const innerHTML = '' 22 | const element = createElement(innerHTML) 23 | charming(element) 24 | t.equal(element.innerHTML, innerHTML) 25 | t.equal(element.getAttribute('aria-label'), null) 26 | }) 27 | 28 | test('should inject spans when `element` has a single child text node', function (t) { 29 | t.plan(2) 30 | const element = createElement('foo') 31 | charming(element) 32 | t.equal(element.getAttribute('aria-label'), 'foo') 33 | t.equal( 34 | element.innerHTML, 35 | '' 36 | ) 37 | }) 38 | 39 | test('should inject spans when `element` has multiple child text nodes', function (t) { 40 | t.plan(2) 41 | const element = createElement( 42 | 'foo bar baz' 43 | ) 44 | charming(element) 45 | t.equal(element.getAttribute('aria-label'), null) 46 | t.equal( 47 | element.innerHTML, 48 | '' 49 | ) 50 | }) 51 | 52 | test('should correctly set `aria-label` when `element` contains adjacent child text nodes', function (t) { 53 | t.plan(2) 54 | const element = createElement() 55 | element.appendChild(document.createTextNode('foo')) 56 | element.appendChild(document.createTextNode('bar')) 57 | charming(element) 58 | t.equal(element.getAttribute('aria-label'), 'foobar') 59 | t.equal( 60 | element.innerHTML, 61 | '' 62 | ) 63 | }) 64 | 65 | test('can inject custom tags', function (t) { 66 | t.plan(2) 67 | const element = createElement('foo') 68 | charming(element, { 69 | tagName: 'b' 70 | }) 71 | t.equal(element.getAttribute('aria-label'), 'foo') 72 | t.equal( 73 | element.innerHTML, 74 | '' 75 | ) 76 | }) 77 | 78 | test('can inject spans without classes', function (t) { 79 | t.plan(2) 80 | const element = createElement('foo') 81 | charming(element, { 82 | setClassName: function () { 83 | return null 84 | } 85 | }) 86 | t.equal(element.getAttribute('aria-label'), 'foo') 87 | t.equal( 88 | element.innerHTML, 89 | '' 90 | ) 91 | }) 92 | 93 | test('can inject spans with a custom class', function (t) { 94 | t.plan(2) 95 | const element = createElement('foo') 96 | charming(element, { 97 | setClassName: function (index, letter) { 98 | return `index-${index} letter-${letter}` 99 | } 100 | }) 101 | t.equal(element.getAttribute('aria-label'), 'foo') 102 | t.equal( 103 | element.innerHTML, 104 | '' 105 | ) 106 | }) 107 | 108 | test('supports passing in a function for splitting the `element` contents', function (t) { 109 | t.plan(2) 110 | const element = createElement('foo bar') 111 | charming(element, { 112 | split: function (string) { 113 | return string.split(/(\s+)/) 114 | }, 115 | setClassName: function (index) { 116 | return `word-${index}` 117 | } 118 | }) 119 | t.equal(element.getAttribute('aria-label'), 'foo bar') 120 | t.equal( 121 | element.innerHTML, 122 | '' 123 | ) 124 | }) 125 | --------------------------------------------------------------------------------