react-text-to-speech
Advanced tools
Comparing version 0.7.1 to 0.8.0
@@ -11,5 +11,7 @@ import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react"; | ||
export type Children = (childrenOptions: ChildrenOptions) => ReactNode; | ||
export type Props = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>; | ||
export type SpanProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>; | ||
export type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>; | ||
export type SpeechProps = { | ||
text: string; | ||
id?: string; | ||
pitch?: number; | ||
@@ -24,7 +26,10 @@ rate?: number; | ||
useStopOverPause?: boolean; | ||
highlightText?: boolean; | ||
highlightProps?: SpanProps; | ||
onError?: Function; | ||
props?: Props; | ||
props?: DivProps; | ||
children?: Children; | ||
}; | ||
export type { IconProps } from "./icons.js"; | ||
export default function Speech({ text, pitch, rate, volume, lang, voiceURI, startBtn, pauseBtn, stopBtn, useStopOverPause, onError, props, children, }: SpeechProps): string | number | boolean | React.JSX.Element | Iterable<React.ReactNode> | null | undefined; | ||
export default function Speech({ text, id, pitch, rate, volume, lang, voiceURI, startBtn, pauseBtn, stopBtn, useStopOverPause, highlightText, highlightProps, onError, props, children, }: SpeechProps): string | number | boolean | React.JSX.Element | Iterable<React.ReactNode> | null | undefined; | ||
export declare function HighlightedText({ id, ...props }: DivProps): React.JSX.Element; |
@@ -1,6 +0,54 @@ | ||
import React, { useEffect, useState } from "react"; | ||
var __rest = (this && this.__rest) || function (s, e) { | ||
var t = {}; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) | ||
t[p] = s[p]; | ||
if (s != null && typeof Object.getOwnPropertySymbols === "function") | ||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { | ||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) | ||
t[p[i]] = s[p[i]]; | ||
} | ||
return t; | ||
}; | ||
import React, { Fragment, cloneElement, isValidElement, useEffect, useMemo, useState } from "react"; | ||
import { createPortal } from "react-dom"; | ||
import { HiMiniStop, HiVolumeOff, HiVolumeUp } from "./icons.js"; | ||
export default function Speech({ text, pitch = 1, rate = 1, volume = 1, lang, voiceURI, startBtn = React.createElement(HiVolumeUp, null), pauseBtn = React.createElement(HiVolumeOff, null), stopBtn = React.createElement(HiMiniStop, null), useStopOverPause, onError = () => alert("Browser not supported! Try some other browser."), props = {}, children, }) { | ||
function JSXToText(element) { | ||
if (isValidElement(element)) { | ||
const { children } = element.props; | ||
if (Array.isArray(children)) | ||
return children.map((child) => JSXToText(child)); | ||
return JSXToText(children); | ||
} | ||
if (typeof element === "string") | ||
return element.split(" "); | ||
if (typeof element === "number") | ||
return [element.toString()]; | ||
return []; | ||
} | ||
function findWordIndex(words, index) { | ||
let currentIndex = 0; | ||
function recursiveSearch(subArray, parentIndex) { | ||
for (let i = 0; i < subArray.length; i++) { | ||
const element = subArray[i]; | ||
if (Array.isArray(element)) { | ||
const result = recursiveSearch(element, i); | ||
if (result !== null) | ||
return `${parentIndex === null ? "" : parentIndex + "-"}${result}`; | ||
} | ||
else if (element) { | ||
currentIndex += element.length + 1; | ||
if (currentIndex > index) | ||
return `${parentIndex === null ? "" : parentIndex + "-"}${i}`; | ||
} | ||
} | ||
return null; | ||
} | ||
return recursiveSearch(words, null); | ||
} | ||
export default function Speech({ text, id, pitch = 1, rate = 1, volume = 1, lang, voiceURI, startBtn = React.createElement(HiVolumeUp, null), pauseBtn = React.createElement(HiVolumeOff, null), stopBtn = React.createElement(HiMiniStop, null), useStopOverPause, highlightText = false, highlightProps = { style: { fontWeight: "bold" } }, onError = () => alert("Browser not supported! Try some other browser."), props = {}, children, }) { | ||
const [speechStatus, setSpeechStatus] = useState("stopped"); | ||
const [useStop, setUseStop] = useState(); | ||
const words = useMemo(() => JSXToText(text), [text]); | ||
const [highlightedIndex, setHighlightedIndex] = useState(null); | ||
const [highlightContainer, setHighlightContainer] = useState(null); | ||
const pause = () => { var _a; return speechStatus !== "paused" && ((_a = window.speechSynthesis) === null || _a === void 0 ? void 0 : _a.pause()); }; | ||
@@ -17,3 +65,3 @@ const stop = () => { var _a; return speechStatus !== "stopped" && ((_a = window.speechSynthesis) === null || _a === void 0 ? void 0 : _a.cancel()); }; | ||
synth.cancel(); | ||
const utterance = new window.SpeechSynthesisUtterance(text === null || text === void 0 ? void 0 : text.replace(/\s/g, " ")); | ||
const utterance = new window.SpeechSynthesisUtterance(words.join(" ").replace(/\s|,/g, " ")); | ||
utterance.pitch = pitch; | ||
@@ -39,5 +87,7 @@ utterance.rate = rate; | ||
setSpeechStatus("stopped"); | ||
setHighlightedIndex(null); | ||
utterance.onpause = null; | ||
utterance.onend = null; | ||
utterance.onerror = null; | ||
utterance.onboundary = null; | ||
if (synth.paused) | ||
@@ -49,4 +99,24 @@ synth.cancel(); | ||
utterance.onerror = setStopped; | ||
if (highlightText) | ||
utterance.onboundary = (event) => setHighlightedIndex(findWordIndex(words, event.charIndex)); | ||
synth.speak(utterance); | ||
} | ||
function highlightedText(element, parentIndex = "") { | ||
var _a; | ||
if (!(highlightedIndex === null || highlightedIndex === void 0 ? void 0 : highlightedIndex.startsWith(parentIndex))) | ||
return element; | ||
if (Array.isArray(element)) | ||
return element.map((child, index) => highlightedText(child, parentIndex === "" ? `${index}` : `${parentIndex}-${index}`)); | ||
if (isValidElement(element)) | ||
return cloneElement(element, { key: (_a = element.key) !== null && _a !== void 0 ? _a : Math.random().toString() }, highlightedText(element.props.children, parentIndex)); | ||
if (typeof element === "string" || typeof element === "number") { | ||
const words = String(element).split(" "); | ||
const index = +highlightedIndex.split("-").at(-1); | ||
return (React.createElement(Fragment, { key: highlightedIndex }, | ||
words.slice(0, index).join(" ") + " ", | ||
React.createElement("span", Object.assign({}, highlightProps), words[index]), | ||
" " + words.slice(index + 1).join(" "))); | ||
} | ||
return element; | ||
} | ||
useEffect(() => { | ||
@@ -57,5 +127,16 @@ var _a, _b; | ||
}, []); | ||
useEffect(() => { | ||
if (highlightText) | ||
setHighlightContainer(document.getElementById(`rtts-${id}`)); | ||
else | ||
setHighlightContainer(null); | ||
}, [highlightText]); | ||
return typeof children === "function" ? (children({ speechStatus, start, pause, stop })) : (React.createElement("div", Object.assign({ style: { display: "flex", columnGap: "1rem" } }, props), | ||
speechStatus !== "started" ? (React.createElement("span", { role: "button", onClick: start }, startBtn)) : useStop === false ? (React.createElement("span", { role: "button", onClick: pause }, pauseBtn)) : (React.createElement("span", { role: "button", onClick: stop }, stopBtn)), | ||
useStop === false && stopBtn && (React.createElement("span", { role: "button", onClick: stop }, stopBtn)))); | ||
useStop === false && stopBtn && (React.createElement("span", { role: "button", onClick: stop }, stopBtn)), | ||
highlightContainer && createPortal(highlightedText(text), highlightContainer))); | ||
} | ||
export function HighlightedText(_a) { | ||
var { id } = _a, props = __rest(_a, ["id"]); | ||
return React.createElement("div", Object.assign({ id: `rtts-${id}` }, props)); | ||
} |
{ | ||
"name": "react-text-to-speech", | ||
"version": "0.7.1", | ||
"version": "0.8.0", | ||
"description": "An easy to use react component for the Web Speech API.", | ||
@@ -34,3 +34,4 @@ "type": "module", | ||
"devDependencies": { | ||
"@types/react": "^18.2.52" | ||
"@types/react": "^18.2.52", | ||
"@types/react-dom": "^18.2.18" | ||
}, | ||
@@ -37,0 +38,0 @@ "scripts": { |
@@ -12,2 +12,3 @@ # react-text-to-speech | ||
- Stops speech instance on page reload. | ||
- Highlights words as they are read. | ||
- Handles multiple speech instances easily. See [Advanced Usage](#advanced-usage) | ||
@@ -51,6 +52,4 @@ - Fully Customizable. See [usage with FaC](#full-customization) | ||
This is the use case where `react-text-to-speech` outshines the other text-to-speech libraries. | ||
Let's assume that you fetch news from any News API and the API returns 3 news in response as shown below. Now if the user clicks on `startBtn` of #1 news (assuming # as id) and then clicks on `startBtn` on #2 news before the speech instance of #1 news ends, then `react-text-to-speech` will not just stop the #1 news speech instance and start the #2 news speech instance, but will also convert the `pauseBtn` of #1 news to `startBtn`, thus avoiding any inconsistency. | ||
Let's assume that you fetch news from any News API and the API returns 3 news in response as shown below. Now if the user clicks on `startBtn` of #1 news (assuming # as id) and then clicks on `startBtn` on #2 news before the speech instance of #1 news ends, then `react-text-to-speech` will not just stop the #1 news speech instance and start the #2 news speech instance, but will also convert the `pauseBtn` of #1 news to `startBtn`, thus avoiding any confusion. | ||
```jsx | ||
@@ -82,2 +81,43 @@ import React from "react"; | ||
#### Highlight Text | ||
If `highlightText` prop to `true`, the words in the text will be highlighted as they are spoken. `<HighlightedText>` component exported by `react-text-to-speech` can be used to accomplish this purpose. | ||
NOTE: `id` of both `<Speech>` and `<HighlightedText>` should be same to link them together. | ||
```jsx | ||
import React from "react"; | ||
import Speech, { HighlightedText } from "react-text-to-speech"; | ||
export default function App() { | ||
return ( | ||
<> | ||
<Speech | ||
id="unique-id" | ||
highlightText={true} | ||
highlightProps={{ style: { color: "white", backgroundColor: "blue" } }} | ||
text={ | ||
<div> | ||
<span>This library is awesome!</span> | ||
<div> | ||
<div> | ||
<span>It can also read and highlight </span> | ||
<span>nested text... </span> | ||
<span> | ||
<span>upto </span> | ||
<span> | ||
<span>any level.</span> | ||
</span> | ||
</span> | ||
</div> | ||
</div> | ||
</div> | ||
} | ||
/> | ||
<HighlightedText id="unique-id" /> | ||
</> | ||
); | ||
} | ||
``` | ||
#### Partial Customization | ||
@@ -158,3 +198,3 @@ | ||
| - | - | - | - | - | | ||
| `text` | `string` | Yes | - | It contains the text to be spoken when `startBtn` is clicked. | | ||
| `text` | `string \| JSX.Element` | Yes | - | It contains the text to be spoken when `startBtn` is clicked. | | ||
| `pitch` | `number (0 to 2)` | No | 1 | The pitch at which the utterance will be spoken. | | ||
@@ -169,2 +209,4 @@ | `rate` | `number (0.1 to 10)` | No | 1 | The speed at which the utterance will be spoken. | | ||
| `useStopOverPause` | `boolean` | No | `navigator.userAgentData.mobile` | Whether the controls should display `stopBtn` instead of `pauseBtn`. In Android devices, `SpeechSynthesis.pause()` behaves like `SpeechSynthesis.cancel()`. See [details](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis/pause) | | ||
| `highlightText` | `boolean` | No | `false` | Whether the words in the text should be highlighted as they are read or not. | | ||
| `highlightProps` | `React.DetailedHTMLProps` | No | `{ style: { fontWeight: "bold" } }` | Props to customise the highlighted word. | | ||
| `onError` | `Function` | No | `() => alert('Browser not supported! Try some other browser.')` | Function to be executed if browser doesn't support `Web Speech API`. | | ||
@@ -171,0 +213,0 @@ | `props` | `React.DetailedHTMLProps` | No | - | Props to customize the `<Speech>` component. | |
21298
196
240
2