UNPKG

react-typewriter

Version:
405 lines (321 loc) 11.1 kB
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;