windups
Version:
A unique typewriter effect library for React.
938 lines (781 loc) • 24.2 kB
JavaScript
import { createElement, Fragment, useReducer, useRef, useCallback, useEffect, useMemo, useDebugValue, useContext, createContext, Children, isValidElement, cloneElement } from 'react';
import breakLines from '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 createElement(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 createElement(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 = useReducer(windupReducer, windupInit, initWindupState),
_React$useReducer$ = _React$useReducer[0],
windup = _React$useReducer$.windup,
didFinishOnce = _React$useReducer$.didFinishOnce,
dispatch = _React$useReducer[1];
var timeoutRef = useRef(null);
var windupIsFinished = isFinished(windup);
var nextCharAtRef = useRef(null);
var pauseDelayRemainingRef = useRef(null);
var isPausedRef = useRef(false);
var skip = useCallback(function () {
if (!windupIsFinished) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
dispatch({
type: "fast-forward"
});
}
}, [windupIsFinished]);
var pause = 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 = 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 = useCallback(function () {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
dispatch({
type: "rewind"
});
}, []); // If windup arg changes, we should reset
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
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
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
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
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 || Fragment;
if (metadata.props && Object.keys(metadata.props).includes("children")) {
return 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 createElement(Outer, Object.assign({
key: metadata.key
}, metadata.props));
}
return createElement(Outer, Object.assign({
key: metadata.key
}, metadata.props), inner);
}
function useWindupString(text, options) {
if (options === void 0) {
options = {};
}
var windupInit = 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);
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__*/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 = useContext(WindupContext),
skip = _React$useContext.skip;
return skip;
}
function useRewind() {
var _React$useContext4 = useContext(WindupContext),
rewind = _React$useContext4.rewind;
return rewind;
}
function useIsFinished() {
var _React$useContext5 = 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 (!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: 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 = 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 = Children.map(children, function (child) {
if (typeof child === "string") {
return child;
}
if (typeof child === "number") {
return child.toString();
}
if (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 = 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(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;
useEffect(function () {
if (isPaused === true) {
pause();
} else {
resume();
}
}, [isPaused, pause, resume]);
return 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 : createElement(Wrapper, {
key: _char + "-" + i
}, _char);
});
}
if (typeof children === "number") {
return children.toString().split("").map(function (_char2) {
return createElement(Wrapper, null, _char2);
});
}
if (!isValidElement(children)) {
return createElement(Fragment, null);
}
if (children.props.children) {
var Outer = children.type;
if (children.props.children instanceof Function) {
return createElement(Outer, Object.assign({
key: children.key
}, children.props));
}
return createElement(Outer, Object.assign({
key: children.key
}, children.props), 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 createElement(Fragment, null, 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 (!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, Children.toArray(children.props.children).reduce(makeGetDescriptorsOfChildren(children.props.fontStyle), []));
}
return [].concat(descriptors, 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 (!isValidElement(children)) {
return [accChildren, accStrings];
}
var _React$Children$toArr = Children.toArray(children.props.children).reduce(reinsertStringsIntoChildren, [[], accStrings]),
subChildrenAcc = _React$Children$toArr[0],
subStringsAcc = _React$Children$toArr[1];
return [[].concat(accChildren, [cloneElement(children, {
children: subChildrenAcc
})]), subStringsAcc];
}
function isStyledTextElement(element) {
return element.type === StyledText;
}
function StyledText(_ref2) {
var children = _ref2.children;
return createElement(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 = 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 createElement("div", {
style: {
whiteSpace: "pre"
}
}, transformedChildren);
};
var Effect = function Effect(_ref) {
var fn = _ref.fn;
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 = Children.map(children, function (ch) {
if (typeof ch === "string") {
return ch;
}
if (typeof ch === "number") {
return ch.toString();
}
if (!isValidElement(ch)) {
return "";
}
if (ch.props.children) {
return textFromChildren(ch.props.children);
}
return "";
});
if (!text) {
return "";
}
return text.join("");
}
export { CharWrapper, Effect, Linebreaker, OnChar, Pace, Pause, StyledText, WindupChildren, defaultGetPace, textFromChildren, useIsFinished, useRewind, useSkip, useWindupString };
//# sourceMappingURL=windups.esm.js.map