react-typewriter
Version:
React components for automatic typing effects.
405 lines (321 loc) • 11.1 kB
JavaScript
import React from 'react';
var babelHelpers = {};
babelHelpers.classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
babelHelpers.createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
babelHelpers.inherits = function (subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
babelHelpers.objectWithoutProperties = function (obj, keys) {
var target = {};
for (var i in obj) {
if (keys.indexOf(i) >= 0) continue;
if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
target[i] = obj[i];
}
return target;
};
babelHelpers.possibleConstructorReturn = function (self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
};
babelHelpers;
// Enclosing scope for local state variables.
var styleComponentSubstring = function () {
var _start = undefined,
_end = undefined,
_styles = undefined,
_index = undefined;
// Will deep clone the component tree, wrapping any text within
// the start/end with a styled span.
function alterComponent(component) {
var _component$props = component.props;
var children = _component$props.children;
var stamp = _component$props.stamp;
var style = _component$props.style;
var cloneProps = undefined;
if (stamp) {
if (_index >= _start && (!_end || _index < _end)) {
cloneProps = {
style: React.addons.update(style || {}, { $merge: _styles })
};
}
_index++;
} else {
cloneProps = { children: React.Children.map(children, alterChild) };
}
if (cloneProps) {
return React.cloneElement(component, cloneProps);
} else {
return component;
}
}
// Alters any text in the child, checking if the text falls within
// the start/end range.
function alterChild(child) {
if (typeof child !== 'string') {
return alterComponent(child);
} else {
var strEnd = child.length + _index;
if (strEnd > _start && (!_end || _index < _end)) {
// compute relative string start and end indexes
var relStartIndex = _start - _index,
relEndIndex = _end ? _end - _index : strEnd;
// generate the substrings
var unstyledTextLeft = child.substring(0, relStartIndex),
styledText = child.substring(relStartIndex, relEndIndex),
unstyledTextRight = child.substring(relEndIndex, strEnd);
var styledSpan = React.createElement(
'span',
{ style: _styles },
styledText
);
child = [unstyledTextLeft, styledSpan, unstyledTextRight];
}
_index = strEnd;
return child;
}
}
/**
* Styles the in any text nodes that are decendants of the component
* if they fall within the specified range. Ranges are relative to
* all the text within the component including text in decendant nodes.
* A specific characters index is calculated as the number of all characters
* indexed before it in an pre-order traversal of the tree minus one.
*
* Example:
* styleComponentSubstring(<p>Hello <a>World</a></p>, {color: 'blue'}, 3, 8);
* >>> <p>Hel<span style="color: blue">lo </span><a><span style="color: blue">Wo</span>rld</a></p>
*
* @param {React Component} component The component to be cloned.
* @param {Object} styles The styles to be applied to the text.
* @param {Number} start The start index.
* @param {Number} end The end index.
* @return {React Component}
*/
return function (component, styles, start, end) {
// reset local state variables
_styles = styles || {};
if (start > end) {
_end = start;
_start = end;
} else {
_start = start || 0;
_end = end;
}
_index = 0;
return alterComponent(component);
};
}();
// returns the character at the components text index position.
var componentTokenAt = function () {
var _index = undefined;
function findComponentTokenAt(component) {
var children = component.props.children;
var childCount = React.Children.count(children);
var token = undefined;
if (childCount <= 1) {
children = [children];
}
var childIndex = 0;
while (!token && childIndex < childCount) {
var child = children[childIndex++];
if (typeof child !== 'string') {
// treat Stamp components as a single token.
if (child.props.stamp) {
if (!_index) {
token = child;
} else {
_index--;
}
} else {
token = findComponentTokenAt(child);
}
} else if (_index - child.length < 0) {
token = child.charAt(_index);
} else {
_index -= child.length;
}
}
return token;
}
/**
* Returns the token/character at the components text index position.
* The index position is the index of a string of all text nodes
* concatinated depth first.
*
* @param {React Component} component Component to search.
* @param {Number} index The index position.
* @return {Char} The token at the index position.
*/
return function (component, index) {
if (index < 0) {
return undefined;
}
_index = index;
return findComponentTokenAt(component);
};
}();
/**
* TypeWriter
*/
var TypeWriter = function (_React$Component) {
babelHelpers.inherits(TypeWriter, _React$Component);
function TypeWriter(props) {
babelHelpers.classCallCheck(this, TypeWriter);
var _this = babelHelpers.possibleConstructorReturn(this, Object.getPrototypeOf(TypeWriter).call(this, props));
_this.state = {
visibleChars: 0
};
_this._handleTimeout = _this._handleTimeout.bind(_this);
return _this;
}
babelHelpers.createClass(TypeWriter, [{
key: 'componentDidMount',
value: function componentDidMount() {
this._timeoutId = setTimeout(this._handleTimeout, 1000);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
clearInterval(this._timeoutId);
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var next = nextProps.typing,
active = this.props.typing;
if (active > 0 && next < 0) {
this.setState({
visibleChars: this.state.visibleChars - 1
});
} else if (active < 0 && next > 0) {
this.setState({
visibleChars: this.state.visibleChars + 1
});
}
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps, nextState) {
return this.state.visibleChars !== nextState.visibleChars;
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps, prevState) {
var _props = this.props;
var maxDelay = _props.maxDelay;
var minDelay = _props.minDelay;
var delayMap = _props.delayMap;
var onTypingEnd = _props.onTypingEnd;
var onTyped = _props.onTyped;
var typing = _props.typing;
var token = componentTokenAt(this, prevState.visibleChars);
var nextToken = componentTokenAt(this, this.state.visibleChars);
if (token && onTyped) {
onTyped(token, prevState.visibleChars);
}
// check the delay map for additional delays at the index.
if (nextToken) {
var timeout = Math.round(Math.random() * (maxDelay - minDelay) + minDelay),
tokenIsString = typeof token === 'string';
if (delayMap) {
for (var i = 0; i < delayMap.length; i++) {
var mapping = delayMap[i];
if (mapping.at === prevState.visibleChars || tokenIsString && token.match(mapping.at)) {
timeout += mapping.delay;
break;
}
}
}
this._timeoutId = setTimeout(this._handleTimeout, timeout);
} else if (onTypingEnd) {
onTypingEnd();
}
}
}, {
key: 'render',
value: function render() {
var _props2 = this.props;
var children = _props2.children;
var fixed = _props2.fixed;
var delayMap = _props2.delayMap;
var typing = _props2.typing;
var maxDelay = _props2.maxDelay;
var minDelay = _props2.minDelay;
var props = babelHelpers.objectWithoutProperties(_props2, ['children', 'fixed', 'delayMap', 'typing', 'maxDelay', 'minDelay']);
var visibleChars = this.state.visibleChars;
var container = React.createElement(
'span',
props,
children
);
var hideStyle = fixed ? { visibility: 'hidden' } : { display: 'none' };
return styleComponentSubstring(container, hideStyle, visibleChars);
}
}, {
key: '_handleTimeout',
value: function _handleTimeout() {
var typing = this.props.typing;
var visibleChars = this.state.visibleChars;
this.setState({
visibleChars: visibleChars + typing
});
}
}]);
return TypeWriter;
}(React.Component);
TypeWriter.propTypes = {
fixed: React.PropTypes.bool,
delayMap: React.PropTypes.arrayOf(React.PropTypes.shape({
at: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(RegExp)]),
delay: React.PropTypes.number
})),
typing: function typing(props, propName) {
var prop = props[propName];
if (!(Number(prop) === prop && prop % 1 === 0) || prop < -1 || prop > 1) {
return new Error('typing property must be an integer between 1 and -1');
}
},
maxDelay: React.PropTypes.number,
minDelay: React.PropTypes.number,
onTypingEnd: React.PropTypes.func,
onTyped: React.PropTypes.func
};
TypeWriter.defaultProps = {
typing: 0,
maxDelay: 100,
minDelay: 20
};
export default TypeWriter;