UNPKG

windups

Version:

A unique typewriter effect library for React.

957 lines (797 loc) 24.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = require('react'); var breakLines = _interopDefault(require('break-styled-lines')); function isPlayedWindup(windup) { return windup.length === 2; } function memberIsWindup(member) { // If it's not an array it can't be a windup if (!Array.isArray(member)) { return false; } // If it has less or more than three members it's not a windup if (member.length !== 3) { return false; } // If its first or second members are not arrays it's not a windup if (!Array.isArray(member[0]) || !Array.isArray(member[1])) { return false; } // Past here we just have to hope ElementType isn't a windup. return true; } function windupFromString(str, metadata) { return [[], str.split(""), metadata]; } function newWindup(arg, metadata) { return [[], arg, metadata]; } function isUnplayed(windup) { var played = windup[0], remaining = windup[1]; if (played.length > 0) { return false; } return remaining.reduce(function (unplayed, member) { if (memberIsWindup(member)) if (memberIsWindup(windup) && unplayed) { return isUnplayed(member); } return unplayed; }, true); } function isFinished(_ref) { var remaining = _ref[1]; return remaining.length === 0; } function fastForward(windup) { var forwardedWindup = next(windup); if (isFinished(forwardedWindup)) { return forwardedWindup; } return fastForward(forwardedWindup); } function rewind(windup) { if (isUnplayed(windup)) { return windup; } var played = windup[0], remaining = windup[1], metadata = windup[2]; var mapRewind = function mapRewind(member) { if (memberIsWindup(member)) { return rewind(member); } return member; }; return [[], [].concat(played.map(mapRewind), remaining.map(mapRewind)), metadata]; } function lastPlayedMember(_ref2) { var played = _ref2[0], remaining = _ref2[1]; var playedFromRemaining = remaining.reduce(function (playedEl, member) { if (memberIsWindup(member)) { if (!isUnplayed(member)) { return lastPlayedElement(member); } } return playedEl; }, undefined); if (playedFromRemaining) { return playedFromRemaining; } var last = played[played.length - 1]; return last; } function lastPlayedElement(windup) { var lastPlayed = lastPlayedMember(windup); if (memberIsWindup(lastPlayed)) { return lastPlayedElement(lastPlayed); } return lastPlayed; } function playedElements(_ref3) { var played = _ref3[0], remaining = _ref3[1]; var playedTransformed = played.map(function (member) { if (memberIsWindup(member)) { var metadata = member[2]; return [playedElements(member), metadata]; } return member; }); var firstRemaning = remaining[0]; if (memberIsWindup(firstRemaning) && !isUnplayed(firstRemaning)) { var metadata = firstRemaning[2]; return [].concat(playedTransformed, [[playedElements(firstRemaning), metadata]]); } return playedTransformed; } function nextElement(_ref4) { var remaining = _ref4[1]; var nextVal = remaining[0]; if (memberIsWindup(nextVal)) { return nextElement(nextVal); } return nextVal; } function next(windup) { // start // [[], [[[], ["h", "i"]], [[], ["n", "o"]]]] // next // [[], [[["h"], ["i"]], ["", ["n", "o"]]]] // next // [["h", "i"], [["n", "o"]]] // next // [["h", "i"] ["n", "o"]], []] if (isFinished(windup)) { return windup; } var played = windup[0], remaining = windup[1], metadata = windup[2]; var firstRemaining = remaining[0], restRemaining = remaining.slice(1); if (memberIsWindup(firstRemaining)) { var nextFirstRemaining = next(firstRemaining); if (isFinished(nextFirstRemaining)) { return [[].concat(played, [nextFirstRemaining]), restRemaining, metadata]; } return [played, [nextFirstRemaining].concat(restRemaining), metadata]; } return [[].concat(played, [firstRemaining]), restRemaining, metadata]; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function defaultGetPace(lastChar, nextChar) { switch (lastChar) { case "—": case "…": return 200; case ".": case ",": return 150; case "?": case "!": if (nextChar !== "!" && nextChar !== "?") { return 150; } return 20; case "-": case " ": case "\n": return 0; default: return 20; } } var Pace = function Pace(_ref) { var children = _ref.children; return React.createElement(React.Fragment, null, children); }; function isPaceElement(element) { return element.type === Pace; } function isMsProp(props) { if ("ms" in props) { return true; } return false; } function paceFromWindup(windup, parentPace) { if (isFinished(windup)) { return undefined; } var remaining = windup[1], metadata = windup[2]; var firstRemaining = remaining[0]; if (firstRemaining && memberIsWindup(firstRemaining)) { return paceFromWindup(firstRemaining, metadata.pace || parentPace); } return metadata.pace || parentPace; } var OnChar = function OnChar(_ref) { var children = _ref.children; return React.createElement(React.Fragment, null, children); }; function isOnCharElement(element) { return element.type === OnChar; } // Should return onChars from // every just played member that is a windup // any first remaining that is played function onCharsFromWindup(windup) { var remaining = windup[1], metadata = windup[2]; var lastPlayed = lastPlayedMember(windup); var firstRemaining = remaining[0]; var onChars = []; if (lastPlayed && memberIsWindup(lastPlayed)) { onChars.push.apply(onChars, onCharsFromWindup(lastPlayed)); } if (firstRemaining && memberIsWindup(firstRemaining) && !isUnplayed(firstRemaining)) { onChars.push.apply(onChars, onCharsFromWindup(firstRemaining)); } if (metadata.onChar) { onChars.push(metadata.onChar); } return onChars; } function initWindupState(windup) { return { windup: windup, didFinishOnce: false }; } function windupReducer(state, action) { switch (action.type) { case "replace": return initWindupState(action.windup); case "next": return _extends({}, state, { windup: next(state.windup) }); case "rewind": return { windup: rewind(state.windup), didFinishOnce: false }; case "fast-forward": return _extends({}, state, { windup: fastForward(state.windup) }); case "finish": return _extends({}, state, { didFinishOnce: true }); default: return state; } } function useWindup(windupInit, options) { var _React$useReducer = React.useReducer(windupReducer, windupInit, initWindupState), _React$useReducer$ = _React$useReducer[0], windup = _React$useReducer$.windup, didFinishOnce = _React$useReducer$.didFinishOnce, dispatch = _React$useReducer[1]; var timeoutRef = React.useRef(null); var windupIsFinished = isFinished(windup); var nextCharAtRef = React.useRef(null); var pauseDelayRemainingRef = React.useRef(null); var isPausedRef = React.useRef(false); var skip = React.useCallback(function () { if (!windupIsFinished) { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } dispatch({ type: "fast-forward" }); } }, [windupIsFinished]); var pause = React.useCallback(function () { if (isPausedRef.current === true) { return; } isPausedRef.current = true; if (timeoutRef.current) { var _nextCharAtRef$curren; clearTimeout(timeoutRef.current); pauseDelayRemainingRef.current = Math.max(0, ((_nextCharAtRef$curren = nextCharAtRef.current) != null ? _nextCharAtRef$curren : 0) - Date.now()); } }, []); var resume = React.useCallback(function () { if (isPausedRef.current !== true) { return; } if (!windupIsFinished) { var _pauseDelayRemainingR; setTimeout(function () { isPausedRef.current = false; dispatch({ type: "next" }); }, (_pauseDelayRemainingR = pauseDelayRemainingRef.current) != null ? _pauseDelayRemainingR : 0); } }, [windupIsFinished]); var rewind = React.useCallback(function () { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } dispatch({ type: "rewind" }); }, []); // If windup arg changes, we should reset React.useEffect(function () { dispatch({ type: "replace", windup: windupInit }); }, [windupInit]); // If skipped is changes to true, we should skip // And if it's changed to false, we should restart React.useEffect(function () { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (options.skipped) { dispatch({ type: "fast-forward" }); } if (options.skipped === false) { dispatch({ type: "rewind" }); } }, [options.skipped]); // When the windup changes, onChar should fire React.useEffect(function () { var onChars = onCharsFromWindup(windup); var lastEl = lastPlayedElement(windup); if (onChars.length > 0 && lastEl) { onChars.forEach(function (onChar) { onChar(lastEl); }); } }, [windup]); // If windup finishes, the onFinished should fire React.useEffect(function () { // Put this in a new context so that the windup finishes visually before firing this if (didFinishOnce === false && windupIsFinished) { var timeout = setTimeout(function () { if (options.onFinished) { options.onFinished(); } dispatch({ type: "finish" }); }, 0); return function () { clearTimeout(timeout); }; } }, [didFinishOnce, windupIsFinished, options]); // the windup effect itself React.useEffect(function () { if (!windupIsFinished) { var getPace = paceFromWindup(windup) || defaultGetPace; var lastEl = lastPlayedElement(windup); var nextEl = nextElement(windup); var pace = lastEl ? getPace(lastEl, nextEl) : 0; nextCharAtRef.current = Date.now() + pace; if (isPausedRef.current !== true) { timeoutRef.current = setTimeout(function () { dispatch({ type: "next" }); }, pace); } return function () { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; } }, [windup, windupIsFinished]); return { windup: windup, skip: skip, pause: pause, resume: resume, rewind: rewind, isFinished: windupIsFinished }; } function renderStringWindup(windup) { var played = isPlayedWindup(windup) ? windup[0] : playedElements(windup); var inner = played.reduce(function (acc, playedEl) { if (typeof playedEl === "string") { return acc + playedEl; } return acc + renderStringWindup(playedEl); }, ""); return inner; } var VOID_TAGS = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]; function renderChildrenWindup(windup) { var metadata = isPlayedWindup(windup) ? windup[1] : windup[2]; var played = isPlayedWindup(windup) ? windup[0] : playedElements(windup); var Outer = metadata.element || React.Fragment; if (metadata.props && Object.keys(metadata.props).includes("children")) { return React.createElement(Outer, Object.assign({ key: metadata.key }, metadata.props)); } var inner = played.reduce(function (acc, playedEl) { if (typeof playedEl === "string") { var accButLast = acc.slice(0, acc.length - 1); var last = acc[acc.length - 1]; return last && typeof last === "string" ? [].concat(accButLast, [last + playedEl]) : [].concat(acc, [playedEl]); } return [].concat(acc, [renderChildrenWindup(playedEl)]); }, []); if (typeof metadata.element === "string" && VOID_TAGS.includes(metadata.element)) { return React.createElement(Outer, Object.assign({ key: metadata.key }, metadata.props)); } return React.createElement(Outer, Object.assign({ key: metadata.key }, metadata.props), inner); } function useWindupString(text, options) { if (options === void 0) { options = {}; } var windupInit = React.useMemo(function () { return windupFromString(text, options); // We can omit options as this is used as an initialisation value and options will not change it // eslint-disable-next-line react-hooks/exhaustive-deps }, [text]); var _useWindup = useWindup(windupInit, options), windup = _useWindup.windup, skip = _useWindup.skip, pause = _useWindup.pause, resume = _useWindup.resume, rewind = _useWindup.rewind, isFinished = _useWindup.isFinished; var rendered = renderStringWindup(windup); React.useDebugValue(rendered); return [rendered, { skip: skip, pause: pause, resume: resume, rewind: rewind, isFinished: isFinished }]; } var Pause = function Pause() { return null; }; function isPauseElement(element) { return element.type === Pause; } var WindupContext = /*#__PURE__*/React.createContext({ skip: function skip() { console.warn("Tried to use the useSkip hook outside of a WindupChildren component!!"); }, pause: function pause() { console.warn("Tried to use the usePause hook outside of a WindupChildren component!!"); }, resume: function resume() { console.warn("Tried to use the useResume hook outside of a WindupChildren component!!"); }, rewind: function rewind() { console.warn("Tried to use the useRewind hook outside of a WindupChildren component!"); }, isFinished: false }); function useSkip() { var _React$useContext = React.useContext(WindupContext), skip = _React$useContext.skip; return skip; } function useRewind() { var _React$useContext4 = React.useContext(WindupContext), rewind = _React$useContext4.rewind; return rewind; } function useIsFinished() { var _React$useContext5 = React.useContext(WindupContext), isFinished = _React$useContext5.isFinished; return isFinished; } function reduceWindupArgs(prevArgs, children) { if (typeof children === "string") { return [].concat(prevArgs, children.split("")); } if (typeof children === "number") { return [].concat(prevArgs, children.toString().split("")); } if (!React.isValidElement(children)) { return prevArgs; } var _children$props = children.props, childrenChildren = _children$props.children, restProps = _objectWithoutPropertiesLoose(_children$props, ["children"]); var paceMetaData = isPaceElement(children) ? { pace: function pace(_char) { if (isMsProp(children.props)) { return children.props.ms; } return children.props.getPace(_char); } } : {}; var onCharMetaData = isOnCharElement(children) ? { onChar: children.props.fn } : {}; var keyProp = children.key ? { key: children.key } : {}; if (isPauseElement(children)) { return [].concat(prevArgs, [windupFromString(" ", _extends({ element: React.Fragment }, keyProp, { props: { children: undefined }, pace: function pace() { return children.props.ms; } }))]); } if (childrenChildren === undefined) { return [].concat(prevArgs, [windupFromString(" ", _extends({ element: children.type, props: _extends({}, restProps, { children: undefined }) }, keyProp, paceMetaData, onCharMetaData))]); } if (typeof childrenChildren === "string") { return [].concat(prevArgs, [windupFromString(childrenChildren, _extends({ element: children.type, props: restProps }, keyProp, paceMetaData, onCharMetaData))]); } if (childrenChildren instanceof Function) { return [].concat(prevArgs, [windupFromString(" ", _extends({ element: children.type, props: _extends({ children: childrenChildren }, restProps) }, keyProp, paceMetaData, onCharMetaData))]); } var newArgs = React.Children.toArray(childrenChildren).reduce(reduceWindupArgs, []); var argsWithMetadata = newArgs.map(function (member) { if (memberIsWindup(member)) { var played = member[0], remaining = member[1], metadata = member[2]; return [played, remaining, _extends({}, paceMetaData, onCharMetaData, metadata)]; } return member; }); return [].concat(prevArgs, [newWindup(argsWithMetadata, _extends({ element: children.type, props: restProps }, keyProp, paceMetaData, onCharMetaData))]); } function buildKeyString(children) { if (children === null || children === undefined) { return ""; } var keys = React.Children.map(children, function (child) { if (typeof child === "string") { return child; } if (typeof child === "number") { return child.toString(); } if (React.isValidElement(child)) { return "#" + (child.key || "") + "<" + buildKeyString(child.props.children) + ">"; } return ""; }); if (!keys) { return ""; } return keys.join(","); } function useChildrenMemo(factory, children) { // Omitting children in favour of using a key instead // eslint-disable-next-line react-hooks/exhaustive-deps var memoChildren = React.useMemo(factory, [buildKeyString(children)]); return memoChildren; } var WindupChildren = function WindupChildren(_ref) { var children = _ref.children, onFinished = _ref.onFinished, skipped = _ref.skipped, _ref$isPaused = _ref.isPaused, isPaused = _ref$isPaused === void 0 ? false : _ref$isPaused; var windupInit = useChildrenMemo(function () { return newWindup(React.Children.toArray(children).reduce(reduceWindupArgs, []), { element: undefined }); }, children); var _useWindup = useWindup(windupInit, { onFinished: onFinished, skipped: skipped }), windup = _useWindup.windup, skip = _useWindup.skip, pause = _useWindup.pause, resume = _useWindup.resume, rewind = _useWindup.rewind, isFinished = _useWindup.isFinished; React.useEffect(function () { if (isPaused === true) { pause(); } else { resume(); } }, [isPaused, pause, resume]); return React.createElement(WindupContext.Provider, { value: { skip: skip, pause: pause, resume: resume, rewind: rewind, isFinished: isFinished } }, renderChildrenWindup(windup)); }; function wrapChildren(children, Wrapper) { if (typeof children === "string") { return children.split("").map(function (_char, i) { return _char === "\n" ? _char : React.createElement(Wrapper, { key: _char + "-" + i }, _char); }); } if (typeof children === "number") { return children.toString().split("").map(function (_char2) { return React.createElement(Wrapper, null, _char2); }); } if (!React.isValidElement(children)) { return React.createElement(React.Fragment, null); } if (children.props.children) { var Outer = children.type; if (children.props.children instanceof Function) { return React.createElement(Outer, Object.assign({ key: children.key }, children.props)); } return React.createElement(Outer, Object.assign({ key: children.key }, children.props), React.Children.map(children.props.children, function (ch) { return wrapChildren(ch, Wrapper); })); } return children; } var CharWrapper = function CharWrapper(_ref) { var children = _ref.children, element = _ref.element; return React.createElement(React.Fragment, null, React.Children.map(children, function (ch) { return wrapChildren(ch, element); })); }; function makeGetDescriptorsOfChildren(defaultFontStyle) { return function (descriptors, children) { if (typeof children === "string") { return [].concat(descriptors, [{ text: children, font: defaultFontStyle }]); } if (typeof children === "number") { return [].concat(descriptors, [{ text: children.toString(), font: defaultFontStyle }]); } if (!React.isValidElement(children)) { return descriptors; } if (isStyledTextElement(children) && typeof children.props.children === "string") { return [].concat(descriptors, [{ text: children.props.children, font: children.props.fontStyle }]); } if (isStyledTextElement(children)) { return [].concat(descriptors, React.Children.toArray(children.props.children).reduce(makeGetDescriptorsOfChildren(children.props.fontStyle), [])); } return [].concat(descriptors, React.Children.toArray(children.props.children).reduce(makeGetDescriptorsOfChildren(defaultFontStyle), [])); }; } function reinsertStringsIntoChildren(_ref, children) { var accChildren = _ref[0], accStrings = _ref[1]; if (typeof children === "string" || typeof children === "number") { var firstString = accStrings[0], restStrings = accStrings.slice(1); return [[].concat(accChildren, [firstString]), restStrings]; } if (!React.isValidElement(children)) { return [accChildren, accStrings]; } var _React$Children$toArr = React.Children.toArray(children.props.children).reduce(reinsertStringsIntoChildren, [[], accStrings]), subChildrenAcc = _React$Children$toArr[0], subStringsAcc = _React$Children$toArr[1]; return [[].concat(accChildren, [React.cloneElement(children, { children: subChildrenAcc })]), subStringsAcc]; } function isStyledTextElement(element) { return element.type === StyledText; } function StyledText(_ref2) { var children = _ref2.children; return React.createElement(React.Fragment, null, children); } var Linebreaker = function Linebreaker(_ref3) { var children = _ref3.children, fontStyle = _ref3.fontStyle, width = _ref3.width; // CAVEATS: // fontStyle must match the font style of the characters inside // non-character elements must not add width to the line. // must be used OUTSIDE of WindupChildren component var childrenArray = React.Children.toArray(children); var descriptors = childrenArray.reduce(makeGetDescriptorsOfChildren(fontStyle), []); var transformedStrings = breakLines(descriptors, width, fontStyle); var _childrenArray$reduce = childrenArray.reduce(reinsertStringsIntoChildren, [[], transformedStrings]), transformedChildren = _childrenArray$reduce[0]; return React.createElement("div", { style: { whiteSpace: "pre" } }, transformedChildren); }; var Effect = function Effect(_ref) { var fn = _ref.fn; React.useEffect(function () { fn(); // We can safely omit fn from dependencies as if its value changes // the whole windup will be re-rendered anyway // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; }; function textFromChildren(children) { var text = React.Children.map(children, function (ch) { if (typeof ch === "string") { return ch; } if (typeof ch === "number") { return ch.toString(); } if (!React.isValidElement(ch)) { return ""; } if (ch.props.children) { return textFromChildren(ch.props.children); } return ""; }); if (!text) { return ""; } return text.join(""); } exports.CharWrapper = CharWrapper; exports.Effect = Effect; exports.Linebreaker = Linebreaker; exports.OnChar = OnChar; exports.Pace = Pace; exports.Pause = Pause; exports.StyledText = StyledText; exports.WindupChildren = WindupChildren; exports.defaultGetPace = defaultGetPace; exports.textFromChildren = textFromChildren; exports.useIsFinished = useIsFinished; exports.useRewind = useRewind; exports.useSkip = useSkip; exports.useWindupString = useWindupString; //# sourceMappingURL=windups.cjs.development.js.map