├── .gitignore ├── package.json ├── LICENSE ├── typical.js ├── test └── index.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@camwiegert/typical", 3 | "version": "0.1.1", 4 | "description": "Animated typing in ~400 bytes", 5 | "scripts": { 6 | "test": "node -r esm test" 7 | }, 8 | "main": "typical.js", 9 | "author": "Cam Wiegert ", 10 | "homepage": "https://github.com/camwiegert/typical", 11 | "repository": "https://github.com/camwiegert/typical", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "esm": "^3.2.25", 15 | "tape": "^4.11.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cam Wiegert 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 | -------------------------------------------------------------------------------- /typical.js: -------------------------------------------------------------------------------- 1 | export async function type(node, ...args) { 2 | for (const arg of args) { 3 | switch (typeof arg) { 4 | case 'string': 5 | await edit(node, arg); 6 | break; 7 | case 'number': 8 | await wait(arg); 9 | break; 10 | case 'function': 11 | await arg(node, ...args); 12 | break; 13 | default: 14 | await arg; 15 | } 16 | } 17 | } 18 | 19 | async function edit(node, text) { 20 | const overlap = getOverlap(node.textContent, text); 21 | await perform(node, [...deleter(node.textContent, overlap), ...writer(text, overlap)]); 22 | } 23 | 24 | async function wait(ms) { 25 | await new Promise(resolve => setTimeout(resolve, ms)); 26 | } 27 | 28 | async function perform(node, edits, speed = 60) { 29 | for (const op of editor(edits)) { 30 | op(node); 31 | await wait(speed + speed * (Math.random() - 0.5)); 32 | } 33 | } 34 | 35 | export function* editor(edits) { 36 | for (const edit of edits) { 37 | yield (node) => requestAnimationFrame(() => node.textContent = edit); 38 | } 39 | } 40 | 41 | export function* writer([...text], startIndex = 0, endIndex = text.length) { 42 | while (startIndex < endIndex) { 43 | yield text.slice(0, ++startIndex).join(''); 44 | } 45 | } 46 | 47 | export function* deleter([...text], startIndex = 0, endIndex = text.length) { 48 | while (endIndex > startIndex) { 49 | yield text.slice(0, --endIndex).join(''); 50 | } 51 | } 52 | 53 | export function getOverlap(start, [...end]) { 54 | return [...start, NaN].findIndex((char, i) => end[i] !== char); 55 | } 56 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | 3 | import { 4 | deleter, 5 | editor, 6 | getOverlap, 7 | writer 8 | } from '../typical.js'; 9 | 10 | test('deleter', t => { 11 | t.ok( 12 | deleter('text')[Symbol.iterator], 13 | 'Should create an iterable' 14 | ); 15 | 16 | t.deepEqual( 17 | [...deleter('text')], 18 | ['tex', 'te', 't', ''], 19 | 'Should create correct steps' 20 | ); 21 | 22 | t.deepEqual( 23 | [...deleter('')], 24 | [], 25 | 'Should handle empty string' 26 | ); 27 | 28 | t.deepEqual( 29 | [...deleter('text', 2)], 30 | ['tex', 'te'], 31 | 'Should handle startIndex' 32 | ); 33 | 34 | t.deepEqual( 35 | [...deleter('text', 0, 2)], 36 | ['t', ''], 37 | 'Should handle endIndex' 38 | ); 39 | 40 | t.deepEqual( 41 | [...deleter('🍕')], 42 | [''], 43 | 'Should handle emoji' 44 | ); 45 | 46 | t.end(); 47 | }); 48 | 49 | test('editor', t => { 50 | t.ok( 51 | editor(deleter('text'))[Symbol.iterator], 52 | 'Should create an iterable' 53 | ); 54 | 55 | t.equal( 56 | [...editor(writer('text'))].length, 4, 57 | 'Should have correct length' 58 | ); 59 | 60 | t.equal( 61 | typeof editor(deleter('text')).next().value, 62 | 'function', 63 | 'Should yield functions' 64 | ); 65 | 66 | t.end(); 67 | }); 68 | 69 | test('getOverlap', t => { 70 | t.equal( 71 | getOverlap('some text', 'some other text'), 5, 72 | 'Should handle partial overlap' 73 | ); 74 | 75 | t.equal( 76 | getOverlap('some text', 'other text'), 0, 77 | 'Should handle no overlap' 78 | ); 79 | 80 | t.equal( 81 | getOverlap('some text', 'some text'), 9, 82 | 'Should handle complete overlap' 83 | ); 84 | 85 | t.equal( 86 | getOverlap('some text', 'some text and'), 9, 87 | 'Should handle write only' 88 | ); 89 | 90 | t.equal( 91 | getOverlap('some text', 'some'), 4, 92 | 'Should handle delete only' 93 | ); 94 | 95 | t.equal( 96 | getOverlap('emoji 🐡', 'emoji 🐡 blowfish'), 7, 97 | 'Should handle emoji' 98 | ); 99 | 100 | t.end(); 101 | }); 102 | 103 | test('writer', t => { 104 | t.ok( 105 | writer('text')[Symbol.iterator], 106 | 'Should create an iterable' 107 | ); 108 | 109 | t.deepEqual( 110 | [...writer('text')], 111 | ['t', 'te', 'tex', 'text'], 112 | 'Should create correct steps' 113 | ); 114 | 115 | t.deepEqual( 116 | [...writer('')], 117 | [], 118 | 'Should handle empty string' 119 | ); 120 | 121 | t.deepEqual( 122 | [...writer('text', 2)], 123 | ['tex', 'text'], 124 | 'Should handle startIndex' 125 | ); 126 | 127 | t.deepEqual( 128 | [...writer('text', 0, 2)], 129 | ['t', 'te'], 130 | 'Should handle endIndex' 131 | ); 132 | 133 | t.deepEqual( 134 | [...writer('📚')], 135 | ['📚'], 136 | 'Should handle emoji' 137 | ); 138 | 139 | t.end(); 140 | }); 141 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # typical 2 | 3 | > Animated typing in ~400 bytes :blowfish: of JavaScript. 4 | 5 | - **Zero dependencies** 6 | - **MIT licensed** [→](https://github.com/camwiegert/typical/tree/master/LICENSE) 7 | - **Emoji support** 8 | - **Smart delete:** only delete what needs deleting 9 | - **Pausing:** pause between steps 10 | - **Looping:** easily loop from any point 11 | - **Waiting:** wait on arbitrary Promises 12 | - **Humanity:** slightly varied typing speed 13 | 14 | [**Demo →**](https://codepen.io/camwiegert/pen/rNNepYo) 15 | 16 | [![](https://repository-images.githubusercontent.com/211405607/1dd6e300-f8b2-11e9-8260-26ad1d49db17)](https://codepen.io/camwiegert/pen/rNNepYo "Demo on CodePen") 17 | 18 | --- 19 | 20 | ## Install 21 | 22 | ```shell 23 | npm install @camwiegert/typical 24 | ``` 25 | 26 |
27 | More install options 28 |

