├── .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 |
--------------------------------------------------------------------------------