├── .gitignore ├── index.js ├── package.json ├── README.md └── src ├── util.js └── Ghostwriter.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .idea/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Ghostwriter from './src/Ghostwriter'; 2 | 3 | export default Ghostwriter; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-ghostwriter", 3 | "version": "0.0.5", 4 | "description": "A React Native module that types strings on demand. Set up your Ghostwriter with custom options, and watch it do its magic. Inspired by Typed.js.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/LansanaCamara/react-native-ghostwriter" 8 | }, 9 | "keywords": [ 10 | "react-native", 11 | "notifications", 12 | "alerts", 13 | "ghostwriter", 14 | "autotyper", 15 | "type" 16 | ], 17 | "author": { 18 | "name": "Lansana Camara" 19 | }, 20 | "license": "MIT", 21 | "maintainers": [ 22 | { 23 | "name": "lansana", 24 | "email": "lxc5296@gmail.com" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ghostwriter 2 | 3 | A React Native module that types strings on demand. Set up your Ghostwriter with custom options, and watch it do its magic. 4 | 5 | Inspired by [Typed.js](https://github.com/mattboldt/typed.js/) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install react-native-ghostwriter 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import React, { Component } from 'react'; 17 | import { 18 | AppRegistry, 19 | StyleSheet, 20 | View 21 | } from 'react-native'; 22 | import Ghostwriter from 'react-native-ghostwriter'; 23 | 24 | class App extends Component { 25 | render() { 26 | let options = { 27 | sequences: [ 28 | { string: "A B C", duration: 2000 }, 29 | { string: "It's easy as, 1 2 3", duration: 2500 }, 30 | { string: 'As simple as, do re me' } 31 | ] 32 | }; 33 | 34 | return ( 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | const styles = StyleSheet.create({ 43 | container: { 44 | flex: 1, 45 | justifyContent: 'center', 46 | alignItems: 'center', 47 | paddingLeft: 25, 48 | paddingRight: 25 49 | } 50 | }); 51 | 52 | AppRegistry.registerComponent('App', () => App); 53 | ``` 54 | 55 | ## Documentation 56 | 57 | Option | Type | Default | Description 58 | -------|------|---------|------------ 59 | clearEverySequence | Boolean | false | If true, each sequence is cleared after it's specified duration 60 | startDelay | Int | 0 | The time (milliseconds) to wait before the typing starts 61 | stringStyles | Object | null | Add custom styles to your sequences 62 | containerStyles | Object | null | Add custom styles to the container of your sequences 63 | sequenceDuration | Int | 1750 | The time (milliseconds) to wait after each sequence before moving to the next sequence. Overridden by the 'duration' property in sequence. 64 | writeSpeed | Int | 0 | A value that represents the speed of the typing. The lower you go, the faster it types. 65 | showCursor | Int | true | Set to false for no cursor 66 | cursorChar | String | "\|" | The cursor character 67 | cursorSpeed | Int | 0 | The speed (in milliseconds) at which the cursor flashes 68 | onComplete | Function | No operations | A callback function that is called after all sequences have been typed 69 | 70 | ## Contributing 71 | 72 | Feel free to contribute by forking, opening issues, pull requests etc. 73 | 74 | All pull requests should be done on the 'dev' branch. 75 | 76 | ## License 77 | 78 | The MIT License (MIT) 79 | 80 | Copyright (c) 2016 Lansana Camara 81 | 82 | 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: 83 | 84 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 85 | 86 | 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. -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An object of helper functions 3 | * 4 | * @type {{}} 5 | */ 6 | let util = { 7 | humanSpeed: humanSpeed, 8 | extend: extend, 9 | arrayEach: arrayEach, 10 | has: has, 11 | isElement: isElement, 12 | isUndefined: isUndefined, 13 | isBoolean, isBoolean, 14 | isNumber: isNumber, 15 | isString: isString, 16 | isObject: isObject, 17 | isFunction: isFunction, 18 | isArray: isArray 19 | }; 20 | 21 | /** Object#toString result shortcuts **/ 22 | const objectTag = '[object Object]'; 23 | const arrayTag = '[object Array]'; 24 | const stringTag = '[object String]'; 25 | const functionTag = '[object Function]'; 26 | const numberTag = '[object Number]'; 27 | const booleanTag = '[object Boolean]'; 28 | 29 | /** Used for native method references */ 30 | const objectProto = Object.prototype; 31 | 32 | /** Used to resolve the internal [[Class]] of values */ 33 | const objectToStr = objectProto.toString; 34 | 35 | /** Native method shortcuts */ 36 | const hasOwnProperty = objectProto.hasOwnProperty; 37 | 38 | /** 39 | * Human speed typing. 40 | * 41 | * @param speed 42 | * @returns {*} 43 | * @private 44 | */ 45 | function humanSpeed(speed) { 46 | return Math.round(Math.random() * (100 - 30)) + speed; 47 | } 48 | 49 | /** 50 | * Merge two objects. 51 | * 52 | * Only set the value if 'source' already has it (no custom 53 | * props to prevent private props from being overridden). 54 | * 55 | * @param source 56 | * @param options 57 | * @returns {*} 58 | * @private 59 | */ 60 | function extend(source, options) { 61 | let key; 62 | 63 | for (key in options) { 64 | if (has(source, key) && has(options, key)) { 65 | source[key] = options[key]; 66 | } 67 | } 68 | 69 | return source; 70 | } 71 | 72 | /** 73 | * Loop through array and use callback on each item 74 | * 75 | * @param arr 76 | * @param iterator 77 | */ 78 | function arrayEach(arr, iterator) { 79 | let index = -1, 80 | length = arr.length; 81 | 82 | while (++index < length) { 83 | iterator(arr[index], index, arr); 84 | } 85 | } 86 | 87 | /** 88 | * Check if object has key as own property. 89 | * 90 | * @param obj 91 | * @param key 92 | * @returns {boolean} 93 | */ 94 | function has(obj, key) { 95 | return obj ? hasOwnProperty.call(obj, key) : false; 96 | } 97 | 98 | /** 99 | * Check if argument is a DOM element. 100 | * 101 | * @param val 102 | * @returns {*|boolean} 103 | */ 104 | function isElement(val) { 105 | return val && val.nodeType === 1; 106 | } 107 | 108 | /** 109 | * Check if argument is undefined. 110 | * 111 | * @param val 112 | * @returns {boolean} 113 | */ 114 | function isUndefined(val) { 115 | return typeof val === 'undefined'; 116 | } 117 | 118 | /** 119 | * Check if argument is a boolean. 120 | * 121 | * @param val 122 | * @returns {boolean|*} 123 | */ 124 | function isBoolean(val) { 125 | return val === true || val === false || val && typeof val === 'object' && objectToStr.call(val) === booleanTag; 126 | } 127 | 128 | /** 129 | * Check if argument is a number. 130 | * 131 | * @param num 132 | * @returns {*|boolean} 133 | */ 134 | function isNumber(num) { 135 | return num && typeof num === 'number' && objectToStr.call(num) === numberTag; 136 | } 137 | 138 | /** 139 | * Check if argument is a string. 140 | * 141 | * @param str 142 | * @returns {*|boolean} 143 | */ 144 | function isString(str) { 145 | return str && typeof str === 'string' && objectToStr.call(str) === stringTag; 146 | } 147 | 148 | /** 149 | * Check if argument is an object. 150 | * 151 | * In JavaScript, an array is an object, but we only want to check for objects 152 | * defined with {}, not [], so we explicitly check for that using objToStr. 153 | * 154 | * @param obj 155 | * @returns {boolean} 156 | */ 157 | function isObject(obj) { 158 | return obj && obj === Object(obj) && objectToStr.call(obj) === objectTag; 159 | } 160 | 161 | /** 162 | * Check if argument is a function. 163 | * 164 | * @param fn 165 | */ 166 | function isFunction(fn) { 167 | return fn && typeof fn === 'function' && objectToStr.call(fn) === functionTag; 168 | } 169 | 170 | /** 171 | * Check if argument is an array. 172 | * 173 | * @param arr 174 | * @returns {*|boolean} 175 | */ 176 | function isArray(arr) { 177 | if (isUndefined(Array.isArray)) { 178 | return arr && objectToStr.call(arr) === arrayTag; 179 | } 180 | 181 | return Array.isArray(arr); 182 | } 183 | 184 | export default util; -------------------------------------------------------------------------------- /src/Ghostwriter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | StyleSheet, 4 | ScrollView, 5 | View, 6 | Text 7 | } from 'react-native'; 8 | import util from './util'; 9 | 10 | let typeTimeout = null; 11 | let cursorInterval = null; 12 | 13 | class Ghostwriter extends Component { 14 | 15 | /** 16 | * Class constructor 17 | * 18 | * @param props 19 | */ 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | startDelay: 0, 25 | string: '', 26 | stringStyles: null, 27 | containerStyles: null, 28 | clearEverySequence: false, 29 | sequences: [], 30 | sequenceDuration: 1750, 31 | writeSpeed: 0, 32 | showCursor: true, 33 | cursorChar: '|', 34 | cursorSpeed: 400, 35 | cursorIndex: 0, 36 | writing: false, 37 | onComplete: () => {} 38 | }; 39 | } 40 | 41 | /** 42 | * Component mounted. 43 | */ 44 | componentDidMount() { 45 | this.setState(util.extend(this.state, this.props.options)); 46 | 47 | this.ghostwriterTimeout = setTimeout(() => { 48 | this.initGhostwriter(); 49 | 50 | if (this.state.showCursor) { 51 | cursorInterval = setInterval(() => { 52 | this.flashCursor(); 53 | }, this.state.cursorSpeed); 54 | } 55 | }, this.state.startDelay); 56 | } 57 | 58 | /** 59 | * Component preparing to unmount. 60 | */ 61 | componentWillUnmount() { 62 | clearTimeout(this.ghostwriterTimeout) 63 | clearTimeout(this.beforeNextSequenceTimeout) 64 | clearTimeout(typeTimeout); 65 | clearInterval(cursorInterval); 66 | } 67 | 68 | /** 69 | * Render the component. 70 | * 71 | * @returns {XML} 72 | */ 73 | render() { 74 | return ( 75 | 76 | 77 | {this.state.string} {this.cursorIsDiplayed() && {this.state.cursorChar} } 78 | 79 | 80 | ); 81 | } 82 | 83 | /** 84 | * The styles for the scroll view. 85 | * 86 | * @returns {*} 87 | */ 88 | containerStyles() { 89 | return this.state.containerStyles ? this.state.containerStyles : styles.container; 90 | } 91 | 92 | /** 93 | * The styles for our string. 94 | * 95 | * @returns {*} 96 | */ 97 | stringStyles() { 98 | return this.state.stringStyles ? this.state.stringStyles : styles.string; 99 | } 100 | 101 | /** 102 | * The state of the cursor (displayed or not) 103 | * 104 | * @returns {boolean} 105 | */ 106 | cursorIsDiplayed() { 107 | return this.state.cursorIndex % 2 ? true : false; 108 | } 109 | 110 | /** 111 | * Flash cursor functionality (increase index). 112 | */ 113 | flashCursor() { 114 | this.setState({ 115 | cursorIndex: this.state.cursorIndex + 1 116 | }); 117 | } 118 | 119 | /** 120 | * Start typing the sequences provided by user. 121 | */ 122 | initGhostwriter() { 123 | let sequences = this.getSequences(), 124 | seqId = 0, 125 | charId = 0; 126 | 127 | this.write(sequences, seqId, charId, util.humanSpeed(this.state.writeSpeed)); 128 | } 129 | 130 | /** 131 | * More forward in the sequence. Adds one the next letter, or moves to the next sentence. 132 | * 133 | * @param sequences 134 | * @param seqId 135 | * @param charId 136 | * @param speed 137 | */ 138 | write(sequences, seqId, charId, speed) { 139 | // Clear typeTimeout at beginning of all ticks to clear any previous ticks. 140 | clearTimeout(typeTimeout); 141 | 142 | let finished = seqId === sequences.length; 143 | 144 | // All sequences complete 145 | if (finished) { 146 | return this.state.onComplete(); 147 | } 148 | 149 | typeTimeout = setTimeout(() => { 150 | let char = sequences[seqId].string[charId]; 151 | 152 | // There are still chars in this sequence 153 | if (!util.isUndefined(char)) { 154 | // Move to next char 155 | charId++; 156 | 157 | // Add new letter to string 158 | this.setState({ 159 | string: this.state.string + char, 160 | writing: true 161 | }); 162 | 163 | this.write(sequences, seqId, charId, util.humanSpeed(this.state.writeSpeed)); 164 | } else { 165 | // Call the callback function of the sequence 166 | this.callback(sequences, seqId); 167 | 168 | let duration; 169 | 170 | // Get the duration for the next sequence. Use custom duration 171 | // if provided by user, else use default. 172 | if (util.has(sequences[seqId], 'duration')) { 173 | duration = sequences[seqId].duration; 174 | } else { 175 | duration = this.state.sequenceDuration; 176 | } 177 | 178 | // If this is not the last sequence in the list of sequences... 179 | if (seqId !== sequences.length - 1) { 180 | this.beforeNextSequence(duration - 100); 181 | } 182 | 183 | this.nextSequence(sequences, seqId, charId, duration); 184 | } 185 | }, speed); 186 | } 187 | 188 | /** 189 | * Prepare the string for the next sequence (e.g., add a space, or clear the string) 190 | * 191 | * @param duration 192 | */ 193 | beforeNextSequence(duration) { 194 | this.setState({ 195 | writing: false 196 | }); 197 | 198 | this.beforeNextSequenceTimeout = setTimeout(() => { 199 | this.setState({ 200 | string: this.state.clearEverySequence ? '' : this.state.string + ' ' 201 | }); 202 | }, duration); 203 | } 204 | 205 | /** 206 | * Jump to the next sequence and start at the first char. 207 | * 208 | * @param sequences 209 | * @param seqId 210 | * @param charId 211 | * @param duration 212 | */ 213 | nextSequence(sequences, seqId, charId, duration) { 214 | // Move to next sequence 215 | seqId++; 216 | 217 | // Reset char index to start on first char of next sequence 218 | charId = 0; 219 | 220 | this.write(sequences, seqId, charId, duration); 221 | } 222 | 223 | /** 224 | * Get an array of sequences, each having their string split into an array of chars. 225 | * 226 | * @returns {Array} 227 | * @private 228 | */ 229 | getSequences() { 230 | let sequences = this.state.sequences; 231 | 232 | util.arrayEach(sequences, (sequence, i) => { 233 | if (util.has(sequence, 'string')) { 234 | sequences[i].string = sequence.string.split(''); 235 | } else { 236 | throw new Error("Your sequences must all contain a 'string' property."); 237 | } 238 | }); 239 | 240 | return sequences; 241 | } 242 | 243 | /** 244 | * Call the callback function of a specific sequence. 245 | * 246 | * @param sequences 247 | * @param seqId 248 | */ 249 | callback(sequences, seqId) { 250 | if (util.has(sequences[seqId], 'callback')) { 251 | if (util.isFunction(sequences[seqId].callback)) { 252 | sequences[seqId].callback(); 253 | } else { 254 | throw new Error(`The callback for sequence #${seqId} must be a function.`); 255 | } 256 | } 257 | } 258 | 259 | } 260 | 261 | /** 262 | * Styles 263 | */ 264 | const styles = StyleSheet.create({ 265 | container: {}, 266 | string: { 267 | fontSize: 18, 268 | fontWeight: "300" 269 | } 270 | }); 271 | 272 | export default Ghostwriter 273 | --------------------------------------------------------------------------------