Instead of using a package manager, you can download typical.js from GitHub and import it locally or import it directly from a CDN like unpkg.

29 |
30 | 31 | ## API 32 | 33 | ```typescript 34 | type(target: HTMLElement, ...steps: any[]) => Promise; 35 | ``` 36 | 37 | The module exports a single function, `type`, which takes a target element as its first argument, and any number of additional arguments as the steps to perform. Additional arguments perform actions based on their type: 38 | 39 | | Type | Action | 40 | |:-----------|:-------------------------| 41 | | `string` | Type text | 42 | | `number` | Pause (milliseconds) | 43 | | `function` | Call with target element | 44 | | `Promise` | Wait for resolution | 45 | 46 | ## Usage 47 | 48 | The most basic usage of `type` is providing a target element and a string to type. 49 | 50 | ```javascript 51 | import { type } from '@camwiegert/typical'; 52 | 53 | type(element, 'text'); 54 | ``` 55 | 56 | ### Pausing 57 | 58 | In order to pause typing at any point, pass a number of milliseconds to pause. 59 | 60 | ```javascript 61 | type(element, 'Hello', 1000, 'Hello world!'); 62 | ``` 63 | 64 | ### Looping 65 | 66 | In order to loop, pass `type` as a parameter to itself at the point at which you'd like to start looping. It can be helpful to alias `type` as `loop` to be explicit. 67 | 68 | ```javascript 69 | import { 70 | type, 71 | type as loop 72 | }; 73 | 74 | const steps = [1000, 'Ready', 1000, 'Set', 1000, 'Go']; 75 | 76 | type(element, ...steps, loop); 77 | ``` 78 | 79 | To loop a finite amount, pass your steps multiple times. 80 | 81 | ```javascript 82 | type(element, ...steps, ...steps, ...steps); 83 | ``` 84 | 85 | ### Waiting 86 | 87 | When passed a `Promise`, `type` will wait for it to resolve before continuing. Because `type` itself returns a `Promise`, that means you can wait on a set of steps to complete before starting another. 88 | 89 | ```javascript 90 | const init = type(target, 'In a moment...', 500); 91 | 92 | type(target, init, 'start', 500, 'looping', loop); 93 | ``` 94 | 95 | ### Functions 96 | 97 | Function arguments are passed the target element, and can be useful for operating on the target element between steps. If you return a `Promise`, `type` will wait for it to resolve. 98 | 99 | ```javascript 100 | const toggle = (element) => 101 | element.classList.toggle('is-typing'); 102 | 103 | type(target, toggle, 'Type me', toggle); 104 | ``` 105 | 106 | ## Support 107 | 108 | - [x] Chrome 109 | - [x] Edge 110 | - [x] Firefox 111 | - [x] Safari 112 | - [ ] Internet Explorer 113 | 114 | ## Related 115 | 116 | - [react-typical](https://github.com/catalinmiron/react-typical) - React component 117 | - [vue-typical](https://github.com/Turkyden/vue-typical) - Vue component 118 | --------------------------------------------------------------------------------