├── .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 [](https://www.npmjs.org/package/charming) [](https://travis-ci.org/yuanqing/charming) [](https://coveralls.io/r/yuanqing/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 | f
32 | o
33 | o
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 | 'foo'
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 | 'foo bar baz'
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 | 'foobar'
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 | 'foo'
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 | 'foo'
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 | 'foo'
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 | 'foo bar'
123 | )
124 | })
125 |
--------------------------------------------------------------------------------